diff --git a/app/build.gradle b/app/build.gradle index 73f11aab0..0f99854c2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -45,7 +45,7 @@ dependencies { compile 'com.google.code.gson:gson:2.4' compile 'com.nononsenseapps:filepicker:3.0.0' compile 'ch.acra:acra:4.9.0' - compile 'com.devbrackets.android:exomedia:3.1.1' + compile 'com.google.android.exoplayer:exoplayer:r2.3.1' testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:1.10.19' testCompile 'org.json:json:20160810' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a7dcd9205..3172506f9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -51,19 +51,7 @@ android:configChanges="keyboard|keyboardHidden|orientation|screenSize" android:label="@string/app_name" android:launchMode="singleInstance" - android:theme="@style/PlayerTheme"> - - - - - - - - - - - - + android:theme="@style/PlayerTheme"/> + android:label="@string/popup_mode_share_menu_title"> diff --git a/app/src/main/java/org/schabi/newpipe/detail/VideoItemDetailFragment.java b/app/src/main/java/org/schabi/newpipe/detail/VideoItemDetailFragment.java index a221e4e94..f1e94ea8e 100644 --- a/app/src/main/java/org/schabi/newpipe/detail/VideoItemDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/detail/VideoItemDetailFragment.java @@ -51,6 +51,7 @@ import org.schabi.newpipe.extractor.stream_info.AudioStream; import org.schabi.newpipe.extractor.stream_info.StreamInfo; import org.schabi.newpipe.extractor.stream_info.VideoStream; import org.schabi.newpipe.info_list.InfoItemBuilder; +import org.schabi.newpipe.player.AbstractPlayer; import org.schabi.newpipe.player.BackgroundPlayer; import org.schabi.newpipe.player.ExoPlayerActivity; import org.schabi.newpipe.player.PlayVideoActivity; @@ -59,6 +60,7 @@ import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.util.NavStack; import org.schabi.newpipe.util.PermissionHelper; +import java.util.ArrayList; import java.util.Vector; import static android.app.Activity.RESULT_OK; @@ -331,10 +333,12 @@ public class VideoItemDetailFragment extends Fragment { // so, I can notify the service through a broadcast, but the problem is // when I click in another video, another thumbnail will be load, and will // notify again, so I send the videoUrl and compare with the service's url - ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail = streamThumbnail; - Intent intent = new Intent(PopupVideoPlayer.InternalListener.ACTION_UPDATE_THUMB); - intent.putExtra(PopupVideoPlayer.VIDEO_URL, info.webpage_url); - getContext().sendBroadcast(intent); + if (getContext() != null) { + ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail = streamThumbnail; + Intent intent = new Intent(AbstractPlayer.ACTION_UPDATE_THUMB); + intent.putExtra(AbstractPlayer.VIDEO_URL, info.webpage_url); + getContext().sendBroadcast(intent); + } } } @@ -388,13 +392,15 @@ public class VideoItemDetailFragment extends Fragment { if (streamThumbnail != null) ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail = streamThumbnail; - VideoStream selectedVideoStream = info.video_streams.get(selectedStreamId); Intent i = new Intent(activity, PopupVideoPlayer.class); - Toast.makeText(activity, "Starting in popup mode", Toast.LENGTH_SHORT).show(); - i.putExtra(PopupVideoPlayer.VIDEO_TITLE, info.title) - .putExtra(PopupVideoPlayer.STREAM_URL, selectedVideoStream.url) - .putExtra(PopupVideoPlayer.CHANNEL_NAME, info.uploader) - .putExtra(PopupVideoPlayer.VIDEO_URL, info.webpage_url); + Toast.makeText(activity, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show(); + i.putExtra(AbstractPlayer.VIDEO_TITLE, info.title) + .putExtra(AbstractPlayer.CHANNEL_NAME, info.uploader) + .putExtra(AbstractPlayer.VIDEO_URL, info.webpage_url) + .putExtra(AbstractPlayer.INDEX_SEL_VIDEO_STREAM, selectedStreamId) + .putExtra(AbstractPlayer.VIDEO_STREAMS_LIST, new ArrayList<>(info.video_streams)); + if (info.start_position > 0) i.putExtra(AbstractPlayer.START_POSITION, info.start_position * 1000); + activity.startService(i); } }); @@ -784,47 +790,27 @@ public class VideoItemDetailFragment extends Fragment { builder.create().show(); } } else { - if (PreferenceManager.getDefaultSharedPreferences(activity) - .getBoolean(activity.getString(R.string.use_exoplayer_key), false)) { - - // TODO: Fix this mess - if (streamThumbnail != null) - ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail = streamThumbnail; - // exo player - - if(info.dashMpdUrl != null && !info.dashMpdUrl.isEmpty()) { - // try dash - Intent intent = new Intent(activity, ExoPlayerActivity.class) - .setData(Uri.parse(info.dashMpdUrl)); - //.putExtra(ExoPlayerActivity.CONTENT_TYPE_EXTRA, Util.TYPE_DASH); - startActivity(intent); - } else if((info.audio_streams != null && !info.audio_streams.isEmpty()) && - (info.video_only_streams != null && !info.video_only_streams.isEmpty())) { - // try smooth streaming - - } else { - //default streaming - Intent intent = new Intent(activity, ExoPlayerActivity.class) - .setDataAndType(Uri.parse(selectedVideoStream.url), - MediaFormat.getMimeById(selectedVideoStream.format)) - - .putExtra(ExoPlayerActivity.VIDEO_TITLE, info.title) - .putExtra(ExoPlayerActivity.CHANNEL_NAME, info.uploader); - //.putExtra(ExoPlayerActivity.CONTENT_TYPE_EXTRA, Util.TYPE_OTHER); - - activity.startActivity(intent); // HERE !!! - } - //------------- - + Intent intent; + if (PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(activity.getString(R.string.use_exoplayer_key), false)) { + // ExoPlayer + if (streamThumbnail != null) ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail = streamThumbnail; + intent = new Intent(activity, ExoPlayerActivity.class) + .putExtra(AbstractPlayer.VIDEO_TITLE, info.title) + .putExtra(AbstractPlayer.VIDEO_URL, info.webpage_url) + .putExtra(AbstractPlayer.CHANNEL_NAME, info.uploader) + .putExtra(AbstractPlayer.INDEX_SEL_VIDEO_STREAM, actionBarHandler.getSelectedVideoStream()) + .putExtra(AbstractPlayer.VIDEO_STREAMS_LIST, new ArrayList<>(info.video_streams)); + if (info.start_position > 0) intent.putExtra(AbstractPlayer.START_POSITION, info.start_position * 1000); } else { // Internal Player - Intent intent = new Intent(activity, PlayVideoActivity.class) + intent = new Intent(activity, PlayVideoActivity.class) .putExtra(PlayVideoActivity.VIDEO_TITLE, info.title) .putExtra(PlayVideoActivity.STREAM_URL, selectedVideoStream.url) .putExtra(PlayVideoActivity.VIDEO_URL, info.webpage_url) .putExtra(PlayVideoActivity.START_POSITION, info.start_position); - activity.startActivity(intent); //also HERE !!! } + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + activity.startActivity(intent); } // -------------------------------------------- diff --git a/app/src/main/java/org/schabi/newpipe/player/AbstractPlayer.java b/app/src/main/java/org/schabi/newpipe/player/AbstractPlayer.java new file mode 100644 index 000000000..a9987bee9 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/AbstractPlayer.java @@ -0,0 +1,1106 @@ +package org.schabi.newpipe.player; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.net.Uri; +import android.os.Build; +import android.os.Handler; +import android.preference.PreferenceManager; +import android.support.v4.content.ContextCompat; +import android.text.TextUtils; +import android.util.Log; +import android.view.Gravity; +import android.view.Menu; +import android.view.MenuItem; +import android.view.SurfaceView; +import android.view.View; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.PopupMenu; +import android.widget.ProgressBar; +import android.widget.SeekBar; +import android.widget.TextView; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.DefaultLoadControl; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; +import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.dash.DashMediaSource; +import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; +import com.google.android.exoplayer2.source.hls.HlsMediaSource; +import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; +import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; +import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; +import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.upstream.cache.CacheDataSource; +import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory; +import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor; +import com.google.android.exoplayer2.upstream.cache.SimpleCache; +import com.google.android.exoplayer2.util.Util; + +import org.schabi.newpipe.ActivityCommunicator; +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.MediaFormat; +import org.schabi.newpipe.extractor.stream_info.VideoStream; + +import java.io.File; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Formatter; +import java.util.List; +import java.util.Locale; +import java.util.Vector; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Common properties of the players + * + * @author mauriciocolli + */ +@SuppressWarnings({"unused", "WeakerAccess"}) +public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBarChangeListener, View.OnClickListener, ExoPlayer.EventListener, PopupMenu.OnMenuItemClickListener, PopupMenu.OnDismissListener, SimpleExoPlayer.VideoListener { + public static final boolean DEBUG = false; + public final String TAG; + + protected Context context; + private SharedPreferences sharedPreferences; + + private static int currentState = -1; + public static final String ACTION_UPDATE_THUMB = "org.schabi.newpipe.player.AbstractPlayer.UPDATE_THUMBNAIL"; + + /*////////////////////////////////////////////////////////////////////////// + // Intent + //////////////////////////////////////////////////////////////////////////*/ + + public static final String VIDEO_URL = "video_url"; + public static final String VIDEO_STREAMS_LIST = "video_streams_list"; + public static final String VIDEO_TITLE = "video_title"; + public static final String INDEX_SEL_VIDEO_STREAM = "index_selected_video_stream"; + public static final String START_POSITION = "start_position"; + public static final String CHANNEL_NAME = "channel_name"; + public static final String STARTED_FROM_NEWPIPE = "started_from_newpipe"; + + private String videoUrl = ""; + private int videoStartPos = -1; + private String videoTitle = ""; + private Bitmap videoThumbnail; + private String channelName = ""; + private int selectedIndexStream; + private ArrayList videoStreamsList; + + /*////////////////////////////////////////////////////////////////////////// + // Player + //////////////////////////////////////////////////////////////////////////*/ + + public static final int FAST_FORWARD_REWIND_AMOUNT = 10000; // 10 Seconds + public static final int DEFAULT_CONTROLS_HIDE_TIME = 3000; // 3 Seconds + public static final String CACHE_FOLDER_NAME = "exoplayer"; + + private boolean startedFromNewPipe = true; + private boolean isPrepared = false; + private boolean wasPlaying = false; + private SimpleExoPlayer simpleExoPlayer; + + @SuppressWarnings("FieldCanBeLocal") + private MediaSource videoSource; + private static CacheDataSourceFactory cacheDataSourceFactory; + private static final DefaultExtractorsFactory extractorsFactory = new DefaultExtractorsFactory(); + private static final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); + + private AtomicBoolean isProgressLoopRunning = new AtomicBoolean(); + private Handler progressLoop; + private Runnable progressUpdate; + + /*////////////////////////////////////////////////////////////////////////// + // Repeat + //////////////////////////////////////////////////////////////////////////*/ + + private RepeatMode currentRepeatMode = RepeatMode.REPEAT_DISABLED; + + public enum RepeatMode { + REPEAT_DISABLED, + REPEAT_ONE, + REPEAT_ALL + } + + /*////////////////////////////////////////////////////////////////////////// + // Views + //////////////////////////////////////////////////////////////////////////*/ + + private View rootView; + + private AspectRatioFrameLayout aspectRatioFrameLayout; + private SurfaceView surfaceView; + private View surfaceForeground; + + private View loadingPanel; + private ImageView endScreen; + private ImageView controlAnimationView; + + private View controlsRoot; + private TextView currentDisplaySeek; + + private View bottomControlsRoot; + private SeekBar playbackSeekBar; + private TextView playbackCurrentTime; + private TextView playbackEndTime; + + private View topControlsRoot; + private TextView qualityTextView; + private ImageButton fullScreenButton; + + private ValueAnimator controlViewAnimator; + + private boolean isQualityPopupMenuVisible = false; + private boolean qualityChanged = false; + private int qualityPopupMenuGroupId = 69; + private PopupMenu qualityPopupMenu; + + /////////////////////////////////////////////////////////////////////////// + + public AbstractPlayer(String debugTag, Context context) { + this.TAG = debugTag; + this.context = context; + this.progressLoop = new Handler(); + this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + + if (cacheDataSourceFactory == null) { + DefaultDataSourceFactory dataSourceFactory = new DefaultDataSourceFactory(context, Util.getUserAgent(context, context.getPackageName()), bandwidthMeter); + File cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME); + if (!cacheDir.exists()) { + //noinspection ResultOfMethodCallIgnored + cacheDir.mkdir(); + } + + Log.d(TAG, "buildMediaSource: cacheDir = " + cacheDir.getAbsolutePath()); + SimpleCache simpleCache = new SimpleCache(cacheDir, new LeastRecentlyUsedCacheEvictor(64 * 1024 * 1024L)); + cacheDataSourceFactory = new CacheDataSourceFactory(simpleCache, dataSourceFactory, CacheDataSource.FLAG_BLOCK_ON_CACHE, 512 * 1024); + } + } + + public void setup(View rootView) { + initViews(rootView); + initListeners(); + if (simpleExoPlayer == null) initPlayer(); + else { + simpleExoPlayer.addListener(this); + simpleExoPlayer.setVideoListener(this); + simpleExoPlayer.setVideoSurfaceView(surfaceView); + } + } + + public void initViews(View rootView) { + this.rootView = rootView; + this.aspectRatioFrameLayout = (AspectRatioFrameLayout) rootView.findViewById(R.id.aspectRatioLayout); + this.surfaceView = (SurfaceView) rootView.findViewById(R.id.surfaceView); + this.surfaceForeground = rootView.findViewById(R.id.surfaceForeground); + this.loadingPanel = rootView.findViewById(R.id.loadingPanel); + this.endScreen = (ImageView) rootView.findViewById(R.id.endScreen); + this.controlAnimationView = (ImageView) rootView.findViewById(R.id.controlAnimationView); + this.controlsRoot = rootView.findViewById(R.id.playbackControlRoot); + this.currentDisplaySeek = (TextView) rootView.findViewById(R.id.currentDisplaySeek); + this.playbackSeekBar = (SeekBar) rootView.findViewById(R.id.playbackSeekBar); + this.playbackCurrentTime = (TextView) rootView.findViewById(R.id.playbackCurrentTime); + this.playbackEndTime = (TextView) rootView.findViewById(R.id.playbackEndTime); + this.bottomControlsRoot = rootView.findViewById(R.id.bottomControls); + this.topControlsRoot = rootView.findViewById(R.id.topControls); + this.qualityTextView = (TextView) rootView.findViewById(R.id.qualityTextView); + this.fullScreenButton = (ImageButton) rootView.findViewById(R.id.fullScreenButton); + + //this.aspectRatioFrameLayout.setAspectRatio(16.0f / 9.0f); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN); + this.playbackSeekBar.getProgressDrawable().setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) this.qualityPopupMenu = new PopupMenu(context, qualityTextView, Gravity.CENTER | Gravity.BOTTOM); + else this.qualityPopupMenu = new PopupMenu(context, qualityTextView); + + ((ProgressBar) this.loadingPanel.findViewById(R.id.progressBarLoadingPanel)).getIndeterminateDrawable().setColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY); + + } + + public void initListeners() { + progressUpdate = new Runnable() { + @Override + public void run() { + //if(DEBUG) Log.d(TAG, "progressUpdate run() called"); + onUpdateProgress((int) simpleExoPlayer.getCurrentPosition(), (int) simpleExoPlayer.getDuration(), simpleExoPlayer.getBufferedPercentage()); + if (isProgressLoopRunning.get()) progressLoop.postDelayed(this, 100); + } + }; + + playbackSeekBar.setOnSeekBarChangeListener(this); + fullScreenButton.setOnClickListener(this); + qualityTextView.setOnClickListener(this); + } + + public void initPlayer() { + if (DEBUG) Log.d(TAG, "initPlayer() called with: context = [" + context + "]"); + + AdaptiveTrackSelection.Factory trackSelectionFactory = new AdaptiveTrackSelection.Factory(bandwidthMeter); + DefaultTrackSelector defaultTrackSelector = new DefaultTrackSelector(trackSelectionFactory); + DefaultLoadControl loadControl = new DefaultLoadControl(); + + simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(context, defaultTrackSelector, loadControl); + simpleExoPlayer.addListener(this); + simpleExoPlayer.setVideoListener(this); + simpleExoPlayer.setVideoSurfaceView(surfaceView); + } + + @SuppressWarnings("unchecked") + public void handleIntent(Intent intent) { + if (DEBUG) Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]"); + if (intent == null) return; + + selectedIndexStream = intent.getIntExtra(INDEX_SEL_VIDEO_STREAM, -1); + + Serializable serializable = intent.getSerializableExtra(VIDEO_STREAMS_LIST); + + if (serializable instanceof ArrayList) videoStreamsList = (ArrayList) serializable; + if (serializable instanceof Vector) videoStreamsList = new ArrayList<>((List) serializable); + + videoUrl = intent.getStringExtra(VIDEO_URL); + videoTitle = intent.getStringExtra(VIDEO_TITLE); + videoStartPos = intent.getIntExtra(START_POSITION, -1); + channelName = intent.getStringExtra(CHANNEL_NAME); + startedFromNewPipe = intent.getBooleanExtra(STARTED_FROM_NEWPIPE, true); + try { + videoThumbnail = ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail; + } catch (Exception e) { + e.printStackTrace(); + } + + playVideo(getSelectedStreamUri(), true); + } + + public void playVideo(Uri videoURI, boolean autoPlay) { + if (DEBUG) Log.d(TAG, "playVideo() called with: videoURI = [" + videoURI + "], autoPlay = [" + autoPlay + "]"); + + if (videoURI == null || simpleExoPlayer == null) { + onError(); + return; + } + + changeState(STATE_LOADING); + isPrepared = false; + qualityChanged = false; + + qualityPopupMenu.getMenu().removeGroup(qualityPopupMenuGroupId); + buildQualityMenu(qualityPopupMenu); + + videoSource = buildMediaSource(videoURI, MediaFormat.getSuffixById(videoStreamsList.get(selectedIndexStream).format)); + + if (simpleExoPlayer.getPlaybackState() != ExoPlayer.STATE_IDLE) simpleExoPlayer.stop(); + if (videoStartPos > 0) simpleExoPlayer.seekTo(videoStartPos); + simpleExoPlayer.prepare(videoSource); + simpleExoPlayer.setPlayWhenReady(autoPlay); + } + + public void destroy() { + if (DEBUG) Log.d(TAG, "destroy() called"); + if (simpleExoPlayer != null) { + simpleExoPlayer.stop(); + simpleExoPlayer.release(); + } + if (progressLoop != null) stopProgressLoop(); + } + + private MediaSource buildMediaSource(Uri uri, String overrideExtension) { + if (DEBUG) Log.d(TAG, "buildMediaSource() called with: uri = [" + uri + "], overrideExtension = [" + overrideExtension + "]"); + int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension); + switch (type) { + case C.TYPE_SS: + return new SsMediaSource(uri, cacheDataSourceFactory, new DefaultSsChunkSource.Factory(cacheDataSourceFactory), null, null); + case C.TYPE_DASH: + return new DashMediaSource(uri, cacheDataSourceFactory, new DefaultDashChunkSource.Factory(cacheDataSourceFactory), null, null); + case C.TYPE_HLS: + return new HlsMediaSource(uri, cacheDataSourceFactory, null, null); + case C.TYPE_OTHER: + return new ExtractorMediaSource(uri, cacheDataSourceFactory, extractorsFactory, null, null); + default: { + throw new IllegalStateException("Unsupported type: " + type); + } + } + } + + public void buildQualityMenu(PopupMenu popupMenu) { + for (int i = 0; i < videoStreamsList.size(); i++) { + VideoStream videoStream = videoStreamsList.get(i); + popupMenu.getMenu().add(qualityPopupMenuGroupId, i, Menu.NONE, MediaFormat.getNameById(videoStream.format) + " " + videoStream.resolution); + } + qualityTextView.setText(videoStreamsList.get(selectedIndexStream).resolution); + popupMenu.setOnMenuItemClickListener(this); + popupMenu.setOnDismissListener(this); + + } + + /*////////////////////////////////////////////////////////////////////////// + // States Implementation + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void changeState(int state) { + if (DEBUG) Log.d(TAG, "changeState() called with: state = [" + state + "]"); + currentState = state; + switch (state) { + case STATE_LOADING: + onLoading(); + break; + case STATE_PLAYING: + onPlaying(); + break; + case STATE_BUFFERING: + onBuffering(); + break; + case STATE_PAUSED: + onPaused(); + break; + case STATE_PAUSED_SEEK: + onPausedSeek(); + break; + case STATE_COMPLETED: + onCompleted(); + break; + } + } + + @Override + public void onLoading() { + if (DEBUG) Log.d(TAG, "onLoading() called"); + + if (!isProgressLoopRunning.get()) startProgressLoop(); + + showAndAnimateControl(-1, true); + playbackSeekBar.setEnabled(true); + playbackSeekBar.setProgress(0); + + // Bug on lower api, disabling and enabling the seekBar resets the thumb color -.-, so sets the color again + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN); + + animateView(endScreen, false, 0, 0); + animateView(controlsRoot, false, 0, 0); + loadingPanel.setBackgroundColor(Color.BLACK); + animateView(loadingPanel, true, 0, 0); + animateView(surfaceForeground, true, 100, 0); + } + + @Override + public void onPlaying() { + if (DEBUG) Log.d(TAG, "onPlaying() called"); + if (!isProgressLoopRunning.get()) startProgressLoop(); + showAndAnimateControl(-1, true); + loadingPanel.setVisibility(View.GONE); + animateView(controlsRoot, false, 500, DEFAULT_CONTROLS_HIDE_TIME, true); + animateView(currentDisplaySeek, false, 200, 0); + } + + @Override + public void onBuffering() { + if (DEBUG) Log.d(TAG, "onBuffering() called"); + loadingPanel.setBackgroundColor(Color.TRANSPARENT); + animateView(loadingPanel, true, 500, 0); + animateView(controlsRoot, false, 0, 0); + } + + @Override + public void onPaused() { + if (DEBUG) Log.d(TAG, "onPaused() called"); + animateView(controlsRoot, true, 500, 100); + loadingPanel.setVisibility(View.GONE); + } + + @Override + public void onPausedSeek() { + if (DEBUG) Log.d(TAG, "onPausedSeek() called"); + showAndAnimateControl(-1, true); + } + + @Override + public void onCompleted() { + if (DEBUG) Log.d(TAG, "onCompleted() called"); + + if (isProgressLoopRunning.get()) stopProgressLoop(); + + if (videoThumbnail != null) endScreen.setImageBitmap(videoThumbnail); + animateView(controlsRoot, true, 500, 0); + animateView(endScreen, true, 800, 0); + animateView(currentDisplaySeek, false, 200, 0); + loadingPanel.setVisibility(View.GONE); + + playbackSeekBar.setMax((int) simpleExoPlayer.getDuration()); + playbackSeekBar.setProgress(playbackSeekBar.getMax()); + playbackSeekBar.setEnabled(false); + playbackEndTime.setText(getTimeString(playbackSeekBar.getMax())); + playbackCurrentTime.setText(playbackEndTime.getText()); + // Bug on lower api, disabling and enabling the seekBar resets the thumb color -.-, so sets the color again + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN); + + animateView(surfaceForeground, true, 100, 0); + + if (currentRepeatMode == RepeatMode.REPEAT_ONE) { + changeState(STATE_LOADING); + getPlayer().seekTo(0); + } + } + + /*////////////////////////////////////////////////////////////////////////// + // ExoPlayer Listener + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onTimelineChanged(Timeline timeline, Object manifest) { + + } + + @Override + public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + + } + + @Override + public void onLoadingChanged(boolean isLoading) { + if (DEBUG) Log.d(TAG, "onLoadingChanged() called with: isLoading = [" + isLoading + "]"); + + if (!isLoading && getCurrentState() == STATE_PAUSED && isProgressLoopRunning.get()) stopProgressLoop(); + else if (isLoading && !isProgressLoopRunning.get()) startProgressLoop(); + } + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + if (DEBUG) Log.d(TAG, "onPlayerStateChanged() called with: playWhenReady = [" + playWhenReady + "], playbackState = [" + playbackState + "]"); + if (getCurrentState() == STATE_PAUSED_SEEK) { + if (DEBUG) Log.d(TAG, "onPlayerStateChanged() currently on PausedSeek"); + return; + } + + switch (playbackState) { + case ExoPlayer.STATE_IDLE: // 1 + isPrepared = false; + break; + case ExoPlayer.STATE_BUFFERING: // 2 + if (isPrepared && getCurrentState() != STATE_LOADING) changeState(STATE_BUFFERING); + break; + case ExoPlayer.STATE_READY: //3 + if (!isPrepared) { + isPrepared = true; + onPrepared(playWhenReady); + break; + } + if (currentState == STATE_PAUSED_SEEK) break; + changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); + break; + case ExoPlayer.STATE_ENDED: // 4 + changeState(STATE_COMPLETED); + isPrepared = false; + break; + } + } + + @Override + public void onPlayerError(ExoPlaybackException error) { + if (DEBUG) Log.d(TAG, "onPlayerError() called with: error = [" + error + "]"); + onError(); + } + + @Override + public void onPositionDiscontinuity() { + if (DEBUG) Log.d(TAG, "onPositionDiscontinuity() called"); + } + + /*////////////////////////////////////////////////////////////////////////// + // ExoPlayer Video Listener + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { + if (DEBUG) { + Log.d(TAG, "onVideoSizeChanged() called with: width / height = [" + width + " / " + height + " = " + (((float) width) / height) + "], unappliedRotationDegrees = [" + unappliedRotationDegrees + "], pixelWidthHeightRatio = [" + pixelWidthHeightRatio + "]"); + } + aspectRatioFrameLayout.setAspectRatio(((float) width) / height); + } + + @Override + public void onRenderedFirstFrame() { + animateView(surfaceForeground, false, 100, 0); + } + + /*////////////////////////////////////////////////////////////////////////// + // General Player + //////////////////////////////////////////////////////////////////////////*/ + + public abstract void onError(); + + public void onPrepared(boolean playWhenReady) { + if (DEBUG) Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]"); + + if (videoStartPos > 0) { + playbackSeekBar.setProgress(videoStartPos); + playbackCurrentTime.setText(getTimeString(videoStartPos)); + videoStartPos = -1; + } + + playbackSeekBar.setMax((int) simpleExoPlayer.getDuration()); + playbackEndTime.setText(getTimeString((int) simpleExoPlayer.getDuration())); + + changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); + } + + public void onUpdateProgress(int currentProgress, int duration, int bufferPercent) { + if (!isPrepared) return; + if (currentState != STATE_PAUSED) { + if (currentState != STATE_PAUSED_SEEK) playbackSeekBar.setProgress(currentProgress); + playbackCurrentTime.setText(getTimeString(currentProgress)); + } + if (simpleExoPlayer.isLoading() || bufferPercent > 90) { + playbackSeekBar.setSecondaryProgress((int) (playbackSeekBar.getMax() * ((float) bufferPercent / 100))); + } + if (DEBUG && bufferPercent % 20 == 0) { //Limit log + Log.d(TAG, "updateProgress() called with: isVisible = " + isControlsVisible() + ", currentProgress = [" + currentProgress + "], duration = [" + duration + "], bufferPercent = [" + bufferPercent + "]"); + } + } + + public void onUpdateThumbnail(Intent intent) { + if (DEBUG) Log.d(TAG, "onUpdateThumbnail() called with: intent = [" + intent + "]"); + if (!intent.getStringExtra(VIDEO_URL).equals(videoUrl)) return; + videoThumbnail = ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail; + } + + public void onVideoPlayPause() { + if (DEBUG) Log.d(TAG, "onVideoPlayPause() called"); + if (currentState == STATE_COMPLETED) { + changeState(STATE_LOADING); + if (qualityChanged) playVideo(getSelectedStreamUri(), true); + simpleExoPlayer.seekTo(0); + return; + } + simpleExoPlayer.setPlayWhenReady(!isPlaying()); + } + + public void onFastRewind() { + if (DEBUG) Log.d(TAG, "onFastRewind() called"); + seekBy(-FAST_FORWARD_REWIND_AMOUNT); + showAndAnimateControl(R.drawable.ic_action_av_fast_rewind, true); + animateView(controlsRoot, false, 100, 0); + } + + public void onFastForward() { + if (DEBUG) Log.d(TAG, "onFastForward() called"); + seekBy(FAST_FORWARD_REWIND_AMOUNT); + showAndAnimateControl(R.drawable.ic_action_av_fast_forward, true); + animateView(controlsRoot, false, 100, 0); + } + + /*////////////////////////////////////////////////////////////////////////// + // OnClick related + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onClick(View v) { + if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]"); + if (v.getId() == fullScreenButton.getId()) { + onFullScreenButtonClicked(); + } else if (v.getId() == qualityTextView.getId()) { + onQualitySelectorClicked(); + } + } + + /** + * Called when an item of the quality selector is selected + */ + @Override + public boolean onMenuItemClick(MenuItem menuItem) { + if (DEBUG) Log.d(TAG, "onMenuItemClick() called with: menuItem = [" + menuItem + "], menuItem.getItemId = [" + menuItem.getItemId() + "]"); + if (selectedIndexStream == menuItem.getItemId()) return true; + setVideoStartPos((int) getPlayer().getCurrentPosition()); + + if (!(getCurrentState() == STATE_COMPLETED)) playVideo(Uri.parse(getVideoStreamsList().get(menuItem.getItemId()).url), wasPlaying); + else qualityChanged = true; + + selectedIndexStream = menuItem.getItemId(); + qualityTextView.setText(menuItem.getTitle()); + return true; + } + + /** + * Called when the quality selector is dismissed + */ + @Override + public void onDismiss(PopupMenu menu) { + if (DEBUG) Log.d(TAG, "onDismiss() called with: menu = [" + menu + "]"); + isQualityPopupMenuVisible = false; + qualityTextView.setText(videoStreamsList.get(selectedIndexStream).resolution); + } + + public abstract void onFullScreenButtonClicked(); + + public void onQualitySelectorClicked() { + if (DEBUG) Log.d(TAG, "onQualitySelectorClicked() called"); + qualityPopupMenu.show(); + isQualityPopupMenuVisible = true; + animateView(getControlsRoot(), true, 300, 0); + + VideoStream videoStream = videoStreamsList.get(selectedIndexStream); + qualityTextView.setText(MediaFormat.getNameById(videoStream.format) + " " + videoStream.resolution); + wasPlaying = isPlaying(); + } + + public void onRepeatClicked() { + if (DEBUG) Log.d(TAG, "onRepeatClicked() called"); + // TODO: implement repeat all when playlist is implemented + + // Switch the modes between DISABLED and REPEAT_ONE, till playlist is implemented + setCurrentRepeatMode(getCurrentRepeatMode() == RepeatMode.REPEAT_DISABLED ? + RepeatMode.REPEAT_ONE : + RepeatMode.REPEAT_DISABLED); + + if (DEBUG) Log.d(TAG, "onRepeatClicked() currentRepeatMode = " + getCurrentRepeatMode().name()); + } + + /*////////////////////////////////////////////////////////////////////////// + // SeekBar Listener + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (DEBUG && fromUser) Log.d(TAG, "onProgressChanged() called with: seekBar = [" + seekBar + "], progress = [" + progress + "]"); + //if (fromUser) playbackCurrentTime.setText(getTimeString(progress)); + if (fromUser) currentDisplaySeek.setText(getTimeString(progress)); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + if (DEBUG) Log.d(TAG, "onStartTrackingTouch() called with: seekBar = [" + seekBar + "]"); + if (getCurrentState() != STATE_PAUSED_SEEK) changeState(STATE_PAUSED_SEEK); + + wasPlaying = isPlaying(); + if (isPlaying()) simpleExoPlayer.setPlayWhenReady(false); + + animateView(controlsRoot, true, 0, 0); + animateView(currentDisplaySeek, true, 300, 0); + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + if (DEBUG) Log.d(TAG, "onStopTrackingTouch() called with: seekBar = [" + seekBar + "]"); + + simpleExoPlayer.seekTo(seekBar.getProgress()); + if (wasPlaying || simpleExoPlayer.getDuration() == seekBar.getProgress()) simpleExoPlayer.setPlayWhenReady(true); + + playbackCurrentTime.setText(getTimeString(seekBar.getProgress())); + animateView(currentDisplaySeek, false, 200, 0); + + if (getCurrentState() == STATE_PAUSED_SEEK) changeState(STATE_BUFFERING); + if (!isProgressLoopRunning.get()) startProgressLoop(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + private static final StringBuilder stringBuilder = new StringBuilder(); + private static final Formatter formatter = new Formatter(stringBuilder, Locale.getDefault()); + + public String getTimeString(int milliSeconds) { + long seconds = (milliSeconds % 60000L) / 1000L; + long minutes = (milliSeconds % 3600000L) / 60000L; + long hours = (milliSeconds % 86400000L) / 3600000L; + long days = (milliSeconds % (86400000L * 7L)) / 86400000L; + + stringBuilder.setLength(0); + return days > 0 ? formatter.format("%d:%02d:%02d:%02d", days, hours, minutes, seconds).toString() + : hours > 0 ? formatter.format("%d:%02d:%02d", hours, minutes, seconds).toString() + : formatter.format("%02d:%02d", minutes, seconds).toString(); + } + + public boolean isControlsVisible() { + return controlsRoot != null && controlsRoot.getVisibility() == View.VISIBLE; + } + + /** + * Show a animation, and depending on goneOnEnd, will stay on the screen or be gone + * + * @param drawableId the drawable that will be used to animate, pass -1 to clear any animation that is visible + * @param goneOnEnd will set the animation view to GONE on the end of the animation + */ + public void showAndAnimateControl(final int drawableId, final boolean goneOnEnd) { + if (DEBUG) Log.d(TAG, "showAndAnimateControl() called with: drawableId = [" + drawableId + "], goneOnEnd = [" + goneOnEnd + "]"); + if (controlViewAnimator != null && controlViewAnimator.isRunning()) { + if (DEBUG) Log.d(TAG, "showAndAnimateControl: controlViewAnimator.isRunning"); + controlViewAnimator.end(); + } + + if (drawableId == -1) { + if (controlAnimationView.getVisibility() == View.VISIBLE) { + controlViewAnimator = ObjectAnimator.ofPropertyValuesHolder(controlAnimationView, + PropertyValuesHolder.ofFloat(View.ALPHA, 1f, 0f), + PropertyValuesHolder.ofFloat(View.SCALE_X, 1.4f, 1f), + PropertyValuesHolder.ofFloat(View.SCALE_Y, 1.4f, 1f) + ).setDuration(300); + controlViewAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + controlAnimationView.setVisibility(View.GONE); + } + }); + controlViewAnimator.start(); + } + return; + } + + float scaleFrom = goneOnEnd ? 1f : 1f, scaleTo = goneOnEnd ? 1.8f : 1.4f; + float alphaFrom = goneOnEnd ? 1f : 0f, alphaTo = goneOnEnd ? 0f : 1f; + + + controlViewAnimator = ObjectAnimator.ofPropertyValuesHolder(controlAnimationView, + PropertyValuesHolder.ofFloat(View.ALPHA, alphaFrom, alphaTo), + PropertyValuesHolder.ofFloat(View.SCALE_X, scaleFrom, scaleTo), + PropertyValuesHolder.ofFloat(View.SCALE_Y, scaleFrom, scaleTo) + ); + controlViewAnimator.setDuration(goneOnEnd ? 1000 : 500); + controlViewAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (goneOnEnd) controlAnimationView.setVisibility(View.GONE); + else controlAnimationView.setVisibility(View.VISIBLE); + } + }); + + + controlAnimationView.setVisibility(View.VISIBLE); + controlAnimationView.setImageDrawable(ContextCompat.getDrawable(context, drawableId)); + controlViewAnimator.start(); + } + + public void animateView(View view, boolean enterOrExit, long duration, long delay) { + animateView(view, enterOrExit, duration, delay, null, false); + } + + public void animateView(View view, boolean enterOrExit, long duration, long delay, boolean hideUi) { + animateView(view, enterOrExit, duration, delay, null, hideUi); + } + + public void animateView(final View view, final boolean enterOrExit, long duration, long delay, final Runnable execOnEnd) { + animateView(view, enterOrExit, duration, delay, execOnEnd, false); + } + + /** + * Animate the view + * + * @param view view that will be animated + * @param enterOrExit true to enter, false to exit + * @param duration how long the animation will take, in milliseconds + * @param delay how long the animation will wait to start, in milliseconds + * @param execOnEnd runnable that will be executed when the animation ends + * @param hideUi need to hide ui when animation ends, + * just a helper for classes extending this + */ + public void animateView(final View view, final boolean enterOrExit, long duration, long delay, final Runnable execOnEnd, boolean hideUi) { + if (DEBUG) { + Log.d(TAG, "animateView() called with: view = [" + view + "], enterOrExit = [" + enterOrExit + "], duration = [" + duration + "], delay = [" + delay + "], execOnEnd = [" + execOnEnd + "]"); + } + if (view.getVisibility() == View.VISIBLE && enterOrExit) { + if (DEBUG) Log.d(TAG, "animateView() view was already visible > view = [" + view + "]"); + view.animate().setListener(null).cancel(); + view.setVisibility(View.VISIBLE); + view.setAlpha(1f); + if (execOnEnd != null) execOnEnd.run(); + return; + } else if ((view.getVisibility() == View.GONE || view.getVisibility() == View.INVISIBLE) && !enterOrExit) { + if (DEBUG) Log.d(TAG, "animateView() view was already gone > view = [" + view + "]"); + view.animate().setListener(null).cancel(); + view.setVisibility(View.GONE); + view.setAlpha(0f); + if (execOnEnd != null) execOnEnd.run(); + return; + } + + view.animate().setListener(null).cancel(); + view.setVisibility(View.VISIBLE); + + if (view == controlsRoot) { + if (enterOrExit) { + view.animate().alpha(1f).setDuration(duration).setStartDelay(delay) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (execOnEnd != null) execOnEnd.run(); + } + }).start(); + } else { + view.animate().alpha(0f) + .setDuration(duration).setStartDelay(delay) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + view.setVisibility(View.GONE); + if (execOnEnd != null) execOnEnd.run(); + } + }) + .start(); + } + return; + } + + if (enterOrExit) { + view.setAlpha(0f); + view.setScaleX(.8f); + view.setScaleY(.8f); + view.animate().alpha(1f).scaleX(1f).scaleY(1f).setDuration(duration).setStartDelay(delay) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (execOnEnd != null) execOnEnd.run(); + } + }).start(); + } else { + view.setAlpha(1f); + view.setScaleX(1f); + view.setScaleY(1f); + view.animate().alpha(0f).scaleX(.8f).scaleY(.8f).setDuration(duration).setStartDelay(delay) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + view.setVisibility(View.GONE); + if (execOnEnd != null) execOnEnd.run(); + } + }) + .start(); + } + } + + private void seekBy(int milliSeconds) { + if (DEBUG) Log.d(TAG, "seekBy() called with: milliSeconds = [" + milliSeconds + "]"); + if (simpleExoPlayer == null) return; + int progress = (int) (simpleExoPlayer.getCurrentPosition() + milliSeconds); + simpleExoPlayer.seekTo(progress); + } + + public boolean isPlaying() { + return simpleExoPlayer.getPlaybackState() == ExoPlayer.STATE_READY && simpleExoPlayer.getPlayWhenReady(); + } + + public boolean isQualityMenuVisible() { + return isQualityPopupMenuVisible; + } + + private void startProgressLoop() { + progressLoop.removeCallbacksAndMessages(null); + isProgressLoopRunning.set(true); + progressLoop.post(progressUpdate); + } + + private void stopProgressLoop() { + isProgressLoopRunning.set(false); + progressLoop.removeCallbacksAndMessages(null); + } + + public void tryDeleteCacheFiles(Context context) { + File cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME); + + if (cacheDir.exists()) { + try { + if (cacheDir.isDirectory()) { + for (File file : cacheDir.listFiles()) { + try { + if (DEBUG) Log.d(TAG, "tryDeleteCacheFiles: " + file.getAbsolutePath() + " deleted = " + file.delete()); + } catch (Exception ignored) { + } + } + } + } catch (Exception ignored) { + } + } + } + + /*////////////////////////////////////////////////////////////////////////// + // Getters and Setters + //////////////////////////////////////////////////////////////////////////*/ + + public SimpleExoPlayer getPlayer() { + return simpleExoPlayer; + } + + public SharedPreferences getSharedPreferences() { + return sharedPreferences; + } + + public AspectRatioFrameLayout getAspectRatioFrameLayout() { + return aspectRatioFrameLayout; + } + + public SurfaceView getSurfaceView() { + return surfaceView; + } + + public RepeatMode getCurrentRepeatMode() { + return currentRepeatMode; + } + + public void setCurrentRepeatMode(RepeatMode mode) { + currentRepeatMode = mode; + } + + public boolean wasPlaying() { + return wasPlaying; + } + + public int getCurrentState() { + return currentState; + } + + public Uri getSelectedStreamUri() { + return Uri.parse(videoStreamsList.get(selectedIndexStream).url); + } + + public int getQualityPopupMenuGroupId() { + return qualityPopupMenuGroupId; + } + + public String getVideoUrl() { + return videoUrl; + } + + public void setVideoUrl(String videoUrl) { + this.videoUrl = videoUrl; + } + + public int getVideoStartPos() { + return videoStartPos; + } + + public void setVideoStartPos(int videoStartPos) { + this.videoStartPos = videoStartPos; + } + + public String getVideoTitle() { + return videoTitle; + } + + public void setVideoTitle(String videoTitle) { + this.videoTitle = videoTitle; + } + + public Bitmap getVideoThumbnail() { + return videoThumbnail; + } + + public void setVideoThumbnail(Bitmap videoThumbnail) { + this.videoThumbnail = videoThumbnail; + } + + public String getChannelName() { + return channelName; + } + + public void setChannelName(String channelName) { + this.channelName = channelName; + } + + public int getSelectedIndexStream() { + return selectedIndexStream; + } + + public void setSelectedIndexStream(int selectedIndexStream) { + this.selectedIndexStream = selectedIndexStream; + } + + public ArrayList getVideoStreamsList() { + return videoStreamsList; + } + + public void setVideoStreamsList(ArrayList videoStreamsList) { + this.videoStreamsList = videoStreamsList; + } + + public boolean isStartedFromNewPipe() { + return startedFromNewPipe; + } + + public void setStartedFromNewPipe(boolean startedFromNewPipe) { + this.startedFromNewPipe = startedFromNewPipe; + } + + public View getRootView() { + return rootView; + } + + public void setRootView(View rootView) { + this.rootView = rootView; + } + + public View getLoadingPanel() { + return loadingPanel; + } + + public ImageView getEndScreen() { + return endScreen; + } + + public ImageView getControlAnimationView() { + return controlAnimationView; + } + + public View getControlsRoot() { + return controlsRoot; + } + + public View getBottomControlsRoot() { + return bottomControlsRoot; + } + + public SeekBar getPlaybackSeekBar() { + return playbackSeekBar; + } + + public TextView getPlaybackCurrentTime() { + return playbackCurrentTime; + } + + public TextView getPlaybackEndTime() { + return playbackEndTime; + } + + public View getTopControlsRoot() { + return topControlsRoot; + } + + public TextView getQualityTextView() { + return qualityTextView; + } + + public ImageButton getFullScreenButton() { + return fullScreenButton; + } + + public PopupMenu getQualityPopupMenu() { + return qualityPopupMenu; + } + + public View getSurfaceForeground() { + return surfaceForeground; + } + + public TextView getCurrentDisplaySeek() { + return currentDisplaySeek; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java index d1c53d85a..88a5a914d 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java @@ -26,7 +26,6 @@ import org.schabi.newpipe.ActivityCommunicator; import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.R; import org.schabi.newpipe.detail.VideoItemDetailActivity; -import org.schabi.newpipe.detail.VideoItemDetailFragment; import org.schabi.newpipe.util.NavStack; import java.io.IOException; @@ -343,7 +342,7 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare /* NotificationCompat.Action pauseButton = new NotificationCompat.Action.Builder - (R.drawable.ic_pause_white_24dp, "Pause", playPI).build(); + (R.drawable.ic_pause_white, "Pause", playPI).build(); */ PendingIntent playPI = PendingIntent.getBroadcast(owner, noteID, @@ -465,7 +464,7 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare RemoteViews views = getContentView(), bigViews = getBigContentView(); int imageSrc; if(isPlaying) { - imageSrc = R.drawable.ic_pause_white_24dp; + imageSrc = R.drawable.ic_pause_white; } else { imageSrc = R.drawable.ic_play_circle_filled_white_24dp; } diff --git a/app/src/main/java/org/schabi/newpipe/player/ExoPlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ExoPlayerActivity.java index c868bb722..58e93fc61 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ExoPlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ExoPlayerActivity.java @@ -1,220 +1,559 @@ package org.schabi.newpipe.player; import android.app.Activity; +import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; +import android.graphics.Color; +import android.media.AudioManager; +import android.net.Uri; +import android.os.Build; import android.os.Bundle; +import android.support.annotation.Nullable; import android.util.Log; +import android.view.GestureDetector; +import android.view.MotionEvent; import android.view.View; import android.view.WindowManager; import android.widget.ImageButton; +import android.widget.PopupMenu; import android.widget.SeekBar; - -import com.devbrackets.android.exomedia.listener.OnCompletionListener; -import com.devbrackets.android.exomedia.listener.OnPreparedListener; -import com.devbrackets.android.exomedia.listener.VideoControlsVisibilityListener; -import com.devbrackets.android.exomedia.ui.widget.EMVideoView; -import com.devbrackets.android.exomedia.ui.widget.VideoControlsMobile; +import android.widget.TextView; +import android.widget.Toast; import org.schabi.newpipe.R; +import org.schabi.newpipe.util.NavStack; +import org.schabi.newpipe.util.PermissionHelper; +import org.schabi.newpipe.util.ThemeHelper; -public class ExoPlayerActivity extends Activity implements OnPreparedListener, OnCompletionListener { - private static final String TAG = "ExoPlayerActivity"; - private static final boolean DEBUG = false; - private EMVideoView videoView; - private CustomVideoControls videoControls; +/** + * Activity Player implementing AbstractPlayer + * + * @author mauriciocolli + */ +public class ExoPlayerActivity extends Activity { + private static final String TAG = ".ExoPlayerActivity"; + private static final boolean DEBUG = AbstractPlayer.DEBUG; - public static final String VIDEO_TITLE = "video_title"; - public static final String CHANNEL_NAME = "channel_name"; - private String videoTitle = ""; - private volatile String channelName = ""; - private int lastPosition; - private boolean isFinished; + private AudioManager audioManager; + private BroadcastReceiver broadcastReceiver; + private GestureDetector gestureDetector; + + private final Runnable hideUiRunnable = new Runnable() { + @Override + public void run() { + hideSystemUi(); + } + }; + private boolean activityPaused; + + private AbstractPlayerImpl playerImpl; + + /*////////////////////////////////////////////////////////////////////////// + // Activity LifeCycle + //////////////////////////////////////////////////////////////////////////*/ @Override - public void onCreate(Bundle savedInstanceState) { + protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); + if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]"); + ThemeHelper.setTheme(this, false); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) getWindow().setStatusBarColor(Color.BLACK); + setVolumeControlStream(AudioManager.STREAM_MUSIC); + audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + + if (getIntent() == null) { + Toast.makeText(this, R.string.general_error, Toast.LENGTH_SHORT).show(); + finish(); + return; + } + setContentView(R.layout.activity_exo_player); - videoView = (EMVideoView) findViewById(R.id.emVideoView); + playerImpl = new AbstractPlayerImpl(); + playerImpl.setup(findViewById(android.R.id.content)); + initReceiver(); + playerImpl.handleIntent(getIntent()); } @Override - protected void onStart() { - super.onStart(); - Intent intent = getIntent(); - videoTitle = intent.getStringExtra(VIDEO_TITLE); - channelName = intent.getStringExtra(CHANNEL_NAME); - videoView.setOnPreparedListener(this); - videoView.setOnCompletionListener(this); - videoView.setVideoURI(intent.getData()); - - videoControls = new CustomVideoControls(this); - videoControls.setTitle(videoTitle); - videoControls.setSubTitle(channelName); - - //We don't need these button until the playlist or queue is implemented - videoControls.setNextButtonRemoved(true); - videoControls.setPreviousButtonRemoved(true); - - videoControls.setVisibilityListener(new VideoControlsVisibilityListener() { - @Override - public void onControlsShown() { - if (DEBUG) Log.d(TAG, "------------ onControlsShown() called"); - showSystemUi(); - } - - @Override - public void onControlsHidden() { - if (DEBUG) Log.d(TAG, "------------ onControlsHidden() called"); - hideSystemUi(); - } - }); - videoView.setControls(videoControls); + protected void onNewIntent(Intent intent) { + if (DEBUG) Log.d(TAG, "onNewIntent() called with: intent = [" + intent + "]"); + super.onNewIntent(intent); + playerImpl.handleIntent(intent); } @Override - public void onPrepared() { - if (DEBUG) Log.d(TAG, "onPrepared() called"); - videoView.start(); + public void onBackPressed() { + if (DEBUG) Log.d(TAG, "onBackPressed() called"); + super.onBackPressed(); + if (playerImpl.isStartedFromNewPipe()) NavStack.getInstance().openDetailActivity(this, playerImpl.getVideoUrl(), 0); + if (playerImpl.isPlaying()) playerImpl.getPlayer().setPlayWhenReady(false); } @Override - public void onCompletion() { - if (DEBUG) Log.d(TAG, "onCompletion() called"); -// videoView.getVideoControls().setButtonListener(); - //videoView.restart(); - videoControls.setRewindButtonRemoved(true); - videoControls.setFastForwardButtonRemoved(true); - isFinished = true; - videoControls.getSeekBar().setEnabled(false); - } - - @Override - protected void onPause() { - super.onPause(); - videoView.stopPlayback(); - lastPosition = videoView.getCurrentPosition(); + protected void onStop() { + super.onStop(); + if (DEBUG) Log.d(TAG, "onStop() called"); + activityPaused = true; + playerImpl.destroy(); + playerImpl.setVideoStartPos((int) playerImpl.getPlayer().getCurrentPosition()); } @Override protected void onResume() { super.onResume(); - if (lastPosition > 0) videoView.seekTo(lastPosition); + if (DEBUG) Log.d(TAG, "onResume() called"); + if (activityPaused) { + //playerImpl.getPlayer().setPlayWhenReady(true); + playerImpl.getPlayPauseButton().setImageResource(R.drawable.ic_play_arrow_white); + playerImpl.initPlayer(); + playerImpl.playVideo(playerImpl.getSelectedStreamUri(), false); + activityPaused = false; + } } @Override protected void onDestroy() { super.onDestroy(); - videoView.stopPlayback(); + if (DEBUG) Log.d(TAG, "onDestroy() called"); + if (playerImpl != null) playerImpl.destroy(); + if (broadcastReceiver != null) unregisterReceiver(broadcastReceiver); } + /*////////////////////////////////////////////////////////////////////////// + // Init + //////////////////////////////////////////////////////////////////////////*/ + + private void initReceiver() { + if (DEBUG) Log.d(TAG, "initReceiver() called"); + broadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (DEBUG) Log.d(TAG, "onReceive() called with: context = [" + context + "], intent = [" + intent + "]"); + switch (intent.getAction()) { + case AbstractPlayer.ACTION_UPDATE_THUMB: + playerImpl.onUpdateThumbnail(intent); + break; + } + } + }; + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(AbstractPlayer.ACTION_UPDATE_THUMB); + registerReceiver(broadcastReceiver, intentFilter); + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + private void showSystemUi() { if (DEBUG) Log.d(TAG, "showSystemUi() called"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + getWindow().getDecorView().setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + ); + } else getWindow().getDecorView().setSystemUiVisibility(0); getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - getWindow().getDecorView().setSystemUiVisibility(0); } private void hideSystemUi() { if (DEBUG) Log.d(TAG, "hideSystemUi() called"); - if (android.os.Build.VERSION.SDK_INT >= 17) { - getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + if (android.os.Build.VERSION.SDK_INT >= 16) { + int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_FULLSCREEN - | View.SYSTEM_UI_FLAG_IMMERSIVE - | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) visibility |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; + getWindow().getDecorView().setSystemUiVisibility(visibility); } getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); } - private class CustomVideoControls extends VideoControlsMobile { - protected static final int FAST_FORWARD_REWIND_AMOUNT = 8000; + /////////////////////////////////////////////////////////////////////////// - protected ImageButton fastForwardButton; - protected ImageButton rewindButton; + @SuppressWarnings({"unused", "WeakerAccess"}) + private class AbstractPlayerImpl extends AbstractPlayer { + private TextView titleTextView; + private TextView channelTextView; + private TextView volumeTextView; + private TextView brightnessTextView; + private ImageButton repeatButton; - public CustomVideoControls(Context context) { - super(context); + private ImageButton playPauseButton; + + AbstractPlayerImpl() { + super("AbstractPlayerImpl" + ExoPlayerActivity.TAG, ExoPlayerActivity.this); } @Override - protected int getLayoutResource() { - return R.layout.exomedia_custom_controls; + public void initViews(View rootView) { + super.initViews(rootView); + this.titleTextView = (TextView) rootView.findViewById(R.id.titleTextView); + this.channelTextView = (TextView) rootView.findViewById(R.id.channelTextView); + this.volumeTextView = (TextView) rootView.findViewById(R.id.volumeTextView); + this.brightnessTextView = (TextView) rootView.findViewById(R.id.brightnessTextView); + this.repeatButton = (ImageButton) rootView.findViewById(R.id.repeatButton); + + this.playPauseButton = (ImageButton) rootView.findViewById(R.id.playPauseButton); + + // Due to a bug on lower API, lets set the alpha instead of using a drawable + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) repeatButton.setImageAlpha(77); + else { //noinspection deprecation + repeatButton.setAlpha(77); + } + } @Override - protected void retrieveViews() { - super.retrieveViews(); - rewindButton = (ImageButton) findViewById(R.id.exomedia_controls_frewind_btn); - fastForwardButton = (ImageButton) findViewById(R.id.exomedia_controls_fforward_btn); + public void initListeners() { + super.initListeners(); + + MySimpleOnGestureListener listener = new MySimpleOnGestureListener(); + gestureDetector = new GestureDetector(context, listener); + gestureDetector.setIsLongpressEnabled(false); + playerImpl.getRootView().setOnTouchListener(listener); + + repeatButton.setOnClickListener(this); + playPauseButton.setOnClickListener(this); } @Override - protected void registerListeners() { - super.registerListeners(); - rewindButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - onRewindClicked(); - } - }); - fastForwardButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - onFastForwardClicked(); - } - }); - } - - public boolean onFastForwardClicked() { - if (videoView == null) return false; - - int newPosition = videoView.getCurrentPosition() + FAST_FORWARD_REWIND_AMOUNT; - if (newPosition > seekBar.getMax()) newPosition = seekBar.getMax(); - - performSeek(newPosition); - return true; - } - - public boolean onRewindClicked() { - if (videoView == null) return false; - - int newPosition = videoView.getCurrentPosition() - FAST_FORWARD_REWIND_AMOUNT; - if (newPosition < 0) newPosition = 0; - - performSeek(newPosition); - return true; + public void handleIntent(Intent intent) { + super.handleIntent(intent); + titleTextView.setText(getVideoTitle()); + channelTextView.setText(getChannelName()); } @Override - public void setFastForwardButtonRemoved(boolean removed) { - fastForwardButton.setVisibility(removed ? View.GONE : View.VISIBLE); + public void playVideo(Uri videoURI, boolean autoPlay) { + super.playVideo(videoURI, autoPlay); + playPauseButton.setImageResource(autoPlay ? R.drawable.ic_pause_white : R.drawable.ic_play_arrow_white); } @Override - public void setRewindButtonRemoved(boolean removed) { - rewindButton.setVisibility(removed ? View.GONE : View.VISIBLE); + public void onFullScreenButtonClicked() { + if (DEBUG) Log.d(TAG, "onFullScreenButtonClicked() called"); + if (playerImpl.getPlayer() == null) return; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + && !PermissionHelper.checkSystemAlertWindowPermission(ExoPlayerActivity.this)) { + Toast.makeText(ExoPlayerActivity.this, R.string.msg_popup_permission, Toast.LENGTH_LONG).show(); + return; + } + + Intent i = new Intent(ExoPlayerActivity.this, PopupVideoPlayer.class); + i.putExtra(AbstractPlayer.VIDEO_TITLE, getVideoTitle()) + .putExtra(AbstractPlayer.CHANNEL_NAME, getChannelName()) + .putExtra(AbstractPlayer.VIDEO_URL, getVideoUrl()) + .putExtra(AbstractPlayer.INDEX_SEL_VIDEO_STREAM, getSelectedIndexStream()) + .putExtra(AbstractPlayer.VIDEO_STREAMS_LIST, getVideoStreamsList()) + .putExtra(AbstractPlayer.START_POSITION, ((int) getPlayer().getCurrentPosition())); + context.startService(i); + ((View) getControlAnimationView().getParent()).setVisibility(View.GONE); + if (playerImpl.isPlaying()) playerImpl.getPlayer().setPlayWhenReady(false); + ExoPlayerActivity.this.finish(); } @Override - protected void onPlayPauseClick() { - super.onPlayPauseClick(); - if (videoView == null) return; - if (DEBUG) Log.d(TAG, "onPlayPauseClick() called" + videoView.getDuration() + " position= " + videoView.getCurrentPosition()); - if (isFinished) { - videoView.restart(); - setRewindButtonRemoved(false); - setFastForwardButtonRemoved(false); - isFinished = false; - seekBar.setEnabled(true); + @SuppressWarnings("deprecation") + public void onRepeatClicked() { + super.onRepeatClicked(); + if (DEBUG) Log.d(TAG, "onRepeatClicked() called"); + switch (getCurrentRepeatMode()) { + case REPEAT_DISABLED: + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) repeatButton.setImageAlpha(77); + else repeatButton.setAlpha(77); + + break; + case REPEAT_ONE: + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) repeatButton.setImageAlpha(255); + else repeatButton.setAlpha(255); + + break; + case REPEAT_ALL: + // Waiting :) + break; } } - private void performSeek(int newPosition) { - internalListener.onSeekEnded(newPosition); + @Override + public void onClick(View v) { + super.onClick(v); + if (v.getId() == repeatButton.getId()) onRepeatClicked(); + else if (v.getId() == playPauseButton.getId()) onVideoPlayPause(); + + if (getCurrentState() != STATE_COMPLETED) { + animateView(playerImpl.getControlsRoot(), true, 300, 0, new Runnable() { + @Override + public void run() { + if (getCurrentState() == STATE_PLAYING && !playerImpl.isQualityMenuVisible()) { + animateView(playerImpl.getControlsRoot(), false, 300, DEFAULT_CONTROLS_HIDE_TIME, true); + } + } + }, false); + } } - public SeekBar getSeekBar() { - return seekBar; + @Override + public void onVideoPlayPause() { + super.onVideoPlayPause(); + if (getPlayer().getPlayWhenReady()) { + animateView(playPauseButton, false, 80, 0, new Runnable() { + @Override + public void run() { + playPauseButton.setImageResource(R.drawable.ic_pause_white); + animateView(playPauseButton, true, 200, 0); + } + }); + } else { + animateView(playPauseButton, false, 80, 0, new Runnable() { + @Override + public void run() { + playPauseButton.setImageResource(R.drawable.ic_play_arrow_white); + animateView(playPauseButton, true, 200, 0); + } + }); + } + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + super.onStopTrackingTouch(seekBar); + if (playerImpl.wasPlaying()) { + hideSystemUi(); + playerImpl.getControlsRoot().setVisibility(View.GONE); + } + } + + @Override + public void onDismiss(PopupMenu menu) { + super.onDismiss(menu); + if (isPlaying()) animateView(getControlsRoot(), false, 500, 0, true); + } + + @Override + public void onError() { + Toast.makeText(context, "Failed to play this video", Toast.LENGTH_SHORT).show(); + finish(); + } + + /*////////////////////////////////////////////////////////////////////////// + // States + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onLoading() { + super.onLoading(); + hideSystemUi(); + playPauseButton.setImageResource(R.drawable.ic_pause_white); + } + + @Override + public void onPaused() { + super.onPaused(); + animateView(playPauseButton, true, 100, 0); + showSystemUi(); + } + + @Override + public void onPausedSeek() { + super.onPausedSeek(); + animateView(playPauseButton, false, 100, 0); + } + + @Override + public void onPlaying() { + super.onPlaying(); + animateView(playPauseButton, true, 500, 0); + } + + @Override + public void onCompleted() { + if (getCurrentRepeatMode() == RepeatMode.REPEAT_ONE) { + playPauseButton.setImageResource(R.drawable.ic_pause_white); + } else { + showSystemUi(); + animateView(playPauseButton, false, 0, 0, new Runnable() { + @Override + public void run() { + playPauseButton.setImageResource(R.drawable.ic_replay_white); + animateView(playPauseButton, true, 300, 0); + } + }); + } + super.onCompleted(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void animateView(View view, boolean enterOrExit, long duration, long delay, final Runnable execOnEnd, boolean hideUi) { + //if (execOnEnd == null) playerImpl.setDefaultAnimationEnd(hideUiRunnable); + + if (hideUi && execOnEnd != null) { + Runnable combinedRunnable = new Runnable() { + @Override + public void run() { + execOnEnd.run(); + hideUiRunnable.run(); + } + }; + super.animateView(view, enterOrExit, duration, delay, combinedRunnable, true); + } else super.animateView(view, enterOrExit, duration, delay, hideUi ? hideUiRunnable : execOnEnd, hideUi); + } + + /////////////////////////////////////////////////////////////////////////// + // Getters + /////////////////////////////////////////////////////////////////////////// + + public TextView getTitleTextView() { + return titleTextView; + } + + public TextView getChannelTextView() { + return channelTextView; + } + + public TextView getVolumeTextView() { + return volumeTextView; + } + + public TextView getBrightnessTextView() { + return brightnessTextView; + } + + public ImageButton getRepeatButton() { + return repeatButton; + } + + public ImageButton getPlayPauseButton() { + return playPauseButton; } } -} + + private class MySimpleOnGestureListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener { + private boolean isMoving; + + @Override + public boolean onDoubleTap(MotionEvent e) { + if (DEBUG) Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY()); + if (!playerImpl.isPlaying()) return false; + if (e.getX() > playerImpl.getRootView().getWidth() / 2) playerImpl.onFastForward(); + else playerImpl.onFastRewind(); + return true; + } + + @Override + public boolean onSingleTapConfirmed(MotionEvent e) { + if (DEBUG) Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]"); + if (playerImpl.getCurrentState() != StateInterface.STATE_PLAYING) return true; + + if (playerImpl.isControlsVisible()) playerImpl.animateView(playerImpl.getControlsRoot(), false, 150, 0, true); + else { + playerImpl.animateView(playerImpl.getControlsRoot(), true, 500, 0, new Runnable() { + @Override + public void run() { + playerImpl.animateView(playerImpl.getControlsRoot(), false, 300, AbstractPlayer.DEFAULT_CONTROLS_HIDE_TIME, true); + } + }); + showSystemUi(); + } + return true; + } + + private final float stepsBrightness = 21, stepBrightness = (1f / stepsBrightness), minBrightness = .01f; + private float currentBrightness = .5f; + + private int currentVolume, maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); + + private final String brightnessUnicode = new String(Character.toChars(0x2600)); + // private final String volumeUnicode = new String(Character.toChars(0x1F50A)); + private final String volumeUnicode = new String(Character.toChars(0x1F508)); + + + private final int MOVEMENT_THRESHOLD = 40; + private final int eventsThreshold = 3; + private boolean triggered = false; + private int eventsNum; + + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + //noinspection PointlessBooleanExpression + if (DEBUG && false) Log.d(TAG, "ExoPlayerActivity.onScroll = " + + ", e1.getRaw = [" + e1.getRawX() + ", " + e1.getRawY() + "]" + + ", e2.getRaw = [" + e2.getRawX() + ", " + e2.getRawY() + "]" + + ", distanceXy = [" + distanceX + ", " + distanceY + "]"); + float abs = Math.abs(e2.getY() - e1.getY()); + if (!triggered) { + triggered = abs > MOVEMENT_THRESHOLD; + return false; + } + + if (eventsNum++ % eventsThreshold != 0 || playerImpl.getCurrentState() == StateInterface.STATE_COMPLETED) return false; + isMoving = true; +// boolean up = !((e2.getY() - e1.getY()) > 0) && distanceY > 0; // Android's origin point is on top + boolean up = distanceY > 0; + + + if (e1.getX() > playerImpl.getRootView().getWidth() / 2) { + currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) + (up ? 1 : -1); + if (currentVolume >= maxVolume) currentVolume = maxVolume; + if (currentVolume <= 0) currentVolume = 0; + audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, currentVolume, 0); + + if (DEBUG) Log.d(TAG, "onScroll().volumeControl, currentVolume = " + currentVolume); + playerImpl.getVolumeTextView().setText(volumeUnicode + " " + Math.round((((float) currentVolume) / maxVolume) * 100) + "%"); + + if (playerImpl.getVolumeTextView().getVisibility() != View.VISIBLE) playerImpl.animateView(playerImpl.getVolumeTextView(), true, 200, 0); + if (playerImpl.getBrightnessTextView().getVisibility() == View.VISIBLE) playerImpl.getBrightnessTextView().setVisibility(View.GONE); + } else { + WindowManager.LayoutParams lp = getWindow().getAttributes(); + currentBrightness += up ? stepBrightness : -stepBrightness; + if (currentBrightness >= 1f) currentBrightness = 1f; + if (currentBrightness <= minBrightness) currentBrightness = minBrightness; + + lp.screenBrightness = currentBrightness; + getWindow().setAttributes(lp); + if (DEBUG) Log.d(TAG, "onScroll().brightnessControl, currentBrightness = " + currentBrightness); + int brightnessNormalized = Math.round(currentBrightness * 100); + + playerImpl.getBrightnessTextView().setText(brightnessUnicode + " " + (brightnessNormalized == 1 ? 0 : brightnessNormalized) + "%"); + + if (playerImpl.getBrightnessTextView().getVisibility() != View.VISIBLE) playerImpl.animateView(playerImpl.getBrightnessTextView(), true, 200, 0); + if (playerImpl.getVolumeTextView().getVisibility() == View.VISIBLE) playerImpl.getVolumeTextView().setVisibility(View.GONE); + } + return true; + } + + private void onScrollEnd() { + if (DEBUG) Log.d(TAG, "onScrollEnd() called"); + triggered = false; + eventsNum = 0; + /* if (playerImpl.getVolumeTextView().getVisibility() == View.VISIBLE) playerImpl.getVolumeTextView().setVisibility(View.GONE); + if (playerImpl.getBrightnessTextView().getVisibility() == View.VISIBLE) playerImpl.getBrightnessTextView().setVisibility(View.GONE);*/ + if (playerImpl.getVolumeTextView().getVisibility() == View.VISIBLE) playerImpl.animateView(playerImpl.getVolumeTextView(), false, 200, 200); + if (playerImpl.getBrightnessTextView().getVisibility() == View.VISIBLE) playerImpl.animateView(playerImpl.getBrightnessTextView(), false, 200, 200); + + if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == StateInterface.STATE_PLAYING) { + playerImpl.animateView(playerImpl.getControlsRoot(), false, 300, AbstractPlayer.DEFAULT_CONTROLS_HIDE_TIME); + } + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + gestureDetector.onTouchEvent(event); + if (event.getAction() == MotionEvent.ACTION_UP && isMoving) { + isMoving = false; + onScrollEnd(); + } + return true; + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayVideoActivity.java b/app/src/main/java/org/schabi/newpipe/player/PlayVideoActivity.java index f13e73b49..49da537ac 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PlayVideoActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/PlayVideoActivity.java @@ -5,9 +5,8 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.content.res.Configuration; -import android.graphics.drawable.Drawable; -import android.media.MediaPlayer; import android.media.AudioManager; +import android.media.MediaPlayer; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -28,7 +27,6 @@ import android.widget.MediaController; import android.widget.ProgressBar; import android.widget.VideoView; -import org.schabi.newpipe.App; import org.schabi.newpipe.R; /** diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index e396ac1b6..661cb1632 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -1,10 +1,5 @@ package org.schabi.newpipe.player; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ObjectAnimator; -import android.animation.PropertyValuesHolder; -import android.animation.ValueAnimator; import android.annotation.SuppressLint; import android.app.NotificationManager; import android.app.PendingIntent; @@ -13,17 +8,15 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.SharedPreferences; import android.content.res.Configuration; +import android.content.res.Resources; import android.graphics.Bitmap; -import android.graphics.Color; import android.graphics.PixelFormat; import android.net.Uri; +import android.os.Build; import android.os.Handler; import android.os.IBinder; -import android.preference.PreferenceManager; import android.support.v4.app.NotificationCompat; -import android.support.v4.content.ContextCompat; import android.util.DisplayMetrics; import android.util.Log; import android.view.GestureDetector; @@ -31,17 +24,10 @@ import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.WindowManager; +import android.widget.PopupMenu; import android.widget.RemoteViews; -import android.widget.SeekBar; import android.widget.Toast; -import com.devbrackets.android.exomedia.listener.OnCompletionListener; -import com.devbrackets.android.exomedia.listener.OnErrorListener; -import com.devbrackets.android.exomedia.listener.OnPreparedListener; -import com.devbrackets.android.exomedia.listener.OnSeekCompletionListener; -import com.devbrackets.android.exomedia.ui.widget.EMVideoView; -import com.devbrackets.android.exomedia.util.Repeater; -import com.devbrackets.android.exomedia.util.TimeFormatUtil; import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener; @@ -56,210 +42,204 @@ import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.stream_info.StreamExtractor; import org.schabi.newpipe.extractor.stream_info.StreamInfo; import org.schabi.newpipe.extractor.stream_info.VideoStream; -import org.schabi.newpipe.player.popup.PopupViewHolder; -import org.schabi.newpipe.player.popup.StateInterface; import org.schabi.newpipe.util.NavStack; +import org.schabi.newpipe.util.ThemeHelper; import java.io.IOException; +import java.util.ArrayList; -public class PopupVideoPlayer extends Service implements StateInterface { +/** + * Service Popup Player implementing AbstractPlayer + * + * @author mauriciocolli + */ +public class PopupVideoPlayer extends Service { private static final String TAG = ".PopupVideoPlayer"; - private static final boolean DEBUG = false; - private static int CURRENT_STATE = -1; + private static final boolean DEBUG = AbstractPlayer.DEBUG; private static final int NOTIFICATION_ID = 40028922; - protected static final int FAST_FORWARD_REWIND_AMOUNT = 10000; // 10 Seconds - protected static final int DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds + public static final String ACTION_CLOSE = "org.schabi.newpipe.player.PopupVideoPlayer.CLOSE"; + public static final String ACTION_PLAY_PAUSE = "org.schabi.newpipe.player.PopupVideoPlayer.PLAY_PAUSE"; + public static final String ACTION_OPEN_DETAIL = "org.schabi.newpipe.player.PopupVideoPlayer.OPEN_DETAIL"; + public static final String ACTION_REPEAT = "org.schabi.newpipe.player.PopupVideoPlayer.REPEAT"; private BroadcastReceiver broadcastReceiver; - private InternalListener internalListener; private WindowManager windowManager; private WindowManager.LayoutParams windowLayoutParams; private GestureDetector gestureDetector; - private ValueAnimator controlViewAnimator; - private PopupViewHolder viewHolder; - private EMVideoView emVideoView; private float screenWidth, screenHeight; private float popupWidth, popupHeight; - private float currentPopupHeight = 200; + private float currentPopupHeight = 110.0f * Resources.getSystem().getDisplayMetrics().density; //private float minimumHeight = 100; // TODO: Use it when implementing the resize of the popup - public static final String VIDEO_URL = "video_url"; - public static final String STREAM_URL = "stream_url"; - public static final String VIDEO_TITLE = "video_title"; - public static final String CHANNEL_NAME = "channel_name"; - + private final String setAlphaMethodName = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) ? "setImageAlpha" : "setAlpha"; private NotificationManager notificationManager; private NotificationCompat.Builder notBuilder; private RemoteViews notRemoteView; - private Uri streamUri; - private String videoUrl = ""; - private String videoTitle = ""; - private volatile String channelName = ""; private ImageLoader imageLoader = ImageLoader.getInstance(); - private DisplayImageOptions displayImageOptions = - new DisplayImageOptions.Builder().cacheInMemory(true).build(); - private volatile Bitmap videoThumbnail; + private DisplayImageOptions displayImageOptions = new DisplayImageOptions.Builder().cacheInMemory(true).build(); - private Repeater progressPollRepeater = new Repeater(); - private SharedPreferences sharedPreferences; + private AbstractPlayerImpl playerImpl; + + /*////////////////////////////////////////////////////////////////////////// + // Service LifeCycle + //////////////////////////////////////////////////////////////////////////*/ @Override public void onCreate() { windowManager = (WindowManager) getSystemService(WINDOW_SERVICE); notificationManager = ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)); - internalListener = new InternalListener(); - viewHolder = new PopupViewHolder(null); - progressPollRepeater.setRepeatListener(internalListener); - progressPollRepeater.setRepeaterDelay(500); - sharedPreferences = PreferenceManager.getDefaultSharedPreferences(PopupVideoPlayer.this); initReceiver(); + + playerImpl = new AbstractPlayerImpl(); + ThemeHelper.setTheme(this, false); } + @Override + @SuppressWarnings("unchecked") + public int onStartCommand(final Intent intent, int flags, int startId) { + if (DEBUG) Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "], flags = [" + flags + "], startId = [" + startId + "]"); + if (playerImpl.getPlayer() == null) initPopup(); + if (!playerImpl.isPlaying()) playerImpl.getPlayer().setPlayWhenReady(true); + + if (imageLoader != null) imageLoader.clearMemoryCache(); + if (intent.getStringExtra(NavStack.URL) != null) { + playerImpl.setStartedFromNewPipe(false); + Thread fetcher = new Thread(new FetcherRunnable(intent)); + fetcher.start(); + } else { + playerImpl.setStartedFromNewPipe(true); + playerImpl.handleIntent(intent); + } + return START_NOT_STICKY; + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + updateScreenSize(); + } + + @Override + public void onDestroy() { + if (DEBUG) Log.d(TAG, "onDestroy() called"); + stopForeground(true); + if (playerImpl != null) { + playerImpl.destroy(); + if (playerImpl.getRootView() != null) windowManager.removeView(playerImpl.getRootView()); + } + if (imageLoader != null) imageLoader.clearMemoryCache(); + if (notificationManager != null) notificationManager.cancel(NOTIFICATION_ID); + if (broadcastReceiver != null) unregisterReceiver(broadcastReceiver); + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + /*////////////////////////////////////////////////////////////////////////// + // Init + //////////////////////////////////////////////////////////////////////////*/ + private void initReceiver() { broadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - if (DEBUG) - Log.d(TAG, "onReceive() called with: context = [" + context + "], intent = [" + intent + "]"); + if (DEBUG) Log.d(TAG, "onReceive() called with: context = [" + context + "], intent = [" + intent + "]"); switch (intent.getAction()) { - case InternalListener.ACTION_CLOSE: - internalListener.onVideoClose(); + case ACTION_CLOSE: + onVideoClose(); break; - case InternalListener.ACTION_PLAY_PAUSE: - internalListener.onVideoPlayPause(); + case ACTION_PLAY_PAUSE: + playerImpl.onVideoPlayPause(); break; - case InternalListener.ACTION_OPEN_DETAIL: - internalListener.onOpenDetail(PopupVideoPlayer.this, videoUrl); + case ACTION_OPEN_DETAIL: + onOpenDetail(PopupVideoPlayer.this, playerImpl.getVideoUrl()); break; - case InternalListener.ACTION_UPDATE_THUMB: - internalListener.onUpdateThumbnail(intent); + case ACTION_REPEAT: + playerImpl.onRepeatClicked(); + break; + case AbstractPlayer.ACTION_UPDATE_THUMB: + playerImpl.onUpdateThumbnail(intent); break; } } }; IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(InternalListener.ACTION_CLOSE); - intentFilter.addAction(InternalListener.ACTION_PLAY_PAUSE); - intentFilter.addAction(InternalListener.ACTION_OPEN_DETAIL); - intentFilter.addAction(InternalListener.ACTION_UPDATE_THUMB); + intentFilter.addAction(ACTION_CLOSE); + intentFilter.addAction(ACTION_PLAY_PAUSE); + intentFilter.addAction(ACTION_OPEN_DETAIL); + intentFilter.addAction(ACTION_REPEAT); + intentFilter.addAction(AbstractPlayer.ACTION_UPDATE_THUMB); registerReceiver(broadcastReceiver, intentFilter); } - @SuppressLint({"RtlHardcoded"}) + @SuppressLint("RtlHardcoded") private void initPopup() { if (DEBUG) Log.d(TAG, "initPopup() called"); View rootView = View.inflate(this, R.layout.player_popup, null); - viewHolder = new PopupViewHolder(rootView); - viewHolder.getPlaybackSeekBar().setOnSeekBarChangeListener(internalListener); - emVideoView = viewHolder.getVideoView(); - emVideoView.setOnPreparedListener(internalListener); - emVideoView.setOnCompletionListener(internalListener); - emVideoView.setOnErrorListener(internalListener); - emVideoView.setOnSeekCompletionListener(internalListener); + playerImpl.setup(rootView); + + updateScreenSize(); windowLayoutParams = new WindowManager.LayoutParams( (int) getMinimumVideoWidth(currentPopupHeight), (int) currentPopupHeight, WindowManager.LayoutParams.TYPE_PHONE, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSLUCENT); + windowLayoutParams.gravity = Gravity.LEFT | Gravity.TOP; MySimpleOnGestureListener listener = new MySimpleOnGestureListener(); gestureDetector = new GestureDetector(this, listener); gestureDetector.setIsLongpressEnabled(false); rootView.setOnTouchListener(listener); - updateScreenSize(); - + playerImpl.getLoadingPanel().setMinimumWidth(windowLayoutParams.width); + playerImpl.getLoadingPanel().setMinimumHeight(windowLayoutParams.height); windowManager.addView(rootView, windowLayoutParams); } - @Override - public int onStartCommand(final Intent intent, int flags, int startId) { - if (DEBUG) Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "], flags = [" + flags + "], startId = [" + startId + "]"); - if (emVideoView == null) initPopup(); - - if (intent.getStringExtra(NavStack.URL) != null) { - Thread fetcher = new Thread(new FetcherRunnable(intent)); - fetcher.start(); - } else { - if (imageLoader != null) imageLoader.clearMemoryCache(); - streamUri = Uri.parse(intent.getStringExtra(STREAM_URL)); - videoUrl = intent.getStringExtra(VIDEO_URL); - videoTitle = intent.getStringExtra(VIDEO_TITLE); - channelName = intent.getStringExtra(CHANNEL_NAME); - try { - videoThumbnail = ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail; - } catch (Exception e) { - e.printStackTrace(); - } - playVideo(streamUri); - } - return START_NOT_STICKY; - } - - private float getMinimumVideoWidth(float height) { - float width = height * (16.0f / 9.0f); // Respect the 16:9 ratio that most videos have - if (DEBUG) Log.d(TAG, "getMinimumVideoWidth() called with: height = [" + height + "], returned: " + width); - return width; - } - - private void updateScreenSize() { - DisplayMetrics metrics = new DisplayMetrics(); - windowManager.getDefaultDisplay().getMetrics(metrics); - - screenWidth = metrics.widthPixels; - screenHeight = metrics.heightPixels; - if (DEBUG) Log.d(TAG, "updateScreenSize() called > screenWidth = " + screenWidth + ", screenHeight = " + screenHeight); - } - - private void seekBy(int milliSeconds) { - if (emVideoView == null) return; - int progress = emVideoView.getCurrentPosition() + milliSeconds; - emVideoView.seekTo(progress); - } - - private void playVideo(Uri videoURI) { - if (DEBUG) Log.d(TAG, "playVideo() called with: streamUri = [" + streamUri + "]"); - - changeState(STATE_LOADING); - - windowLayoutParams.width = (int) getMinimumVideoWidth(currentPopupHeight); - windowManager.updateViewLayout(viewHolder.getRootView(), windowLayoutParams); - - if (videoURI == null || emVideoView == null || viewHolder.getRootView() == null) { - Toast.makeText(this, "Failed to play this video", Toast.LENGTH_SHORT).show(); - stopSelf(); - return; - } - if (emVideoView.isPlaying()) emVideoView.stopPlayback(); - emVideoView.setVideoURI(videoURI); - - notBuilder = createNotification(); - startForeground(NOTIFICATION_ID, notBuilder.build()); - notificationManager.notify(NOTIFICATION_ID, this.notBuilder.build()); - } + /*////////////////////////////////////////////////////////////////////////// + // Notification + //////////////////////////////////////////////////////////////////////////*/ private NotificationCompat.Builder createNotification() { notRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_popup_notification); - if (videoThumbnail != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, videoThumbnail); + + if (playerImpl.getVideoThumbnail() != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, playerImpl.getVideoThumbnail()); else notRemoteView.setImageViewResource(R.id.notificationCover, R.drawable.dummy_thumbnail); + notRemoteView.setTextViewText(R.id.notificationSongName, playerImpl.getVideoTitle()); + notRemoteView.setTextViewText(R.id.notificationArtist, playerImpl.getChannelName()); + notRemoteView.setOnClickPendingIntent(R.id.notificationPlayPause, - PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(InternalListener.ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT)); + PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT)); notRemoteView.setOnClickPendingIntent(R.id.notificationStop, - PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(InternalListener.ACTION_CLOSE), PendingIntent.FLAG_UPDATE_CURRENT)); - notRemoteView.setTextViewText(R.id.notificationSongName, videoTitle); - notRemoteView.setTextViewText(R.id.notificationArtist, channelName); + PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_CLOSE), PendingIntent.FLAG_UPDATE_CURRENT)); notRemoteView.setOnClickPendingIntent(R.id.notificationContent, - PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(InternalListener.ACTION_OPEN_DETAIL), PendingIntent.FLAG_UPDATE_CURRENT)); + PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_OPEN_DETAIL), PendingIntent.FLAG_UPDATE_CURRENT)); + notRemoteView.setOnClickPendingIntent(R.id.notificationRepeat, + PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_REPEAT), PendingIntent.FLAG_UPDATE_CURRENT)); + + switch (playerImpl.getCurrentRepeatMode()) { + case REPEAT_DISABLED: + notRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, 77); + break; + case REPEAT_ONE: + notRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, 255); + break; + case REPEAT_ALL: + // Waiting :) + break; + } return new NotificationCompat.Builder(this) .setOngoing(true) - .setSmallIcon(R.drawable.ic_play_arrow_white_48dp) + .setSmallIcon(R.drawable.ic_play_arrow_white) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setContent(notRemoteView); } @@ -276,384 +256,175 @@ public class PopupVideoPlayer extends Service implements StateInterface { notificationManager.notify(NOTIFICATION_ID, notBuilder.build()); } - /** - * Show a animation, and depending on goneOnEnd, will stay on the screen or be gone - * - * @param drawableId the drawable that will be used to animate, pass -1 to clear any animation that is visible - * @param goneOnEnd will set the animation view to GONE on the end of the animation - */ - private void showAndAnimateControl(final int drawableId, final boolean goneOnEnd) { - if (DEBUG) Log.d(TAG, "showAndAnimateControl() called with: drawableId = [" + drawableId + "], goneOnEnd = [" + goneOnEnd + "]"); - if (controlViewAnimator != null && controlViewAnimator.isRunning()) { - if (DEBUG) Log.d(TAG, "showAndAnimateControl: controlViewAnimator.isRunning"); - controlViewAnimator.end(); - } - if (drawableId == -1) { - if (viewHolder.getControlAnimationView().getVisibility() == View.VISIBLE) { - controlViewAnimator = ObjectAnimator.ofPropertyValuesHolder(viewHolder.getControlAnimationView(), - PropertyValuesHolder.ofFloat(View.ALPHA, 1f, 0f), - PropertyValuesHolder.ofFloat(View.SCALE_X, 1.4f, 1f), - PropertyValuesHolder.ofFloat(View.SCALE_Y, 1.4f, 1f) - ).setDuration(300); - controlViewAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - viewHolder.getControlAnimationView().setVisibility(View.GONE); - } - }); - controlViewAnimator.start(); - } - return; - } + /*////////////////////////////////////////////////////////////////////////// + // Misc + //////////////////////////////////////////////////////////////////////////*/ - float scaleFrom = goneOnEnd ? 1f : 1f, scaleTo = goneOnEnd ? 1.8f : 1.4f; - float alphaFrom = goneOnEnd ? 1f : 0f, alphaTo = goneOnEnd ? 0f : 1f; - - - controlViewAnimator = ObjectAnimator.ofPropertyValuesHolder(viewHolder.getControlAnimationView(), - PropertyValuesHolder.ofFloat(View.ALPHA, alphaFrom, alphaTo), - PropertyValuesHolder.ofFloat(View.SCALE_X, scaleFrom, scaleTo), - PropertyValuesHolder.ofFloat(View.SCALE_Y, scaleFrom, scaleTo) - ); - controlViewAnimator.setDuration(goneOnEnd ? 1000 : 500); - controlViewAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - if (goneOnEnd) viewHolder.getControlAnimationView().setVisibility(View.GONE); - else viewHolder.getControlAnimationView().setVisibility(View.VISIBLE); - } - }); - - - viewHolder.getControlAnimationView().setVisibility(View.VISIBLE); - viewHolder.getControlAnimationView().setImageDrawable(ContextCompat.getDrawable(PopupVideoPlayer.this, drawableId)); - controlViewAnimator.start(); + public void onVideoClose() { + if (DEBUG) Log.d(TAG, "onVideoClose() called"); + stopSelf(); } - /** - * Animate the view - * - * @param enterOrExit true to enter, false to exit - * @param duration how long the animation will take, in milliseconds - * @param delay how long the animation will wait to start, in milliseconds - */ - private void animateView(final View view, final boolean enterOrExit, long duration, long delay) { - if (DEBUG) Log.d(TAG, "animateView() called with: view = [" + view + "], enterOrExit = [" + enterOrExit + "], duration = [" + duration + "], delay = [" + delay + "]"); - if (view.getVisibility() == View.VISIBLE && enterOrExit) { - if (DEBUG) Log.d(TAG, "animateLoadingPanel() > view.getVisibility() == View.VISIBLE && enterOrExit"); - view.animate().setListener(null).cancel(); - view.setVisibility(View.VISIBLE); - return; - } - - view.animate().setListener(null).cancel(); - view.setVisibility(View.VISIBLE); - - if (view == viewHolder.getControlsRoot()) { - if (enterOrExit) { - view.setAlpha(0f); - view.animate().alpha(1f).setDuration(duration).setStartDelay(delay).setListener(null).start(); - } else { - view.setAlpha(1f); - view.animate().alpha(0f) - .setDuration(duration).setStartDelay(delay) - .setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - view.setVisibility(View.GONE); - } - }) - .start(); - } - return; - } - - if (enterOrExit) { - view.setAlpha(0f); - view.setScaleX(.8f); - view.setScaleY(.8f); - view.animate().alpha(1f).scaleX(1f).scaleY(1f).setDuration(duration).setStartDelay(delay).setListener(null).start(); - } else { - view.setAlpha(1f); - view.setScaleX(1f); - view.setScaleY(1f); - view.animate().alpha(0f).scaleX(.8f).scaleY(.8f).setDuration(duration).setStartDelay(delay) - .setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - view.setVisibility(View.GONE); - } - }) - .start(); - } + public void onOpenDetail(Context context, String videoUrl) { + if (DEBUG) Log.d(TAG, "onOpenDetail() called with: context = [" + context + "], videoUrl = [" + videoUrl + "]"); + Intent i = new Intent(context, VideoItemDetailActivity.class); + i.putExtra(NavStack.SERVICE_ID, 0) + .putExtra(NavStack.URL, videoUrl) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(i); + //NavStack.getInstance().openDetailActivity(context, videoUrl, 0); } - @Override - public void onConfigurationChanged(Configuration newConfig) { - updateScreenSize(); + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + private float getMinimumVideoWidth(float height) { + float width = height * (16.0f / 9.0f); // Respect the 16:9 ratio that most videos have + if (DEBUG) Log.d(TAG, "getMinimumVideoWidth() called with: height = [" + height + "], returned: " + width); + return width; } - @Override - public void onDestroy() { - if (DEBUG) Log.d(TAG, "onDestroy() called"); - stopForeground(true); - if (emVideoView != null) emVideoView.stopPlayback(); - if (imageLoader != null) imageLoader.clearMemoryCache(); - if (viewHolder.getRootView() != null) windowManager.removeView(viewHolder.getRootView()); - if (notificationManager != null) notificationManager.cancel(NOTIFICATION_ID); - if (progressPollRepeater != null) { - progressPollRepeater.stop(); - progressPollRepeater.setRepeatListener(null); - } - if (broadcastReceiver != null) unregisterReceiver(broadcastReceiver); - } + private void updateScreenSize() { + DisplayMetrics metrics = new DisplayMetrics(); + windowManager.getDefaultDisplay().getMetrics(metrics); - @Override - public IBinder onBind(Intent intent) { - return null; + screenWidth = metrics.widthPixels; + screenHeight = metrics.heightPixels; + if (DEBUG) Log.d(TAG, "updateScreenSize() called > screenWidth = " + screenWidth + ", screenHeight = " + screenHeight); } - /////////////////////////////////////////////////////////////////////////// - // States Implementation /////////////////////////////////////////////////////////////////////////// - @Override - public void changeState(int state) { - if (DEBUG) Log.d(TAG, "changeState() called with: state = [" + state + "]"); - CURRENT_STATE = state; - switch (state) { - case STATE_LOADING: - onLoading(); - break; - case STATE_PLAYING: - onPlaying(); - break; - case STATE_PAUSED: - onPaused(); - break; - case STATE_PAUSED_SEEK: - onPausedSeek(); - break; - case STATE_COMPLETED: - onCompleted(); - break; + private class AbstractPlayerImpl extends AbstractPlayer { + AbstractPlayerImpl() { + super("AbstractPlayerImpl" + PopupVideoPlayer.TAG, PopupVideoPlayer.this); } - } - - @Override - public void onLoading() { - if (DEBUG) Log.d(TAG, "onLoading() called"); - updateNotification(R.drawable.ic_play_arrow_white_48dp); - - showAndAnimateControl(-1, true); - viewHolder.getPlaybackSeekBar().setEnabled(true); - viewHolder.getPlaybackSeekBar().setProgress(0); - viewHolder.getLoadingPanel().setBackgroundColor(Color.BLACK); - animateView(viewHolder.getLoadingPanel(), true, 500, 0); - viewHolder.getEndScreen().setVisibility(View.GONE); - viewHolder.getControlsRoot().setVisibility(View.GONE); - } - - @Override - public void onPlaying() { - if (DEBUG) Log.d(TAG, "onPlaying() called"); - updateNotification(R.drawable.ic_pause_white_24dp); - - showAndAnimateControl(-1, true); - viewHolder.getLoadingPanel().setVisibility(View.GONE); - animateView(viewHolder.getControlsRoot(), false, 500, DEFAULT_CONTROLS_HIDE_TIME); - } - - @Override - public void onPaused() { - if (DEBUG) Log.d(TAG, "onPaused() called"); - updateNotification(R.drawable.ic_play_arrow_white_48dp); - - showAndAnimateControl(R.drawable.ic_play_arrow_white_48dp, false); - animateView(viewHolder.getControlsRoot(), true, 500, 100); - viewHolder.getLoadingPanel().setVisibility(View.GONE); - } - - @Override - public void onPausedSeek() { - if (DEBUG) Log.d(TAG, "onPausedSeek() called"); - updateNotification(R.drawable.ic_play_arrow_white_48dp); - - showAndAnimateControl(-1, true); - viewHolder.getLoadingPanel().setBackgroundColor(Color.TRANSPARENT); - animateView(viewHolder.getLoadingPanel(), true, 300, 0); - } - - @Override - public void onCompleted() { - if (DEBUG) Log.d(TAG, "onCompleted() called"); - updateNotification(R.drawable.ic_replay_white); - showAndAnimateControl(R.drawable.ic_replay_white, false); - animateView(viewHolder.getControlsRoot(), true, 500, 0); - animateView(viewHolder.getEndScreen(), true, 200, 0); - viewHolder.getLoadingPanel().setVisibility(View.GONE); - viewHolder.getPlaybackSeekBar().setEnabled(false); - viewHolder.getPlaybackCurrentTime().setText(viewHolder.getPlaybackEndTime().getText()); - if (videoThumbnail != null) viewHolder.getEndScreen().setImageBitmap(videoThumbnail); - } - - /** - * This class joins all the necessary listeners - */ - @SuppressWarnings({"WeakerAccess"}) - public class InternalListener implements SeekBar.OnSeekBarChangeListener, OnPreparedListener, OnSeekCompletionListener, OnCompletionListener, OnErrorListener, Repeater.RepeatListener { - public static final String ACTION_CLOSE = "org.schabi.newpipe.player.PopupVideoPlayer.CLOSE"; - public static final String ACTION_PLAY_PAUSE = "org.schabi.newpipe.player.PopupVideoPlayer.PLAY_PAUSE"; - public static final String ACTION_OPEN_DETAIL = "org.schabi.newpipe.player.PopupVideoPlayer.OPEN_DETAIL"; - public static final String ACTION_UPDATE_THUMB = "org.schabi.newpipe.player.PopupVideoPlayer.UPDATE_THUMBNAIL"; @Override - public void onPrepared() { - if (DEBUG) Log.d(TAG, "onPrepared() called"); - viewHolder.getPlaybackSeekBar().setMax(emVideoView.getDuration()); - viewHolder.getPlaybackEndTime().setText(TimeFormatUtil.formatMs(emVideoView.getDuration())); + public void playVideo(Uri videoURI, boolean autoPlay) { + super.playVideo(videoURI, autoPlay); - changeState(STATE_PLAYING); - progressPollRepeater.start(); - emVideoView.start(); + windowLayoutParams.width = (int) getMinimumVideoWidth(currentPopupHeight); + windowManager.updateViewLayout(getRootView(), windowLayoutParams); + notBuilder = createNotification(); + startForeground(NOTIFICATION_ID, notBuilder.build()); + notificationManager.notify(NOTIFICATION_ID, notBuilder.build()); } - public void onUpdateProgress(int currentProgress, int duration, int bufferPercent) { - if (viewHolder.isControlsVisible() && CURRENT_STATE != STATE_PAUSED_SEEK) { - viewHolder.getPlaybackSeekBar().setProgress(currentProgress); - viewHolder.getPlaybackCurrentTime().setText(TimeFormatUtil.formatMs(currentProgress)); - viewHolder.getPlaybackSeekBar().setSecondaryProgress((int) (viewHolder.getPlaybackSeekBar().getMax() * ((float) bufferPercent / 100))); + @Override + public void onFullScreenButtonClicked() { + if (DEBUG) Log.d(TAG, "onFullScreenButtonClicked() called"); + Intent intent; + //if (getSharedPreferences().getBoolean(getResources().getString(R.string.use_exoplayer_key), false)) { + // TODO: Remove this check when ExoPlayer is the default + // For now just disable the non-exoplayer player + //noinspection ConstantConditions,ConstantIfStatement + if (true) { + intent = new Intent(PopupVideoPlayer.this, ExoPlayerActivity.class) + .putExtra(AbstractPlayer.VIDEO_TITLE, getVideoTitle()) + .putExtra(AbstractPlayer.VIDEO_URL, getVideoUrl()) + .putExtra(AbstractPlayer.CHANNEL_NAME, getChannelName()) + .putExtra(AbstractPlayer.INDEX_SEL_VIDEO_STREAM, getSelectedIndexStream()) + .putExtra(AbstractPlayer.VIDEO_STREAMS_LIST, getVideoStreamsList()) + .putExtra(AbstractPlayer.START_POSITION, ((int) getPlayer().getCurrentPosition())); + if (!playerImpl.isStartedFromNewPipe()) intent.putExtra(AbstractPlayer.STARTED_FROM_NEWPIPE, false); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + } else { + intent = new Intent(PopupVideoPlayer.this, PlayVideoActivity.class) + .putExtra(PlayVideoActivity.VIDEO_TITLE, getVideoTitle()) + .putExtra(PlayVideoActivity.STREAM_URL, getSelectedStreamUri().toString()) + .putExtra(PlayVideoActivity.VIDEO_URL, getVideoUrl()) + .putExtra(PlayVideoActivity.START_POSITION, Math.round(getPlayer().getCurrentPosition() / 1000f)); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } - if (DEBUG && bufferPercent % 10 == 0) { //Limit log - Log.d(TAG, "updateProgress() called with: isVisible = " + viewHolder.isControlsVisible() + ", currentProgress = [" + currentProgress + "], duration = [" + duration + "], bufferPercent = [" + bufferPercent + "]"); + context.startActivity(intent); + stopSelf(); + } + + @Override + public void onRepeatClicked() { + super.onRepeatClicked(); + switch (getCurrentRepeatMode()) { + case REPEAT_DISABLED: + // Drawable didn't work on low API :/ + //notRemoteView.setImageViewResource(R.id.notificationRepeat, R.drawable.ic_repeat_disabled_white); + // Set the icon to 30% opacity - 255 (max) * .3 + notRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, 77); + break; + case REPEAT_ONE: + notRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, 255); + break; + case REPEAT_ALL: + // Waiting :) + break; } - } - - public void onOpenDetail(Context context, String videoUrl) { - if (DEBUG) Log.d(TAG, "onOpenDetail() called with: context = [" + context + "], videoUrl = [" + videoUrl + "]"); - Intent i = new Intent(context, VideoItemDetailActivity.class); - i.putExtra(NavStack.SERVICE_ID, 0); - i.putExtra(NavStack.URL, videoUrl); - i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(i); - //NavStack.getInstance().openDetailActivity(context, videoUrl, 0); - } - - public void onUpdateThumbnail(Intent intent) { - if (DEBUG) Log.d(TAG, "onUpdateThumbnail() called"); - if (!intent.getStringExtra(VIDEO_URL).equals(videoUrl)) return; - videoThumbnail = ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail; - if (videoThumbnail != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, videoThumbnail); updateNotification(-1); } - public void onVideoClose() { - if (DEBUG) Log.d(TAG, "onVideoClose() called"); + @Override + public void onUpdateThumbnail(Intent intent) { + super.onUpdateThumbnail(intent); + if (getVideoThumbnail() != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, getVideoThumbnail()); + updateNotification(-1); + } + + @Override + public void onDismiss(PopupMenu menu) { + super.onDismiss(menu); + if (isPlaying()) animateView(getControlsRoot(), false, 500, 0); + } + + @Override + public void onError() { + Toast.makeText(context, "Failed to play this video", Toast.LENGTH_SHORT).show(); stopSelf(); } - public void onVideoPlayPause() { - if (DEBUG) Log.d(TAG, "onVideoPlayPause() called"); - if (CURRENT_STATE == STATE_COMPLETED) { - changeState(STATE_LOADING); - emVideoView.restart(); - return; - } - if (emVideoView.isPlaying()) { - emVideoView.pause(); - progressPollRepeater.stop(); - internalListener.onRepeat(); - changeState(STATE_PAUSED); - } else { - emVideoView.start(); - progressPollRepeater.start(); - changeState(STATE_PLAYING); - } - } + /*////////////////////////////////////////////////////////////////////////// + // States + //////////////////////////////////////////////////////////////////////////*/ - public void onFastRewind() { - if (DEBUG) Log.d(TAG, "onFastRewind() called"); - seekBy(-FAST_FORWARD_REWIND_AMOUNT); - internalListener.onRepeat(); - changeState(STATE_PAUSED_SEEK); - - showAndAnimateControl(R.drawable.ic_action_av_fast_rewind, true); - } - - public void onFastForward() { - if (DEBUG) Log.d(TAG, "onFastForward() called"); - seekBy(FAST_FORWARD_REWIND_AMOUNT); - internalListener.onRepeat(); - changeState(STATE_PAUSED_SEEK); - - showAndAnimateControl(R.drawable.ic_action_av_fast_forward, true); + @Override + public void onLoading() { + super.onLoading(); + updateNotification(R.drawable.ic_play_arrow_white); } @Override - public void onSeekComplete() { - if (DEBUG) Log.d(TAG, "onSeekComplete() called"); - - if (!emVideoView.isPlaying()) emVideoView.start(); - changeState(STATE_PLAYING); - /*if (emVideoView.isPlaying()) changeState(STATE_PLAYING); - else changeState(STATE_PAUSED);*/ + public void onPlaying() { + super.onPlaying(); + updateNotification(R.drawable.ic_pause_white); } @Override - public void onCompletion() { - if (DEBUG) Log.d(TAG, "onCompletion() called"); - changeState(STATE_COMPLETED); - progressPollRepeater.stop(); + public void onBuffering() { + super.onBuffering(); + updateNotification(R.drawable.ic_play_arrow_white); } @Override - public boolean onError() { - if (DEBUG) Log.d(TAG, "onError() called"); - stopSelf(); - return true; - } - - /////////////////////////////////////////////////////////////////////////// - // SeekBar Listener - /////////////////////////////////////////////////////////////////////////// - - @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - if (DEBUG) Log.d(TAG, "onProgressChanged() called with: seekBar = [" + seekBar + "], progress = [" + progress + "], fromUser = [" + fromUser + "]"); - viewHolder.getPlaybackCurrentTime().setText(TimeFormatUtil.formatMs(progress)); + public void onPaused() { + super.onPaused(); + updateNotification(R.drawable.ic_play_arrow_white); + showAndAnimateControl(R.drawable.ic_play_arrow_white, false); } @Override - public void onStartTrackingTouch(SeekBar seekBar) { - if (DEBUG) Log.d(TAG, "onStartTrackingTouch() called with: seekBar = [" + seekBar + "]"); - - changeState(STATE_PAUSED_SEEK); - if (emVideoView.isPlaying()) emVideoView.pause(); - animateView(viewHolder.getControlsRoot(), true, 300, 0); - viewHolder.getControlsRoot().setAlpha(1f); + public void onPausedSeek() { + super.onPausedSeek(); + updateNotification(R.drawable.ic_play_arrow_white); } @Override - public void onStopTrackingTouch(SeekBar seekBar) { - if (DEBUG) Log.d(TAG, "onProgressChanged() called with: seekBar = [" + seekBar + "], progress = [" + seekBar.getProgress() + "]"); - emVideoView.seekTo(seekBar.getProgress()); - + public void onCompleted() { + super.onCompleted(); + updateNotification(R.drawable.ic_replay_white); + showAndAnimateControl(R.drawable.ic_replay_white, false); } - /////////////////////////////////////////////////////////////////////////// - // Repeater Listener - /////////////////////////////////////////////////////////////////////////// - - /** - * Don't mistake this with anything related to the player itself, it's the {@link Repeater.RepeatListener#onRepeat} - * It's used for pool the progress of the video - */ - @Override - public void onRepeat() { - onUpdateProgress(emVideoView.getCurrentPosition(), emVideoView.getDuration(), emVideoView.getBufferPercentage()); - } } private class MySimpleOnGestureListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener { @@ -663,42 +434,33 @@ public class PopupVideoPlayer extends Service implements StateInterface { @Override public boolean onDoubleTap(MotionEvent e) { if (DEBUG) Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY()); - if (!emVideoView.isPlaying()) return false; - if (e.getX() > popupWidth / 2) internalListener.onFastForward(); - else internalListener.onFastRewind(); + if (!playerImpl.isPlaying()) return false; + if (e.getX() > popupWidth / 2) playerImpl.onFastForward(); + else playerImpl.onFastRewind(); return true; } @Override public boolean onSingleTapConfirmed(MotionEvent e) { if (DEBUG) Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]"); - if (emVideoView == null) return false; - internalListener.onVideoPlayPause(); + if (playerImpl.getPlayer() == null) return false; + playerImpl.onVideoPlayPause(); return true; } - @Override public boolean onDown(MotionEvent e) { if (DEBUG) Log.d(TAG, "onDown() called with: e = [" + e + "]"); initialPopupX = windowLayoutParams.x; initialPopupY = windowLayoutParams.y; - popupWidth = viewHolder.getRootView().getWidth(); - popupHeight = viewHolder.getRootView().getHeight(); + popupWidth = playerImpl.getRootView().getWidth(); + popupHeight = playerImpl.getRootView().getHeight(); return false; } - @Override - public void onShowPress(MotionEvent e) { - if (DEBUG) Log.d(TAG, "onShowPress() called with: e = [" + e + "]"); - /*viewHolder.getControlsRoot().animate().setListener(null).cancel(); - viewHolder.getControlsRoot().setAlpha(1f); - viewHolder.getControlsRoot().setVisibility(View.VISIBLE);*/ - animateView(viewHolder.getControlsRoot(), true, 200, 0); - } - @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + if (!isMoving || playerImpl.getControlsRoot().getAlpha() != 1f) playerImpl.animateView(playerImpl.getControlsRoot(), true, 30, 0); isMoving = true; float diffX = (int) (e2.getRawX() - e1.getRawX()), posX = (int) (initialPopupX + diffX); float diffY = (int) (e2.getRawY() - e1.getRawY()), posY = (int) (initialPopupY + diffY); @@ -712,20 +474,21 @@ public class PopupVideoPlayer extends Service implements StateInterface { windowLayoutParams.x = (int) posX; windowLayoutParams.y = (int) posY; - if (DEBUG) Log.d(TAG, "PopupVideoPlayer.onScroll = " + + //noinspection PointlessBooleanExpression + if (DEBUG && false) Log.d(TAG, "PopupVideoPlayer.onScroll = " + ", e1.getRaw = [" + e1.getRawX() + ", " + e1.getRawY() + "]" + ", e2.getRaw = [" + e2.getRawX() + ", " + e2.getRawY() + "]" + ", distanceXy = [" + distanceX + ", " + distanceY + "]" + ", posXy = [" + posX + ", " + posY + "]" + ", popupWh rootView.get wh = [" + popupWidth + " x " + popupHeight + "]"); - windowManager.updateViewLayout(viewHolder.getRootView(), windowLayoutParams); + windowManager.updateViewLayout(playerImpl.getRootView(), windowLayoutParams); return true; } private void onScrollEnd() { if (DEBUG) Log.d(TAG, "onScrollEnd() called"); - if (viewHolder.isControlsVisible() && CURRENT_STATE == STATE_PLAYING) { - animateView(viewHolder.getControlsRoot(), false, 300, DEFAULT_CONTROLS_HIDE_TIME); + if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == StateInterface.STATE_PLAYING) { + playerImpl.animateView(playerImpl.getControlsRoot(), false, 300, AbstractPlayer.DEFAULT_CONTROLS_HIDE_TIME); } } @@ -763,48 +526,56 @@ public class PopupVideoPlayer extends Service implements StateInterface { if (service == null) return; streamExtractor = service.getExtractorInstance(intent.getStringExtra(NavStack.URL)); StreamInfo info = StreamInfo.getVideoInfo(streamExtractor); - String defaultResolution = sharedPreferences.getString( + String defaultResolution = playerImpl.getSharedPreferences().getString( getResources().getString(R.string.default_resolution_key), getResources().getString(R.string.default_resolution_value)); - String chosen = "", secondary = "", fallback = ""; + VideoStream chosen = null, secondary = null, fallback = null; + playerImpl.setVideoStreamsList(info.video_streams instanceof ArrayList + ? (ArrayList) info.video_streams + : new ArrayList<>(info.video_streams)); + for (VideoStream item : info.video_streams) { if (DEBUG && printStreams) { - Log.d(TAG, "StreamExtractor: current Item" + Log.d(TAG, "FetcherRunnable.StreamExtractor: current Item" + ", item.resolution = " + item.resolution + ", item.format = " + item.format + ", item.url = " + item.url); } if (defaultResolution.equals(item.resolution)) { if (item.format == MediaFormat.MPEG_4.id) { - chosen = item.url; - if (DEBUG) - Log.d(TAG, "StreamExtractor: CHOSEN item" - + ", item.resolution = " + item.resolution - + ", item.format = " + item.format - + ", item.url = " + item.url); - } else if (item.format == 2) secondary = item.url; - else fallback = item.url; - + chosen = item; + if (DEBUG) Log.d(TAG, "FetcherRunnable.StreamExtractor: CHOSEN item, item.resolution = " + item.resolution + ", item.format = " + item.format + ", item.url = " + item.url); + } else if (item.format == 2) secondary = item; + else fallback = item; } } - if (!chosen.trim().isEmpty()) streamUri = Uri.parse(chosen); - else if (!secondary.trim().isEmpty()) streamUri = Uri.parse(secondary); - else if (!fallback.trim().isEmpty()) streamUri = Uri.parse(fallback); - else streamUri = Uri.parse(info.video_streams.get(0).url); - if (DEBUG && printStreams) Log.d(TAG, "StreamExtractor: chosen = " + chosen + int selectedIndexStream; + + if (chosen != null) selectedIndexStream = info.video_streams.indexOf(chosen); + else if (secondary != null) selectedIndexStream = info.video_streams.indexOf(secondary); + else if (fallback != null) selectedIndexStream = info.video_streams.indexOf(fallback); + else selectedIndexStream = 0; + + playerImpl.setSelectedIndexStream(selectedIndexStream); + + if (DEBUG && printStreams) Log.d(TAG, "FetcherRunnable.StreamExtractor: chosen = " + chosen + "\n, secondary = " + secondary + "\n, fallback = " + fallback + "\n, info.video_streams.get(0).url = " + info.video_streams.get(0).url); - videoUrl = info.webpage_url; - videoTitle = info.title; - channelName = info.uploader; + + playerImpl.setVideoUrl(info.webpage_url); + playerImpl.setVideoTitle(info.title); + playerImpl.setChannelName(info.uploader); + if (info.start_position > 0) playerImpl.setVideoStartPos(info.start_position * 1000); + else playerImpl.setVideoStartPos(-1); + mainHandler.post(new Runnable() { @Override public void run() { - playVideo(streamUri); + playerImpl.playVideo(playerImpl.getSelectedStreamUri(), true); } }); imageLoader.loadImage(info.thumbnail_url, displayImageOptions, new SimpleImageLoadingListener() { @@ -813,9 +584,10 @@ public class PopupVideoPlayer extends Service implements StateInterface { mainHandler.post(new Runnable() { @Override public void run() { - videoThumbnail = loadedImage; - if (videoThumbnail != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, videoThumbnail); + playerImpl.setVideoThumbnail(loadedImage); + if (loadedImage != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage); updateNotification(-1); + ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail = loadedImage; } }); } @@ -841,4 +613,5 @@ public class PopupVideoPlayer extends Service implements StateInterface { } } } -} + +} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/player/popup/StateInterface.java b/app/src/main/java/org/schabi/newpipe/player/StateInterface.java similarity index 71% rename from app/src/main/java/org/schabi/newpipe/player/popup/StateInterface.java rename to app/src/main/java/org/schabi/newpipe/player/StateInterface.java index 94ea41470..7b3681ab8 100644 --- a/app/src/main/java/org/schabi/newpipe/player/popup/StateInterface.java +++ b/app/src/main/java/org/schabi/newpipe/player/StateInterface.java @@ -1,8 +1,9 @@ -package org.schabi.newpipe.player.popup; +package org.schabi.newpipe.player; public interface StateInterface { int STATE_LOADING = 123; - int STATE_PLAYING = 125; + int STATE_PLAYING = 124; + int STATE_BUFFERING = 125; int STATE_PAUSED = 126; int STATE_PAUSED_SEEK = 127; int STATE_COMPLETED = 128; @@ -11,6 +12,7 @@ public interface StateInterface { void onLoading(); void onPlaying(); + void onBuffering(); void onPaused(); void onPausedSeek(); void onCompleted(); diff --git a/app/src/main/java/org/schabi/newpipe/player/popup/PopupViewHolder.java b/app/src/main/java/org/schabi/newpipe/player/popup/PopupViewHolder.java deleted file mode 100644 index 22895668e..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/popup/PopupViewHolder.java +++ /dev/null @@ -1,93 +0,0 @@ -package org.schabi.newpipe.player.popup; - -import android.graphics.Color; -import android.graphics.PorterDuff; -import android.os.Build; -import android.view.View; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.SeekBar; -import android.widget.TextView; - -import com.devbrackets.android.exomedia.ui.widget.EMVideoView; - -import org.schabi.newpipe.R; - -public class PopupViewHolder { - private View rootView; - private EMVideoView videoView; - private View loadingPanel; - private ImageView endScreen; - private ImageView controlAnimationView; - private LinearLayout controlsRoot; - private SeekBar playbackSeekBar; - private TextView playbackCurrentTime; - private TextView playbackEndTime; - - public PopupViewHolder(View rootView) { - if (rootView == null) return; - this.rootView = rootView; - this.videoView = (EMVideoView) rootView.findViewById(R.id.popupVideoView); - this.loadingPanel = rootView.findViewById(R.id.loadingPanel); - this.endScreen = (ImageView) rootView.findViewById(R.id.endScreen); - this.controlAnimationView = (ImageView) rootView.findViewById(R.id.controlAnimationView); - this.controlsRoot = (LinearLayout) rootView.findViewById(R.id.playbackControlRoot); - this.playbackSeekBar = (SeekBar) rootView.findViewById(R.id.playbackSeekBar); - this.playbackCurrentTime = (TextView) rootView.findViewById(R.id.playbackCurrentTime); - this.playbackEndTime = (TextView) rootView.findViewById(R.id.playbackEndTime); - doModifications(); - } - - private void doModifications() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN); - playbackSeekBar.getProgressDrawable().setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY); - } - - public boolean isControlsVisible() { - return controlsRoot != null && controlsRoot.getVisibility() == View.VISIBLE; - } - - public boolean isVisible(View view) { - return view != null && view.getVisibility() == View.VISIBLE; - } - - /////////////////////////////////////////////////////////////////////////// - // GETTERS - /////////////////////////////////////////////////////////////////////////// - - public View getRootView() { - return rootView; - } - - public EMVideoView getVideoView() { - return videoView; - } - - public View getLoadingPanel() { - return loadingPanel; - } - - public ImageView getEndScreen() { - return endScreen; - } - - public ImageView getControlAnimationView() { - return controlAnimationView; - } - - public LinearLayout getControlsRoot() { - return controlsRoot; - } - - public SeekBar getPlaybackSeekBar() { - return playbackSeekBar; - } - - public TextView getPlaybackCurrentTime() { - return playbackCurrentTime; - } - - public TextView getPlaybackEndTime() { - return playbackEndTime; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/search_fragment/SearchInfoItemFragment.java b/app/src/main/java/org/schabi/newpipe/search_fragment/SearchInfoItemFragment.java index 6bf261e14..cb3869c01 100644 --- a/app/src/main/java/org/schabi/newpipe/search_fragment/SearchInfoItemFragment.java +++ b/app/src/main/java/org/schabi/newpipe/search_fragment/SearchInfoItemFragment.java @@ -19,17 +19,14 @@ import android.view.inputmethod.InputMethodManager; import android.widget.ProgressBar; import android.widget.Toast; -import org.schabi.newpipe.ChannelActivity; +import org.schabi.newpipe.R; import org.schabi.newpipe.ReCaptchaActivity; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.search.SearchEngine; import org.schabi.newpipe.extractor.search.SearchResult; import org.schabi.newpipe.info_list.InfoItemBuilder; -import org.schabi.newpipe.report.ErrorActivity; -import org.schabi.newpipe.R; -import org.schabi.newpipe.detail.VideoItemDetailActivity; -import org.schabi.newpipe.detail.VideoItemDetailFragment; import org.schabi.newpipe.info_list.InfoListAdapter; +import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.util.NavStack; import java.util.EnumSet; diff --git a/app/src/main/res/drawable-xxhdpi/ic_close_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_close_white.png similarity index 100% rename from app/src/main/res/drawable-xxhdpi/ic_close_white_24dp.png rename to app/src/main/res/drawable-hdpi/ic_close_white.png diff --git a/app/src/main/res/drawable-hdpi/ic_close_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_close_white_24dp.png deleted file mode 100644 index ceb1a1eeb..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_close_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_fullscreen_exit_white.png b/app/src/main/res/drawable-hdpi/ic_fullscreen_exit_white.png new file mode 100644 index 000000000..159bea7fd Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_fullscreen_exit_white.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_fullscreen_white.png b/app/src/main/res/drawable-hdpi/ic_fullscreen_white.png new file mode 100644 index 000000000..9b8131124 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_fullscreen_white.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_pause_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_pause_white.png similarity index 100% rename from app/src/main/res/drawable-xxhdpi/ic_pause_white_24dp.png rename to app/src/main/res/drawable-hdpi/ic_pause_white.png diff --git a/app/src/main/res/drawable-hdpi/ic_pause_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_pause_white_24dp.png deleted file mode 100644 index 4d2ea05c4..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_pause_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_play_arrow_white_48dp.png b/app/src/main/res/drawable-hdpi/ic_play_arrow_white.png similarity index 100% rename from app/src/main/res/drawable-hdpi/ic_play_arrow_white_48dp.png rename to app/src/main/res/drawable-hdpi/ic_play_arrow_white.png diff --git a/app/src/main/res/drawable-hdpi/ic_repeat_white.png b/app/src/main/res/drawable-hdpi/ic_repeat_white.png new file mode 100644 index 000000000..5de7a2951 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_repeat_white.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_close_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_close_white.png similarity index 100% rename from app/src/main/res/drawable-xhdpi/ic_close_white_24dp.png rename to app/src/main/res/drawable-mdpi/ic_close_white.png diff --git a/app/src/main/res/drawable-mdpi/ic_close_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_close_white_24dp.png deleted file mode 100644 index af7f8288d..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_close_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_fullscreen_exit_white.png b/app/src/main/res/drawable-mdpi/ic_fullscreen_exit_white.png new file mode 100644 index 000000000..364bad0b8 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_fullscreen_exit_white.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_fullscreen_white.png b/app/src/main/res/drawable-mdpi/ic_fullscreen_white.png new file mode 100644 index 000000000..4423c7ce9 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_fullscreen_white.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_pause_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_pause_white.png similarity index 100% rename from app/src/main/res/drawable-xhdpi/ic_pause_white_24dp.png rename to app/src/main/res/drawable-mdpi/ic_pause_white.png diff --git a/app/src/main/res/drawable-mdpi/ic_pause_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_pause_white_24dp.png deleted file mode 100644 index 2272d478c..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_pause_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_play_arrow_white_48dp.png b/app/src/main/res/drawable-mdpi/ic_play_arrow_white.png similarity index 100% rename from app/src/main/res/drawable-mdpi/ic_play_arrow_white_48dp.png rename to app/src/main/res/drawable-mdpi/ic_play_arrow_white.png diff --git a/app/src/main/res/drawable-mdpi/ic_repeat_white.png b/app/src/main/res/drawable-mdpi/ic_repeat_white.png new file mode 100644 index 000000000..ad8b8c0df Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_repeat_white.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_close_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_close_white.png similarity index 100% rename from app/src/main/res/drawable-xxxhdpi/ic_close_white_24dp.png rename to app/src/main/res/drawable-xhdpi/ic_close_white.png diff --git a/app/src/main/res/drawable-xhdpi/ic_fullscreen_exit_white.png b/app/src/main/res/drawable-xhdpi/ic_fullscreen_exit_white.png new file mode 100644 index 000000000..ef360fe40 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_fullscreen_exit_white.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_fullscreen_white.png b/app/src/main/res/drawable-xhdpi/ic_fullscreen_white.png new file mode 100644 index 000000000..c1dcfb290 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_fullscreen_white.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_pause_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_pause_white.png similarity index 100% rename from app/src/main/res/drawable-xxxhdpi/ic_pause_white_24dp.png rename to app/src/main/res/drawable-xhdpi/ic_pause_white.png diff --git a/app/src/main/res/drawable-xhdpi/ic_play_arrow_white_48dp.png b/app/src/main/res/drawable-xhdpi/ic_play_arrow_white.png similarity index 100% rename from app/src/main/res/drawable-xhdpi/ic_play_arrow_white_48dp.png rename to app/src/main/res/drawable-xhdpi/ic_play_arrow_white.png diff --git a/app/src/main/res/drawable-xhdpi/ic_repeat_white.png b/app/src/main/res/drawable-xhdpi/ic_repeat_white.png new file mode 100644 index 000000000..c13d00242 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_repeat_white.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_close_white.png b/app/src/main/res/drawable-xxhdpi/ic_close_white.png new file mode 100644 index 000000000..4927bc242 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_close_white.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_fullscreen_exit_white.png b/app/src/main/res/drawable-xxhdpi/ic_fullscreen_exit_white.png new file mode 100644 index 000000000..b7f4133fd Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_fullscreen_exit_white.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_fullscreen_white.png b/app/src/main/res/drawable-xxhdpi/ic_fullscreen_white.png new file mode 100644 index 000000000..a0a1b4d4f Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_fullscreen_white.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_pause_white.png b/app/src/main/res/drawable-xxhdpi/ic_pause_white.png new file mode 100644 index 000000000..3ea7e03e5 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_pause_white.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_play_arrow_white_48dp.png b/app/src/main/res/drawable-xxhdpi/ic_play_arrow_white.png similarity index 100% rename from app/src/main/res/drawable-xxhdpi/ic_play_arrow_white_48dp.png rename to app/src/main/res/drawable-xxhdpi/ic_play_arrow_white.png diff --git a/app/src/main/res/drawable-xxhdpi/ic_repeat_white.png b/app/src/main/res/drawable-xxhdpi/ic_repeat_white.png new file mode 100644 index 000000000..bf7607966 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_repeat_white.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_close_white.png b/app/src/main/res/drawable-xxxhdpi/ic_close_white.png new file mode 100644 index 000000000..1ab231275 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_close_white.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_fullscreen_exit_white.png b/app/src/main/res/drawable-xxxhdpi/ic_fullscreen_exit_white.png new file mode 100644 index 000000000..b47b3f8bd Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_fullscreen_exit_white.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_fullscreen_white.png b/app/src/main/res/drawable-xxxhdpi/ic_fullscreen_white.png new file mode 100644 index 000000000..ea9f18ae6 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_fullscreen_white.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_pause_white.png b/app/src/main/res/drawable-xxxhdpi/ic_pause_white.png new file mode 100644 index 000000000..76482b1fd Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_pause_white.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_play_arrow_white_48dp.png b/app/src/main/res/drawable-xxxhdpi/ic_play_arrow_white.png similarity index 100% rename from app/src/main/res/drawable-xxxhdpi/ic_play_arrow_white_48dp.png rename to app/src/main/res/drawable-xxxhdpi/ic_play_arrow_white.png diff --git a/app/src/main/res/drawable-xxxhdpi/ic_repeat_white.png b/app/src/main/res/drawable-xxxhdpi/ic_repeat_white.png new file mode 100644 index 000000000..a59db47ee Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_repeat_white.png differ diff --git a/app/src/main/res/drawable/popup_controls_bg.xml b/app/src/main/res/drawable/player_controls_bg.xml similarity index 69% rename from app/src/main/res/drawable/popup_controls_bg.xml rename to app/src/main/res/drawable/player_controls_bg.xml index d04812bd8..9c9468112 100644 --- a/app/src/main/res/drawable/popup_controls_bg.xml +++ b/app/src/main/res/drawable/player_controls_bg.xml @@ -1,8 +1,7 @@ diff --git a/app/src/main/res/drawable/player_top_controls_bg.xml b/app/src/main/res/drawable/player_top_controls_bg.xml new file mode 100644 index 000000000..b7cdecc87 --- /dev/null +++ b/app/src/main/res/drawable/player_top_controls_bg.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_exo_player.xml b/app/src/main/res/layout/activity_exo_player.xml index 5653532ec..72f8aa897 100644 --- a/app/src/main/res/layout/activity_exo_player.xml +++ b/app/src/main/res/layout/activity_exo_player.xml @@ -1,15 +1,303 @@ - - + + android:background="@android:color/black" + android:gravity="center"> - + android:layout_height="match_parent" + android:layout_gravity="center"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/exomedia_custom_controls.xml b/app/src/main/res/layout/exomedia_custom_controls.xml deleted file mode 100644 index dedaf7908..000000000 --- a/app/src/main/res/layout/exomedia_custom_controls.xml +++ /dev/null @@ -1,180 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/player_notification.xml b/app/src/main/res/layout/player_notification.xml index 43ac993ca..22a60418b 100644 --- a/app/src/main/res/layout/player_notification.xml +++ b/app/src/main/res/layout/player_notification.xml @@ -65,7 +65,7 @@ android:background="#00ffffff" android:clickable="true" android:scaleType="fitXY" - android:src="@drawable/ic_pause_white_24dp" /> + android:src="@drawable/ic_pause_white" /> + android:src="@drawable/ic_close_white" /> diff --git a/app/src/main/res/layout/player_notification_expanded.xml b/app/src/main/res/layout/player_notification_expanded.xml index 0fb6a8eb2..4a81d2ca3 100644 --- a/app/src/main/res/layout/player_notification_expanded.xml +++ b/app/src/main/res/layout/player_notification_expanded.xml @@ -58,7 +58,7 @@ android:background="#00ffffff" android:clickable="true" android:scaleType="fitXY" - android:src="@drawable/ic_close_white_24dp" /> + android:src="@drawable/ic_close_white" /> diff --git a/app/src/main/res/layout/player_popup.xml b/app/src/main/res/layout/player_popup.xml index 6d1860408..a3b2b80b6 100644 --- a/app/src/main/res/layout/player_popup.xml +++ b/app/src/main/res/layout/player_popup.xml @@ -4,18 +4,29 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + android:background="@android:color/black" android:gravity="center"> - + android:layout_gravity="center"> - + + + + + + tools:ignore="ContentDescription" + tools:visibility="visible"/> + + + + + + + + + + + + + + + + + + + + + + + + android:orientation="horizontal" + android:weightSum="5"> + + tools:ignore="ContentDescription" + tools:visibility="visible"/> - - - - - - - - - - + tools:ignore="RtlHardcoded" + tools:text="1:06:29" + tools:visibility="visible"/> + tools:text="a long, long, long, long, long title"/> + tools:text="a long, long artist"/> + + + android:padding="5dp" + android:scaleType="fitCenter" + android:src="@drawable/ic_close_white" + tools:ignore="ContentDescription,RtlHardcoded"/> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 263c6bc14..f91c51f3f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -25,6 +25,7 @@ Settings Use external video player Use external audio player + NewPipe Popup mode Video download path Path to store downloaded videos in. @@ -75,6 +76,7 @@ Other %1$s - NewPipe Playing in background + Playing in popup mode https://www.c3s.cc/ Play Content diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 606da4eb0..9cf8160c3 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -2,7 +2,7 @@