- 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:exported="false"/>
<activity
android:name=".player.BackgroundPlayerActivity"
android:launchMode="singleTop"
android:label="@string/title_activity_background_player"/>
<service
android:name=".player.PopupVideoPlayer"
android:exported="false"/>

View File

@ -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,10 +193,8 @@ public final class BackgroundPlayer extends Service {
//////////////////////////////////////////////////////////////////////////*/
private void resetNotification() {
if (shouldUpdateNotification) {
notBuilder = createNotification();
}
}
private NotificationCompat.Builder createNotification() {
notRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification);
@ -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();

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.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;

View File

@ -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<TrackGroupInfo> 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

View File

@ -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<StreamInfo> onSuccess = new Consumer<StreamInfo>() {
@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());
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);
mediaSource.prepareSource(exoPlayer, false, listener);
state = STATE_LOADED;
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);
}
public void add(final PlayQueueItem data) {
public void add(final PlayQueueItem... data) {
playQueue.append(data);
}
@ -136,7 +136,6 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
return count;
}
// don't ask why we have to do that this way... it's android accept it -.-
@Override
public int getItemViewType(int position) {
if(header != null && position == 0) {
@ -167,15 +166,17 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int i) {
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if(holder instanceof PlayQueueItemHolder) {
if(header != null) {
i--;
}
playQueueItemBuilder.buildStreamInfoItem((PlayQueueItemHolder) holder, playQueue.getStreams().get(i));
} else if(holder instanceof HFHolder && i == 0 && header != null) {
// Ensure header does not interfere with list building
if (header != null) position--;
// Build the list item
playQueueItemBuilder.buildStreamInfoItem((PlayQueueItemHolder) holder, playQueue.getStreams().get(position));
// 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;
} 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;
}
}

View File

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

View File

@ -5,9 +5,11 @@ import android.view.LayoutInflater;
import android.view.View;
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 {
@ -15,68 +17,44 @@ public class PlayQueueItemBuilder {
private static final String TAG = PlayQueueItemBuilder.class.toString();
public interface OnSelectedListener {
void selected(int serviceId, String url, String title);
void selected(PlayQueueItem item);
}
private OnSelectedListener onStreamInfoItemSelectedListener;
private OnSelectedListener onItemClickListener;
public PlayQueueItemBuilder() {}
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) {
if (!TextUtils.isEmpty(item.getTitle())) holder.itemVideoTitleView.setText(item.getTitle());
if (!TextUtils.isEmpty(item.getUploader())) holder.itemAdditionalDetailsView.setText(item.getUploader());
if (item.getDuration() > 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();
}

View File

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

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:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:padding="6dp">
<ImageView
@ -54,6 +55,7 @@
android:maxLines="1"
android:textAppearance="?android:attr/textAppearanceLarge"
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"/>
<TextView
@ -66,5 +68,6 @@
android:lines="1"
android:textAppearance="?android:attr/textAppearanceSmall"
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>

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-->
<attr name="toolbar_shadow_drawable" format="reference"/>
<attr name="selector_color" format="color"/>
<attr name="separator_color" format="color"/>
<attr name="contrast_background_color" format="color"/>
</resources>

View File

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

View File

@ -26,6 +26,7 @@
<item name="language">@drawable/ic_language_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="contrast_background_color">@color/light_contrast_background_color</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="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="contrast_background_color">@color/dark_contrast_background_color</item>
<item name="toolbar_shadow_drawable">@drawable/toolbar_shadow_dark</item>