diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8ae994de7..02b3265a0 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -38,6 +38,11 @@
android:name=".player.BackgroundPlayer"
android:exported="false"/>
+
+
diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java
index 3483f5eb0..887759640 100644
--- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java
+++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java
@@ -27,6 +27,7 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Bitmap;
import android.net.wifi.WifiManager;
+import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
import android.os.PowerManager;
@@ -36,9 +37,9 @@ import android.support.v4.app.NotificationCompat;
import android.util.Log;
import android.widget.RemoteViews;
+import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.source.MediaSource;
-import com.google.android.exoplayer2.source.MergingMediaSource;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.MainActivity;
@@ -51,9 +52,6 @@ import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.ThemeHelper;
-import java.util.ArrayList;
-import java.util.List;
-
/**
* Base players joining the common properties
@@ -78,13 +76,30 @@ public final class BackgroundPlayer extends Service {
private PowerManager.WakeLock wakeLock;
private WifiManager.WifiLock wifiLock;
+ /*//////////////////////////////////////////////////////////////////////////
+ // Service-Activity Binder
+ //////////////////////////////////////////////////////////////////////////*/
+
+ public interface PlayerEventListener {
+ void onPlaybackUpdate(int state, int repeatMode, PlaybackParameters parameters);
+ void onProgressUpdate(int currentProgress, int duration, int bufferPercent);
+ void onMetadataUpdate(StreamInfo info);
+ void onServiceStopped();
+ }
+
+ private PlayerEventListener activityListener;
+ private IBinder mBinder;
+
+ class LocalBinder extends Binder {
+ BasePlayerImpl getBackgroundPlayerInstance() {
+ return BackgroundPlayer.this.basePlayerImpl;
+ }
+ }
+
/*//////////////////////////////////////////////////////////////////////////
// Notification
//////////////////////////////////////////////////////////////////////////*/
private static final int NOTIFICATION_ID = 123789;
-
- private boolean shouldUpdateNotification;
-
private NotificationManager notificationManager;
private NotificationCompat.Builder notBuilder;
private RemoteViews notRemoteView;
@@ -105,6 +120,8 @@ public final class BackgroundPlayer extends Service {
ThemeHelper.setTheme(this);
basePlayerImpl = new BasePlayerImpl(this);
basePlayerImpl.setup();
+
+ mBinder = new LocalBinder();
}
@Override
@@ -124,13 +141,19 @@ public final class BackgroundPlayer extends Service {
@Override
public IBinder onBind(Intent intent) {
- return null;
+ return mBinder;
}
/*//////////////////////////////////////////////////////////////////////////
// Actions
//////////////////////////////////////////////////////////////////////////*/
+ public void openControl(final Context context) {
+ final Intent intent = new Intent(context, BackgroundPlayerActivity.class);
+ context.startActivity(intent);
+ context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
+ }
+
public void onOpenDetail(Context context, String videoUrl, String videoTitle) {
if (DEBUG) Log.d(TAG, "onOpenDetail() called with: context = [" + context + "], videoUrl = [" + videoUrl + "]");
Intent i = new Intent(context, MainActivity.class);
@@ -144,7 +167,11 @@ public final class BackgroundPlayer extends Service {
}
private void onClose() {
- if (basePlayerImpl != null) basePlayerImpl.destroyPlayer();
+ if (basePlayerImpl != null) {
+ basePlayerImpl.stopActivityBinding();
+ basePlayerImpl.destroyPlayer();
+ }
+
stopForeground(true);
releaseWifiAndCpu();
stopSelf();
@@ -152,8 +179,6 @@ public final class BackgroundPlayer extends Service {
private void onScreenOnOff(boolean on) {
if (DEBUG) Log.d(TAG, "onScreenOnOff() called with: on = [" + on + "]");
- shouldUpdateNotification = on;
-
if (on) {
if (basePlayerImpl.isPlaying() && !basePlayerImpl.isProgressLoopRunning()) {
basePlayerImpl.startProgressLoop();
@@ -168,9 +193,7 @@ public final class BackgroundPlayer extends Service {
//////////////////////////////////////////////////////////////////////////*/
private void resetNotification() {
- if (shouldUpdateNotification) {
- notBuilder = createNotification();
- }
+ notBuilder = createNotification();
}
private NotificationCompat.Builder createNotification() {
@@ -211,7 +234,7 @@ public final class BackgroundPlayer extends Service {
break;
case Player.REPEAT_MODE_ONE:
// todo change image
- remoteViews.setInt(R.id.notificationRepeat, setAlphaMethodName, 255);
+ remoteViews.setInt(R.id.notificationRepeat, setAlphaMethodName, 168);
break;
case Player.REPEAT_MODE_ALL:
remoteViews.setInt(R.id.notificationRepeat, setAlphaMethodName, 255);
@@ -227,7 +250,7 @@ public final class BackgroundPlayer extends Service {
*/
private synchronized void updateNotification(int drawableId) {
//if (DEBUG) Log.d(TAG, "updateNotification() called with: drawableId = [" + drawableId + "]");
- if (notBuilder == null || !shouldUpdateNotification) return;
+ if (notBuilder == null) return;
if (drawableId != -1) {
if (notRemoteView != null) notRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId);
if (bigNotRemoteView != null) bigNotRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId);
@@ -270,7 +293,7 @@ public final class BackgroundPlayer extends Service {
//////////////////////////////////////////////////////////////////////////
- private class BasePlayerImpl extends BasePlayer {
+ protected class BasePlayerImpl extends BasePlayer {
BasePlayerImpl(Context context) {
super(context);
@@ -280,8 +303,7 @@ public final class BackgroundPlayer extends Service {
public void handleIntent(Intent intent) {
super.handleIntent(intent);
- shouldUpdateNotification = true;
- notBuilder = createNotification();
+ resetNotification();
startForeground(NOTIFICATION_ID, notBuilder.build());
if (bigNotRemoteView != null) bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 0, false);
@@ -329,23 +351,6 @@ public final class BackgroundPlayer extends Service {
@Override
public void onRepeatClicked() {
super.onRepeatClicked();
-
- int opacity = 255;
- switch (simpleExoPlayer.getRepeatMode()) {
- case Player.REPEAT_MODE_OFF:
- opacity = 77;
- break;
- case Player.REPEAT_MODE_ONE:
- // todo change image
- opacity = 168;
- break;
- case Player.REPEAT_MODE_ALL:
- opacity = 255;
- break;
- }
- if (notRemoteView != null) notRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, opacity);
- if (bigNotRemoteView != null) bigNotRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, opacity);
- updateNotification(-1);
}
@Override
@@ -368,6 +373,7 @@ public final class BackgroundPlayer extends Service {
}
updateNotification(-1);
+ updateProgress(currentProgress, duration, bufferPercent);
}
@Override
@@ -386,16 +392,6 @@ public final class BackgroundPlayer extends Service {
triggerProgressUpdate();
}
- @Override
- public void onLoadingChanged(boolean isLoading) {
- // Disable default behavior
- }
-
- @Override
- public void onRepeatModeChanged(int i) {
-
- }
-
@Override
public void destroy() {
super.destroy();
@@ -408,6 +404,42 @@ public final class BackgroundPlayer extends Service {
exception.printStackTrace();
}
+ /*//////////////////////////////////////////////////////////////////////////
+ // ExoPlayer Listener
+ //////////////////////////////////////////////////////////////////////////*/
+
+ @Override
+ public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
+ super.onPlaybackParametersChanged(playbackParameters);
+ updatePlayback();
+ }
+
+ @Override
+ public void onLoadingChanged(boolean isLoading) {
+ // Disable default behavior
+ }
+
+ @Override
+ public void onRepeatModeChanged(int i) {
+ int opacity = 255;
+ switch (simpleExoPlayer.getRepeatMode()) {
+ case Player.REPEAT_MODE_OFF:
+ opacity = 77;
+ break;
+ case Player.REPEAT_MODE_ONE:
+ // todo change image
+ opacity = 168;
+ break;
+ case Player.REPEAT_MODE_ALL:
+ opacity = 255;
+ break;
+ }
+ if (notRemoteView != null) notRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, opacity);
+ if (bigNotRemoteView != null) bigNotRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, opacity);
+ updateNotification(-1);
+ updatePlayback();
+ }
+
/*//////////////////////////////////////////////////////////////////////////
// Playback Listener
//////////////////////////////////////////////////////////////////////////*/
@@ -422,11 +454,14 @@ public final class BackgroundPlayer extends Service {
bigNotRemoteView.setTextViewText(R.id.notificationSongName, getVideoTitle());
bigNotRemoteView.setTextViewText(R.id.notificationArtist, getUploaderName());
updateNotification(-1);
+ updateMetadata();
}
@Override
public MediaSource sourceOf(final StreamInfo info) {
final int index = ListHelper.getDefaultAudioFormat(context, info.audio_streams);
+ if (index < 0) return null;
+
final AudioStream audio = info.audio_streams.get(index);
return buildMediaSource(audio.url, MediaFormat.getSuffixById(audio.format));
}
@@ -435,6 +470,43 @@ public final class BackgroundPlayer extends Service {
public void shutdown() {
super.shutdown();
stopSelf();
+ stopActivityBinding();
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Activity Event Listener
+ //////////////////////////////////////////////////////////////////////////*/
+
+ public void setActivityListener(PlayerEventListener listener) {
+ activityListener = listener;
+ updateMetadata();
+ updatePlayback();
+ triggerProgressUpdate();
+ }
+
+ private void updateMetadata() {
+ if (activityListener != null && currentInfo != null) {
+ activityListener.onMetadataUpdate(currentInfo);
+ }
+ }
+
+ private void updatePlayback() {
+ if (activityListener != null) {
+ activityListener.onPlaybackUpdate(currentState, simpleExoPlayer.getRepeatMode(), simpleExoPlayer.getPlaybackParameters());
+ }
+ }
+
+ private void updateProgress(int currentProgress, int duration, int bufferPercent) {
+ if (activityListener != null) {
+ activityListener.onProgressUpdate(currentProgress, duration, bufferPercent);
+ }
+ }
+
+ private void stopActivityBinding() {
+ if (activityListener != null) {
+ activityListener.onServiceStopped();
+ activityListener = null;
+ }
}
/*//////////////////////////////////////////////////////////////////////////
@@ -469,7 +541,7 @@ public final class BackgroundPlayer extends Service {
onVideoPlayPause();
break;
case ACTION_OPEN_DETAIL:
- onOpenDetail(BackgroundPlayer.this, getVideoUrl(), getVideoTitle());
+ openControl(BackgroundPlayer.this);
break;
case ACTION_REPEAT:
onRepeatClicked();
@@ -493,6 +565,12 @@ public final class BackgroundPlayer extends Service {
// States
//////////////////////////////////////////////////////////////////////////*/
+ @Override
+ public void changeState(int state) {
+ super.changeState(state);
+ updatePlayback();
+ }
+
@Override
public void onBlocked() {
super.onBlocked();
diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java
new file mode 100644
index 000000000..127594956
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java
@@ -0,0 +1,305 @@
+package org.schabi.newpipe.player;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.Toolbar;
+import android.util.Log;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+import com.google.android.exoplayer2.PlaybackParameters;
+import com.google.android.exoplayer2.Player;
+
+import org.schabi.newpipe.R;
+import org.schabi.newpipe.extractor.stream.StreamInfo;
+import org.schabi.newpipe.playlist.PlayQueueItem;
+import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
+import org.schabi.newpipe.settings.SettingsActivity;
+import org.schabi.newpipe.util.Localization;
+import org.schabi.newpipe.util.ThemeHelper;
+
+public class BackgroundPlayerActivity extends AppCompatActivity
+ implements BackgroundPlayer.PlayerEventListener, SeekBar.OnSeekBarChangeListener, View.OnClickListener {
+
+ private static final String TAG = "BGPlayerActivity";
+
+ private boolean isServiceBound;
+ private ServiceConnection serviceConnection;
+
+ private BackgroundPlayer.BasePlayerImpl player;
+
+ private boolean isSeeking;
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Views
+ ////////////////////////////////////////////////////////////////////////////
+
+ private View rootView;
+
+ private RecyclerView itemsList;
+
+ private TextView metadataTitle;
+ private TextView metadataArtist;
+
+ private SeekBar progressSeekBar;
+ private TextView progressCurrentTime;
+ private TextView progressEndTime;
+
+ private ImageButton repeatButton;
+ private ImageButton backwardButton;
+ private ImageButton playPauseButton;
+ private ImageButton forwardButton;
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Activity Lifecycle
+ ////////////////////////////////////////////////////////////////////////////
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ ThemeHelper.setTheme(this);
+ setContentView(R.layout.activity_background_player);
+ rootView = findViewById(R.id.main_content);
+
+ final Toolbar toolbar = rootView.findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ getSupportActionBar().setTitle(R.string.title_activity_background_player);
+
+ serviceConnection = backgroundPlayerConnection();
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ final Intent mIntent = new Intent(this, BackgroundPlayer.class);
+ final boolean success = bindService(mIntent, serviceConnection, BIND_AUTO_CREATE);
+ if (!success) unbindService(serviceConnection);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ finish();
+ return true;
+ case R.id.action_settings:
+ Intent intent = new Intent(this, SettingsActivity.class);
+ startActivity(intent);
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ if(isServiceBound) {
+ unbindService(serviceConnection);
+ isServiceBound = false;
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Service Connection
+ ////////////////////////////////////////////////////////////////////////////
+
+ private ServiceConnection backgroundPlayerConnection() {
+ return new ServiceConnection() {
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ Log.d(TAG, "Background player service is disconnected");
+ isServiceBound = false;
+ player = null;
+ finish();
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ Log.d(TAG, "Background player service is connected");
+ final BackgroundPlayer.LocalBinder mLocalBinder = (BackgroundPlayer.LocalBinder) service;
+ player = mLocalBinder.getBackgroundPlayerInstance();
+ if (player == null) {
+ finish();
+ } else {
+ isServiceBound = true;
+ buildComponents();
+
+ player.setActivityListener(BackgroundPlayerActivity.this);
+ }
+ }
+ };
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Component Building
+ ////////////////////////////////////////////////////////////////////////////
+
+ private void buildComponents() {
+ buildQueue();
+ buildMetadata();
+ buildSeekBar();
+ buildControls();
+ }
+
+ private void buildQueue() {
+ itemsList = findViewById(R.id.play_queue);
+ itemsList.setLayoutManager(new LinearLayoutManager(this));
+ itemsList.setAdapter(player.playQueueAdapter);
+ itemsList.setClickable(true);
+
+ player.playQueueAdapter.setSelectedListener(new PlayQueueItemBuilder.OnSelectedListener() {
+ @Override
+ public void selected(PlayQueueItem item) {
+ final int index = player.playQueue.indexOf(item);
+ if (index != -1) player.playQueue.setIndex(index);
+ }
+ });
+ }
+
+ private void buildMetadata() {
+ metadataTitle = rootView.findViewById(R.id.song_name);
+ metadataArtist = rootView.findViewById(R.id.artist_name);
+ }
+
+ private void buildSeekBar() {
+ progressCurrentTime = rootView.findViewById(R.id.current_time);
+ progressSeekBar = rootView.findViewById(R.id.seek_bar);
+ progressEndTime = rootView.findViewById(R.id.end_time);
+
+ progressSeekBar.setOnSeekBarChangeListener(this);
+ }
+
+ private void buildControls() {
+ repeatButton = rootView.findViewById(R.id.control_repeat);
+ backwardButton = rootView.findViewById(R.id.control_backward);
+ playPauseButton = rootView.findViewById(R.id.control_play_pause);
+ forwardButton = rootView.findViewById(R.id.control_forward);
+
+ repeatButton.setOnClickListener(this);
+ backwardButton.setOnClickListener(this);
+ playPauseButton.setOnClickListener(this);
+ forwardButton.setOnClickListener(this);
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Component On-Click Listener
+ ////////////////////////////////////////////////////////////////////////////
+
+ @Override
+ public void onClick(View view) {
+ if (view.getId() == repeatButton.getId()) {
+ player.onRepeatClicked();
+ } else if (view.getId() == backwardButton.getId()) {
+ player.onPlayPrevious();
+ } else if (view.getId() == playPauseButton.getId()) {
+ player.onVideoPlayPause();
+ } else if (view.getId() == forwardButton.getId()) {
+ player.onPlayNext();
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Seekbar Listener
+ ////////////////////////////////////////////////////////////////////////////
+
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ if (fromUser) progressCurrentTime.setText(Localization.getDurationString(progress / 1000));
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ isSeeking = true;
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ player.simpleExoPlayer.seekTo(seekBar.getProgress());
+ isSeeking = false;
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Binding Service Listener
+ ////////////////////////////////////////////////////////////////////////////
+
+ @Override
+ public void onPlaybackUpdate(int state, int repeatMode, PlaybackParameters parameters) {
+ switch (state) {
+ case BasePlayer.STATE_PAUSED:
+ playPauseButton.setImageResource(R.drawable.ic_play_arrow_white);
+ break;
+ case BasePlayer.STATE_PLAYING:
+ playPauseButton.setImageResource(R.drawable.ic_pause_white);
+ break;
+ case BasePlayer.STATE_COMPLETED:
+ playPauseButton.setImageResource(R.drawable.ic_replay_white);
+ break;
+ default:
+ break;
+ }
+
+ int alpha = 255;
+ switch (repeatMode) {
+ case Player.REPEAT_MODE_OFF:
+ alpha = 77;
+ break;
+ case Player.REPEAT_MODE_ONE:
+ // todo change image
+ alpha = 168;
+ break;
+ case Player.REPEAT_MODE_ALL:
+ alpha = 255;
+ break;
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ repeatButton.setImageAlpha(alpha);
+ } else {
+ repeatButton.setAlpha(alpha);
+ }
+
+ if (parameters != null) {
+ final float speed = parameters.speed;
+ final float pitch = parameters.pitch;
+ }
+ }
+
+ @Override
+ public void onProgressUpdate(int currentProgress, int duration, int bufferPercent) {
+ // Set buffer progress
+ progressSeekBar.setSecondaryProgress((int) (progressSeekBar.getMax() * ((float) bufferPercent / 100)));
+
+ // Set Duration
+ progressSeekBar.setMax(duration);
+ progressEndTime.setText(Localization.getDurationString(duration / 1000));
+
+ // Set current time if not seeking
+ if (!isSeeking) {
+ progressSeekBar.setProgress(currentProgress);
+ progressCurrentTime.setText(Localization.getDurationString(currentProgress / 1000));
+ }
+ }
+
+ @Override
+ public void onMetadataUpdate(StreamInfo info) {
+ if (info != null) {
+ metadataTitle.setText(info.name);
+ metadataArtist.setText(info.uploader_name);
+ }
+ }
+
+ @Override
+ public void onServiceStopped() {
+ finish();
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java
index f3450f59f..7a014b3be 100644
--- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java
+++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java
@@ -27,7 +27,6 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
-import android.content.res.Resources;
import android.graphics.Bitmap;
import android.media.AudioManager;
import android.media.audiofx.AudioEffect;
@@ -35,7 +34,6 @@ import android.net.Uri;
import android.preference.PreferenceManager;
import android.support.annotation.Nullable;
import android.text.TextUtils;
-import android.util.DisplayMetrics;
import android.util.Log;
import android.view.View;
@@ -72,28 +70,21 @@ import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvicto
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
import com.google.android.exoplayer2.util.Util;
import com.nostra13.universalimageloader.core.ImageLoader;
-import com.nostra13.universalimageloader.core.assist.ImageSize;
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
import org.schabi.newpipe.Downloader;
import org.schabi.newpipe.R;
-import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfo;
-import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.player.playback.MediaSourceManager;
import org.schabi.newpipe.player.playback.PlaybackListener;
-import org.schabi.newpipe.playlist.ExternalPlayQueue;
import org.schabi.newpipe.playlist.PlayQueue;
-import org.schabi.newpipe.playlist.PlayQueueItem;
-import org.schabi.newpipe.playlist.SinglePlayQueue;
+import org.schabi.newpipe.playlist.PlayQueueAdapter;
import java.io.File;
import java.io.Serializable;
import java.text.DecimalFormat;
import java.text.NumberFormat;
-import java.util.ArrayList;
import java.util.Formatter;
-import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
@@ -124,6 +115,8 @@ public abstract class BasePlayer implements Player.EventListener,
protected BroadcastReceiver broadcastReceiver;
protected IntentFilter intentFilter;
+ protected PlayQueueAdapter playQueueAdapter;
+
/*//////////////////////////////////////////////////////////////////////////
// Intent
//////////////////////////////////////////////////////////////////////////*/
@@ -285,6 +278,9 @@ public abstract class BasePlayer implements Player.EventListener,
playQueue = queue;
playQueue.init();
playbackManager = new MediaSourceManager(this, playQueue);
+
+ if (playQueueAdapter != null) playQueueAdapter.dispose();
+ playQueueAdapter = new PlayQueueAdapter(playQueue);
}
public void initThumbnail(final String url) {
@@ -816,6 +812,7 @@ public abstract class BasePlayer implements Player.EventListener,
private final Formatter formatter = new Formatter(stringBuilder, Locale.getDefault());
private final NumberFormat speedFormatter = new DecimalFormat("0.##x");
+ // todo: merge this into Localization
public String getTimeString(int milliSeconds) {
long seconds = (milliSeconds % 60000L) / 1000L;
long minutes = (milliSeconds % 3600000L) / 60000L;
diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java
index e0ef71e3f..482503cb6 100644
--- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java
+++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java
@@ -64,13 +64,9 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.VideoStream;
-import org.schabi.newpipe.playlist.PlayQueue;
-import org.schabi.newpipe.playlist.PlayQueueItem;
-import org.schabi.newpipe.playlist.SinglePlayQueue;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.ListHelper;
-import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
@@ -111,6 +107,7 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
private List trackGroupInfos;
private int videoRendererIndex = -1;
private TrackGroupArray videoTrackGroups;
+ private TrackGroup selectedVideoTrackGroup;
private boolean startedFromNewPipe = true;
protected boolean wasPlaying = false;
@@ -211,7 +208,7 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
public void initPlayer() {
super.initPlayer();
simpleExoPlayer.setVideoSurfaceView(surfaceView);
- simpleExoPlayer.setVideoListener(this);
+ simpleExoPlayer.addVideoListener(this);
trackSelector.setTunnelingAudioSessionId(C.generateAudioSessionIdV21(context));
}
@@ -229,6 +226,79 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
);
}
+ /*//////////////////////////////////////////////////////////////////////////
+ // UI Builders
+ //////////////////////////////////////////////////////////////////////////*/
+
+ private final class TrackGroupInfo {
+ final int track;
+ final int group;
+ final Format format;
+
+ TrackGroupInfo(final int track, final int group, final Format format) {
+ this.track = track;
+ this.group = group;
+ this.format = format;
+ }
+ }
+
+ private void buildQualityMenu() {
+ if (qualityPopupMenu == null || videoTrackGroups == null || selectedVideoTrackGroup == null || videoTrackGroups.length != availableStreams.size()) return;
+
+ qualityPopupMenu.getMenu().removeGroup(qualityPopupMenuGroupId);
+ trackGroupInfos = new ArrayList<>();
+ int acc = 0;
+
+ // Each group represent a source in sorted order of how the media source was built
+ for (int groupIndex = 0; groupIndex < videoTrackGroups.length; groupIndex++) {
+ final TrackGroup group = videoTrackGroups.get(groupIndex);
+ final VideoStream stream = availableStreams.get(groupIndex);
+
+ // For each source, there may be one or multiple tracks depending on the source type
+ for (int trackIndex = 0; trackIndex < group.length; trackIndex++) {
+ final Format format = group.getFormat(trackIndex);
+ final boolean isSetCurrent = selectedVideoTrackGroup.indexOf(format) != -1;
+
+ if (group.length == 1 && videoTrackGroups.length == availableStreams.size()) {
+ // If the source is non-adaptive (extractor source), then we use the resolution contained in the stream
+ if (isSetCurrent) qualityTextView.setText(stream.resolution);
+
+ final String menuItem = MediaFormat.getNameById(stream.format) + " " +
+ stream.resolution + " (" + format.width + "x" + format.height + ")";
+ qualityPopupMenu.getMenu().add(qualityPopupMenuGroupId, acc, Menu.NONE, menuItem);
+ } else {
+ // Otherwise, we have an adaptive source, which contains multiple formats and
+ // thus have no inherent quality format
+ if (isSetCurrent) qualityTextView.setText(resolutionStringOf(format));
+
+ final MediaFormat mediaFormat = MediaFormat.getFromMimeType(format.sampleMimeType);
+ final String mediaName = mediaFormat == null ? format.sampleMimeType : mediaFormat.name;
+
+ final String menuItem = mediaName + " " + format.width + "x" + format.height;
+ qualityPopupMenu.getMenu().add(qualityPopupMenuGroupId, acc, Menu.NONE, menuItem);
+ }
+
+ trackGroupInfos.add(new TrackGroupInfo(trackIndex, groupIndex, format));
+ acc++;
+ }
+ }
+
+ qualityPopupMenu.setOnMenuItemClickListener(this);
+ qualityPopupMenu.setOnDismissListener(this);
+ }
+
+ private void buildPlaybackSpeedMenu() {
+ if (playbackSpeedPopupMenu == null) return;
+
+ playbackSpeedPopupMenu.getMenu().removeGroup(playbackSpeedPopupMenuGroupId);
+ for (int i = 0; i < PLAYBACK_SPEEDS.length; i++) {
+ playbackSpeedPopupMenu.getMenu().add(playbackSpeedPopupMenuGroupId, i, Menu.NONE, formatSpeed(PLAYBACK_SPEEDS[i]));
+ }
+ playbackSpeed.setText(formatSpeed(getPlaybackSpeed()));
+ playbackSpeedPopupMenu.setOnMenuItemClickListener(this);
+ playbackSpeedPopupMenu.setOnDismissListener(this);
+ }
+
/*//////////////////////////////////////////////////////////////////////////
// Playback Listener
//////////////////////////////////////////////////////////////////////////*/
@@ -243,8 +313,8 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
selectedStreamIndex = ListHelper.getDefaultResolutionIndex(context, videos);
}
- playbackSpeedPopupMenu.getMenu().removeGroup(playbackSpeedPopupMenuGroupId);
- buildPlaybackSpeedMenu(playbackSpeedPopupMenu);
+ buildPlaybackSpeedMenu();
+ buildQualityMenu();
}
public MediaSource sourceOf(final StreamInfo info) {
@@ -259,15 +329,6 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
return new MergingMediaSource(sources.toArray(new MediaSource[sources.size()]));
}
- private void buildPlaybackSpeedMenu(PopupMenu popupMenu) {
- for (int i = 0; i < PLAYBACK_SPEEDS.length; i++) {
- popupMenu.getMenu().add(playbackSpeedPopupMenuGroupId, i, Menu.NONE, formatSpeed(PLAYBACK_SPEEDS[i]));
- }
- playbackSpeed.setText(formatSpeed(getPlaybackSpeed()));
- popupMenu.setOnMenuItemClickListener(this);
- popupMenu.setOnDismissListener(this);
- }
-
/*//////////////////////////////////////////////////////////////////////////
// States Implementation
//////////////////////////////////////////////////////////////////////////*/
@@ -343,22 +404,6 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
// ExoPlayer Video Listener
//////////////////////////////////////////////////////////////////////////*/
- private class TrackGroupInfo {
- final int track;
- final int group;
- final String label;
- final String resolution;
- final Format format;
-
- TrackGroupInfo(final int track, final int group, final String label, final String resolution, final Format format) {
- this.track = track;
- this.group = group;
- this.label = label;
- this.resolution = resolution;
- this.format = format;
- }
- }
-
@Override
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
super.onTracksChanged(trackGroups, trackSelections);
@@ -376,52 +421,9 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
}
}
videoTrackGroups = trackSelector.getCurrentMappedTrackInfo().getTrackGroups(videoRendererIndex);
- final TrackGroup selectedTrackGroup = trackSelections.get(videoRendererIndex).getTrackGroup();
+ selectedVideoTrackGroup = trackSelections.get(videoRendererIndex).getTrackGroup();
- qualityPopupMenu.getMenu().removeGroup(qualityPopupMenuGroupId);
- buildQualityMenu(qualityPopupMenu, videoTrackGroups, selectedTrackGroup);
- }
-
- private void buildQualityMenu(PopupMenu popupMenu, TrackGroupArray videoTrackGroups, TrackGroup selectedTrackGroup) {
- trackGroupInfos = new ArrayList<>();
- int acc = 0;
-
- // Each group represent a source in sorted order of how the media source was built
- for (int groupIndex = 0; groupIndex < videoTrackGroups.length; groupIndex++) {
- final TrackGroup group = videoTrackGroups.get(groupIndex);
- final VideoStream stream = availableStreams.get(groupIndex);
-
- // For each source, there may be one or multiple tracks depending on the source type
- for (int trackIndex = 0; trackIndex < group.length; trackIndex++) {
- final Format format = group.getFormat(trackIndex);
- final boolean isSetCurrent = selectedTrackGroup.indexOf(format) != -1;
-
- if (group.length == 1 && videoTrackGroups.length == availableStreams.size()) {
- // If the source is non-adaptive (extractor source), then we use the resolution contained in the stream
- if (isSetCurrent) qualityTextView.setText(stream.resolution);
-
- final String menuItem = MediaFormat.getNameById(stream.format) + " " +
- stream.resolution + " (" + format.width + "x" + format.height + ")";
- popupMenu.getMenu().add(qualityPopupMenuGroupId, acc, Menu.NONE, menuItem);
- } else {
- // Otherwise, we have an adaptive source, which contains multiple formats and
- // thus have no inherent quality format
- if (isSetCurrent) qualityTextView.setText(resolutionStringOf(format));
-
- final MediaFormat mediaFormat = MediaFormat.getFromMimeType(format.sampleMimeType);
- final String mediaName = mediaFormat == null ? format.sampleMimeType : mediaFormat.name;
-
- final String menuItem = mediaName + " " + format.width + "x" + format.height;
- popupMenu.getMenu().add(qualityPopupMenuGroupId, acc, Menu.NONE, menuItem);
- }
-
- trackGroupInfos.add(new TrackGroupInfo(trackIndex, groupIndex, MediaFormat.getNameById(stream.format), stream.resolution, format));
- acc++;
- }
- }
-
- popupMenu.setOnMenuItemClickListener(this);
- popupMenu.setOnDismissListener(this);
+ buildQualityMenu();
}
@Override
diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/DeferredMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/DeferredMediaSource.java
index fccd064c3..0d25f5d59 100644
--- a/app/src/main/java/org/schabi/newpipe/player/mediasource/DeferredMediaSource.java
+++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/DeferredMediaSource.java
@@ -13,7 +13,6 @@ import org.schabi.newpipe.playlist.PlayQueueItem;
import java.io.IOException;
-import io.reactivex.SingleObserver;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;
@@ -86,7 +85,7 @@ public final class DeferredMediaSource implements MediaSource {
*
* If loading fails here, an error will be propagated out and result in a
* {@link com.google.android.exoplayer2.ExoPlaybackException}, which is delegated
- * out to the player.
+ * to the player.
* */
public synchronized void load() {
if (state != STATE_PREPARED || stream == null || loader != null) return;
@@ -95,15 +94,23 @@ public final class DeferredMediaSource implements MediaSource {
final Consumer onSuccess = new Consumer() {
@Override
public void accept(StreamInfo streamInfo) throws Exception {
- if (exoPlayer == null && listener == null) {
- error = new Throwable("Stream info loading failed. URL: " + stream.getUrl());
- } else {
- Log.d(TAG, " Loaded: [" + stream.getTitle() + "] with url: " + stream.getUrl());
+ Log.d(TAG, " Loaded: [" + stream.getTitle() + "] with url: " + stream.getUrl());
+ state = STATE_LOADED;
- mediaSource = callback.sourceOf(streamInfo);
- mediaSource.prepareSource(exoPlayer, false, listener);
- state = STATE_LOADED;
+ if (exoPlayer == null || listener == null || streamInfo == null) {
+ error = new Throwable("Stream info loading failed. URL: " + stream.getUrl());
+ return;
}
+
+ mediaSource = callback.sourceOf(streamInfo);
+ if (mediaSource == null) {
+ error = new Throwable("Unable to resolve source from stream info. URL: " + stream.getUrl() +
+ ", audio count: " + streamInfo.audio_streams.size() +
+ ", video count: " + streamInfo.video_only_streams.size() + streamInfo.video_streams.size());
+ return;
+ }
+
+ mediaSource.prepareSource(exoPlayer, false, listener);
}
};
diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueAdapter.java b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueAdapter.java
index e6437a248..edb56474c 100644
--- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueAdapter.java
+++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueAdapter.java
@@ -74,7 +74,7 @@ public class PlayQueueAdapter extends RecyclerView.Adapter 0) {
- holder.itemDurationView.setText(getDurationString(item.getDuration()));
+ holder.itemDurationView.setText(Localization.getDurationString(item.getDuration()));
} else {
holder.itemDurationView.setVisibility(View.GONE);
}
+ ImageLoader.getInstance().displayImage(item.getThumbnailUrl(), holder.itemThumbnailView, IMAGE_OPTIONS);
+
holder.itemRoot.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
- if(onStreamInfoItemSelectedListener != null) {
- onStreamInfoItemSelectedListener.selected(item.getServiceId(), item.getUrl(), item.getTitle());
+ if (onItemClickListener != null) {
+ onItemClickListener.selected(item);
}
}
});
}
-
- public static String getDurationString(long duration) {
- if(duration < 0) {
- duration = 0;
- }
- String output;
- long days = duration / (24 * 60 * 60); /* greater than a day */
- duration %= (24 * 60 * 60);
- long hours = duration / (60 * 60); /* greater than an hour */
- duration %= (60 * 60);
- long minutes = duration / 60;
- long seconds = duration % 60;
-
- //handle days
- if (days > 0) {
- output = String.format(Locale.US, "%d:%02d:%02d:%02d", days, hours, minutes, seconds);
- } else if(hours > 0) {
- output = String.format(Locale.US, "%d:%02d:%02d", hours, minutes, seconds);
- } else {
- output = String.format(Locale.US, "%d:%02d", minutes, seconds);
- }
- return output;
- }
+ private static final DisplayImageOptions IMAGE_OPTIONS =
+ new DisplayImageOptions.Builder()
+ .cacheInMemory(true)
+ .showImageOnFail(R.drawable.dummy_thumbnail)
+ .showImageForEmptyUri(R.drawable.dummy_thumbnail)
+ .showImageOnLoading(R.drawable.dummy_thumbnail)
+ .build();
}
diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemHolder.java b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemHolder.java
index d6bb9665a..747b49512 100644
--- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemHolder.java
+++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemHolder.java
@@ -31,13 +31,17 @@ import org.schabi.newpipe.info_list.holder.InfoItemHolder;
public class PlayQueueItemHolder extends RecyclerView.ViewHolder {
- public final TextView itemVideoTitleView, itemDurationView;
+ public final TextView itemVideoTitleView, itemDurationView, itemAdditionalDetailsView;
+ public final ImageView itemThumbnailView;
+
public final View itemRoot;
public PlayQueueItemHolder(View v) {
super(v);
itemRoot = v.findViewById(R.id.itemRoot);
- itemVideoTitleView = (TextView) v.findViewById(R.id.itemVideoTitleView);
- itemDurationView = (TextView) v.findViewById(R.id.itemDurationView);
+ itemVideoTitleView = v.findViewById(R.id.itemVideoTitleView);
+ itemDurationView = v.findViewById(R.id.itemDurationView);
+ itemAdditionalDetailsView = v.findViewById(R.id.itemAdditionalDetails);
+ itemThumbnailView = v.findViewById(R.id.itemThumbnailView);
}
}
diff --git a/app/src/main/res/color/dark_selector.xml b/app/src/main/res/color/dark_selector.xml
new file mode 100644
index 000000000..fc89e8f82
--- /dev/null
+++ b/app/src/main/res/color/dark_selector.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/color/light_selector.xml b/app/src/main/res/color/light_selector.xml
new file mode 100644
index 000000000..8451b387f
--- /dev/null
+++ b/app/src/main/res/color/light_selector.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_background_player.xml b/app/src/main/res/layout/activity_background_player.xml
new file mode 100644
index 000000000..fdc11acd0
--- /dev/null
+++ b/app/src/main/res/layout/activity_background_player.xml
@@ -0,0 +1,186 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/play_queue_item.xml b/app/src/main/res/layout/play_queue_item.xml
index 52fac4e31..4aee38713 100644
--- a/app/src/main/res/layout/play_queue_item.xml
+++ b/app/src/main/res/layout/play_queue_item.xml
@@ -7,6 +7,7 @@
android:layout_height="48dp"
android:background="?attr/selectableItemBackground"
android:clickable="true"
+ android:focusable="true"
android:padding="6dp">
+ android:textColor="?attr/selector_color"
+ tools:text="Uploader"/>
\ No newline at end of file
diff --git a/app/src/main/res/layout/playlist_item.xml b/app/src/main/res/layout/playlist_item.xml
deleted file mode 100644
index cb734ae15..000000000
--- a/app/src/main/res/layout/playlist_item.xml
+++ /dev/null
@@ -1,51 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
index dd92f916a..047038e50 100644
--- a/app/src/main/res/values/attrs.xml
+++ b/app/src/main/res/values/attrs.xml
@@ -22,6 +22,7 @@
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 506e9f16b..7609a7730 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -292,4 +292,7 @@
Top 50
New & hot
%1$s/%2$s
+
+
+ Background Player
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index fa37f0e5d..c556cce50 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -26,6 +26,7 @@
- @drawable/ic_language_black_24dp
- @drawable/ic_history_black_24dp
+ - @color/light_selector
- @color/light_separator_color
- @color/light_contrast_background_color
- @drawable/toolbar_shadow_light
@@ -60,6 +61,7 @@
- @drawable/ic_language_white_24dp
- @drawable/ic_history_white_24dp
+ - @color/dark_selector
- @color/dark_separator_color
- @color/dark_contrast_background_color
- @drawable/toolbar_shadow_dark