- 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.
This commit is contained in:
John Zhen M 2017-10-02 23:38:46 -07:00 committed by John Zhen Mo
parent bd9ee18e56
commit a9aee21e58
18 changed files with 794 additions and 249 deletions

View File

@ -38,6 +38,11 @@
android:name=".player.BackgroundPlayer" android:name=".player.BackgroundPlayer"
android:exported="false"/> android:exported="false"/>
<activity
android:name=".player.BackgroundPlayerActivity"
android:launchMode="singleTop"
android:label="@string/title_activity_background_player"/>
<service <service
android:name=".player.PopupVideoPlayer" android:name=".player.PopupVideoPlayer"
android:exported="false"/> android:exported="false"/>

View File

@ -27,6 +27,7 @@ import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.net.wifi.WifiManager; import android.net.wifi.WifiManager;
import android.os.Binder;
import android.os.Build; import android.os.Build;
import android.os.IBinder; import android.os.IBinder;
import android.os.PowerManager; import android.os.PowerManager;
@ -36,9 +37,9 @@ import android.support.v4.app.NotificationCompat;
import android.util.Log; import android.util.Log;
import android.widget.RemoteViews; import android.widget.RemoteViews;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MergingMediaSource;
import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.MainActivity; 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.ListHelper;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
import java.util.ArrayList;
import java.util.List;
/** /**
* Base players joining the common properties * Base players joining the common properties
@ -78,13 +76,30 @@ public final class BackgroundPlayer extends Service {
private PowerManager.WakeLock wakeLock; private PowerManager.WakeLock wakeLock;
private WifiManager.WifiLock wifiLock; 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 // Notification
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
private static final int NOTIFICATION_ID = 123789; private static final int NOTIFICATION_ID = 123789;
private boolean shouldUpdateNotification;
private NotificationManager notificationManager; private NotificationManager notificationManager;
private NotificationCompat.Builder notBuilder; private NotificationCompat.Builder notBuilder;
private RemoteViews notRemoteView; private RemoteViews notRemoteView;
@ -105,6 +120,8 @@ public final class BackgroundPlayer extends Service {
ThemeHelper.setTheme(this); ThemeHelper.setTheme(this);
basePlayerImpl = new BasePlayerImpl(this); basePlayerImpl = new BasePlayerImpl(this);
basePlayerImpl.setup(); basePlayerImpl.setup();
mBinder = new LocalBinder();
} }
@Override @Override
@ -124,13 +141,19 @@ public final class BackgroundPlayer extends Service {
@Override @Override
public IBinder onBind(Intent intent) { public IBinder onBind(Intent intent) {
return null; return mBinder;
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Actions // 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) { public void onOpenDetail(Context context, String videoUrl, String videoTitle) {
if (DEBUG) Log.d(TAG, "onOpenDetail() called with: context = [" + context + "], videoUrl = [" + videoUrl + "]"); if (DEBUG) Log.d(TAG, "onOpenDetail() called with: context = [" + context + "], videoUrl = [" + videoUrl + "]");
Intent i = new Intent(context, MainActivity.class); Intent i = new Intent(context, MainActivity.class);
@ -144,7 +167,11 @@ public final class BackgroundPlayer extends Service {
} }
private void onClose() { private void onClose() {
if (basePlayerImpl != null) basePlayerImpl.destroyPlayer(); if (basePlayerImpl != null) {
basePlayerImpl.stopActivityBinding();
basePlayerImpl.destroyPlayer();
}
stopForeground(true); stopForeground(true);
releaseWifiAndCpu(); releaseWifiAndCpu();
stopSelf(); stopSelf();
@ -152,8 +179,6 @@ public final class BackgroundPlayer extends Service {
private void onScreenOnOff(boolean on) { private void onScreenOnOff(boolean on) {
if (DEBUG) Log.d(TAG, "onScreenOnOff() called with: on = [" + on + "]"); if (DEBUG) Log.d(TAG, "onScreenOnOff() called with: on = [" + on + "]");
shouldUpdateNotification = on;
if (on) { if (on) {
if (basePlayerImpl.isPlaying() && !basePlayerImpl.isProgressLoopRunning()) { if (basePlayerImpl.isPlaying() && !basePlayerImpl.isProgressLoopRunning()) {
basePlayerImpl.startProgressLoop(); basePlayerImpl.startProgressLoop();
@ -168,9 +193,7 @@ public final class BackgroundPlayer extends Service {
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
private void resetNotification() { private void resetNotification() {
if (shouldUpdateNotification) { notBuilder = createNotification();
notBuilder = createNotification();
}
} }
private NotificationCompat.Builder createNotification() { private NotificationCompat.Builder createNotification() {
@ -211,7 +234,7 @@ public final class BackgroundPlayer extends Service {
break; break;
case Player.REPEAT_MODE_ONE: case Player.REPEAT_MODE_ONE:
// todo change image // todo change image
remoteViews.setInt(R.id.notificationRepeat, setAlphaMethodName, 255); remoteViews.setInt(R.id.notificationRepeat, setAlphaMethodName, 168);
break; break;
case Player.REPEAT_MODE_ALL: case Player.REPEAT_MODE_ALL:
remoteViews.setInt(R.id.notificationRepeat, setAlphaMethodName, 255); remoteViews.setInt(R.id.notificationRepeat, setAlphaMethodName, 255);
@ -227,7 +250,7 @@ public final class BackgroundPlayer extends Service {
*/ */
private synchronized void updateNotification(int drawableId) { private synchronized void updateNotification(int drawableId) {
//if (DEBUG) Log.d(TAG, "updateNotification() called with: drawableId = [" + drawableId + "]"); //if (DEBUG) Log.d(TAG, "updateNotification() called with: drawableId = [" + drawableId + "]");
if (notBuilder == null || !shouldUpdateNotification) return; if (notBuilder == null) return;
if (drawableId != -1) { if (drawableId != -1) {
if (notRemoteView != null) notRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId); if (notRemoteView != null) notRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId);
if (bigNotRemoteView != null) bigNotRemoteView.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) { BasePlayerImpl(Context context) {
super(context); super(context);
@ -280,8 +303,7 @@ public final class BackgroundPlayer extends Service {
public void handleIntent(Intent intent) { public void handleIntent(Intent intent) {
super.handleIntent(intent); super.handleIntent(intent);
shouldUpdateNotification = true; resetNotification();
notBuilder = createNotification();
startForeground(NOTIFICATION_ID, notBuilder.build()); startForeground(NOTIFICATION_ID, notBuilder.build());
if (bigNotRemoteView != null) bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 0, false); if (bigNotRemoteView != null) bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 0, false);
@ -329,23 +351,6 @@ public final class BackgroundPlayer extends Service {
@Override @Override
public void onRepeatClicked() { public void onRepeatClicked() {
super.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 @Override
@ -368,6 +373,7 @@ public final class BackgroundPlayer extends Service {
} }
updateNotification(-1); updateNotification(-1);
updateProgress(currentProgress, duration, bufferPercent);
} }
@Override @Override
@ -386,16 +392,6 @@ public final class BackgroundPlayer extends Service {
triggerProgressUpdate(); triggerProgressUpdate();
} }
@Override
public void onLoadingChanged(boolean isLoading) {
// Disable default behavior
}
@Override
public void onRepeatModeChanged(int i) {
}
@Override @Override
public void destroy() { public void destroy() {
super.destroy(); super.destroy();
@ -408,6 +404,42 @@ public final class BackgroundPlayer extends Service {
exception.printStackTrace(); 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 // Playback Listener
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -422,11 +454,14 @@ public final class BackgroundPlayer extends Service {
bigNotRemoteView.setTextViewText(R.id.notificationSongName, getVideoTitle()); bigNotRemoteView.setTextViewText(R.id.notificationSongName, getVideoTitle());
bigNotRemoteView.setTextViewText(R.id.notificationArtist, getUploaderName()); bigNotRemoteView.setTextViewText(R.id.notificationArtist, getUploaderName());
updateNotification(-1); updateNotification(-1);
updateMetadata();
} }
@Override @Override
public MediaSource sourceOf(final StreamInfo info) { public MediaSource sourceOf(final StreamInfo info) {
final int index = ListHelper.getDefaultAudioFormat(context, info.audio_streams); final int index = ListHelper.getDefaultAudioFormat(context, info.audio_streams);
if (index < 0) return null;
final AudioStream audio = info.audio_streams.get(index); final AudioStream audio = info.audio_streams.get(index);
return buildMediaSource(audio.url, MediaFormat.getSuffixById(audio.format)); return buildMediaSource(audio.url, MediaFormat.getSuffixById(audio.format));
} }
@ -435,6 +470,43 @@ public final class BackgroundPlayer extends Service {
public void shutdown() { public void shutdown() {
super.shutdown(); super.shutdown();
stopSelf(); 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(); onVideoPlayPause();
break; break;
case ACTION_OPEN_DETAIL: case ACTION_OPEN_DETAIL:
onOpenDetail(BackgroundPlayer.this, getVideoUrl(), getVideoTitle()); openControl(BackgroundPlayer.this);
break; break;
case ACTION_REPEAT: case ACTION_REPEAT:
onRepeatClicked(); onRepeatClicked();
@ -493,6 +565,12 @@ public final class BackgroundPlayer extends Service {
// States // States
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@Override
public void changeState(int state) {
super.changeState(state);
updatePlayback();
}
@Override @Override
public void onBlocked() { public void onBlocked() {
super.onBlocked(); super.onBlocked();

View File

@ -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();
}
}

View File

@ -27,7 +27,6 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.media.AudioManager; import android.media.AudioManager;
import android.media.audiofx.AudioEffect; import android.media.audiofx.AudioEffect;
@ -35,7 +34,6 @@ import android.net.Uri;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Log; import android.util.Log;
import android.view.View; 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.upstream.cache.SimpleCache;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.assist.ImageSize;
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener; import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
import org.schabi.newpipe.Downloader; import org.schabi.newpipe.Downloader;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfo; 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.MediaSourceManager;
import org.schabi.newpipe.player.playback.PlaybackListener; import org.schabi.newpipe.player.playback.PlaybackListener;
import org.schabi.newpipe.playlist.ExternalPlayQueue;
import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.playlist.PlayQueueAdapter;
import org.schabi.newpipe.playlist.SinglePlayQueue;
import java.io.File; import java.io.File;
import java.io.Serializable; import java.io.Serializable;
import java.text.DecimalFormat; import java.text.DecimalFormat;
import java.text.NumberFormat; import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Formatter; import java.util.Formatter;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -124,6 +115,8 @@ public abstract class BasePlayer implements Player.EventListener,
protected BroadcastReceiver broadcastReceiver; protected BroadcastReceiver broadcastReceiver;
protected IntentFilter intentFilter; protected IntentFilter intentFilter;
protected PlayQueueAdapter playQueueAdapter;
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Intent // Intent
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -285,6 +278,9 @@ public abstract class BasePlayer implements Player.EventListener,
playQueue = queue; playQueue = queue;
playQueue.init(); playQueue.init();
playbackManager = new MediaSourceManager(this, playQueue); playbackManager = new MediaSourceManager(this, playQueue);
if (playQueueAdapter != null) playQueueAdapter.dispose();
playQueueAdapter = new PlayQueueAdapter(playQueue);
} }
public void initThumbnail(final String url) { 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 Formatter formatter = new Formatter(stringBuilder, Locale.getDefault());
private final NumberFormat speedFormatter = new DecimalFormat("0.##x"); private final NumberFormat speedFormatter = new DecimalFormat("0.##x");
// todo: merge this into Localization
public String getTimeString(int milliSeconds) { public String getTimeString(int milliSeconds) {
long seconds = (milliSeconds % 60000L) / 1000L; long seconds = (milliSeconds % 60000L) / 1000L;
long minutes = (milliSeconds % 3600000L) / 60000L; long minutes = (milliSeconds % 3600000L) / 60000L;

View File

@ -64,13 +64,9 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.VideoStream; 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.AnimationUtils;
import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.ListHelper;
import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -111,6 +107,7 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
private List<TrackGroupInfo> trackGroupInfos; private List<TrackGroupInfo> trackGroupInfos;
private int videoRendererIndex = -1; private int videoRendererIndex = -1;
private TrackGroupArray videoTrackGroups; private TrackGroupArray videoTrackGroups;
private TrackGroup selectedVideoTrackGroup;
private boolean startedFromNewPipe = true; private boolean startedFromNewPipe = true;
protected boolean wasPlaying = false; protected boolean wasPlaying = false;
@ -211,7 +208,7 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
public void initPlayer() { public void initPlayer() {
super.initPlayer(); super.initPlayer();
simpleExoPlayer.setVideoSurfaceView(surfaceView); simpleExoPlayer.setVideoSurfaceView(surfaceView);
simpleExoPlayer.setVideoListener(this); simpleExoPlayer.addVideoListener(this);
trackSelector.setTunnelingAudioSessionId(C.generateAudioSessionIdV21(context)); 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 // Playback Listener
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -243,8 +313,8 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
selectedStreamIndex = ListHelper.getDefaultResolutionIndex(context, videos); selectedStreamIndex = ListHelper.getDefaultResolutionIndex(context, videos);
} }
playbackSpeedPopupMenu.getMenu().removeGroup(playbackSpeedPopupMenuGroupId); buildPlaybackSpeedMenu();
buildPlaybackSpeedMenu(playbackSpeedPopupMenu); buildQualityMenu();
} }
public MediaSource sourceOf(final StreamInfo info) { 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()])); 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 // States Implementation
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -343,22 +404,6 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
// ExoPlayer Video Listener // 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 @Override
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
super.onTracksChanged(trackGroups, trackSelections); super.onTracksChanged(trackGroups, trackSelections);
@ -376,52 +421,9 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
} }
} }
videoTrackGroups = trackSelector.getCurrentMappedTrackInfo().getTrackGroups(videoRendererIndex); videoTrackGroups = trackSelector.getCurrentMappedTrackInfo().getTrackGroups(videoRendererIndex);
final TrackGroup selectedTrackGroup = trackSelections.get(videoRendererIndex).getTrackGroup(); selectedVideoTrackGroup = trackSelections.get(videoRendererIndex).getTrackGroup();
qualityPopupMenu.getMenu().removeGroup(qualityPopupMenuGroupId); buildQualityMenu();
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);
} }
@Override @Override

View File

@ -13,7 +13,6 @@ import org.schabi.newpipe.playlist.PlayQueueItem;
import java.io.IOException; import java.io.IOException;
import io.reactivex.SingleObserver;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable; import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer; 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 * If loading fails here, an error will be propagated out and result in a
* {@link com.google.android.exoplayer2.ExoPlaybackException}, which is delegated * {@link com.google.android.exoplayer2.ExoPlaybackException}, which is delegated
* out to the player. * to the player.
* */ * */
public synchronized void load() { public synchronized void load() {
if (state != STATE_PREPARED || stream == null || loader != null) return; if (state != STATE_PREPARED || stream == null || loader != null) return;
@ -95,15 +94,23 @@ public final class DeferredMediaSource implements MediaSource {
final Consumer<StreamInfo> onSuccess = new Consumer<StreamInfo>() { final Consumer<StreamInfo> onSuccess = new Consumer<StreamInfo>() {
@Override @Override
public void accept(StreamInfo streamInfo) throws Exception { public void accept(StreamInfo streamInfo) throws Exception {
if (exoPlayer == null && listener == null) { Log.d(TAG, " Loaded: [" + stream.getTitle() + "] with url: " + stream.getUrl());
error = new Throwable("Stream info loading failed. URL: " + stream.getUrl()); state = STATE_LOADED;
} else {
Log.d(TAG, " Loaded: [" + stream.getTitle() + "] with url: " + stream.getUrl());
mediaSource = callback.sourceOf(streamInfo); if (exoPlayer == null || listener == null || streamInfo == null) {
mediaSource.prepareSource(exoPlayer, false, listener); error = new Throwable("Stream info loading failed. URL: " + stream.getUrl());
state = STATE_LOADED; 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);
} }
}; };

View File

@ -74,7 +74,7 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
playQueue.append(data); playQueue.append(data);
} }
public void add(final PlayQueueItem data) { public void add(final PlayQueueItem... data) {
playQueue.append(data); playQueue.append(data);
} }
@ -136,7 +136,6 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
return count; return count;
} }
// don't ask why we have to do that this way... it's android accept it -.-
@Override @Override
public int getItemViewType(int position) { public int getItemViewType(int position) {
if(header != null && position == 0) { if(header != null && position == 0) {
@ -167,15 +166,17 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
} }
@Override @Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int i) { public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if(holder instanceof PlayQueueItemHolder) { if(holder instanceof PlayQueueItemHolder) {
if(header != null) { // Ensure header does not interfere with list building
i--; if (header != null) position--;
} // Build the list item
playQueueItemBuilder.buildStreamInfoItem((PlayQueueItemHolder) holder, playQueue.getStreams().get(i)); playQueueItemBuilder.buildStreamInfoItem((PlayQueueItemHolder) holder, playQueue.getStreams().get(position));
} else if(holder instanceof HFHolder && i == 0 && header != null) { // Check if the current item should be selected/highlighted
holder.itemView.setSelected(playQueue.getIndex() == position);
} else if(holder instanceof HFHolder && position == 0 && header != null) {
((HFHolder) holder).view = header; ((HFHolder) holder).view = header;
} else if(holder instanceof HFHolder && i == playQueue.getStreams().size() && footer != null && showFooter) { } else if(holder instanceof HFHolder && position == playQueue.getStreams().size() && footer != null && showFooter) {
((HFHolder) holder).view = footer; ((HFHolder) holder).view = footer;
} }
} }

View File

@ -20,6 +20,8 @@ public class PlayQueueItem implements Serializable {
final private String url; final private String url;
final private int serviceId; final private int serviceId;
final private long duration; final private long duration;
final private String thumbnailUrl;
final private String uploader;
private Throwable error; private Throwable error;
@ -30,6 +32,8 @@ public class PlayQueueItem implements Serializable {
this.url = streamInfo.url; this.url = streamInfo.url;
this.serviceId = streamInfo.service_id; this.serviceId = streamInfo.service_id;
this.duration = streamInfo.duration; this.duration = streamInfo.duration;
this.thumbnailUrl = streamInfo.thumbnail_url;
this.uploader = streamInfo.uploader_name;
this.stream = Single.just(streamInfo); this.stream = Single.just(streamInfo);
} }
@ -39,6 +43,8 @@ public class PlayQueueItem implements Serializable {
this.url = streamInfoItem.url; this.url = streamInfoItem.url;
this.serviceId = streamInfoItem.service_id; this.serviceId = streamInfoItem.service_id;
this.duration = streamInfoItem.duration; this.duration = streamInfoItem.duration;
this.thumbnailUrl = streamInfoItem.thumbnail_url;
this.uploader = streamInfoItem.uploader_name;
} }
@NonNull @NonNull
@ -59,6 +65,14 @@ public class PlayQueueItem implements Serializable {
return duration; return duration;
} }
public String getThumbnailUrl() {
return thumbnailUrl;
}
public String getUploader() {
return uploader;
}
@Nullable @Nullable
public Throwable getError() { public Throwable getError() {
return error; return error;

View File

@ -5,9 +5,11 @@ import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import org.schabi.newpipe.R; import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import java.util.Locale; import org.schabi.newpipe.R;
import org.schabi.newpipe.util.Localization;
public class PlayQueueItemBuilder { public class PlayQueueItemBuilder {
@ -15,68 +17,44 @@ public class PlayQueueItemBuilder {
private static final String TAG = PlayQueueItemBuilder.class.toString(); private static final String TAG = PlayQueueItemBuilder.class.toString();
public interface OnSelectedListener { public interface OnSelectedListener {
void selected(int serviceId, String url, String title); void selected(PlayQueueItem item);
} }
private OnSelectedListener onStreamInfoItemSelectedListener; private OnSelectedListener onItemClickListener;
public PlayQueueItemBuilder() {} public PlayQueueItemBuilder() {}
public void setOnSelectedListener(OnSelectedListener listener) { public void setOnSelectedListener(OnSelectedListener listener) {
this.onStreamInfoItemSelectedListener = listener; this.onItemClickListener = listener;
} }
public View buildView(ViewGroup parent, final PlayQueueItem item) {
final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
final View itemView = inflater.inflate(R.layout.play_queue_item, parent, false);
final PlayQueueItemHolder holder = new PlayQueueItemHolder(itemView);
buildStreamInfoItem(holder, item);
return itemView;
}
public void buildStreamInfoItem(PlayQueueItemHolder holder, final PlayQueueItem item) { public void buildStreamInfoItem(PlayQueueItemHolder holder, final PlayQueueItem item) {
if (!TextUtils.isEmpty(item.getTitle())) holder.itemVideoTitleView.setText(item.getTitle()); if (!TextUtils.isEmpty(item.getTitle())) holder.itemVideoTitleView.setText(item.getTitle());
if (!TextUtils.isEmpty(item.getUploader())) holder.itemAdditionalDetailsView.setText(item.getUploader());
if (item.getDuration() > 0) { if (item.getDuration() > 0) {
holder.itemDurationView.setText(getDurationString(item.getDuration())); holder.itemDurationView.setText(Localization.getDurationString(item.getDuration()));
} else { } else {
holder.itemDurationView.setVisibility(View.GONE); holder.itemDurationView.setVisibility(View.GONE);
} }
ImageLoader.getInstance().displayImage(item.getThumbnailUrl(), holder.itemThumbnailView, IMAGE_OPTIONS);
holder.itemRoot.setOnClickListener(new View.OnClickListener() { holder.itemRoot.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View view) { public void onClick(View view) {
if(onStreamInfoItemSelectedListener != null) { if (onItemClickListener != null) {
onStreamInfoItemSelectedListener.selected(item.getServiceId(), item.getUrl(), item.getTitle()); onItemClickListener.selected(item);
} }
} }
}); });
} }
private static final DisplayImageOptions IMAGE_OPTIONS =
public static String getDurationString(long duration) { new DisplayImageOptions.Builder()
if(duration < 0) { .cacheInMemory(true)
duration = 0; .showImageOnFail(R.drawable.dummy_thumbnail)
} .showImageForEmptyUri(R.drawable.dummy_thumbnail)
String output; .showImageOnLoading(R.drawable.dummy_thumbnail)
long days = duration / (24 * 60 * 60); /* greater than a day */ .build();
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;
}
} }

View File

@ -31,13 +31,17 @@ import org.schabi.newpipe.info_list.holder.InfoItemHolder;
public class PlayQueueItemHolder extends RecyclerView.ViewHolder { 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 final View itemRoot;
public PlayQueueItemHolder(View v) { public PlayQueueItemHolder(View v) {
super(v); super(v);
itemRoot = v.findViewById(R.id.itemRoot); itemRoot = v.findViewById(R.id.itemRoot);
itemVideoTitleView = (TextView) v.findViewById(R.id.itemVideoTitleView); itemVideoTitleView = v.findViewById(R.id.itemVideoTitleView);
itemDurationView = (TextView) v.findViewById(R.id.itemDurationView); itemDurationView = v.findViewById(R.id.itemDurationView);
itemAdditionalDetailsView = v.findViewById(R.id.itemAdditionalDetails);
itemThumbnailView = v.findViewById(R.id.itemThumbnailView);
} }
} }

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true" android:color="@color/dark_youtube_primary_color"/>
<item android:color="@color/dark_youtube_accent_color"/>
</selector>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true" android:color="@color/light_youtube_primary_color"/>
<item android:color="@color/light_youtube_accent_color"/>
</selector>

View File

@ -0,0 +1,186 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="org.schabi.newpipe.player.BackgroundPlayerActivity">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/appbar_padding_top"
android:theme="@style/ThemeOverlay.AppCompat.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.ActionBar">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_weight="1"
android:background="?attr/colorPrimary"
app:layout_scrollFlags="scroll|enterAlways"
app:title="@string/app_name"/>
</android.support.design.widget.AppBarLayout>
<android.support.v7.widget.RecyclerView
android:id="@+id/play_queue"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/appbar"
android:layout_above="@+id/metadata"
android:scrollbars="vertical"
app:layoutManager="LinearLayoutManager"
tools:listitem="@layout/play_queue_item"/>
<LinearLayout
android:id="@+id/metadata"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/progress_bar"
android:orientation="vertical"
android:padding="8dp"
tools:ignore="RtlHardcoded,RtlSymmetry">
<TextView
android:id="@+id/song_name"
style="@android:style/TextAppearance.StatusBar.EventContent.Title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="2"
android:textSize="14sp"
android:textColor="?attr/colorAccent"
tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis nec aliquam augue, eget cursus est. Ut id tristique enim, ut scelerisque tellus. Sed ultricies ipsum non mauris ultricies, commodo malesuada velit porta."/>
<TextView
android:id="@+id/artist_name"
style="@android:style/TextAppearance.StatusBar.EventContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textSize="12sp"
tools:text="Duis posuere arcu condimentum lobortis mattis."/>
</LinearLayout>
<LinearLayout
android:id="@+id/progress_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/playback_controls"
android:gravity="center"
android:orientation="horizontal"
android:paddingLeft="16dp"
android:paddingRight="16dp">
<TextView
android:id="@+id/current_time"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:minHeight="40dp"
android:text="-:--:--"
android:textColor="?attr/colorAccent"
tools:ignore="HardcodedText"
tools:text="1:06:29"/>
<android.support.v7.widget.AppCompatSeekBar
android:id="@+id/seek_bar"
style="@style/Widget.AppCompat.SeekBar"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:paddingBottom="4dp"
android:paddingTop="8dp"
tools:progress="25"
tools:secondaryProgress="50"/>
<TextView
android:id="@+id/end_time"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:text="-:--:--"
android:textColor="?attr/colorAccent"
tools:ignore="HardcodedText"
tools:text="1:23:49"/>
</LinearLayout>
<RelativeLayout
android:id="@+id/playback_controls"
android:layout_width="match_parent"
android:layout_height="60dp"
android:paddingTop="10dp"
android:layout_alignParentBottom="true"
android:orientation="horizontal"
android:background="@drawable/player_controls_bg"
tools:ignore="RtlHardcoded">
<ImageButton
android:id="@+id/control_repeat"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_marginLeft="8dp"
android:background="#00000000"
android:clickable="true"
android:focusable="true"
android:scaleType="fitXY"
android:src="@drawable/ic_repeat_white"
tools:ignore="ContentDescription" />
<ImageButton
android:id="@+id/control_backward"
android:layout_width="40dp"
android:layout_height="match_parent"
android:layout_centerVertical="true"
android:layout_marginRight="5dp"
android:layout_toLeftOf="@+id/control_play_pause"
android:background="#00000000"
android:clickable="true"
android:focusable="true"
android:padding="2dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_action_av_fast_rewind"
tools:ignore="ContentDescription"/>
<ImageButton
android:id="@+id/control_play_pause"
android:layout_width="50dp"
android:layout_height="match_parent"
android:layout_centerVertical="true"
android:layout_marginRight="5dp"
android:layout_toLeftOf="@+id/control_forward"
android:background="#00000000"
android:padding="2dp"
android:clickable="true"
android:focusable="true"
android:scaleType="fitCenter"
android:src="@drawable/ic_pause_white"
tools:ignore="ContentDescription"/>
<ImageButton
android:id="@+id/control_forward"
android:layout_width="40dp"
android:layout_height="match_parent"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginRight="8dp"
android:background="#00000000"
android:clickable="true"
android:focusable="true"
android:padding="2dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_action_av_fast_forward"
tools:ignore="ContentDescription"/>
</RelativeLayout>
</RelativeLayout>

View File

@ -7,6 +7,7 @@
android:layout_height="48dp" android:layout_height="48dp"
android:background="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground"
android:clickable="true" android:clickable="true"
android:focusable="true"
android:padding="6dp"> android:padding="6dp">
<ImageView <ImageView
@ -54,6 +55,7 @@
android:maxLines="1" android:maxLines="1"
android:textAppearance="?android:attr/textAppearanceLarge" android:textAppearance="?android:attr/textAppearanceLarge"
android:textSize="@dimen/video_item_search_title_text_size" android:textSize="@dimen/video_item_search_title_text_size"
android:textColor="?attr/selector_color"
tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique vitae sem vitae blanditLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsum"/> tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique vitae sem vitae blanditLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsum"/>
<TextView <TextView
@ -66,5 +68,6 @@
android:lines="1" android:lines="1"
android:textAppearance="?android:attr/textAppearanceSmall" android:textAppearance="?android:attr/textAppearanceSmall"
android:textSize="@dimen/video_item_search_upload_date_text_size" android:textSize="@dimen/video_item_search_upload_date_text_size"
tools:text="Uploader • 2 years ago • 10M views"/> android:textColor="?attr/selector_color"
tools:text="Uploader"/>
</RelativeLayout> </RelativeLayout>

View File

@ -1,51 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/itemRoot"
android:layout_width="match_parent"
android:layout_height="@dimen/video_item_search_height"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:padding="@dimen/video_item_search_padding">
<ImageView
android:id="@+id/itemThumbnailView"
android:layout_width="@dimen/video_item_search_thumbnail_image_width"
android:layout_height="@dimen/video_item_search_thumbnail_image_height"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginRight="@dimen/video_item_search_image_right_margin"
android:contentDescription="@string/list_thumbnail_view_description"
android:scaleType="centerCrop"
android:src="@drawable/dummy_thumbnail"
tools:ignore="RtlHardcoded"/>
<TextView
android:id="@+id/itemPlaylistTitleView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_toRightOf="@id/itemThumbnailView"
android:layout_toEndOf="@id/itemThumbnailView"
android:ellipsize="end"
android:lines="3"
android:maxLines="3"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textSize="@dimen/video_item_search_title_text_size"
tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique vitae sem vitae blanditLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsum"/>
<TextView
android:id="@+id/itemAdditionalDetails"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_toRightOf="@id/itemThumbnailView"
android:layout_toEndOf="@id/itemThumbnailView"
android:lines="1"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textSize="@dimen/video_item_search_upload_date_text_size"
android:text="@string/playlist"/>
</RelativeLayout>

View File

@ -22,6 +22,7 @@
<!-- Can't refer to colors directly into drawable's xml--> <!-- Can't refer to colors directly into drawable's xml-->
<attr name="toolbar_shadow_drawable" format="reference"/> <attr name="toolbar_shadow_drawable" format="reference"/>
<attr name="selector_color" format="color"/>
<attr name="separator_color" format="color"/> <attr name="separator_color" format="color"/>
<attr name="contrast_background_color" format="color"/> <attr name="contrast_background_color" format="color"/>
</resources> </resources>

View File

@ -292,4 +292,7 @@
<string name="top_50">Top 50</string> <string name="top_50">Top 50</string>
<string name="new_and_hot">New &amp; hot</string> <string name="new_and_hot">New &amp; hot</string>
<string name="service_kiosk_string" translatable="false">%1$s/%2$s</string> <string name="service_kiosk_string" translatable="false">%1$s/%2$s</string>
<!-- Player -->
<string name="title_activity_background_player">Background Player</string>
</resources> </resources>

View File

@ -26,6 +26,7 @@
<item name="language">@drawable/ic_language_black_24dp</item> <item name="language">@drawable/ic_language_black_24dp</item>
<item name="history">@drawable/ic_history_black_24dp</item> <item name="history">@drawable/ic_history_black_24dp</item>
<item name="selector_color">@color/light_selector</item>
<item name="separator_color">@color/light_separator_color</item> <item name="separator_color">@color/light_separator_color</item>
<item name="contrast_background_color">@color/light_contrast_background_color</item> <item name="contrast_background_color">@color/light_contrast_background_color</item>
<item name="toolbar_shadow_drawable">@drawable/toolbar_shadow_light</item> <item name="toolbar_shadow_drawable">@drawable/toolbar_shadow_light</item>
@ -60,6 +61,7 @@
<item name="language">@drawable/ic_language_white_24dp</item> <item name="language">@drawable/ic_language_white_24dp</item>
<item name="history">@drawable/ic_history_white_24dp</item> <item name="history">@drawable/ic_history_white_24dp</item>
<item name="selector_color">@color/dark_selector</item>
<item name="separator_color">@color/dark_separator_color</item> <item name="separator_color">@color/dark_separator_color</item>
<item name="contrast_background_color">@color/dark_contrast_background_color</item> <item name="contrast_background_color">@color/dark_contrast_background_color</item>
<item name="toolbar_shadow_drawable">@drawable/toolbar_shadow_dark</item> <item name="toolbar_shadow_drawable">@drawable/toolbar_shadow_dark</item>