From bc7188c8a8c8036ed67a93aa2572f5de676721ce Mon Sep 17 00:00:00 2001 From: John Zhen Mo Date: Thu, 15 Mar 2018 23:42:46 -0700 Subject: [PATCH] -Added media session implementation for all players. -Extracted version numbers in gradle dependencies. -Updated ExoPlayer to 2.7.1. -Updated RxJava to 2.1.10, RxAndroid to 2.0.2 and RxBinding to 2.1.1. -Removed deprecated implementation of media buttons. --- app/build.gradle | 36 +++--- app/src/main/AndroidManifest.xml | 6 - .../newpipe/player/BackgroundPlayer.java | 54 --------- .../org/schabi/newpipe/player/BasePlayer.java | 12 +- .../newpipe/player/helper/AudioReactor.java | 33 ++---- .../player/helper/MediaSessionManager.java | 38 ++++++ .../mediasession/DummyPlaybackPreparer.java | 45 +++++++ .../mediasession/MediaSessionCallback.java | 17 +++ .../mediasession/PlayQueueNavigator.java | 111 ++++++++++++++++++ .../PlayQueuePlaybackController.java | 31 +++++ .../playback/BasePlayerMediaSession.java | 77 ++++++++++++ 11 files changed, 358 insertions(+), 102 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/mediasession/DummyPlaybackPreparer.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionCallback.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueuePlaybackController.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java diff --git a/app/build.gradle b/app/build.gradle index 952bc3067..9b2569a66 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -49,6 +49,11 @@ android { ext { supportLibVersion = '27.1.0' + exoPlayerLibVersion = '2.7.1' + roomDbLibVersion = '1.0.0' + leakCanaryVersion = '1.5.4' + okHttpVersion = '1.5.0' + icepickVersion = '3.2.0' } dependencies { androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2') { @@ -73,27 +78,28 @@ dependencies { implementation 'de.hdodenhof:circleimageview:2.2.0' implementation 'com.github.nirhart:ParallaxScroll:dd53d1f9d1' implementation 'com.nononsenseapps:filepicker:4.2.1' - implementation 'com.google.android.exoplayer:exoplayer:2.7.1' + implementation "com.google.android.exoplayer:exoplayer:$exoPlayerLibVersion" + implementation "com.google.android.exoplayer:extension-mediasession:$exoPlayerLibVersion" - debugImplementation 'com.facebook.stetho:stetho:1.5.0' - debugImplementation 'com.facebook.stetho:stetho-urlconnection:1.5.0' + debugImplementation "com.facebook.stetho:stetho:$okHttpVersion" + debugImplementation "com.facebook.stetho:stetho-urlconnection:$okHttpVersion" debugImplementation 'com.android.support:multidex:1.0.3' - implementation 'io.reactivex.rxjava2:rxjava:2.1.7' - implementation 'io.reactivex.rxjava2:rxandroid:2.0.1' - implementation 'com.jakewharton.rxbinding2:rxbinding:2.0.0' + implementation 'io.reactivex.rxjava2:rxjava:2.1.10' + implementation 'io.reactivex.rxjava2:rxandroid:2.0.2' + implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1' - implementation 'android.arch.persistence.room:runtime:1.0.0' - implementation 'android.arch.persistence.room:rxjava2:1.0.0' - annotationProcessor 'android.arch.persistence.room:compiler:1.0.0' + implementation "android.arch.persistence.room:runtime:$roomDbLibVersion" + implementation "android.arch.persistence.room:rxjava2:$roomDbLibVersion" + annotationProcessor "android.arch.persistence.room:compiler:$roomDbLibVersion" - implementation 'frankiesardo:icepick:3.2.0' - annotationProcessor 'frankiesardo:icepick-processor:3.2.0' + implementation "frankiesardo:icepick:$icepickVersion" + annotationProcessor "frankiesardo:icepick-processor:$icepickVersion" - debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4' - betaImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4' - releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4' + debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakCanaryVersion" + betaImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryVersion" + releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryVersion" implementation 'com.squareup.okhttp3:okhttp:3.9.1' - debugImplementation 'com.facebook.stetho:stetho-okhttp3:1.5.0' + debugImplementation "com.facebook.stetho:stetho-okhttp3:$okHttpVersion" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 18b3222a0..1be8c1f2c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -43,12 +43,6 @@ android:launchMode="singleTask" android:label="@string/title_activity_background_player"/> - - - - - - = Build.VERSION_CODES.O; - private static final boolean CAN_USE_MEDIA_BUTTONS = - Build.VERSION.SDK_INT <= Build.VERSION_CODES.O_MR1; - private static final String MEDIA_BUTTON_DEPRECATED_ERROR = - "registerMediaButtonEventReceiver has been deprecated and maybe not supported anymore."; - private static final int DUCK_DURATION = 1500; private static final float DUCK_AUDIO_TO = .2f; @@ -42,7 +37,8 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au private final AudioFocusRequest request; - public AudioReactor(@NonNull final Context context, @NonNull final SimpleExoPlayer player) { + public AudioReactor(@NonNull final Context context, + @NonNull final SimpleExoPlayer player) { this.player = player; this.context = context; this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); @@ -59,6 +55,11 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au } } + public void dispose() { + abandonAudioFocus(); + player.removeAudioDebugListener(this); + } + /*////////////////////////////////////////////////////////////////////////// // Audio Manager //////////////////////////////////////////////////////////////////////////*/ @@ -91,22 +92,6 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au audioManager.setStreamVolume(STREAM_TYPE, volume, 0); } - public void registerMediaButtonEventReceiver(ComponentName componentName) { - if (CAN_USE_MEDIA_BUTTONS) { - audioManager.registerMediaButtonEventReceiver(componentName); - } else { - Log.e(TAG, MEDIA_BUTTON_DEPRECATED_ERROR); - } - } - - public void unregisterMediaButtonEventReceiver(ComponentName componentName) { - if (CAN_USE_MEDIA_BUTTONS) { - audioManager.unregisterMediaButtonEventReceiver(componentName); - } else { - Log.e(TAG, MEDIA_BUTTON_DEPRECATED_ERROR); - } - } - /*////////////////////////////////////////////////////////////////////////// // AudioFocus //////////////////////////////////////////////////////////////////////////*/ diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java b/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java new file mode 100644 index 000000000..8405e45fd --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java @@ -0,0 +1,38 @@ +package org.schabi.newpipe.player.helper; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.v4.media.session.MediaSessionCompat; + +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; + +import org.schabi.newpipe.player.mediasession.DummyPlaybackPreparer; +import org.schabi.newpipe.player.mediasession.MediaSessionCallback; +import org.schabi.newpipe.player.mediasession.PlayQueueNavigator; +import org.schabi.newpipe.player.mediasession.PlayQueuePlaybackController; + +public class MediaSessionManager { + private static final String TAG = "MediaSessionManager"; + + private final MediaSessionCompat mediaSession; + private final MediaSessionConnector sessionConnector; + + public MediaSessionManager(@NonNull final Context context, + @NonNull final Player player, + @NonNull final MediaSessionCallback callback) { + this.mediaSession = new MediaSessionCompat(context, TAG); + this.sessionConnector = new MediaSessionConnector(mediaSession, + new PlayQueuePlaybackController(callback)); + this.sessionConnector.setQueueNavigator(new PlayQueueNavigator(mediaSession, callback)); + this.sessionConnector.setPlayer(player, new DummyPlaybackPreparer()); + } + + public MediaSessionCompat getMediaSession() { + return mediaSession; + } + + public MediaSessionConnector getSessionConnector() { + return sessionConnector; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/DummyPlaybackPreparer.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/DummyPlaybackPreparer.java new file mode 100644 index 000000000..431a90d8a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/mediasession/DummyPlaybackPreparer.java @@ -0,0 +1,45 @@ +package org.schabi.newpipe.player.mediasession; + +import android.net.Uri; +import android.os.Bundle; +import android.os.ResultReceiver; + +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; + +public class DummyPlaybackPreparer implements MediaSessionConnector.PlaybackPreparer { + @Override + public long getSupportedPrepareActions() { + return 0; + } + + @Override + public void onPrepare() { + + } + + @Override + public void onPrepareFromMediaId(String mediaId, Bundle extras) { + + } + + @Override + public void onPrepareFromSearch(String query, Bundle extras) { + + } + + @Override + public void onPrepareFromUri(Uri uri, Bundle extras) { + + } + + @Override + public String[] getCommands() { + return new String[0]; + } + + @Override + public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) { + + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionCallback.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionCallback.java new file mode 100644 index 000000000..a1a57a87d --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionCallback.java @@ -0,0 +1,17 @@ +package org.schabi.newpipe.player.mediasession; + +import android.support.v4.media.MediaDescriptionCompat; + +public interface MediaSessionCallback { + void onSkipToPrevious(); + void onSkipToNext(); + void onSkipToIndex(final int index); + + int getCurrentPlayingIndex(); + int getQueueSize(); + MediaDescriptionCompat getQueueMetadata(final int index); + + void onPlay(); + void onPause(); + void onSetShuffle(final boolean isShuffled); +} diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java new file mode 100644 index 000000000..429c26fd9 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java @@ -0,0 +1,111 @@ +package org.schabi.newpipe.player.mediasession; + +import android.os.Bundle; +import android.os.ResultReceiver; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.media.session.MediaSessionCompat; + +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; +import com.google.android.exoplayer2.util.Util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_NEXT; +import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS; +import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM; + + +public class PlayQueueNavigator implements MediaSessionConnector.QueueNavigator { + public static final int DEFAULT_MAX_QUEUE_SIZE = 10; + + private final MediaSessionCompat mediaSession; + private final MediaSessionCallback callback; + private final int maxQueueSize; + + private long activeQueueItemId; + + public PlayQueueNavigator(@NonNull final MediaSessionCompat mediaSession, + @NonNull final MediaSessionCallback callback) { + this.mediaSession = mediaSession; + this.callback = callback; + this.maxQueueSize = DEFAULT_MAX_QUEUE_SIZE; + + this.activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID; + } + + @Override + public long getSupportedQueueNavigatorActions(@Nullable Player player) { + return ACTION_SKIP_TO_NEXT | ACTION_SKIP_TO_PREVIOUS | ACTION_SKIP_TO_QUEUE_ITEM; + } + + @Override + public void onTimelineChanged(Player player) { + publishFloatingQueueWindow(); + } + + @Override + public void onCurrentWindowIndexChanged(Player player) { + if (activeQueueItemId == MediaSessionCompat.QueueItem.UNKNOWN_ID + || player.getCurrentTimeline().getWindowCount() > maxQueueSize) { + publishFloatingQueueWindow(); + } else if (!player.getCurrentTimeline().isEmpty()) { + activeQueueItemId = player.getCurrentWindowIndex(); + } + } + + @Override + public long getActiveQueueItemId(@Nullable Player player) { + return callback.getCurrentPlayingIndex(); + } + + @Override + public void onSkipToPrevious(Player player) { + callback.onSkipToPrevious(); + } + + @Override + public void onSkipToQueueItem(Player player, long id) { + callback.onSkipToIndex((int) id); + } + + @Override + public void onSkipToNext(Player player) { + callback.onSkipToNext(); + } + + private void publishFloatingQueueWindow() { + if (callback.getQueueSize() == 0) { + mediaSession.setQueue(Collections.emptyList()); + activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID; + return; + } + + // Yes this is almost a copypasta, got a problem with that? =\ + int windowCount = callback.getQueueSize(); + int currentWindowIndex = callback.getCurrentPlayingIndex(); + int queueSize = Math.min(maxQueueSize, windowCount); + int startIndex = Util.constrainValue(currentWindowIndex - ((queueSize - 1) / 2), 0, + windowCount - queueSize); + + List queue = new ArrayList<>(); + for (int i = startIndex; i < startIndex + queueSize; i++) { + queue.add(new MediaSessionCompat.QueueItem(callback.getQueueMetadata(i), i)); + } + mediaSession.setQueue(queue); + activeQueueItemId = currentWindowIndex; + } + + @Override + public String[] getCommands() { + return new String[0]; + } + + @Override + public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) { + + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueuePlaybackController.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueuePlaybackController.java new file mode 100644 index 000000000..2aa41bd63 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueuePlaybackController.java @@ -0,0 +1,31 @@ +package org.schabi.newpipe.player.mediasession; + +import android.support.v4.media.session.PlaybackStateCompat; + +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.ext.mediasession.DefaultPlaybackController; + +public class PlayQueuePlaybackController extends DefaultPlaybackController { + private final MediaSessionCallback callback; + + public PlayQueuePlaybackController(final MediaSessionCallback callback) { + super(); + this.callback = callback; + } + + @Override + public void onPlay(Player player) { + callback.onPlay(); + } + + @Override + public void onPause(Player player) { + callback.onPause(); + } + + @Override + public void onSetShuffleMode(Player player, int shuffleMode) { + callback.onSetShuffle(shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL + || shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java b/app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java new file mode 100644 index 000000000..07504542c --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java @@ -0,0 +1,77 @@ +package org.schabi.newpipe.player.playback; + +import android.net.Uri; +import android.support.v4.media.MediaDescriptionCompat; + +import org.schabi.newpipe.player.BasePlayer; +import org.schabi.newpipe.player.mediasession.MediaSessionCallback; +import org.schabi.newpipe.playlist.PlayQueueItem; + +public class BasePlayerMediaSession implements MediaSessionCallback { + private BasePlayer player; + + public BasePlayerMediaSession(final BasePlayer player) { + this.player = player; + } + + @Override + public void onSkipToPrevious() { + player.onPlayPrevious(); + } + + @Override + public void onSkipToNext() { + player.onPlayNext(); + } + + @Override + public void onSkipToIndex(int index) { + if (player.getPlayQueue() == null) return; + player.onSelected(player.getPlayQueue().getItem(index)); + } + + @Override + public int getCurrentPlayingIndex() { + if (player.getPlayQueue() == null) return -1; + return player.getPlayQueue().getIndex(); + } + + @Override + public int getQueueSize() { + if (player.getPlayQueue() == null) return -1; + return player.getPlayQueue().size(); + } + + @Override + public MediaDescriptionCompat getQueueMetadata(int index) { + if (player.getPlayQueue() == null || player.getPlayQueue().getItem(index) == null) { + return null; + } + + final PlayQueueItem item = player.getPlayQueue().getItem(index); + MediaDescriptionCompat.Builder descriptionBuilder = new MediaDescriptionCompat.Builder() + .setMediaId(String.valueOf(index)) + .setTitle(item.getTitle()) + .setSubtitle(item.getUploader()); + + final Uri thumbnailUri = Uri.parse(item.getThumbnailUrl()); + if (thumbnailUri != null) descriptionBuilder.setIconUri(thumbnailUri); + + return descriptionBuilder.build(); + } + + @Override + public void onPlay() { + if (!player.isPlaying()) player.onVideoPlayPause(); + } + + @Override + public void onPause() { + if (player.isPlaying()) player.onVideoPlayPause(); + } + + @Override + public void onSetShuffle(boolean isShuffled) { + player.onShuffleModeEnabledChanged(isShuffled); + } +}