I've followed the documentation regarding restarting an inactive media session but it's not working for me. According to the documentation as long as you have the proper entry in the AndroidManifest it should just work.
In the logcat, I can see it's connecting to the MediaBrowserService but nothing happens:
2023-02-14 13:23:20.187 11970-11970 MediaBrowserCompat D Connecting to a MediaBrowserService.
2023-02-14 13:23:20.216 11970-11970 ExoPlayerImpl I Init 690e213 [AndroidXMedia3/1.0.0-beta03] [redfin, Pixel 5, Google, 33]
2023-02-14 13:23:20.253 11970-11970 ExoPlayerImpl I Release 690e213 [AndroidXMedia3/1.0.0-beta03] [redfin, Pixel 5, Google, 33] [media3.common, media3.exoplayer, media3.decoder, media3.datasource, media3.extractor]
2023-02-14 13:23:20.760 11970-11970 MediaBrowserCompat D Connecting to a MediaBrowserService.
2023-02-14 13:23:20.783 11970-11970 ExoPlayerImpl I Init 7df60bd [AndroidXMedia3/1.0.0-beta03] [redfin, Pixel 5, Google, 33]
2023-02-14 13:23:20.819 11970-11970 ExoPlayerImpl I Release 7df60bd [AndroidXMedia3/1.0.0-beta03] [redfin, Pixel 5, Google, 33] [media3.common, media3.exoplayer, media3.decoder, media3.datasource, media3.extractor]
2023-02-14 13:23:26.319 11970-11970 MediaBrowserCompat D Connecting to a MediaBrowserService.
2023-02-14 13:23:26.345 11970-11970 ExoPlayerImpl I Init 540e5f3 [AndroidXMedia3/1.0.0-beta03] [redfin, Pixel 5, Google, 33]
2023-02-14 13:23:26.388 11970-11970 ExoPlayerImpl I Release 540e5f3 [AndroidXMedia3/1.0.0-beta03] [redfin, Pixel 5, Google, 33] [media3.common, media3.exoplayer, media3.decoder, media3.datasource, media3.extractor]
2023-02-14 13:23:27.001 11970-11970 MediaBrowserCompat D Connecting to a MediaBrowserService.
2023-02-14 13:23:27.025 11970-11970 ExoPlayerImpl I Init f1d97e0 [AndroidXMedia3/1.0.0-beta03] [redfin, Pixel 5, Google, 33]
I'm know ExoPlayer has a MediaSessionConnector but the documentation doesn't say to use it and there's little code examples on how to implement it.
This is a stripped down version of my MediaBrowserService code:
public class MediaPlayerService extends MediaBrowserServiceCompat {
private ExoPlayer mPlayer;
private MediaSessionCompat mMediaSessionCompat;
#Override
public void onCreate() {
super.onCreate();
mMediaSessionCompat = new MediaSessionCompat(this, MediaPlayerService.class.getSimpleName());
mMediaSessionCompat.setCallback(mMediaSessionCallback);
mMediaSessionCompat.setActive(true);
mPlaybackBuilder = new PlaybackStateCompat.Builder();
mPlaybackBuilder.setActions(
PlaybackStateCompat.ACTION_PLAY_PAUSE |
PlaybackStateCompat.ACTION_PLAY |
PlaybackStateCompat.ACTION_PAUSE |
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS |
PlaybackStateCompat.ACTION_SKIP_TO_NEXT);
setSessionToken(mMediaSessionCompat.getSessionToken());
mPlaybackAttributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build();
mPlayer = new ExoPlayer.Builder(mContext).build();
mPlayer.addListener(new PlayerEventListener());
}
private final MediaSessionCompat.Callback mMediaSessionCallback = new MediaSessionCompat.Callback() {
#Override
public void onStop() {
super.onStop();
StopAudio();
}
#Override
public void onPlay() {
super.onPlay();
PlayAudio();
}
#Override
public void onPause() {
super.onPause();
PauseAudio();
}
};
private class PlayerEventListener implements Player.Listener {
#Override
public void onPlaybackStateChanged(int state) {
if (state == Player.STATE_BUFFERING) {
} else if (state == Player.STATE_READY) {
} else if (state == Player.STATE_ENDED)
}
}
#Override
public void onDestroy() {
super.onDestroy();
mMediaSessionCompat.setMetadata(null);
mMediaSessionCompat.setActive(false);
mMediaSessionCompat.release();
mPlayer.release();
}
}
The clue is this part in the documentation:
If Android can identify the last active media session, it tries to
restart the session by sending an ACTION_MEDIA_BUTTON Intent to a
manifest-registered component (such as a service or
BroadcastReceiver).
This means you need to override onMediaButtonEvent in the MediaSessionCompat.Callback and restart playback there:
#Override
public boolean onMediaButtonEvent(Intent mediaButtonEvent) {
KeyEvent keyEvent = (KeyEvent) mediaButtonEvent.getExtras().get(Intent.EXTRA_KEY_EVENT);
if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PLAY) {
//restart playback
}
return super.onMediaButtonEvent(mediaButtonEvent);
}
Related
I have integrated twilio programmable video for video calling between 2 devices, but problem is, in some android devices remote audio is very low. And I have tested other applications like whatsapp, whatsapp audio is loud on those devices.
I have tried audioManager.setMode but it is not working.
#Override
protected void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_video_call);
audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
// Other functionalities
}
Please advise.
I followed the suggest Twilio audio debugging but did not get anywhere, after playing about with what they had suggested I was able to create this function which I call in the onCreate of the activity using Twilio
private fun requestAudioFocus() {
val playbackAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build()
val focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
.setAudioAttributes(playbackAttributes)
.setAcceptsDelayedFocusGain(true)
.setOnAudioFocusChangeListener { }
.build()
audioManager.requestAudioFocus(focusRequest)
audioManager.isSpeakerphoneOn = !isHeadphonesPlugged(this)
}
And the check headphone function is
fun isHeadphonesPlugged(context:Context): Boolean {
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager?
val audioDevices = audioManager!!.getDevices(AudioManager.GET_DEVICES_ALL)
for (deviceInfo in audioDevices) {
if (deviceInfo.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES
|| deviceInfo.type == AudioDeviceInfo.TYPE_WIRED_HEADSET) {
return true
}
}
return false
}
I define the audio manager before calling requestFocus()
audioManager = this.getSystemService(Context.AUDIO_SERVICE) as AudioManager
Hopefully this helps someone!
I can create games with invites, I can invite and accept invitations, the thing mostly works fine. The problem appears when I make a game for 3 or more people:
user A creates game with 3 people or 2 people at least
user A chooses to invite someone and then adds another person to auto-pick for the room
User A invites user B
User B accepts
They both wait until the "Start now" button appears
User B presses the start now button and OnRoomConnected() is called 3 times for some reason and the game doesn't start (the room was also never left as far as I can tell, because this user can't receive invitations anymore)
Nothing changes from the perspective of user A, he still waits for the game to start or search for another auto pick opponent
I made sure that the problem is not from my code. I created a separate simple project, that I used only for testing purposes and it does exactly the same thing. So I was starting to think maybe it's not a problem from my side and I didn't see similar problems on the internet. So I decided to ask here. What should I do? What could be the problem?
That's basically it. Even if I restrict the number of players to 3 or 4 (min and max number of players are equal, 3 or 4), it still lets me start the game prematurely and I have the same problem with OnRoomConnected() being called multiple times and the game doesn't start.
Thanks in advance. If you have a link or something that would help me solve this problem, it would be greatly appreciated.
Here's the basic code I used for logging in the game and creating a room.
public class GPGM : MonoBehaviour, RealTimeMultiplayerListener{
public static GPGM instance;
public static int target;
private void Awake()
{
target = 60;
QualitySettings.vSyncCount = 0;
Application.targetFrameRate = target;
if (instance == null)
{
DontDestroyOnLoad(gameObject);
instance = this;
}
else if (instance != this)
{
Destroy(gameObject);
}
}
// Use this for initialization
void Start () {
Login();
}
// Update is called once per frame
void Update () {
}
public void Login()
{
StartCoroutine(checkInternetConnection((isConnected) =>
{
LoginGPG();
}));
}
IEnumerator checkInternetConnection(Action<bool> action)
{
WWW www;
www = new WWW("http://google.com");
yield return www;
if (!String.IsNullOrEmpty(www.error))
{
Debug.Log("DebugM | no internet connection");
action(false);
}
else
{
Debug.Log("DebugM | There IS connection");
action(true);
}
}
public void LoginGPG()
{
PlayGamesClientConfiguration config = new PlayGamesClientConfiguration.Builder().WithInvitationDelegate(OnInvitationReceived).Build();
PlayGamesPlatform.InitializeInstance(config);
PlayGamesPlatform.DebugLogEnabled = true;
PlayGamesPlatform.Activate();
Debug.Log("DebugM | LoginGPG");
Auth();
}
public void Auth()
{
Debug.Log("DebugM | Auth");
try
{
//doesn't work sometimes for some reason. It gives null data if success is false
//reason for false success is unknown
Social.localUser.Authenticate((bool succes) =>
{
if (succes)
{
Debug.Log("DebugM | Logged in");
}else
{
Debug.Log("DebugM | authentication failed");
}
});
}
catch (Exception e)
{
Debug.Log("DebugM | Auth() has failed with error: " + e.Message);
}
}
public void OnInvitationReceived(Invitation invitation, bool shouldAutoAccept)
{
StartCoroutine(InvitationCo(invitation, shouldAutoAccept));
}
Invitation mIncomingInvitation;
IEnumerator InvitationCo(Invitation invitation, bool shouldAutoAccept)
{
yield return new WaitUntil(() => SceneManager.GetActiveScene().name == "Lobby");
Debug.Log("DebugM | Invitation has been received!!!");
//StartCoroutine(LM.LoadingAnim());
if (shouldAutoAccept)
{
Debug.Log("DebugM | Should auto accept: TRUE");
PlayGamesPlatform.Instance.RealTime.AcceptInvitation(invitation.InvitationId, instance);
}
else
{
// The user has not yet indicated that they want to accept this invitation.
// We should *not* automatically accept it. Rather we store it and
// display an in-game popup:
Debug.Log("DebugM | Should auto accept: FALSE");
Lobby LM = FindObjectOfType<Lobby>();
LM.invPanel.SetActive(true);
mIncomingInvitation = invitation;
}
}
public void AcceptGoogleInv(GameObject panel)
{
if (mIncomingInvitation != null)
{
// show the popup
//string who = (mIncomingInvitation.Inviter != null &&
// mIncomingInvitation.Inviter.DisplayName != null) ?
// mIncomingInvitation.Inviter.DisplayName : "Someone";
Debug.Log("DebugM | Invitation has been accepted");
PlayGamesPlatform.Instance.RealTime.AcceptInvitation(mIncomingInvitation.InvitationId, instance);
panel.SetActive(false);
}
}
public void CreateQuickRoom()
{
PlayGamesPlatform.Instance.RealTime.CreateWithInvitationScreen(1, 3, 1, instance );
}
public void OnRoomSetupProgress(float percent)
{
Debug.Log("OnRoomSetupProgress()");
PlayGamesPlatform.Instance.RealTime.ShowWaitingRoomUI();
}
public void OnRoomConnected(bool success)
{
SceneManager.LoadScene("Game");
Debug.Log("DebugM | Room conected");
}
public void OnLeftRoom()
{
throw new NotImplementedException();
}
public void OnParticipantLeft(Participant participant)
{
throw new NotImplementedException();
}
public void OnPeersConnected(string[] participantIds)
{
throw new NotImplementedException();
}
public void OnPeersDisconnected(string[] participantIds)
{
throw new NotImplementedException();
}
public void OnRealTimeMessageReceived(bool isReliable, string senderId, byte[] data)
{
throw new NotImplementedException();
}}
Edit (pictures):
This is when I wait for the last auto pick slot to fill (it works like this only when people were invited to the game)
The game goes to lobby for the person who pressed start and the others still wait for the last autopick even if 1 player practically left the room
You can do it like:
public void OnRoomConnected (bool success)
{
if (success)
{
//Start the game here
SceneManager.LoadScene("Game");
Debug.Log("DebugM | Room conected");
}
else
{
//Do somthing else.
}
}
or the best way to do it by checking connected participans count.
public void OnPeersConnected (string[] participantIds)
{
List<Participant> playerscount = PlayGamesPlatform.Instance.RealTime.GetConnectedParticipants();
if (playerscount != null && playerscount.Count > 1)//this condition should be decided by you.
{
//Start the game here
SceneManager.LoadScene("Game");
}
}
Help is needed. Thanks to David for the last answer to the question -AltBeacon service in separate android process.
I'm trying to implement on the Xamarin Android - AltBeacon Library in the service and in a separate process. (A separate process is needed to ensure that scanning works constantly, round the clock.
What would the android not cut the scanning after a certain time, after the phone goes into sleep mode).
1. What did I do after David's instructions -
-added attributes in the service definition:
[Service (Name = SERVICE_NAME, Process = ": myProcess", Enabled = true, Exported = false, IsolatedProcess = false)]
installed a new version of the library - AltBeacon 2.11. To date, Xamarin does not have version 2.11. The latest version is 2.7
I downloaded the AltBeacon v2.11 native library and wrapped it with Managed Callable Wrappers (MCW). [(https://developer.xamarin.com/guides/android/advanced_topics/binding-a-java-library/)][1].
As a result, I got a full library on Xamarin.
- Connected this library to the project. For testing, I removed the Process: myProcess attribute and ran the application.
In one process, everything works well. The service works and finds the bicons. Perfectly!
But -
As soon as I started the service in another process I stopped getting the bicons.
After the other method that triggers this is OnBeaconServiceConnect ().
public void OnBeaconServiceConnect()
{
for (int i = 0; i < guids.Length; i++)
{
var uuid = Identifier.Parse(guids[i]);
var region = new Region("R" + i, uuid, null, null);
beaconManager.StartRangingBeaconsInRegion(region);
}
}
It turns out that the method does not work in the Range Notifier object =(
A rough example of the implementation of the service:
public class myService:Service
{
private BeaconWorker beaconWorker;
public myService()
{
beaconWorker=new beaconWorker(DroidApplication.CurrentInstanceApp);
}
public void MainMethod()
{
var notification = ...Build();
StartForeground(s_id, notification);
while(true)
{
StartMainWork(guids)
}
}
public void StartMainWork(string guid)
{
beaconWorker.GetResult();
}
}
/////////////////////////////////////
public class BeaconWorker:IBeaconConsumer
{
List<Beacon> Beacons;
private Context context;
private RangeNotifier rangeNotifier;
private BeaconManager beaconManager;
//This is main configuring scanning
public BeaconWorker(Context context)
{
Context = context;
this.guids = ...;
rangeNotifier = new RangeNotifier();
BeaconManager.SetDebug(true);
beaconManager =
BeaconManager.GetInstanceForApplication(context);
beaconManager.SetForegroundBetweenScanPeriod(1000);
beaconManager.SetForegroundScanPeriod(1000);
beaconManager.SetBackgroundMode(false);
var beaconParser = new BeaconParser();
beaconParser.SetBeaconLayout("...");
beaconManager.BeaconParsers.Add(beaconParser);
beaconManager.SetRangeNotifier(rangeNotifier);
beaconManager.ApplySettings();
}
//Method that do bind
public void GetResult()
{
beaconManager.Bind(this);
Task.Wait(3000);
beaconManager.UnBind(this);
}
//Implement IBeaconConsumer BindService
public bool BindService(Intent p0, IServiceConnection p1, int p2)
{
return context.BindService(p0, p1, Bind.AutoCreate);
}
public void OnBeaconServiceConnect()
{
for (int i = 0; i < guids.Length; i++)
{
var uuid = Identifier.Parse(guids[i]);
var region = new Region("R" + i, uuid, null, null);
beaconManager.StartRangingBeaconsInRegion(region);
}
}
private class RangeNotifier
{
//THIS METHOD DOES NOT INVOKE =(
public void DidRangeBeaconsInRegion(ICollection<Beacon> beacons, Region region)
{
this.Beacons = beacons;
}
}
}
In the logs of the phone there is such information:
Time Device Name Type PID Tag Message
01-18 18:52:25.370 AGM A8 Warning 17056 BeaconManager Ranging/Monitoring may not be controlled from a separate BeaconScanner process. To remove this warning, please wrap this call in: if (beaconManager.isMainProcess())
Time Device Name Type PID Tag Message
01-18 18:52:25.370 AGM A8 Debug 17056 BeaconManager we have a connection to the service now
Time Device Name Type PID Tag Message
01-18 18:52:25.361 AGM A8 Debug 17056 BeaconManager consumer count is now: 1
Time Device Name Type PID Tag Message
01-18 18:52:25.363 AGM A8 Info 17024 BeaconService beaconService version 2.11 is starting up on the main process
I know about the verification David pointed out in the answer to IsMainProcess() method. But how can I call this check if all this happens in the service itself and not in the main application process?
Tell me where I'm wrong. What else can I add?
In the article https://github.com/AltBeacon/android-beacon-library/pull/479
on how much I realized that it makes no sense to specify a RangeNotifier in a separate process. Is it so?
I will be grateful for any help! Thank you!
I made an app it run perfectly in api 20+ but for android version 4.4 and less it is getting crashed with error NoClassDefFoundError: android.media.session.MediaSessionManager this is the stack trace that i am getting in the developer console .
java.lang.NoClassDefFoundError: android.media.session.MediaSessionManager
at beatbox.neelay.beatbox.MediaService.initMediaSession(MediaService.java:634)
at beatbox.neelay.beatbox.MediaService.onStartCommand(MediaService.java:170)
at android.app.ActivityThread.handleServiceArgs(ActivityThread.java:2913)
at android.app.ActivityThread.access$2100(ActivityThread.java:151)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1442)
at android.os.Handler.dispatchMessage(Handler.java:110)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:5339)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:515)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:828)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:644)
at dalvik.system.NativeStart.main(Native Method)
All I am able to understand from this is the error is in the initMediaSession method .this is my initMediaSession method
private void initMediaSession() throws RemoteException {
if (mediaSessionManager != null) return; //mediaSessionManager exists
mediaSessionManager = (MediaSessionManager) getSystemService(Context.MEDIA_SESSION_SERVICE);
// Create a new MediaSession
mediaSession = new MediaSessionCompat(getApplicationContext(), "AudioPlayer");
//Get MediaSessions transport controls
transportControls = mediaSession.getController().getTransportControls();
//set MediaSession -> ready to receive media commands
mediaSession.setActive(true);
//indicate that the MediaSession handles transport control commands
// through its MediaSessionCompat.Callback.
mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
//Set mediaSession's MetaData
updateMetaData();
// passing the data
// Attach Callback to receive MediaSession updates
mediaSession.setCallback(new MediaSessionCompat.Callback() {
// Implement callbacks
#Override
public void onPlay() {
super.onPlay();
messagesent();
a = false;
resumeMedia();
buildNotification(PlaybackStatus.PLAYING);
}
#Override
public void onPause() {
super.onPause();
messagesent();
a = true;
pauseMedia();
buildNotification(PlaybackStatus.PAUSED);
}
#Override
public void onSkipToNext() {
super.onSkipToNext();
skipToNext();
updateMetaData();
buildNotification(PlaybackStatus.PLAYING);
}
#Override
public void onSkipToPrevious() {
super.onSkipToPrevious();
skipToPrevious();
updateMetaData();
buildNotification(PlaybackStatus.PLAYING);
}
#Override
public void onStop() {
super.onStop();
removeNotification();
//Stop the service
pauseMedia();
messagesent();
stopSelf();
}
#Override
public void onSeekTo(long position) {
super.onSeekTo(position);
}
});
}
I dont understand why it is getting crashend for 4.4 and less devices and how i can fix this .I googled and got this but this post dont tell how tofix this.
MediaSessionManager was only added in api 21 (5.0)
If it's absolutely necessary to use it then you can set your min sdk to 21 or check your build number with:
android.os.Build.VERSION.SDK
and not call this service with devices with lower sdks
It seems that the reason may be due to multidex. Check your apk method count at Get Method Count
You can enable multidex by adding dependency
compile 'com.android.support:multidex:1.0.1'
then enable it in config
defaultConfig {
multiDexEnabled true
}
add following snippet in android section of your
dexOptions {
javaMaxHeapSize "4g"
preDexLibraries false
}
afterEvaluate {
tasks.matching {
it.name.startsWith('dex')
}.each { dx ->
if (dx.additionalParameters == null) {
dx.additionalParameters = []
}
dx.additionalParameters += '--multi-dex'
// this is optional
dx.additionalParameters += "--main-dex-list=$projectDir/multidex.keep".toString()
}
}
compileOptions {
incremental false
}
I am also following the same tutorial and had same issue. I found the solution to this. Simply check if your SDK >21 then only use method initMediaSession();
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
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