Can't set volume, volume control is not forwarded to the system

后端 未结 1 1685
春和景丽
春和景丽 2021-01-22 21:49

I tried to use the Android MediaPlayer framework to play a mp3 file (see this question).

After I managed to make it work, I quickly recognized,

1条回答
  •  粉色の甜心
    2021-01-22 21:57

    While I hate to constantly answer my own questions, I found a solution after a couple of hours playing with the Android API, digging through some documentations and so on.

    My solution is partially based the answer, given by @josé-pereda on the topic "javafxports how to call android native Media Player".

    I created an interface for the tasks "volumeUp", "volumeDown" and "mute":

    public interface NativeVolumeService {
        void volumeUp();
        void volumeDown();
        void mute();
    }
    

    Then, based on the following answer on how to set the system volume on Android, I came up with the following implementation on Android:

    import java.util.ArrayList;
    import java.util.logging.Logger;
    
    import android.content.Context;
    import android.media.AudioManager;
    import android.view.KeyEvent;
    import android.view.View;
    import android.view.ViewGroup;
    import my.package.platform.NativeVolumeService;
    import javafxports.android.FXActivity;
    
    public class NativeVolumeServiceAndroid implements NativeVolumeService {
    
        private static final Logger LOG = Logger.getLogger(NativeVolumeServiceAndroid.class.getName());
    
        private final AudioManager audioManager;
    
        private final int maxVolume;
        private int preMuteVolume = 0;
        private int currentVolume = 0;
    
        public NativeVolumeServiceAndroid() {
            audioManager = (AudioManager) FXActivity.getInstance().getSystemService(Context.AUDIO_SERVICE);
            maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
        }
    
        @Override
        public void volumeUp() {
            LOG.info("dispatch volume up event");
            KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_VOLUME_UP);
            dispatchEvent(event, true, false);
        }
    
        @Override
        public void volumeDown() {
            LOG.info("dispatch volume down event");
            KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_VOLUME_DOWN);
            dispatchEvent(event, false, false);
        }
    
        @Override
        public void mute() {
            LOG.info("dispatch volume mute event");
            KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_VOLUME_MUTE);
            dispatchEvent(event, false, true);
        }
    
        private void dispatchEvent(KeyEvent event, boolean up, boolean mute) {
    
            // hardware key events (amongst others) get caught by the JavaFXPorts engine (or better: the Dalvik impl from Oracle)
            // to circumvent this, we need to do the volume adjustment the hard way: via the AudioManager
    
            // see: https://developer.android.com/reference/android/media/AudioManager.html
            // see: https://stackoverflow.com/questions/9164347/setting-the-android-system-volume?rq=1
    
            // reason: 
            // FXActivity registers a FXDalvikEntity, which etends the surface view and passing a key processor 
            // called KeyEventProcessor - this one catches all key events and matches them to JavaFX representations.
            // Unfortunately, we cannot bypass this, so we need the AudioManager
    
            currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
            if (mute) {
                if (currentVolume > 0) {
                    preMuteVolume = currentVolume;
                    audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, AudioManager.ADJUST_SAME,
                            AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE | AudioManager.FLAG_SHOW_UI);
                } else {
                    preMuteVolume = 0;
                    audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, preMuteVolume, AudioManager.FLAG_SHOW_UI);
                }
            } else if (up) {
                audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC,
                        (currentVolume + 1) <= maxVolume ? AudioManager.ADJUST_RAISE : AudioManager.ADJUST_SAME, AudioManager.FLAG_SHOW_UI);
            } else if (!up) {
                audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC,
                        (currentVolume - 1) >= 0 ? AudioManager.ADJUST_LOWER : AudioManager.ADJUST_SAME, AudioManager.FLAG_SHOW_UI);
            }
        }
    }
    

    to integrate it, I created the appropriate instance in my main class (I need this globally - you will see, why)

    private void instantiateNativeVolumeService() {
        String serviceName = NativeVolumeService.class.getName();
        if (Platform.isDesktop()) {
            serviceName += "Desktop";
        } else if (Platform.isAndroid()) {
            serviceName += "Android";
        }
        try {
            volumeService = (NativeVolumeService) Class.forName(serviceName).newInstance();
        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
            LOG.log(Level.SEVERE, "Could not get an instance of NativeAudioService for platform " + Platform.getCurrent(), e);
        }
    }
    

    volumeService is a class variable.

    Then I registered an event handler on my Stages Scene:

    @Override
    public void start(Stage stage) throws Exception {
        // initiate everything
        scene.addEventFilter(KeyEvent.ANY, this::handleGlobalKeyEvents);
        // do more stuff, if needed
    }
    

    And finally, the method handleGlobalKeyEvents looks like this:

    private void handleGlobalKeyEvents(KeyEvent event) {
        // use a more specific key event type like
        // --> KeyEvent.KEY_RELEASED == event.getEventType()
        // --> KeyEvent.KEY_PRESSED == event.getEventType()
        // without it, we would react on both events, thus doing one operation too much
        if (event.getCode().equals(KeyCode.VOLUME_UP) && KeyEvent.KEY_PRESSED == event.getEventType()) {
            if (volumeService != null) {
                volumeService.volumeUp();
                event.consume();
            }
        }
        if (event.getCode().equals(KeyCode.VOLUME_DOWN) && KeyEvent.KEY_PRESSED == event.getEventType()) {
            if (volumeService != null) {
                volumeService.volumeDown();
                event.consume();
            }
        }
    }
    

    In the end, the solution is as clean as it gets and not too complicated. Only the way until it worked was a bit nasty.

    @JoséPereda: If you want to integrate this solution as a charm down plugin or so, please feel free, but it would be nice to be mentioned and notified, if you do.

    Regards, Daniel

    0 讨论(0)
提交回复
热议问题