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);
+ }
+}