2

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, that the volume up/down events are caught by the class javafxports.android.KeyEventProcessor and never get forwarded. I tried to circumvent that, but to no avail.

Are there any means to dispatch the event to the system where it not gets caught?

Thanks and regards, Daniel

Community
  • 1
  • 1
dzim
  • 1,131
  • 2
  • 12
  • 28
  • http://stackoverflow.com/questions/40562263/how-to-play-a-sound-on-android-using-gluon-javafx-project, https://bitbucket.org/javafxports/android/issues/73/hardware-button-key-events-report – jns Dec 01 '16 at 15:02
  • Wow! what a great response! Just providing two links, that seem to suggest it's not solved and not solvable, or what? Come on! This is Android we are talking about and it is solvable! - See my answer, I will provide on this topic. – dzim Dec 02 '16 at 08:17
  • I see. Since you are constantly answering your own questions anyway, your are not interested in links which may provide some information on the topic. – jns Dec 02 '16 at 19:46
  • The problem is: the links only state the issues, with no solution to them whatsoever. I mean, I could just have let the question unanswered, even if I've found one. And your "comment" was only a list of links. Nothing else. I'm sorry to say this, but this is not helpful (considering the lack on valuable information behind those links). – dzim Dec 02 '16 at 21:21

1 Answers1

2

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

Community
  • 1
  • 1
dzim
  • 1,131
  • 2
  • 12
  • 28