From 420f99f31784ff821e15884d5b98777e3c2083af Mon Sep 17 00:00:00 2001 From: polymorphicshade Date: Tue, 26 Jan 2021 17:01:27 -0700 Subject: [PATCH] SponsorBlock: started work on a local player implementation --- app/src/main/AndroidManifest.xml | 40 ++- .../schabi/newpipe/LocalPlayerActivity.java | 311 ++++++++++++++++++ .../newpipe/download/DownloadDialog.java | 10 +- .../fragments/detail/VideoDetailFragment.java | 1 + .../org/schabi/newpipe/util/VideoSegment.java | 4 +- .../us/shandian/giga/get/FinishedMission.java | 1 + .../java/us/shandian/giga/get/Mission.java | 4 + .../giga/get/sqlite/FinishedMissionStore.java | 12 +- .../giga/service/DownloadManagerService.java | 29 +- .../giga/ui/adapter/MissionAdapter.java | 41 ++- .../main/res/layout/activity_local_player.xml | 16 + app/src/main/res/menu/mission.xml | 4 + app/src/main/res/values/settings_keys.xml | 1 + app/src/main/res/values/strings.xml | 3 + app/src/main/res/xml/extra_settings.xml | 7 + build.gradle | 2 +- 16 files changed, 460 insertions(+), 26 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/LocalPlayerActivity.java create mode 100644 app/src/main/res/layout/activity_local_player.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d240d123f..beaaf8a01 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -22,8 +22,8 @@ android:label="@string/app_name" android:logo="@mipmap/ic_launcher" android:requestLegacyExternalStorage="true" - android:theme="@style/OpeningTheme" android:resizeableActivity="true" + android:theme="@style/OpeningTheme" tools:ignore="AllowBackup"> - - @@ -80,14 +78,11 @@ - - - - + + - @@ -262,7 +257,9 @@ + + @@ -280,7 +277,7 @@ - + @@ -296,7 +293,6 @@ - @@ -308,20 +304,28 @@ - + + + - - + android:exported="false" /> - - - + + - + + \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/LocalPlayerActivity.java b/app/src/main/java/org/schabi/newpipe/LocalPlayerActivity.java new file mode 100644 index 000000000..9acc41480 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/LocalPlayerActivity.java @@ -0,0 +1,311 @@ +package org.schabi.newpipe; + +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.preference.PreferenceManager; + +import com.google.android.exoplayer2.Player; +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.ui.PlayerView; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.grack.nanojson.JsonObject; +import com.grack.nanojson.JsonParser; + +import org.schabi.newpipe.player.helper.PlayerHelper; +import org.schabi.newpipe.util.VideoSegment; + +import java.util.ArrayList; + +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.BasePlayer.STATE_BLOCKED; +import static org.schabi.newpipe.player.BasePlayer.STATE_BUFFERING; +import static org.schabi.newpipe.player.BasePlayer.STATE_COMPLETED; +import static org.schabi.newpipe.player.BasePlayer.STATE_PAUSED; +import static org.schabi.newpipe.player.BasePlayer.STATE_PAUSED_SEEK; +import static org.schabi.newpipe.player.BasePlayer.STATE_PLAYING; + +public class LocalPlayerActivity extends AppCompatActivity implements Player.EventListener { + private SimpleExoPlayer simpleExoPlayer; + private SerialDisposable progressUpdateReactor; + private int lastCurrentProgress = -1; + protected static final int PROGRESS_LOOP_INTERVAL_MILLIS = 500; + public static final String TAG = "LocalPlayer"; + private VideoSegment[] segments; + private SharedPreferences mPrefs; + + @Override + protected void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_local_player); + + this.mPrefs = PreferenceManager.getDefaultSharedPreferences(App.getApp()); + + initPlayer(); + initSegments(); + } + + private void initPlayer() { + final String uri = getIntent().getDataString(); + + if (uri == null) { + return; + } + + simpleExoPlayer = new SimpleExoPlayer + .Builder(this) + .build(); + simpleExoPlayer.addListener(this); + simpleExoPlayer.setSeekParameters(PlayerHelper.getSeekParameters(this)); + simpleExoPlayer.setHandleAudioBecomingNoisy(true); + final MediaSource videoSource = new ProgressiveMediaSource + .Factory(new DefaultDataSourceFactory(this, DownloaderImpl.USER_AGENT)) + .createMediaSource(Uri.parse(uri)); + simpleExoPlayer.prepare(videoSource); + final PlayerView playerView = findViewById(R.id.player_view); + playerView.setPlayer(simpleExoPlayer); + + this.progressUpdateReactor = new SerialDisposable(); + } + + private void initSegments() { + final String segmentsJson = getIntent().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")) { + final JsonObject itemObject = (JsonObject) item; + + final double startTime = itemObject.getDouble("start"); + final double endTime = itemObject.getDouble("end"); + final String category = itemObject.getString("category"); + + final VideoSegment segment = new VideoSegment(startTime, endTime, category); + segmentsArrayList.add(segment); + } + + segments = segmentsArrayList.toArray(new VideoSegment[0]); + } catch (final Exception e) { + Log.e(TAG, "Error initializing segments", e); + } + } + } + + @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; + } + } + + 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; + } + + 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( + 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(); + } + } + + 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); + } + } + + 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; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index e80a0ab21..0e5b3075e 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -60,6 +60,7 @@ import org.schabi.newpipe.util.SecondaryStreamHelper; import org.schabi.newpipe.util.StreamItemAdapter; import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper; import org.schabi.newpipe.util.ThemeHelper; +import org.schabi.newpipe.util.VideoSegment; import java.io.File; import java.io.IOException; @@ -124,6 +125,8 @@ public class DownloadDialog extends DialogFragment private SharedPreferences prefs; + private VideoSegment[] segments; + public static DownloadDialog newInstance(final StreamInfo info) { final DownloadDialog dialog = new DownloadDialog(); dialog.setInfo(info); @@ -190,6 +193,10 @@ public class DownloadDialog extends DialogFragment this.selectedSubtitleIndex = ssi; } + public void setVideoSegments(final VideoSegment[] seg) { + this.segments = seg; + } + @Override public void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -942,7 +949,8 @@ public class DownloadDialog extends DialogFragment } DownloadManagerService.startMission(context, urls, storage, kind, threads, - currentInfo.getUrl(), psName, psArgs, nearLength, recoveryInfo); + currentInfo.getUrl(), psName, psArgs, nearLength, recoveryInfo, + segments); dismiss(); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 77ceb4a51..66b31742d 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -1678,6 +1678,7 @@ public final class VideoDetailFragment downloadDialog.setAudioStreams(currentInfo.getAudioStreams()); downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex); downloadDialog.setSubtitleStreams(currentInfo.getSubtitles()); + downloadDialog.setVideoSegments(player.getVideoSegments()); downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog"); } catch (final Exception e) { diff --git a/app/src/main/java/org/schabi/newpipe/util/VideoSegment.java b/app/src/main/java/org/schabi/newpipe/util/VideoSegment.java index e639993d1..5fdad49ef 100644 --- a/app/src/main/java/org/schabi/newpipe/util/VideoSegment.java +++ b/app/src/main/java/org/schabi/newpipe/util/VideoSegment.java @@ -1,6 +1,8 @@ package org.schabi.newpipe.util; -public class VideoSegment { +import java.io.Serializable; + +public class VideoSegment implements Serializable { public double startTime; public double endTime; public String category; diff --git a/app/src/main/java/us/shandian/giga/get/FinishedMission.java b/app/src/main/java/us/shandian/giga/get/FinishedMission.java index 29f3c6296..84450a96b 100644 --- a/app/src/main/java/us/shandian/giga/get/FinishedMission.java +++ b/app/src/main/java/us/shandian/giga/get/FinishedMission.java @@ -13,6 +13,7 @@ public class FinishedMission extends Mission { timestamp = mission.timestamp; kind = mission.kind; storage = mission.storage; + segmentsJson = mission.segmentsJson; } } diff --git a/app/src/main/java/us/shandian/giga/get/Mission.java b/app/src/main/java/us/shandian/giga/get/Mission.java index ecb0eaebd..329ff7485 100644 --- a/app/src/main/java/us/shandian/giga/get/Mission.java +++ b/app/src/main/java/us/shandian/giga/get/Mission.java @@ -2,6 +2,8 @@ package us.shandian.giga.get; import androidx.annotation.NonNull; +import org.schabi.newpipe.util.VideoSegment; + import java.io.Serializable; import java.util.Calendar; @@ -35,6 +37,8 @@ public abstract class Mission implements Serializable { */ public StoredFileHelper storage; + public String segmentsJson; + public long getTimestamp() { return timestamp; } diff --git a/app/src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java b/app/src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java index 1d1dca0df..e8bbf0e6e 100644 --- a/app/src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java +++ b/app/src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java @@ -26,7 +26,7 @@ public class FinishedMissionStore extends SQLiteOpenHelper { // TODO: use NewPipeSQLiteHelper ('s constants) when playlist branch is merged (?) private static final String DATABASE_NAME = "downloads.db"; - private static final int DATABASE_VERSION = 4; + private static final int DATABASE_VERSION = 5; /** * The table name of download missions (old) @@ -55,6 +55,8 @@ public class FinishedMissionStore extends SQLiteOpenHelper { private static final String KEY_PATH = "path"; + private static final String KEY_SEGMENTS = "segments"; + /** * The statement to create the table */ @@ -65,6 +67,7 @@ public class FinishedMissionStore extends SQLiteOpenHelper { KEY_DONE + " INTEGER NOT NULL, " + KEY_TIMESTAMP + " INTEGER NOT NULL, " + KEY_KIND + " TEXT NOT NULL, " + + KEY_SEGMENTS + " TEXT, " + " UNIQUE(" + KEY_TIMESTAMP + ", " + KEY_PATH + "));"; @@ -120,6 +123,11 @@ public class FinishedMissionStore extends SQLiteOpenHelper { cursor.close(); db.execSQL("DROP TABLE " + MISSIONS_TABLE_NAME_v2); + oldVersion++; + } + + if (oldVersion == 4) { + db.execSQL("ALTER TABLE " + FINISHED_TABLE_NAME + " ADD COLUMN " + KEY_SEGMENTS + " TEXT;"); } } @@ -136,6 +144,7 @@ public class FinishedMissionStore extends SQLiteOpenHelper { values.put(KEY_DONE, downloadMission.length); values.put(KEY_TIMESTAMP, downloadMission.timestamp); values.put(KEY_KIND, String.valueOf(downloadMission.kind)); + values.put(KEY_SEGMENTS, downloadMission.segmentsJson); return values; } @@ -153,6 +162,7 @@ public class FinishedMissionStore extends SQLiteOpenHelper { mission.length = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_DONE)); mission.timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_TIMESTAMP)); mission.kind = kind.charAt(0); + mission.segmentsJson = cursor.getString(cursor.getColumnIndexOrThrow(KEY_SEGMENTS)); try { mission.storage = new StoredFileHelper(context,null, Uri.parse(path), ""); diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java index e77196445..cbba4cd0a 100755 --- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java @@ -38,9 +38,13 @@ import androidx.annotation.StringRes; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat.Builder; +import com.grack.nanojson.JsonStringWriter; +import com.grack.nanojson.JsonWriter; + import org.schabi.newpipe.R; import org.schabi.newpipe.download.DownloadActivity; import org.schabi.newpipe.player.helper.LockManager; +import org.schabi.newpipe.util.VideoSegment; import java.io.File; import java.io.IOException; @@ -80,6 +84,7 @@ public class DownloadManagerService extends Service { private static final String EXTRA_PARENT_PATH = "DownloadManagerService.extra.storageParentPath"; private static final String EXTRA_STORAGE_TAG = "DownloadManagerService.extra.storageTag"; private static final String EXTRA_RECOVERY_INFO = "DownloadManagerService.extra.recoveryInfo"; + private static final String EXTRA_SEGMENTS = "DownloadManagerService.extra.segments"; private static final String ACTION_RESET_DOWNLOAD_FINISHED = APPLICATION_ID + ".reset_download_finished"; private static final String ACTION_OPEN_DOWNLOADS_FINISHED = APPLICATION_ID + ".open_downloads_finished"; @@ -388,7 +393,8 @@ public class DownloadManagerService extends Service { */ public static void startMission(Context context, String[] urls, StoredFileHelper storage, char kind, int threads, String source, String psName, - String[] psArgs, long nearLength, MissionRecoveryInfo[] recoveryInfo) { + String[] psArgs, long nearLength, MissionRecoveryInfo[] recoveryInfo, + VideoSegment[] segments) { Intent intent = new Intent(context, DownloadManagerService.class); intent.setAction(Intent.ACTION_RUN); intent.putExtra(EXTRA_URLS, urls); @@ -403,6 +409,7 @@ public class DownloadManagerService extends Service { intent.putExtra(EXTRA_PARENT_PATH, storage.getParentUri()); intent.putExtra(EXTRA_PATH, storage.getUri()); intent.putExtra(EXTRA_STORAGE_TAG, storage.getTag()); + intent.putExtra(EXTRA_SEGMENTS, segments); context.startService(intent); } @@ -419,6 +426,7 @@ public class DownloadManagerService extends Service { long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0); String tag = intent.getStringExtra(EXTRA_STORAGE_TAG); Parcelable[] parcelRecovery = intent.getParcelableArrayExtra(EXTRA_RECOVERY_INFO); + VideoSegment[] segments = (VideoSegment[]) intent.getSerializableExtra(EXTRA_SEGMENTS); StoredFileHelper storage; try { @@ -443,6 +451,25 @@ public class DownloadManagerService extends Service { mission.nearLength = nearLength; mission.recoveryInfo = recovery; + if (segments != null && segments.length > 0) { + try { + final JsonStringWriter writer = JsonWriter.string() + .object() + .array("segments"); + for (final VideoSegment segment : segments) { + writer.object() + .value("start", segment.startTime) + .value("end", segment.endTime) + .value("category", segment.category) + .end(); + } + writer.end().end(); + mission.segmentsJson = writer.done(); + } catch (final Exception e) { + e.printStackTrace(); + } + } + if (ps != null) ps.setTemporalDir(DownloadManager.pickAvailableTemporalDir(this)); diff --git a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java index f102206c1..28f0c9f21 100644 --- a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java +++ b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java @@ -4,6 +4,7 @@ import android.annotation.SuppressLint; import android.app.NotificationManager; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.graphics.Color; import android.net.Uri; import android.os.Build; @@ -30,6 +31,7 @@ import androidx.core.app.NotificationCompat; import androidx.core.content.ContextCompat; import androidx.core.content.FileProvider; import androidx.core.view.ViewCompat; +import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView.Adapter; @@ -37,7 +39,9 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder; import com.google.android.material.snackbar.Snackbar; +import org.schabi.newpipe.App; import org.schabi.newpipe.BuildConfig; +import org.schabi.newpipe.LocalPlayerActivity; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.report.ErrorActivity; @@ -122,8 +126,12 @@ public class MissionAdapter extends Adapter implements Handler.Callb private final CompositeDisposable compositeDisposable = new CompositeDisposable(); + private SharedPreferences mPrefs; + public MissionAdapter(Context context, @NonNull DownloadManager downloadManager, View emptyMessage, View root) { mContext = context; + mPrefs = PreferenceManager.getDefaultSharedPreferences(App.getApp()); + mDownloadManager = downloadManager; mInflater = LayoutInflater.from(mContext); @@ -338,7 +346,25 @@ public class MissionAdapter extends Adapter implements Handler.Callb } } - private void viewWithFileProvider(Mission mission) { + private void open(Mission mission) { + if (checkInvalidFile(mission)) return; + + String mimeType = resolveMimeType(mission); + + if (BuildConfig.DEBUG) + Log.v(TAG, "Mime: " + mimeType + " package: " + BuildConfig.APPLICATION_ID + ".provider"); + + Uri uri = resolveShareableUri(mission); + + Intent intent = new Intent(mContext, LocalPlayerActivity.class); + intent.setDataAndType(uri, mimeType); + intent.putExtra("segments", mission.segmentsJson); + intent.setFlags(FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); + + mContext.startActivity(intent); + } + + private void openExternally(Mission mission) { if (checkInvalidFile(mission)) return; String mimeType = resolveMimeType(mission); @@ -679,6 +705,9 @@ public class MissionAdapter extends Adapter implements Handler.Callb applyChanges(); checkMasterButtonsVisibility(); return true; + case R.id.open_externally: + openExternally(h.item.mission); + return true; case R.id.md5: case R.id.sha1: final NotificationManager notificationManager @@ -896,8 +925,14 @@ public class MissionAdapter extends Adapter implements Handler.Callb itemView.setHapticFeedbackEnabled(true); itemView.setOnClickListener(v -> { - if (item.mission instanceof FinishedMission) - viewWithFileProvider(item.mission); + if (item.mission instanceof FinishedMission) { + if (mPrefs.getBoolean(mContext + .getString(R.string.enable_local_player_key), false)) { + open(item.mission); + } else { + openExternally(item.mission); + } + } }); itemView.setOnLongClickListener(v -> { diff --git a/app/src/main/res/layout/activity_local_player.xml b/app/src/main/res/layout/activity_local_player.xml new file mode 100644 index 000000000..9f314fa60 --- /dev/null +++ b/app/src/main/res/layout/activity_local_player.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/mission.xml b/app/src/main/res/menu/mission.xml index 4273c1ed6..77e56dce8 100644 --- a/app/src/main/res/menu/mission.xml +++ b/app/src/main/res/menu/mission.xml @@ -29,6 +29,10 @@ android:id="@+id/delete" android:title="@string/delete" /> + + diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index f21a8a694..a6e774526 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -364,6 +364,7 @@ sponsor_block_clear_whitelist + enable_local_player disable_tablet_ui disable_tv_ui diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 151cc74bb..a65f03171 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -360,6 +360,7 @@ Checksum Dismiss Rename + Open With... New mission OK @@ -750,6 +751,8 @@ Extras Tweaks, workarounds, and other miscellaneous settings belong here. Experimental Settings + Enable Local Player (alpha) + Use a built-in player for local playback. This is still in early development so there will probably be a lot of issues, including conflicts with the existing player. Disable Tablet UI Ignore tablet layouts. This is intended for specific workarounds. You may need to restart the app to see the effects. Disable TV UI diff --git a/app/src/main/res/xml/extra_settings.xml b/app/src/main/res/xml/extra_settings.xml index 342336027..b809ab6cb 100644 --- a/app/src/main/res/xml/extra_settings.xml +++ b/app/src/main/res/xml/extra_settings.xml @@ -13,6 +13,13 @@ android:layout="@layout/settings_category_header_layout" android:title="@string/experimental_settings"> + +