SponsorBlock: started work on a local player implementation
This commit is contained in:
parent
991c12c539
commit
420f99f317
16 changed files with 460 additions and 26 deletions
|
@ -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">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
|
@ -56,11 +56,9 @@
|
|||
android:name=".player.BackgroundPlayerActivity"
|
||||
android:label="@string/title_activity_play_queue"
|
||||
android:launchMode="singleTask" />
|
||||
|
||||
<activity
|
||||
android:name=".settings.SettingsActivity"
|
||||
android:label="@string/settings" />
|
||||
|
||||
<activity
|
||||
android:name=".about.AboutActivity"
|
||||
android:label="@string/title_activity_about" />
|
||||
|
@ -80,14 +78,11 @@
|
|||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ExitActivity"
|
||||
android:label="@string/general_error"
|
||||
android:theme="@android:style/Theme.NoDisplay" />
|
||||
<activity android:name=".report.ErrorActivity" />
|
||||
|
||||
<!-- giga get related -->
|
||||
<activity android:name=".report.ErrorActivity" /> <!-- giga get related -->
|
||||
<activity
|
||||
android:name=".download.DownloadActivity"
|
||||
android:label="@string/app_name"
|
||||
|
@ -101,10 +96,10 @@
|
|||
android:theme="@style/FilePickerThemeDark">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.GET_CONTENT" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ReCaptchaActivity"
|
||||
android:label="@string/recaptcha" />
|
||||
|
@ -262,7 +257,9 @@
|
|||
<!-- Share filter -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
|
||||
|
@ -280,7 +277,7 @@
|
|||
<data android:host="media.ccc.de" />
|
||||
<!-- video prefix -->
|
||||
<data android:pathPrefix="/v/" />
|
||||
<!-- channel prefix-->
|
||||
<!-- channel prefix -->
|
||||
<data android:pathPrefix="/c/" />
|
||||
<data android:pathPrefix="/b/" />
|
||||
</intent-filter>
|
||||
|
@ -296,7 +293,6 @@
|
|||
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
|
||||
<data android:host="framatube.org" />
|
||||
<data android:host="media.assassinate-you.net" />
|
||||
<data android:host="peertube.co.uk" />
|
||||
|
@ -308,20 +304,28 @@
|
|||
<data android:host="video.ploud.fr" />
|
||||
<data android:host="video.lqdn.fr" />
|
||||
<data android:host="skeptikon.fr" />
|
||||
|
||||
<data android:pathPrefix="/videos/" /> <!-- it contains playlists -->
|
||||
<data android:pathPrefix="/accounts/" />
|
||||
<data android:pathPrefix="/video-channels/" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".LocalPlayerActivity"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTask"
|
||||
android:foregroundServiceType="mediaPlayback" />
|
||||
|
||||
<service
|
||||
android:name=".RouterActivity$FetcherService"
|
||||
android:exported="false" />
|
||||
|
||||
<!-- see https://github.com/TeamNewPipe/NewPipe/issues/3947 -->
|
||||
android:exported="false" /> <!-- see https://github.com/TeamNewPipe/NewPipe/issues/3947 -->
|
||||
<!-- Version < 3.0. DeX Mode and Screen Mirroring support -->
|
||||
<meta-data android:name="com.samsung.android.keepalive.density" android:value="true"/>
|
||||
<!-- Version >= 3.0. DeX Dual Mode support -->
|
||||
<meta-data android:name="com.samsung.android.multidisplay.keep_process_alive" android:value="true"/>
|
||||
<meta-data
|
||||
android:name="com.samsung.android.keepalive.density"
|
||||
android:value="true" /> <!-- Version >= 3.0. DeX Dual Mode support -->
|
||||
<meta-data
|
||||
android:name="com.samsung.android.multidisplay.keep_process_alive"
|
||||
android:value="true" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
</manifest>
|
311
app/src/main/java/org/schabi/newpipe/LocalPlayerActivity.java
Normal file
311
app/src/main/java/org/schabi/newpipe/LocalPlayerActivity.java
Normal file
|
@ -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<VideoSegment> 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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -13,6 +13,7 @@ public class FinishedMission extends Mission {
|
|||
timestamp = mission.timestamp;
|
||||
kind = mission.kind;
|
||||
storage = mission.storage;
|
||||
segmentsJson = mission.segmentsJson;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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), "");
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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<ViewHolder> 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<ViewHolder> 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<ViewHolder> 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<ViewHolder> 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 -> {
|
||||
|
|
16
app/src/main/res/layout/activity_local_player.xml
Normal file
16
app/src/main/res/layout/activity_local_player.xml
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black"
|
||||
tools:context=".LocalPlayerActivity">
|
||||
<com.google.android.exoplayer2.ui.PlayerView
|
||||
android:id="@+id/player_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:show_buffering="when_playing"
|
||||
app:show_shuffle_button="true"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -29,6 +29,10 @@
|
|||
android:id="@+id/delete"
|
||||
android:title="@string/delete" />
|
||||
|
||||
<item
|
||||
android:id="@+id/open_externally"
|
||||
android:title="@string/open_with" />
|
||||
|
||||
<item
|
||||
android:id="@+id/error_message_view"
|
||||
android:title="@string/show_error" />
|
||||
|
|
|
@ -364,6 +364,7 @@
|
|||
<string name="sponsor_block_clear_whitelist_key" translatable="false">sponsor_block_clear_whitelist</string>
|
||||
|
||||
<!-- Extras -->
|
||||
<string name="enable_local_player_key" translatable="false">enable_local_player</string>
|
||||
<string name="disable_tablet_ui_key" translatable="false">disable_tablet_ui</string>
|
||||
<string name="disable_tv_ui_key" translatable="false">disable_tv_ui</string>
|
||||
|
||||
|
|
|
@ -360,6 +360,7 @@
|
|||
<string name="checksum">Checksum</string>
|
||||
<string name="dismiss">Dismiss</string>
|
||||
<string name="rename">Rename</string>
|
||||
<string name="open_with">Open With...</string>
|
||||
<!-- Fragment -->
|
||||
<string name="add">New mission</string>
|
||||
<string name="finish">OK</string>
|
||||
|
@ -750,6 +751,8 @@
|
|||
<string name="extras">Extras</string>
|
||||
<string name="extras_todo_summary">Tweaks, workarounds, and other miscellaneous settings belong here.</string>
|
||||
<string name="experimental_settings">Experimental Settings</string>
|
||||
<string name="enable_local_player_title">Enable Local Player (alpha)</string>
|
||||
<string name="enable_local_player_summary">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.</string>
|
||||
<string name="disable_tablet_ui_title">Disable Tablet UI</string>
|
||||
<string name="disable_tablet_ui_summary">Ignore tablet layouts. This is intended for specific workarounds. You may need to restart the app to see the effects.</string>
|
||||
<string name="disable_tv_ui_title">Disable TV UI</string>
|
||||
|
|
|
@ -13,6 +13,13 @@
|
|||
android:layout="@layout/settings_category_header_layout"
|
||||
android:title="@string/experimental_settings">
|
||||
|
||||
<SwitchPreference
|
||||
app:iconSpaceReserved="false"
|
||||
android:defaultValue="false"
|
||||
android:key="@string/enable_local_player_key"
|
||||
android:summary="@string/enable_local_player_summary"
|
||||
android:title="@string/enable_local_player_title"/>
|
||||
|
||||
<SwitchPreference
|
||||
app:iconSpaceReserved="false"
|
||||
android:defaultValue="false"
|
||||
|
|
|
@ -7,7 +7,7 @@ buildscript {
|
|||
google()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.1.1'
|
||||
classpath 'com.android.tools.build:gradle:4.1.2'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
|
|
Loading…
Reference in a new issue