From a9aee21e58e501f10b2f93de745d736118831d8d Mon Sep 17 00:00:00 2001 From: John Zhen M Date: Mon, 2 Oct 2017 23:38:46 -0700 Subject: [PATCH] - Improved play queue adapter for selection. - Fixed media source resolution on background player for streams without an audio only stream. - Fixed background player not updating when screen turns back on. - Fixed background player notification switching to wrong repeat mode icon opacity on click. --- app/src/main/AndroidManifest.xml | 5 + .../newpipe/player/BackgroundPlayer.java | 172 +++++++--- .../player/BackgroundPlayerActivity.java | 305 ++++++++++++++++++ .../org/schabi/newpipe/player/BasePlayer.java | 17 +- .../schabi/newpipe/player/VideoPlayer.java | 156 ++++----- .../mediasource/DeferredMediaSource.java | 25 +- .../newpipe/playlist/PlayQueueAdapter.java | 19 +- .../newpipe/playlist/PlayQueueItem.java | 14 + .../playlist/PlayQueueItemBuilder.java | 62 ++-- .../newpipe/playlist/PlayQueueItemHolder.java | 10 +- app/src/main/res/color/dark_selector.xml | 5 + app/src/main/res/color/light_selector.xml | 5 + .../res/layout/activity_background_player.xml | 186 +++++++++++ app/src/main/res/layout/play_queue_item.xml | 5 +- app/src/main/res/layout/playlist_item.xml | 51 --- app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/strings.xml | 3 + app/src/main/res/values/styles.xml | 2 + 18 files changed, 794 insertions(+), 249 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java create mode 100644 app/src/main/res/color/dark_selector.xml create mode 100644 app/src/main/res/color/light_selector.xml create mode 100644 app/src/main/res/layout/activity_background_player.xml delete mode 100644 app/src/main/res/layout/playlist_item.xml 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