diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 127e42dce..a30764ab3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -318,7 +318,8 @@ android:name=".LocalPlayerActivity" android:label="@string/app_name" android:launchMode="singleTask" - android:foregroundServiceType="mediaPlayback" /> + android:foregroundServiceType="mediaPlayback" + android:configChanges="orientation|screenSize|layoutDirection" /> result = new ArrayList<>(); + + final String segmentsJson = intent.getStringExtra("segments"); if (segmentsJson != null && segmentsJson.length() > 0) { try { - final ArrayList segmentsArrayList = new ArrayList<>(); final JsonObject obj = JsonParser.object().from(segmentsJson); for (final Object item : obj.getArray("segments")) { @@ -96,216 +110,63 @@ public class LocalPlayerActivity extends AppCompatActivity implements Player.Eve final String category = itemObject.getString("category"); final VideoSegment segment = new VideoSegment(startTime, endTime, category); - segmentsArrayList.add(segment); + result.add(segment); } - - segments = segmentsArrayList.toArray(new VideoSegment[0]); } catch (final Exception e) { Log.e(TAG, "Error initializing segments", e); } } + + return result.toArray(new VideoSegment[0]); } - @Override - protected void onDestroy() { - super.onDestroy(); - simpleExoPlayer.removeListener(this); - simpleExoPlayer.stop(); - simpleExoPlayer.release(); - progressUpdateReactor.set(null); - } - - @Override - public void onPlayerStateChanged(final boolean playWhenReady, final int playbackState) { - switch (playbackState) { - case Player.STATE_IDLE: - break; - case Player.STATE_BUFFERING: - break; - case Player.STATE_READY: - changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); - break; - case Player.STATE_ENDED: - changeState(STATE_COMPLETED); - break; + private void setKeepScreenOn(final boolean keepScreenOn) { + if (keepScreenOn) { + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } else { + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } } - public void changeState(final int state) { - switch (state) { - case STATE_BLOCKED: - onBlocked(); - 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; - } - } - - private void onBlocked() { - if (!isProgressLoopRunning()) { - startProgressLoop(); - } - } - - private void onPlaying() { - if (!isProgressLoopRunning()) { - startProgressLoop(); - } - } - - private void onBuffering() { - } - - private void onPaused() { - if (isProgressLoopRunning()) { - stopProgressLoop(); - } - } - - private void onPausedSeek() { - } - - private void onCompleted() { - if (isProgressLoopRunning()) { - stopProgressLoop(); - } - } - - public boolean isProgressLoopRunning() { - return progressUpdateReactor.get() != null; - } - - protected void startProgressLoop() { - progressUpdateReactor.set(getProgressReactor()); - } - - protected void stopProgressLoop() { - progressUpdateReactor.set(null); - } - - private Disposable getProgressReactor() { - return Observable.interval(PROGRESS_LOOP_INTERVAL_MILLIS, MILLISECONDS, - AndroidSchedulers.mainThread()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(ignored -> triggerProgressUpdate(), - error -> Log.e(TAG, "Progress update failure: ", error)); - } - - private void triggerProgressUpdate() { - if (simpleExoPlayer == null) { - return; - } - final int currentProgress = Math.max((int) simpleExoPlayer.getCurrentPosition(), 0); - - final boolean isRewind = currentProgress < lastCurrentProgress; - - lastCurrentProgress = currentProgress; - - if (!mPrefs.getBoolean( - this.getString(R.string.sponsor_block_enable_key), false)) { - return; + private void hideSystemUi(final boolean isLandscape) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + getWindow().getAttributes().layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; } - final VideoSegment segment = getSkippableSegment(currentProgress); - if (segment == null) { - return; + int visibility; + + if (isLandscape) { + visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; + } else { + visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; } - int skipTarget = isRewind - ? (int) Math.ceil((segment.startTime)) - 1 - : (int) Math.ceil((segment.endTime)); - - if (skipTarget < 0) { - skipTarget = 0; + if (!isInMultiWindow()) { + visibility |= View.SYSTEM_UI_FLAG_FULLSCREEN; } - // temporarily force EXACT seek parameters to prevent infinite skip looping - final SeekParameters seekParams = simpleExoPlayer.getSeekParameters(); - simpleExoPlayer.setSeekParameters(SeekParameters.EXACT); + getWindow().getDecorView().setSystemUiVisibility(visibility); - seekTo(skipTarget); - - simpleExoPlayer.setSeekParameters(seekParams); - - if (mPrefs.getBoolean( - this.getString(R.string.sponsor_block_notifications_key), false)) { - String toastText = ""; - - switch (segment.category) { - case "sponsor": - toastText = this - .getString(R.string.sponsor_block_skip_sponsor_toast); - break; - case "intro": - toastText = this - .getString(R.string.sponsor_block_skip_intro_toast); - break; - case "outro": - toastText = this - .getString(R.string.sponsor_block_skip_outro_toast); - break; - case "interaction": - toastText = this - .getString(R.string.sponsor_block_skip_interaction_toast); - break; - case "selfpromo": - toastText = this - .getString(R.string.sponsor_block_skip_self_promo_toast); - break; - case "music_offtopic": - toastText = this - .getString(R.string.sponsor_block_skip_non_music_toast); - break; - } - - Toast.makeText(this, toastText, Toast.LENGTH_SHORT).show(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && (isInMultiWindow())) { + getWindow().setStatusBarColor(Color.TRANSPARENT); + getWindow().setNavigationBarColor(Color.TRANSPARENT); } + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); } - public void seekTo(final long positionMillis) { - if (simpleExoPlayer != null) { - // prevent invalid positions when fast-forwarding/-rewinding - long normalizedPositionMillis = positionMillis; - if (normalizedPositionMillis < 0) { - normalizedPositionMillis = 0; - } else if (normalizedPositionMillis > simpleExoPlayer.getDuration()) { - normalizedPositionMillis = simpleExoPlayer.getDuration(); - } - - simpleExoPlayer.seekTo(normalizedPositionMillis); - } + private boolean isInMultiWindow() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInMultiWindowMode(); } - public VideoSegment getSkippableSegment(final int progress) { - if (segments == null) { - return null; - } - - for (final VideoSegment segment : segments) { - if (progress < segment.startTime) { - continue; - } - - if (progress > segment.endTime) { - continue; - } - - return segment; - } - - return null; + boolean isLandscape() { + final DisplayMetrics metrics = getResources().getDisplayMetrics(); + return metrics.heightPixels < metrics.widthPixels; } } diff --git a/app/src/main/java/org/schabi/newpipe/player/LocalPlayer.java b/app/src/main/java/org/schabi/newpipe/player/LocalPlayer.java new file mode 100644 index 000000000..aa5397a11 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/LocalPlayer.java @@ -0,0 +1,304 @@ +package org.schabi.newpipe.player; + +import android.content.Context; +import android.content.SharedPreferences; +import android.net.Uri; +import android.util.Log; +import android.widget.Toast; + +import androidx.preference.PreferenceManager; + +import com.google.android.exoplayer2.Player.EventListener; +import com.google.android.exoplayer2.SeekParameters; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.ProgressiveMediaSource; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; + +import org.schabi.newpipe.App; +import org.schabi.newpipe.DownloaderImpl; +import org.schabi.newpipe.R; +import org.schabi.newpipe.player.helper.PlayerHelper; +import org.schabi.newpipe.util.VideoSegment; + +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.disposables.SerialDisposable; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.schabi.newpipe.player.Player.STATE_BLOCKED; +import static org.schabi.newpipe.player.Player.STATE_BUFFERING; +import static org.schabi.newpipe.player.Player.STATE_COMPLETED; +import static org.schabi.newpipe.player.Player.STATE_PAUSED; +import static org.schabi.newpipe.player.Player.STATE_PAUSED_SEEK; +import static org.schabi.newpipe.player.Player.STATE_PLAYING; + +public class LocalPlayer implements EventListener { + private static final String TAG = "LocalPlayer"; + private static final int PROGRESS_LOOP_INTERVAL_MILLIS = 500; + + private final Context context; + private final SharedPreferences mPrefs; + private SimpleExoPlayer simpleExoPlayer; + private SerialDisposable progressUpdateReactor; + private VideoSegment[] videoSegments; + private LocalPlayerListener listener; + private int lastCurrentProgress = -1; + + public LocalPlayer(final Context context) { + this.context = context; + this.mPrefs = PreferenceManager.getDefaultSharedPreferences(App.getApp()); + } + + public void initPlayer(final String uri, final VideoSegment[] segments) { + this.videoSegments = segments; + this.progressUpdateReactor = new SerialDisposable(); + + simpleExoPlayer = new SimpleExoPlayer + .Builder(context) + .build(); + simpleExoPlayer.addListener(this); + simpleExoPlayer.setSeekParameters(PlayerHelper.getSeekParameters(context)); + simpleExoPlayer.setHandleAudioBecomingNoisy(true); + + if (uri == null || uri.length() == 0) { + return; + } + + final MediaSource videoSource = new ProgressiveMediaSource + .Factory(new DefaultDataSourceFactory(context, DownloaderImpl.USER_AGENT)) + .createMediaSource(Uri.parse(uri)); + simpleExoPlayer.prepare(videoSource); + } + + public SimpleExoPlayer getPlayer() { + return this.simpleExoPlayer; + } + + public void setListener(final LocalPlayerListener listener) { + this.listener = listener; + } + + public void destroy() { + simpleExoPlayer.removeListener(this); + simpleExoPlayer.stop(); + simpleExoPlayer.release(); + progressUpdateReactor.set(null); + } + + @Override + public void onPlayerStateChanged(final boolean playWhenReady, final int playbackState) { + switch (playbackState) { + case com.google.android.exoplayer2.Player.STATE_IDLE: + break; + case com.google.android.exoplayer2.Player.STATE_BUFFERING: + break; + case com.google.android.exoplayer2.Player.STATE_READY: + changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); + break; + case com.google.android.exoplayer2.Player.STATE_ENDED: + changeState(STATE_COMPLETED); + break; + } + } + + private boolean isProgressLoopRunning() { + return progressUpdateReactor.get() != null; + } + + private void startProgressLoop() { + progressUpdateReactor.set(getProgressReactor()); + } + + private void stopProgressLoop() { + progressUpdateReactor.set(null); + } + + private Disposable getProgressReactor() { + return Observable.interval(PROGRESS_LOOP_INTERVAL_MILLIS, MILLISECONDS, + AndroidSchedulers.mainThread()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(ignored -> triggerProgressUpdate(), + error -> Log.e(TAG, "Progress update failure: ", error)); + } + + private void changeState(final int state) { + switch (state) { + case STATE_BLOCKED: + onBlocked(); + 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; + } + } + + private void onBlocked() { + if (!isProgressLoopRunning()) { + startProgressLoop(); + } + + if (listener != null) { + listener.onBlocked(simpleExoPlayer); + } + } + + private void onPlaying() { + if (!isProgressLoopRunning()) { + startProgressLoop(); + } + + if (listener != null) { + listener.onPlaying(simpleExoPlayer); + } + } + + private void onBuffering() { + if (listener != null) { + listener.onBuffering(simpleExoPlayer); + } + } + + private void onPaused() { + if (isProgressLoopRunning()) { + stopProgressLoop(); + } + + if (listener != null) { + listener.onPaused(simpleExoPlayer); + } + } + + private void onPausedSeek() { + if (listener != null) { + listener.onPausedSeek(simpleExoPlayer); + } + } + + private void onCompleted() { + if (isProgressLoopRunning()) { + stopProgressLoop(); + } + + if (listener != null) { + listener.onCompleted(simpleExoPlayer); + } + } + + private void triggerProgressUpdate() { + if (simpleExoPlayer == null) { + return; + } + final int currentProgress = Math.max((int) simpleExoPlayer.getCurrentPosition(), 0); + + final boolean isRewind = currentProgress < lastCurrentProgress; + + lastCurrentProgress = currentProgress; + + if (!mPrefs.getBoolean( + context.getString(R.string.sponsor_block_enable_key), false)) { + return; + } + + final VideoSegment segment = getSkippableSegment(currentProgress); + if (segment == null) { + return; + } + + int skipTarget = isRewind + ? (int) Math.ceil((segment.startTime)) - 1 + : (int) Math.ceil((segment.endTime)); + + if (skipTarget < 0) { + skipTarget = 0; + } + + // temporarily force EXACT seek parameters to prevent infinite skip looping + final SeekParameters seekParams = simpleExoPlayer.getSeekParameters(); + simpleExoPlayer.setSeekParameters(SeekParameters.EXACT); + + seekTo(skipTarget); + + simpleExoPlayer.setSeekParameters(seekParams); + + if (mPrefs.getBoolean( + context.getString(R.string.sponsor_block_notifications_key), false)) { + String toastText = ""; + + switch (segment.category) { + case "sponsor": + toastText = context + .getString(R.string.sponsor_block_skip_sponsor_toast); + break; + case "intro": + toastText = context + .getString(R.string.sponsor_block_skip_intro_toast); + break; + case "outro": + toastText = context + .getString(R.string.sponsor_block_skip_outro_toast); + break; + case "interaction": + toastText = context + .getString(R.string.sponsor_block_skip_interaction_toast); + break; + case "selfpromo": + toastText = context + .getString(R.string.sponsor_block_skip_self_promo_toast); + break; + case "music_offtopic": + toastText = context + .getString(R.string.sponsor_block_skip_non_music_toast); + break; + } + + Toast.makeText(context, toastText, Toast.LENGTH_SHORT).show(); + } + } + + private void seekTo(final long positionMillis) { + if (simpleExoPlayer != null) { + long normalizedPositionMillis = positionMillis; + if (normalizedPositionMillis < 0) { + normalizedPositionMillis = 0; + } else if (normalizedPositionMillis > simpleExoPlayer.getDuration()) { + normalizedPositionMillis = simpleExoPlayer.getDuration(); + } + + simpleExoPlayer.seekTo(normalizedPositionMillis); + } + } + + private VideoSegment getSkippableSegment(final int progress) { + if (videoSegments == null) { + return null; + } + + for (final VideoSegment segment : videoSegments) { + if (progress < segment.startTime) { + continue; + } + + if (progress > segment.endTime) { + continue; + } + + return segment; + } + + return null; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/LocalPlayerListener.java b/app/src/main/java/org/schabi/newpipe/player/LocalPlayerListener.java new file mode 100644 index 000000000..103534545 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/LocalPlayerListener.java @@ -0,0 +1,12 @@ +package org.schabi.newpipe.player; + +import com.google.android.exoplayer2.SimpleExoPlayer; + +public interface LocalPlayerListener { + void onBlocked(SimpleExoPlayer player); + void onPlaying(SimpleExoPlayer player); + void onBuffering(SimpleExoPlayer player); + void onPaused(SimpleExoPlayer player); + void onPausedSeek(SimpleExoPlayer player); + void onCompleted(SimpleExoPlayer player); +}