From 83dba19c60cd2490f98ef4f8c37742e4e98675b3 Mon Sep 17 00:00:00 2001 From: Aleksandr Ivanov Date: Mon, 12 Feb 2024 19:17:13 +0300 Subject: [PATCH] Playback state tracking improvements --- .../externalevents/MediaStateReceiver.java | 124 ++++++++++++++++++ .../externalevents/NotificationListener.java | 56 ++++---- 2 files changed, 147 insertions(+), 33 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/MediaStateReceiver.java diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/MediaStateReceiver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/MediaStateReceiver.java new file mode 100644 index 000000000..2545c31e8 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/MediaStateReceiver.java @@ -0,0 +1,124 @@ +/* Copyright (C) 2024 Aleksandr Ivanov + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ + +package nodomain.freeyourgadget.gadgetbridge.externalevents; + +import android.media.MediaMetadata; +import android.media.session.MediaController; +import android.media.session.PlaybackState; + +import androidx.annotation.Nullable; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; +import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; +import nodomain.freeyourgadget.gadgetbridge.util.MediaManager; + +public class MediaStateReceiver extends MediaController.Callback { + private long lastPosition = 0; + private long lastUpdateTime = 0; + + private MediaController mMediaController; + + public MediaStateReceiver(MediaController mediaController) { + mMediaController = mediaController; + } + + public boolean isPlaybackActive() { + // https://developer.android.com/reference/android/media/session/PlaybackState#isActive() + + switch (mMediaController.getPlaybackState().getState()) { + case PlaybackState.STATE_BUFFERING: + case PlaybackState.STATE_CONNECTING: + case PlaybackState.STATE_FAST_FORWARDING: + case PlaybackState.STATE_PLAYING: + case PlaybackState.STATE_REWINDING: + case PlaybackState.STATE_SKIPPING_TO_NEXT: + case PlaybackState.STATE_SKIPPING_TO_PREVIOUS: + case PlaybackState.STATE_SKIPPING_TO_QUEUE_ITEM: + return true; + default: + return false; + } + } + + public void startReceiving() { + setMetadata(mMediaController.getMetadata()); + setPlaybackState(mMediaController.getPlaybackState()); + mMediaController.registerCallback(this); + } + + public void stopReceiving() { + mMediaController.unregisterCallback(this); + } + + @Override + public void onMetadataChanged(@Nullable MediaMetadata metadata) { + super.onMetadataChanged(metadata); + setMetadata(metadata); + } + + @Override + public void onPlaybackStateChanged(@Nullable PlaybackState state) { + super.onPlaybackStateChanged(state); + setPlaybackState(state); + } + + private void setMetadata(@Nullable MediaMetadata metadata) { + final MusicSpec musicSpec = MediaManager.extractMusicSpec(metadata); + + if (musicSpec != null) { + GBApplication.deviceService().onSetMusicInfo(musicSpec); + } + } + + private void setPlaybackState(@Nullable PlaybackState state) { + if (state.getState() == PlaybackState.STATE_PLAYING && !doStateUpdate(state)) { + return; + } + + final MusicStateSpec stateSpec = MediaManager.extractMusicStateSpec(state); + + if (stateSpec != null) { + GBApplication.deviceService().onSetMusicState(stateSpec); + } + } + + private boolean doStateUpdate(PlaybackState state) + { + // To prevent spamming device with state updates + + float speed = state.getPlaybackSpeed(); + + long currentTime = System.currentTimeMillis(); + long currentPosition = state.getPosition(); + + long timeDiff = (long) (speed * (currentTime - lastUpdateTime)); + long positionDiff = currentPosition - lastPosition; + + long epsilon = (long) Math.abs(speed * 50); + + if (Math.abs(timeDiff - positionDiff) > epsilon) + { + lastUpdateTime = currentTime; + lastPosition = currentPosition; + return true; + } + + return false; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java index 3727d6f2c..948c1af4d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java @@ -36,7 +36,6 @@ import android.graphics.drawable.Drawable; import android.media.session.MediaController; import android.media.session.MediaSession; import android.os.Bundle; -import android.os.Handler; import android.os.PowerManager; import android.os.Process; import android.os.UserHandle; @@ -75,14 +74,11 @@ import nodomain.freeyourgadget.gadgetbridge.externalevents.notifications.GoogleM import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.AppNotificationType; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; -import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; -import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationType; import nodomain.freeyourgadget.gadgetbridge.service.DeviceCommunicationService; import nodomain.freeyourgadget.gadgetbridge.util.BitmapUtil; import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue; -import nodomain.freeyourgadget.gadgetbridge.util.MediaManager; import nodomain.freeyourgadget.gadgetbridge.util.NotificationUtils; import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; @@ -126,9 +122,7 @@ public class NotificationListener extends NotificationListenerService { private long activeCallPostTime; private int mLastCallCommand = CallSpec.CALL_UNDEFINED; - private final Handler mHandler = new Handler(); - private Runnable mSetMusicInfoRunnable = null; - private Runnable mSetMusicStateRunnable = null; + private MediaStateReceiver mMediaStateReceiver = null; private GoogleMapsNotificationHandler googleMapsNotificationHandler = new GoogleMapsNotificationHandler(); @@ -245,6 +239,11 @@ public class NotificationListener extends NotificationListenerService { LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver); notificationStack.clear(); notificationsActive.clear(); + + if (mMediaStateReceiver != null) { + mMediaStateReceiver.stopReceiving(); + } + super.onDestroy(); } @@ -669,7 +668,7 @@ public class NotificationListener extends NotificationListenerService { private boolean handleMediaSessionNotification(final StatusBarNotification sbn) { final MediaSession.Token token = sbn.getNotification().extras.getParcelable(Notification.EXTRA_MEDIA_SESSION); - return token != null && handleMediaSessionNotification(token); + return handleMediaSessionNotification(token); } /** @@ -680,38 +679,29 @@ public class NotificationListener extends NotificationListenerService { */ public boolean handleMediaSessionNotification(MediaSession.Token mediaSession) { try { - final MediaController c = new MediaController(getApplicationContext(), mediaSession); - if (c.getMetadata() == null) { + if (mediaSession == null) + { + if (mMediaStateReceiver != null) { + mMediaStateReceiver.stopReceiving(); + mMediaStateReceiver = null; + } + return false; } - final MusicStateSpec stateSpec = MediaManager.extractMusicStateSpec(c.getPlaybackState()); - final MusicSpec musicSpec = MediaManager.extractMusicSpec(c.getMetadata()); + MediaController mediaController = new MediaController(getApplicationContext(), mediaSession); + MediaStateReceiver mediaStateReceiver = new MediaStateReceiver(mediaController); - // finally, tell the device about it - if (mSetMusicInfoRunnable != null) { - mHandler.removeCallbacks(mSetMusicInfoRunnable); + if (mMediaStateReceiver != null && mediaStateReceiver.isPlaybackActive()) + { + mMediaStateReceiver.stopReceiving(); + mMediaStateReceiver = null; } - mSetMusicInfoRunnable = new Runnable() { - @Override - public void run() { - GBApplication.deviceService().onSetMusicInfo(musicSpec); - } - }; - mHandler.postDelayed(mSetMusicInfoRunnable, 100); - if (stateSpec != null) { - if (mSetMusicStateRunnable != null) { - mHandler.removeCallbacks(mSetMusicStateRunnable); - } - mSetMusicStateRunnable = new Runnable() { - @Override - public void run() { - GBApplication.deviceService().onSetMusicState(stateSpec); - } - }; + if (mMediaStateReceiver == null) { + mMediaStateReceiver = mediaStateReceiver; + mMediaStateReceiver.startReceiving(); } - mHandler.postDelayed(mSetMusicStateRunnable, 100); return true; } catch (final NullPointerException | SecurityException e) {