Merge branch 'pfix' into dev
This commit is contained in:
commit
a3468b51e2
|
@ -73,7 +73,7 @@ dependencies {
|
||||||
implementation 'de.hdodenhof:circleimageview:2.2.0'
|
implementation 'de.hdodenhof:circleimageview:2.2.0'
|
||||||
implementation 'com.github.nirhart:ParallaxScroll:dd53d1f9d1'
|
implementation 'com.github.nirhart:ParallaxScroll:dd53d1f9d1'
|
||||||
implementation 'com.nononsenseapps:filepicker:3.0.1'
|
implementation 'com.nononsenseapps:filepicker:3.0.1'
|
||||||
implementation 'com.google.android.exoplayer:exoplayer:r2.5.4'
|
implementation 'com.google.android.exoplayer:exoplayer:2.6.0'
|
||||||
|
|
||||||
debugImplementation 'com.facebook.stetho:stetho:1.5.0'
|
debugImplementation 'com.facebook.stetho:stetho:1.5.0'
|
||||||
debugImplementation 'com.facebook.stetho:stetho-urlconnection:1.5.0'
|
debugImplementation 'com.facebook.stetho:stetho-urlconnection:1.5.0'
|
||||||
|
|
|
@ -65,6 +65,12 @@ public class DebugApp extends App {
|
||||||
Stetho.initialize(initializer);
|
Stetho.initialize(initializer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isDisposedRxExceptionsReported() {
|
||||||
|
return PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
.getBoolean(getString(R.string.allow_disposed_exceptions_key), true);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected RefWatcher installLeakCanary() {
|
protected RefWatcher installLeakCanary() {
|
||||||
return LeakCanary.refWatcher(this)
|
return LeakCanary.refWatcher(this)
|
||||||
|
|
|
@ -31,9 +31,13 @@ import org.schabi.newpipe.util.StateSaver;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InterruptedIOException;
|
import java.io.InterruptedIOException;
|
||||||
import java.net.SocketException;
|
import java.net.SocketException;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import io.reactivex.annotations.NonNull;
|
import io.reactivex.annotations.NonNull;
|
||||||
import io.reactivex.exceptions.CompositeException;
|
import io.reactivex.exceptions.CompositeException;
|
||||||
|
import io.reactivex.exceptions.MissingBackpressureException;
|
||||||
|
import io.reactivex.exceptions.OnErrorNotImplementedException;
|
||||||
import io.reactivex.exceptions.UndeliverableException;
|
import io.reactivex.exceptions.UndeliverableException;
|
||||||
import io.reactivex.functions.Consumer;
|
import io.reactivex.functions.Consumer;
|
||||||
import io.reactivex.plugins.RxJavaPlugins;
|
import io.reactivex.plugins.RxJavaPlugins;
|
||||||
|
@ -104,31 +108,58 @@ public class App extends Application {
|
||||||
RxJavaPlugins.setErrorHandler(new Consumer<Throwable>() {
|
RxJavaPlugins.setErrorHandler(new Consumer<Throwable>() {
|
||||||
@Override
|
@Override
|
||||||
public void accept(@NonNull Throwable throwable) throws Exception {
|
public void accept(@NonNull Throwable throwable) throws Exception {
|
||||||
Log.e(TAG, "RxJavaPlugins.ErrorHandler called with -> : throwable = [" + throwable.getClass().getName() + "]");
|
Log.e(TAG, "RxJavaPlugins.ErrorHandler called with -> : " +
|
||||||
|
"throwable = [" + throwable.getClass().getName() + "]");
|
||||||
|
|
||||||
if (throwable instanceof UndeliverableException) {
|
if (throwable instanceof UndeliverableException) {
|
||||||
// As UndeliverableException is a wrapper, get the cause of it to get the "real" exception
|
// As UndeliverableException is a wrapper, get the cause of it to get the "real" exception
|
||||||
throwable = throwable.getCause();
|
throwable = throwable.getCause();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final List<Throwable> errors;
|
||||||
if (throwable instanceof CompositeException) {
|
if (throwable instanceof CompositeException) {
|
||||||
for (Throwable element : ((CompositeException) throwable).getExceptions()) {
|
errors = ((CompositeException) throwable).getExceptions();
|
||||||
if (checkThrowable(element)) return;
|
} else {
|
||||||
|
errors = Collections.singletonList(throwable);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final Throwable error : errors) {
|
||||||
|
if (isThrowableIgnored(error)) return;
|
||||||
|
if (isThrowableCritical(error)) {
|
||||||
|
reportException(error);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (checkThrowable(throwable)) return;
|
// Out-of-lifecycle exceptions should only be reported if a debug user wishes so,
|
||||||
|
// When exception is not reported, log it
|
||||||
|
if (isDisposedRxExceptionsReported()) {
|
||||||
|
reportException(throwable);
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "RxJavaPlugin: Undeliverable Exception received: ", throwable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isThrowableIgnored(@NonNull final Throwable throwable) {
|
||||||
|
// Don't crash the application over a simple network problem
|
||||||
|
return ExtractorHelper.hasAssignableCauseThrowable(throwable,
|
||||||
|
IOException.class, SocketException.class, // network api cancellation
|
||||||
|
InterruptedException.class, InterruptedIOException.class); // blocking code disposed
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isThrowableCritical(@NonNull final Throwable throwable) {
|
||||||
|
// Though these exceptions cannot be ignored
|
||||||
|
return ExtractorHelper.hasAssignableCauseThrowable(throwable,
|
||||||
|
NullPointerException.class, IllegalArgumentException.class, // bug in app
|
||||||
|
OnErrorNotImplementedException.class, MissingBackpressureException.class,
|
||||||
|
IllegalStateException.class); // bug in operator
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reportException(@NonNull final Throwable throwable) {
|
||||||
// Throw uncaught exception that will trigger the report system
|
// Throw uncaught exception that will trigger the report system
|
||||||
Thread.currentThread().getUncaughtExceptionHandler()
|
Thread.currentThread().getUncaughtExceptionHandler()
|
||||||
.uncaughtException(Thread.currentThread(), throwable);
|
.uncaughtException(Thread.currentThread(), throwable);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean checkThrowable(@NonNull Throwable throwable) {
|
|
||||||
// Don't crash the application over a simple network problem
|
|
||||||
return ExtractorHelper.hasAssignableCauseThrowable(throwable,
|
|
||||||
IOException.class, SocketException.class, InterruptedException.class, InterruptedIOException.class);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -182,4 +213,8 @@ public class App extends Application {
|
||||||
protected RefWatcher installLeakCanary() {
|
protected RefWatcher installLeakCanary() {
|
||||||
return RefWatcher.DISABLED;
|
return RefWatcher.DISABLED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected boolean isDisposedRxExceptionsReported() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,6 @@
|
||||||
|
|
||||||
package org.schabi.newpipe;
|
package org.schabi.newpipe;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
@ -28,7 +27,6 @@ import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
import android.support.design.widget.NavigationView;
|
import android.support.design.widget.NavigationView;
|
||||||
import android.support.v4.app.Fragment;
|
import android.support.v4.app.Fragment;
|
||||||
import android.support.v4.view.GravityCompat;
|
import android.support.v4.view.GravityCompat;
|
||||||
|
@ -264,22 +262,6 @@ public class MainActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ShowToast")
|
|
||||||
private void onHeapDumpToggled(@NonNull MenuItem item) {
|
|
||||||
final boolean isHeapDumpEnabled = !item.isChecked();
|
|
||||||
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(this).edit()
|
|
||||||
.putBoolean(getString(R.string.allow_heap_dumping_key), isHeapDumpEnabled).apply();
|
|
||||||
item.setChecked(isHeapDumpEnabled);
|
|
||||||
|
|
||||||
final String heapDumpNotice;
|
|
||||||
if (isHeapDumpEnabled) {
|
|
||||||
heapDumpNotice = getString(R.string.enable_leak_canary_notice);
|
|
||||||
} else {
|
|
||||||
heapDumpNotice = getString(R.string.disable_leak_canary_notice);
|
|
||||||
}
|
|
||||||
Toast.makeText(getApplicationContext(), heapDumpNotice, Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Menu
|
// Menu
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
@ -301,10 +283,6 @@ public class MainActivity extends AppCompatActivity {
|
||||||
inflater.inflate(R.menu.main_menu, menu);
|
inflater.inflate(R.menu.main_menu, menu);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (DEBUG) {
|
|
||||||
getMenuInflater().inflate(R.menu.debug_menu, menu);
|
|
||||||
}
|
|
||||||
|
|
||||||
ActionBar actionBar = getSupportActionBar();
|
ActionBar actionBar = getSupportActionBar();
|
||||||
if (actionBar != null) {
|
if (actionBar != null) {
|
||||||
actionBar.setDisplayHomeAsUpEnabled(false);
|
actionBar.setDisplayHomeAsUpEnabled(false);
|
||||||
|
@ -315,17 +293,6 @@ public class MainActivity extends AppCompatActivity {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
|
||||||
MenuItem heapDumpToggle = menu.findItem(R.id.action_toggle_heap_dump);
|
|
||||||
if (heapDumpToggle != null) {
|
|
||||||
final boolean isToggled = PreferenceManager.getDefaultSharedPreferences(this)
|
|
||||||
.getBoolean(getString(R.string.allow_heap_dumping_key), false);
|
|
||||||
heapDumpToggle.setChecked(isToggled);
|
|
||||||
}
|
|
||||||
return super.onPrepareOptionsMenu(menu);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
if (DEBUG) Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]");
|
if (DEBUG) Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]");
|
||||||
|
@ -346,9 +313,6 @@ public class MainActivity extends AppCompatActivity {
|
||||||
case R.id.action_history:
|
case R.id.action_history:
|
||||||
NavigationHelper.openHistory(this);
|
NavigationHelper.openHistory(this);
|
||||||
return true;
|
return true;
|
||||||
case R.id.action_toggle_heap_dump:
|
|
||||||
onHeapDumpToggled(item);
|
|
||||||
return true;
|
|
||||||
default:
|
default:
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,6 +81,10 @@ import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.disposables.CompositeDisposable;
|
import io.reactivex.disposables.CompositeDisposable;
|
||||||
import io.reactivex.disposables.Disposable;
|
import io.reactivex.disposables.Disposable;
|
||||||
|
|
||||||
|
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_INTERNAL;
|
||||||
|
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_PERIOD_TRANSITION;
|
||||||
|
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK;
|
||||||
|
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT;
|
||||||
import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString;
|
import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -279,6 +283,11 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
||||||
if (playbackManager != null) playbackManager.dispose();
|
if (playbackManager != null) playbackManager.dispose();
|
||||||
if (audioReactor != null) audioReactor.abandonAudioFocus();
|
if (audioReactor != null) audioReactor.abandonAudioFocus();
|
||||||
if (databaseUpdateReactor != null) databaseUpdateReactor.dispose();
|
if (databaseUpdateReactor != null) databaseUpdateReactor.dispose();
|
||||||
|
|
||||||
|
if (playQueueAdapter != null) {
|
||||||
|
playQueueAdapter.unsetSelectedListener();
|
||||||
|
playQueueAdapter.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void destroy() {
|
public void destroy() {
|
||||||
|
@ -460,11 +469,11 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
||||||
final PlayQueueItem currentSourceItem = playQueue.getItem();
|
final PlayQueueItem currentSourceItem = playQueue.getItem();
|
||||||
|
|
||||||
// Check if already playing correct window
|
// Check if already playing correct window
|
||||||
final boolean isCurrentWindowCorrect =
|
final boolean isCurrentPeriodCorrect =
|
||||||
simpleExoPlayer.getCurrentWindowIndex() == currentSourceIndex;
|
simpleExoPlayer.getCurrentPeriodIndex() == currentSourceIndex;
|
||||||
|
|
||||||
// Check if recovering
|
// Check if recovering
|
||||||
if (isCurrentWindowCorrect && currentSourceItem != null) {
|
if (isCurrentPeriodCorrect && currentSourceItem != null) {
|
||||||
/* Recovering with sub-second position may cause a long buffer delay in ExoPlayer,
|
/* Recovering with sub-second position may cause a long buffer delay in ExoPlayer,
|
||||||
* rounding this position to the nearest second will help alleviate this.*/
|
* rounding this position to the nearest second will help alleviate this.*/
|
||||||
final long position = currentSourceItem.getRecoveryPosition();
|
final long position = currentSourceItem.getRecoveryPosition();
|
||||||
|
@ -605,17 +614,25 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPositionDiscontinuity() {
|
public void onPositionDiscontinuity(int reason) {
|
||||||
|
if (DEBUG) Log.d(TAG, "onPositionDiscontinuity() called with reason = [" + reason + "]");
|
||||||
// Refresh the playback if there is a transition to the next video
|
// Refresh the playback if there is a transition to the next video
|
||||||
final int newWindowIndex = simpleExoPlayer.getCurrentWindowIndex();
|
final int newPeriodIndex = simpleExoPlayer.getCurrentPeriodIndex();
|
||||||
if (DEBUG) Log.d(TAG, "onPositionDiscontinuity() called with window index = [" + newWindowIndex + "]");
|
|
||||||
|
|
||||||
// If the user selects a new track, then the discontinuity occurs after the index is changed.
|
/* Discontinuity reasons!! Thank you ExoPlayer lords */
|
||||||
// Therefore, the only source that causes a discrepancy would be gapless transition,
|
switch (reason) {
|
||||||
// which can only offset the current track by +1.
|
case DISCONTINUITY_REASON_PERIOD_TRANSITION:
|
||||||
if (newWindowIndex == playQueue.getIndex() + 1 ||
|
if (newPeriodIndex == playQueue.getIndex()) {
|
||||||
(newWindowIndex == 0 && playQueue.getIndex() == playQueue.size() - 1)) {
|
registerView();
|
||||||
playQueue.offsetIndex(+1);
|
} else {
|
||||||
|
playQueue.offsetIndex(+1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case DISCONTINUITY_REASON_SEEK:
|
||||||
|
case DISCONTINUITY_REASON_SEEK_ADJUSTMENT:
|
||||||
|
case DISCONTINUITY_REASON_INTERNAL:
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
playbackManager.load();
|
playbackManager.load();
|
||||||
}
|
}
|
||||||
|
@ -625,6 +642,16 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
||||||
if (DEBUG) Log.d(TAG, "onRepeatModeChanged() called with: mode = [" + i + "]");
|
if (DEBUG) Log.d(TAG, "onRepeatModeChanged() called with: mode = [" + i + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
|
||||||
|
if (DEBUG) Log.d(TAG, "onShuffleModeEnabledChanged() called with: " +
|
||||||
|
"mode = [" + shuffleModeEnabled + "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSeekProcessed() {
|
||||||
|
if (DEBUG) Log.d(TAG, "onSeekProcessed() called");
|
||||||
|
}
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Playback Listener
|
// Playback Listener
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
@ -668,19 +695,14 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
||||||
if (currentSourceIndex != playQueue.getIndex()) {
|
if (currentSourceIndex != playQueue.getIndex()) {
|
||||||
Log.e(TAG, "Play Queue may be desynchronized: item index=[" + currentSourceIndex +
|
Log.e(TAG, "Play Queue may be desynchronized: item index=[" + currentSourceIndex +
|
||||||
"], queue index=[" + playQueue.getIndex() + "]");
|
"], queue index=[" + playQueue.getIndex() + "]");
|
||||||
} else if (simpleExoPlayer.getCurrentWindowIndex() != currentSourceIndex || !isPlaying()) {
|
} else if (simpleExoPlayer.getCurrentPeriodIndex() != currentSourceIndex || !isPlaying()) {
|
||||||
final long startPos = info != null ? info.start_position : 0;
|
final long startPos = info != null ? info.start_position : 0;
|
||||||
if (DEBUG) Log.d(TAG, "Rewinding to correct window: " + currentSourceIndex +
|
if (DEBUG) Log.d(TAG, "Rewinding to correct window: " + currentSourceIndex +
|
||||||
" at: " + getTimeString((int)startPos));
|
" at: " + getTimeString((int)startPos));
|
||||||
simpleExoPlayer.seekTo(currentSourceIndex, startPos);
|
simpleExoPlayer.seekTo(currentSourceIndex, startPos);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: update exoplayer to 2.6.x in order to register view count on repeated streams
|
registerView();
|
||||||
databaseUpdateReactor.add(recordManager.onViewed(currentInfo).onErrorComplete()
|
|
||||||
.subscribe(
|
|
||||||
ignored -> {/* successful */},
|
|
||||||
error -> Log.e(TAG, "Player onViewed() failure: ", error)
|
|
||||||
));
|
|
||||||
initThumbnail(info == null ? item.getThumbnailUrl() : info.thumbnail_url);
|
initThumbnail(info == null ? item.getThumbnailUrl() : info.thumbnail_url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -814,6 +836,15 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
||||||
// Utils
|
// Utils
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
private void registerView() {
|
||||||
|
if (databaseUpdateReactor == null || recordManager == null || currentInfo == null) return;
|
||||||
|
databaseUpdateReactor.add(recordManager.onViewed(currentInfo).onErrorComplete()
|
||||||
|
.subscribe(
|
||||||
|
ignored -> {/* successful */},
|
||||||
|
error -> Log.e(TAG, "Player onViewed() failure: ", error)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
protected void reload() {
|
protected void reload() {
|
||||||
if (playbackManager != null) {
|
if (playbackManager != null) {
|
||||||
playbackManager.reset();
|
playbackManager.reset();
|
||||||
|
|
|
@ -61,6 +61,9 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||||
|
|
||||||
private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80;
|
private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80;
|
||||||
|
|
||||||
|
private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 10;
|
||||||
|
private static final int MAXIMUM_INITIAL_DRAG_VELOCITY = 25;
|
||||||
|
|
||||||
private View rootView;
|
private View rootView;
|
||||||
|
|
||||||
private RecyclerView itemsList;
|
private RecyclerView itemsList;
|
||||||
|
@ -211,6 +214,15 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||||
unbindService(serviceConnection);
|
unbindService(serviceConnection);
|
||||||
serviceBound = false;
|
serviceBound = false;
|
||||||
stopPlayerListener();
|
stopPlayerListener();
|
||||||
|
|
||||||
|
if (player != null && player.getPlayQueueAdapter() != null) {
|
||||||
|
player.getPlayQueueAdapter().unsetSelectedListener();
|
||||||
|
}
|
||||||
|
if (itemsList != null) itemsList.setAdapter(null);
|
||||||
|
if (itemTouchHelper != null) itemTouchHelper.attachToRecyclerView(null);
|
||||||
|
|
||||||
|
itemsList = null;
|
||||||
|
itemTouchHelper = null;
|
||||||
player = null;
|
player = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -385,7 +397,19 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||||
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
|
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
|
||||||
return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) {
|
return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) {
|
||||||
@Override
|
@Override
|
||||||
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) {
|
public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize,
|
||||||
|
int viewSizeOutOfBounds, int totalSize,
|
||||||
|
long msSinceStartScroll) {
|
||||||
|
final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize,
|
||||||
|
viewSizeOutOfBounds, totalSize, msSinceStartScroll);
|
||||||
|
final int clampedAbsVelocity = Math.max(MINIMUM_INITIAL_DRAG_VELOCITY,
|
||||||
|
Math.min(Math.abs(standardSpeed), MAXIMUM_INITIAL_DRAG_VELOCITY));
|
||||||
|
return clampedAbsVelocity * (int) Math.signum(viewSizeOutOfBounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source,
|
||||||
|
RecyclerView.ViewHolder target) {
|
||||||
if (source.getItemViewType() != target.getItemViewType()) {
|
if (source.getItemViewType() != target.getItemViewType()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -263,7 +263,9 @@ public abstract class VideoPlayer extends BasePlayer
|
||||||
VideoStream videoStream = availableStreams.get(i);
|
VideoStream videoStream = availableStreams.get(i);
|
||||||
qualityPopupMenu.getMenu().add(qualityPopupMenuGroupId, i, Menu.NONE, MediaFormat.getNameById(videoStream.format) + " " + videoStream.resolution);
|
qualityPopupMenu.getMenu().add(qualityPopupMenuGroupId, i, Menu.NONE, MediaFormat.getNameById(videoStream.format) + " " + videoStream.resolution);
|
||||||
}
|
}
|
||||||
qualityTextView.setText(getSelectedVideoStream().resolution);
|
if (getSelectedVideoStream() != null) {
|
||||||
|
qualityTextView.setText(getSelectedVideoStream().resolution);
|
||||||
|
}
|
||||||
qualityPopupMenu.setOnMenuItemClickListener(this);
|
qualityPopupMenu.setOnMenuItemClickListener(this);
|
||||||
qualityPopupMenu.setOnDismissListener(this);
|
qualityPopupMenu.setOnDismissListener(this);
|
||||||
}
|
}
|
||||||
|
@ -326,7 +328,7 @@ public abstract class VideoPlayer extends BasePlayer
|
||||||
qualityTextView.setVisibility(View.GONE);
|
qualityTextView.setVisibility(View.GONE);
|
||||||
playbackSpeedTextView.setVisibility(View.GONE);
|
playbackSpeedTextView.setVisibility(View.GONE);
|
||||||
|
|
||||||
if (info != null) {
|
if (info != null && info.video_streams.size() + info.video_only_streams.size() > 0) {
|
||||||
final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context,
|
final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context,
|
||||||
info.video_streams, info.video_only_streams, false);
|
info.video_streams, info.video_only_streams, false);
|
||||||
availableStreams = new ArrayList<>(videos);
|
availableStreams = new ArrayList<>(videos);
|
||||||
|
@ -337,48 +339,62 @@ public abstract class VideoPlayer extends BasePlayer
|
||||||
}
|
}
|
||||||
|
|
||||||
buildQualityMenu();
|
buildQualityMenu();
|
||||||
buildPlaybackSpeedMenu();
|
|
||||||
qualityTextView.setVisibility(View.VISIBLE);
|
qualityTextView.setVisibility(View.VISIBLE);
|
||||||
playbackSpeedTextView.setVisibility(View.VISIBLE);
|
surfaceView.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
surfaceView.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildPlaybackSpeedMenu();
|
||||||
|
playbackSpeedTextView.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Nullable
|
@Nullable
|
||||||
public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) {
|
public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) {
|
||||||
final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false);
|
List<MediaSource> mediaSources = new ArrayList<>();
|
||||||
|
|
||||||
|
// Create video stream source
|
||||||
|
final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context,
|
||||||
|
info.video_streams, info.video_only_streams, false);
|
||||||
final int index;
|
final int index;
|
||||||
if (playbackQuality == null) {
|
if (videos.isEmpty()) {
|
||||||
|
index = -1;
|
||||||
|
} else if (playbackQuality == null) {
|
||||||
index = getDefaultResolutionIndex(videos);
|
index = getDefaultResolutionIndex(videos);
|
||||||
} else {
|
} else {
|
||||||
index = getOverrideResolutionIndex(videos, getPlaybackQuality());
|
index = getOverrideResolutionIndex(videos, getPlaybackQuality());
|
||||||
}
|
}
|
||||||
if (index < 0 || index >= videos.size()) return null;
|
final VideoStream video = index >= 0 && index < videos.size() ? videos.get(index) : null;
|
||||||
final VideoStream video = videos.get(index);
|
if (video != null) {
|
||||||
|
final MediaSource streamSource = buildMediaSource(video.getUrl(),
|
||||||
List<MediaSource> mediaSources = new ArrayList<>();
|
MediaFormat.getSuffixById(video.getFormatId()));
|
||||||
// Create video stream source
|
mediaSources.add(streamSource);
|
||||||
final MediaSource streamSource = buildMediaSource(video.getUrl(),
|
}
|
||||||
MediaFormat.getSuffixById(video.getFormatId()));
|
|
||||||
mediaSources.add(streamSource);
|
|
||||||
|
|
||||||
// Create optional audio stream source
|
// Create optional audio stream source
|
||||||
final AudioStream audio = ListHelper.getHighestQualityAudio(info.audio_streams);
|
final List<AudioStream> audioStreams = info.getAudioStreams();
|
||||||
if (video.isVideoOnly && audio != null) {
|
final AudioStream audio = audioStreams.isEmpty() ? null : audioStreams.get(
|
||||||
// Merge with audio stream in case if video does not contain audio
|
ListHelper.getDefaultAudioFormat(context, audioStreams));
|
||||||
|
// Use the audio stream if there is no video stream, or
|
||||||
|
// Merge with audio stream in case if video does not contain audio
|
||||||
|
if (audio != null && ((video != null && video.isVideoOnly) || video == null)) {
|
||||||
final MediaSource audioSource = buildMediaSource(audio.getUrl(),
|
final MediaSource audioSource = buildMediaSource(audio.getUrl(),
|
||||||
MediaFormat.getSuffixById(audio.getFormatId()));
|
MediaFormat.getSuffixById(audio.getFormatId()));
|
||||||
mediaSources.add(audioSource);
|
mediaSources.add(audioSource);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If there is no audio or video sources, then this media source cannot be played back
|
||||||
|
if (mediaSources.isEmpty()) return null;
|
||||||
|
// Below are auxiliary media sources
|
||||||
|
|
||||||
// Create subtitle sources
|
// Create subtitle sources
|
||||||
for (final Subtitles subtitle : info.getSubtitles()) {
|
for (final Subtitles subtitle : info.getSubtitles()) {
|
||||||
final String mimeType = PlayerHelper.mimeTypesOf(subtitle.getFileType());
|
final String mimeType = PlayerHelper.mimeTypesOf(subtitle.getFileType());
|
||||||
if (mimeType == null) continue;
|
if (mimeType == null || context == null) continue;
|
||||||
|
|
||||||
final Format textFormat = Format.createTextSampleFormat(null, mimeType,
|
final Format textFormat = Format.createTextSampleFormat(null, mimeType,
|
||||||
SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(subtitle));
|
SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle));
|
||||||
final MediaSource textSource = new SingleSampleMediaSource(
|
final MediaSource textSource = new SingleSampleMediaSource(
|
||||||
Uri.parse(subtitle.getURL()), cacheDataSourceFactory, textFormat, TIME_UNSET);
|
Uri.parse(subtitle.getURL()), cacheDataSourceFactory, textFormat, TIME_UNSET);
|
||||||
mediaSources.add(textSource);
|
mediaSources.add(textSource);
|
||||||
|
@ -658,7 +674,9 @@ public abstract class VideoPlayer extends BasePlayer
|
||||||
public void onDismiss(PopupMenu menu) {
|
public void onDismiss(PopupMenu menu) {
|
||||||
if (DEBUG) Log.d(TAG, "onDismiss() called with: menu = [" + menu + "]");
|
if (DEBUG) Log.d(TAG, "onDismiss() called with: menu = [" + menu + "]");
|
||||||
isSomePopupMenuVisible = false;
|
isSomePopupMenuVisible = false;
|
||||||
qualityTextView.setText(getSelectedVideoStream().resolution);
|
if (getSelectedVideoStream() != null) {
|
||||||
|
qualityTextView.setText(getSelectedVideoStream().resolution);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onQualitySelectorClicked() {
|
public void onQualitySelectorClicked() {
|
||||||
|
@ -668,8 +686,12 @@ public abstract class VideoPlayer extends BasePlayer
|
||||||
showControls(300);
|
showControls(300);
|
||||||
|
|
||||||
final VideoStream videoStream = getSelectedVideoStream();
|
final VideoStream videoStream = getSelectedVideoStream();
|
||||||
final String qualityText = MediaFormat.getNameById(videoStream.format) + " " + videoStream.resolution;
|
if (videoStream != null) {
|
||||||
qualityTextView.setText(qualityText);
|
final String qualityText = MediaFormat.getNameById(videoStream.getFormatId()) + " "
|
||||||
|
+ videoStream.resolution;
|
||||||
|
qualityTextView.setText(qualityText);
|
||||||
|
}
|
||||||
|
|
||||||
wasPlaying = simpleExoPlayer.getPlayWhenReady();
|
wasPlaying = simpleExoPlayer.getPlayWhenReady();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -864,8 +886,11 @@ public abstract class VideoPlayer extends BasePlayer
|
||||||
return wasPlaying;
|
return wasPlaying;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
public VideoStream getSelectedVideoStream() {
|
public VideoStream getSelectedVideoStream() {
|
||||||
return availableStreams.get(selectedStreamIndex);
|
return (selectedStreamIndex >= 0 && availableStreams != null &&
|
||||||
|
availableStreams.size() > selectedStreamIndex) ?
|
||||||
|
availableStreams.get(selectedStreamIndex) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Handler getControlsVisibilityHandler() {
|
public Handler getControlsVisibilityHandler() {
|
||||||
|
|
|
@ -181,7 +181,9 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au
|
||||||
public void onAudioInputFormatChanged(Format format) {}
|
public void onAudioInputFormatChanged(Format format) {}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAudioTrackUnderrun(int i, long l, long l1) {}
|
public void onAudioSinkUnderrun(int bufferSize,
|
||||||
|
long bufferSizeMs,
|
||||||
|
long elapsedSinceLastFeedMs) {}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAudioDisabled(DecoderCounters decoderCounters) {}
|
public void onAudioDisabled(DecoderCounters decoderCounters) {}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package org.schabi.newpipe.player.helper;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.DefaultLoadControl;
|
import com.google.android.exoplayer2.DefaultLoadControl;
|
||||||
import com.google.android.exoplayer2.LoadControl;
|
import com.google.android.exoplayer2.LoadControl;
|
||||||
import com.google.android.exoplayer2.Renderer;
|
import com.google.android.exoplayer2.Renderer;
|
||||||
|
@ -10,6 +11,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||||
import com.google.android.exoplayer2.upstream.Allocator;
|
import com.google.android.exoplayer2.upstream.Allocator;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultAllocator;
|
import com.google.android.exoplayer2.upstream.DefaultAllocator;
|
||||||
|
|
||||||
|
import static com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS;
|
||||||
|
|
||||||
public class LoadController implements LoadControl {
|
public class LoadController implements LoadControl {
|
||||||
|
|
||||||
public static final String TAG = "LoadController";
|
public static final String TAG = "LoadController";
|
||||||
|
@ -23,16 +26,17 @@ public class LoadController implements LoadControl {
|
||||||
public LoadController(final Context context) {
|
public LoadController(final Context context) {
|
||||||
this(PlayerHelper.getMinBufferMs(context),
|
this(PlayerHelper.getMinBufferMs(context),
|
||||||
PlayerHelper.getMaxBufferMs(context),
|
PlayerHelper.getMaxBufferMs(context),
|
||||||
PlayerHelper.getBufferForPlaybackMs(context),
|
PlayerHelper.getBufferForPlaybackMs(context));
|
||||||
PlayerHelper.getBufferForPlaybackAfterRebufferMs(context));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public LoadController(final int minBufferMs,
|
public LoadController(final int minBufferMs,
|
||||||
final int maxBufferMs,
|
final int maxBufferMs,
|
||||||
final long bufferForPlaybackMs,
|
final int bufferForPlaybackMs) {
|
||||||
final long bufferForPlaybackAfterRebufferMs) {
|
final DefaultAllocator allocator = new DefaultAllocator(true,
|
||||||
final DefaultAllocator allocator = new DefaultAllocator(true, 65536);
|
C.DEFAULT_BUFFER_SEGMENT_SIZE);
|
||||||
internalLoadControl = new DefaultLoadControl(allocator, minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs);
|
|
||||||
|
internalLoadControl = new DefaultLoadControl(allocator, minBufferMs, maxBufferMs,
|
||||||
|
bufferForPlaybackMs, DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
|
|
@ -66,9 +66,11 @@ public class PlayerHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public static String captionLanguageOf(@NonNull final Subtitles subtitles) {
|
public static String captionLanguageOf(@NonNull final Context context,
|
||||||
|
@NonNull final Subtitles subtitles) {
|
||||||
final String displayName = subtitles.getLocale().getDisplayName(subtitles.getLocale());
|
final String displayName = subtitles.getLocale().getDisplayName(subtitles.getLocale());
|
||||||
return displayName + (subtitles.isAutoGenerated() ? " (auto-generated)" : "");
|
return displayName + (subtitles.isAutoGenerated() ?
|
||||||
|
" (" + context.getString(R.string.caption_auto_generated)+ ")" : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String resizeTypeOf(@NonNull final Context context,
|
public static String resizeTypeOf(@NonNull final Context context,
|
||||||
|
@ -113,12 +115,8 @@ public class PlayerHelper {
|
||||||
return 30000;
|
return 30000;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static long getBufferForPlaybackMs(@NonNull final Context context) {
|
public static int getBufferForPlaybackMs(@NonNull final Context context) {
|
||||||
return 2500L;
|
return 2500;
|
||||||
}
|
|
||||||
|
|
||||||
public static long getBufferForPlaybackAfterRebufferMs(@NonNull final Context context) {
|
|
||||||
return 5000L;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isUsingDSP(@NonNull final Context context) {
|
public static boolean isUsingDSP(@NonNull final Context context) {
|
||||||
|
|
|
@ -114,32 +114,10 @@ public final class DeferredMediaSource implements MediaSource {
|
||||||
|
|
||||||
Log.d(TAG, "Loading: [" + stream.getTitle() + "] with url: " + stream.getUrl());
|
Log.d(TAG, "Loading: [" + stream.getTitle() + "] with url: " + stream.getUrl());
|
||||||
|
|
||||||
final Function<StreamInfo, MediaSource> onReceive = new Function<StreamInfo, MediaSource>() {
|
|
||||||
@Override
|
|
||||||
public MediaSource apply(StreamInfo streamInfo) throws Exception {
|
|
||||||
return onStreamInfoReceived(stream, streamInfo);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
final Consumer<MediaSource> onSuccess = new Consumer<MediaSource>() {
|
|
||||||
@Override
|
|
||||||
public void accept(MediaSource mediaSource) throws Exception {
|
|
||||||
onMediaSourceReceived(mediaSource);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
final Consumer<Throwable> onError = new Consumer<Throwable>() {
|
|
||||||
@Override
|
|
||||||
public void accept(Throwable throwable) throws Exception {
|
|
||||||
onStreamInfoError(throwable);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loader = stream.getStream()
|
loader = stream.getStream()
|
||||||
.observeOn(Schedulers.io())
|
.map(streamInfo -> onStreamInfoReceived(stream, streamInfo))
|
||||||
.map(onReceive)
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(onSuccess, onError);
|
.subscribe(this::onMediaSourceReceived, this::onStreamInfoError);
|
||||||
}
|
}
|
||||||
|
|
||||||
private MediaSource onStreamInfoReceived(@NonNull final PlayQueueItem item,
|
private MediaSource onStreamInfoReceived(@NonNull final PlayQueueItem item,
|
||||||
|
|
|
@ -4,7 +4,6 @@ import android.support.annotation.Nullable;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource;
|
import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource;
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
|
||||||
|
|
||||||
import org.reactivestreams.Subscriber;
|
import org.reactivestreams.Subscriber;
|
||||||
import org.reactivestreams.Subscription;
|
import org.reactivestreams.Subscription;
|
||||||
|
@ -21,6 +20,7 @@ import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.annotations.NonNull;
|
import io.reactivex.annotations.NonNull;
|
||||||
|
import io.reactivex.disposables.CompositeDisposable;
|
||||||
import io.reactivex.disposables.Disposable;
|
import io.reactivex.disposables.Disposable;
|
||||||
import io.reactivex.disposables.SerialDisposable;
|
import io.reactivex.disposables.SerialDisposable;
|
||||||
import io.reactivex.functions.Consumer;
|
import io.reactivex.functions.Consumer;
|
||||||
|
@ -48,6 +48,8 @@ public class MediaSourceManager {
|
||||||
private Subscription playQueueReactor;
|
private Subscription playQueueReactor;
|
||||||
private SerialDisposable syncReactor;
|
private SerialDisposable syncReactor;
|
||||||
|
|
||||||
|
private PlayQueueItem syncedItem;
|
||||||
|
|
||||||
private boolean isBlocked;
|
private boolean isBlocked;
|
||||||
|
|
||||||
public MediaSourceManager(@NonNull final PlaybackListener listener,
|
public MediaSourceManager(@NonNull final PlaybackListener listener,
|
||||||
|
@ -86,12 +88,7 @@ public class MediaSourceManager {
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
private DeferredMediaSource.Callback getSourceBuilder() {
|
private DeferredMediaSource.Callback getSourceBuilder() {
|
||||||
return new DeferredMediaSource.Callback() {
|
return playbackListener::sourceOf;
|
||||||
@Override
|
|
||||||
public MediaSource sourceOf(PlayQueueItem item, StreamInfo info) {
|
|
||||||
return playbackListener.sourceOf(item, info);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -109,6 +106,7 @@ public class MediaSourceManager {
|
||||||
|
|
||||||
playQueueReactor = null;
|
playQueueReactor = null;
|
||||||
syncReactor = null;
|
syncReactor = null;
|
||||||
|
syncedItem = null;
|
||||||
sources = null;
|
sources = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,6 +126,8 @@ public class MediaSourceManager {
|
||||||
* */
|
* */
|
||||||
public void reset() {
|
public void reset() {
|
||||||
tryBlock();
|
tryBlock();
|
||||||
|
|
||||||
|
syncedItem = null;
|
||||||
populateSources();
|
populateSources();
|
||||||
}
|
}
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -241,22 +241,28 @@ public class MediaSourceManager {
|
||||||
final PlayQueueItem currentItem = playQueue.getItem();
|
final PlayQueueItem currentItem = playQueue.getItem();
|
||||||
if (currentItem == null) return;
|
if (currentItem == null) return;
|
||||||
|
|
||||||
final Consumer<StreamInfo> syncPlayback = new Consumer<StreamInfo>() {
|
final Consumer<StreamInfo> onSuccess = info -> syncInternal(currentItem, info);
|
||||||
@Override
|
final Consumer<Throwable> onError = throwable -> {
|
||||||
public void accept(StreamInfo streamInfo) throws Exception {
|
Log.e(TAG, "Sync error:", throwable);
|
||||||
playbackListener.sync(currentItem, streamInfo);
|
syncInternal(currentItem, null);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
final Consumer<Throwable> onError = new Consumer<Throwable>() {
|
if (syncedItem != currentItem) {
|
||||||
@Override
|
syncedItem = currentItem;
|
||||||
public void accept(Throwable throwable) throws Exception {
|
final Disposable sync = currentItem.getStream()
|
||||||
Log.e(TAG, "Sync error:", throwable);
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
playbackListener.sync(currentItem,null);
|
.subscribe(onSuccess, onError);
|
||||||
}
|
syncReactor.set(sync);
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
syncReactor.set(currentItem.getStream().subscribe(syncPlayback, onError));
|
private void syncInternal(@android.support.annotation.NonNull final PlayQueueItem item,
|
||||||
|
@Nullable final StreamInfo info) {
|
||||||
|
if (playQueue == null || playbackListener == null) return;
|
||||||
|
// Ensure the current item is up to date with the play queue
|
||||||
|
if (playQueue.getItem() == item && playQueue.getItem() == syncedItem) {
|
||||||
|
playbackListener.sync(syncedItem,info);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadDebounced() {
|
private void loadDebounced() {
|
||||||
|
@ -313,12 +319,7 @@ public class MediaSourceManager {
|
||||||
return debouncedLoadSignal
|
return debouncedLoadSignal
|
||||||
.debounce(loadDebounceMillis, TimeUnit.MILLISECONDS)
|
.debounce(loadDebounceMillis, TimeUnit.MILLISECONDS)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(new Consumer<Long>() {
|
.subscribe(timestamp -> loadImmediate());
|
||||||
@Override
|
|
||||||
public void accept(Long timestamp) throws Exception {
|
|
||||||
loadImmediate();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Media Source List Manipulation
|
// Media Source List Manipulation
|
||||||
|
|
|
@ -33,6 +33,8 @@ public interface PlaybackListener {
|
||||||
* Signals to the listener to synchronize the player's window to the manager's
|
* Signals to the listener to synchronize the player's window to the manager's
|
||||||
* window.
|
* window.
|
||||||
*
|
*
|
||||||
|
* Occurs once only per play queue item change.
|
||||||
|
*
|
||||||
* May be called only after unblock is called.
|
* May be called only after unblock is called.
|
||||||
* */
|
* */
|
||||||
void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info);
|
void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info);
|
||||||
|
|
|
@ -73,6 +73,10 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
|
||||||
playQueueItemBuilder.setOnSelectedListener(listener);
|
playQueueItemBuilder.setOnSelectedListener(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void unsetSelectedListener() {
|
||||||
|
playQueueItemBuilder.setOnSelectedListener(null);
|
||||||
|
}
|
||||||
|
|
||||||
private void startReactor() {
|
private void startReactor() {
|
||||||
final Observer<PlayQueueEvent> observer = new Observer<PlayQueueEvent>() {
|
final Observer<PlayQueueEvent> observer = new Observer<PlayQueueEvent>() {
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -104,17 +104,9 @@ public class PlayQueueItem implements Serializable {
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private Single<StreamInfo> getInfo() {
|
private Single<StreamInfo> getInfo() {
|
||||||
final Consumer<Throwable> onError = new Consumer<Throwable>() {
|
|
||||||
@Override
|
|
||||||
public void accept(Throwable throwable) throws Exception {
|
|
||||||
error = throwable;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return ExtractorHelper.getStreamInfo(this.serviceId, this.url, false)
|
return ExtractorHelper.getStreamInfo(this.serviceId, this.url, false)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.doOnError(throwable -> error = throwable);
|
||||||
.doOnError(onError);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
|
@ -53,24 +53,18 @@ public class PlayQueueItemBuilder {
|
||||||
|
|
||||||
ImageLoader.getInstance().displayImage(item.getThumbnailUrl(), holder.itemThumbnailView, imageOptions);
|
ImageLoader.getInstance().displayImage(item.getThumbnailUrl(), holder.itemThumbnailView, imageOptions);
|
||||||
|
|
||||||
holder.itemRoot.setOnClickListener(new View.OnClickListener() {
|
holder.itemRoot.setOnClickListener(view -> {
|
||||||
@Override
|
if (onItemClickListener != null) {
|
||||||
public void onClick(View view) {
|
onItemClickListener.selected(item, view);
|
||||||
if (onItemClickListener != null) {
|
|
||||||
onItemClickListener.selected(item, view);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
holder.itemRoot.setOnLongClickListener(new View.OnLongClickListener() {
|
holder.itemRoot.setOnLongClickListener(view -> {
|
||||||
@Override
|
if (onItemClickListener != null) {
|
||||||
public boolean onLongClick(View view) {
|
onItemClickListener.held(item, view);
|
||||||
if (onItemClickListener != null) {
|
return true;
|
||||||
onItemClickListener.held(item, view);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
holder.itemThumbnailView.setOnTouchListener(getOnTouchListener(holder));
|
holder.itemThumbnailView.setOnTouchListener(getOnTouchListener(holder));
|
||||||
|
@ -78,26 +72,21 @@ public class PlayQueueItemBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
private View.OnTouchListener getOnTouchListener(final PlayQueueItemHolder holder) {
|
private View.OnTouchListener getOnTouchListener(final PlayQueueItemHolder holder) {
|
||||||
return new View.OnTouchListener() {
|
return (view, motionEvent) -> {
|
||||||
@Override
|
view.performClick();
|
||||||
public boolean onTouch(View view, MotionEvent motionEvent) {
|
if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN
|
||||||
view.performClick();
|
&& onItemClickListener != null) {
|
||||||
if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
|
onItemClickListener.onStartDrag(holder);
|
||||||
onItemClickListener.onStartDrag(holder);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private DisplayImageOptions buildImageOptions(final int widthPx, final int heightPx) {
|
private DisplayImageOptions buildImageOptions(final int widthPx, final int heightPx) {
|
||||||
final BitmapProcessor bitmapProcessor = new BitmapProcessor() {
|
final BitmapProcessor bitmapProcessor = bitmap -> {
|
||||||
@Override
|
final Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, widthPx, heightPx, false);
|
||||||
public Bitmap process(Bitmap bitmap) {
|
bitmap.recycle();
|
||||||
final Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, widthPx, heightPx, false);
|
return resizedBitmap;
|
||||||
bitmap.recycle();
|
|
||||||
return resizedBitmap;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return new DisplayImageOptions.Builder()
|
return new DisplayImageOptions.Builder()
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
package org.schabi.newpipe.settings;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
|
||||||
|
public class DebugSettingsFragment extends BasePreferenceFragment {
|
||||||
|
@Override
|
||||||
|
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||||
|
addPreferencesFromResource(R.xml.debug_settings);
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,11 +3,19 @@ package org.schabi.newpipe.settings;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.v7.preference.Preference;
|
import android.support.v7.preference.Preference;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.BuildConfig;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
|
||||||
public class MainSettingsFragment extends BasePreferenceFragment {
|
public class MainSettingsFragment extends BasePreferenceFragment {
|
||||||
|
public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release");
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||||
addPreferencesFromResource(R.xml.main_settings);
|
addPreferencesFromResource(R.xml.main_settings);
|
||||||
|
|
||||||
|
if (!DEBUG) {
|
||||||
|
final Preference debug = findPreference(getString(R.string.debug_pref_screen_key));
|
||||||
|
getPreferenceScreen().removePreference(debug);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,6 +64,7 @@ public class NewPipeSettings {
|
||||||
PreferenceManager.setDefaultValues(context, R.xml.history_settings, true);
|
PreferenceManager.setDefaultValues(context, R.xml.history_settings, true);
|
||||||
PreferenceManager.setDefaultValues(context, R.xml.main_settings, true);
|
PreferenceManager.setDefaultValues(context, R.xml.main_settings, true);
|
||||||
PreferenceManager.setDefaultValues(context, R.xml.video_audio_settings, true);
|
PreferenceManager.setDefaultValues(context, R.xml.video_audio_settings, true);
|
||||||
|
PreferenceManager.setDefaultValues(context, R.xml.debug_settings, true);
|
||||||
|
|
||||||
getVideoDownloadFolder(context);
|
getVideoDownloadFolder(context);
|
||||||
getAudioDownloadFolder(context);
|
getAudioDownloadFolder(context);
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
|
||||||
|
|
||||||
<item android:id="@+id/action_toggle_heap_dump"
|
|
||||||
android:orderInCategory="9999"
|
|
||||||
android:checkable="true"
|
|
||||||
android:title="@string/toggle_leak_canary"
|
|
||||||
android:visible="true"
|
|
||||||
app:showAsAction="never"/>
|
|
||||||
|
|
||||||
</menu>
|
|
|
@ -373,5 +373,4 @@
|
||||||
<string name="caption_none">Keine Untertitel</string>
|
<string name="caption_none">Keine Untertitel</string>
|
||||||
|
|
||||||
<string name="caption_font_size_settings_title">Schriftgröße der Untertitel</string>
|
<string name="caption_font_size_settings_title">Schriftgröße der Untertitel</string>
|
||||||
<string name="toggle_leak_canary">"Speicherlecks nachverfolgen "</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -395,10 +395,6 @@
|
||||||
<string name="normal_caption_font_size">Carattere normale</string>
|
<string name="normal_caption_font_size">Carattere normale</string>
|
||||||
<string name="larger_caption_font_size">Carattere più grande</string>
|
<string name="larger_caption_font_size">Carattere più grande</string>
|
||||||
|
|
||||||
<string name="toggle_leak_canary">Controllo delle perdite</string>
|
<string name="drawer_header_action_paceholder_text">A breve qualcosa si troverà qui ;D</string>
|
||||||
<string name="enable_leak_canary_notice">Controllo delle perdite di memoria abilitato, l\'applicazione può non rispondere mentre effettua il dumping dell\'heap</string>
|
|
||||||
<string name="disable_leak_canary_notice">Controllo delle perdite di memoria disabilitato</string>
|
|
||||||
<string name="drawer_header_action_paceholder_text">A breve qualcosa si troverà qui ;D</string>
|
|
||||||
|
|
||||||
|
</resources>
|
||||||
</resources>
|
|
||||||
|
|
|
@ -382,8 +382,4 @@
|
||||||
<string name="smaller_caption_font_size">Mindre skrift</string>
|
<string name="smaller_caption_font_size">Mindre skrift</string>
|
||||||
<string name="normal_caption_font_size">Normal skrift</string>
|
<string name="normal_caption_font_size">Normal skrift</string>
|
||||||
<string name="larger_caption_font_size">Større skrift</string>
|
<string name="larger_caption_font_size">Større skrift</string>
|
||||||
|
|
||||||
<string name="toggle_leak_canary">Hold oppsyn med lekkasjer</string>
|
|
||||||
<string name="enable_leak_canary_notice">Oppsyn med minnelekasjer påslått, programmet kan slutte å svare under haug-dumping</string>
|
|
||||||
<string name="disable_leak_canary_notice">Oppsyn med minnelekasjer slått av</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -391,10 +391,5 @@ te openen in pop-upmodus</string>
|
||||||
<string name="normal_caption_font_size">Normaal lettertype</string>
|
<string name="normal_caption_font_size">Normaal lettertype</string>
|
||||||
<string name="larger_caption_font_size">Groter lettertype</string>
|
<string name="larger_caption_font_size">Groter lettertype</string>
|
||||||
|
|
||||||
<string name="toggle_leak_canary">Controleren op lekken</string>
|
<string name="drawer_header_action_paceholder_text">Hier zal binnenkort iets verschijnen ;D</string>
|
||||||
<string name="enable_leak_canary_notice">Controleren op geheugenlekken ingeschakeld, tijdens heapdumping kan de app tijdelijk niet reageren</string>
|
</resources>
|
||||||
<string name="disable_leak_canary_notice">Controleren op geheugenlekken uitgeschakeld</string>
|
|
||||||
<string name="drawer_header_action_paceholder_text">Hier zal binnenkort iets verschijnen ;D</string>
|
|
||||||
|
|
||||||
|
|
||||||
</resources>
|
|
||||||
|
|
|
@ -368,8 +368,4 @@ abrir em modo popup</string>
|
||||||
<string name="smaller_caption_font_size">Fonte menor</string>
|
<string name="smaller_caption_font_size">Fonte menor</string>
|
||||||
<string name="normal_caption_font_size">Fonte normal</string>
|
<string name="normal_caption_font_size">Fonte normal</string>
|
||||||
<string name="larger_caption_font_size">Maior fonte</string>
|
<string name="larger_caption_font_size">Maior fonte</string>
|
||||||
|
|
||||||
<string name="toggle_leak_canary">Monitorar vazamentos de memória</string>
|
|
||||||
<string name="enable_leak_canary_notice">Monitoramento de vazamentos de memória habilitado, o aplicativo pode ficar sem responder quando estiver descarregando pilha de memória</string>
|
|
||||||
<string name="disable_leak_canary_notice">Monitoramento de vazamentos de memória desabilitado</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -391,8 +391,4 @@ otvorenie okna na popredí</string>
|
||||||
<string name="smaller_caption_font_size">Menšie Písmo</string>
|
<string name="smaller_caption_font_size">Menšie Písmo</string>
|
||||||
<string name="normal_caption_font_size">Normálne Písmo</string>
|
<string name="normal_caption_font_size">Normálne Písmo</string>
|
||||||
<string name="larger_caption_font_size">Väčšie Písmo</string>
|
<string name="larger_caption_font_size">Väčšie Písmo</string>
|
||||||
|
|
||||||
<string name="toggle_leak_canary">Monitorovanie pretečenia</string>
|
|
||||||
<string name="enable_leak_canary_notice">Monitorovanie pretečenia pamäte je povolené, pri hromadnom zbere môže aplikácia prestať reagovať</string>
|
|
||||||
<string name="disable_leak_canary_notice">Monitorovanie pretečenia pamäte je vypnuté</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -384,8 +384,4 @@
|
||||||
<string name="smaller_caption_font_size">Küçük Yazı Tipi</string>
|
<string name="smaller_caption_font_size">Küçük Yazı Tipi</string>
|
||||||
<string name="normal_caption_font_size">Olağan Yazı Tipi</string>
|
<string name="normal_caption_font_size">Olağan Yazı Tipi</string>
|
||||||
<string name="larger_caption_font_size">Büyük Yazı Tipi</string>
|
<string name="larger_caption_font_size">Büyük Yazı Tipi</string>
|
||||||
|
|
||||||
<string name="toggle_leak_canary">Sızıntıları Gözlemle</string>
|
|
||||||
<string name="enable_leak_canary_notice">Bellek sızıntısı gözlemleme etkinleştirildi, uygulama yığın atımı sırasında yanıtsız kalabilir</string>
|
|
||||||
<string name="disable_leak_canary_notice">Bellek sızıntısı gözlemleme devre dışı</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -84,8 +84,11 @@
|
||||||
<string name="last_orientation_landscape_key" translatable="false">last_orientation_landscape_key</string>
|
<string name="last_orientation_landscape_key" translatable="false">last_orientation_landscape_key</string>
|
||||||
|
|
||||||
<!-- DEBUG ONLY -->
|
<!-- DEBUG ONLY -->
|
||||||
|
<string name="debug_pref_screen_key" translatable="false">debug_pref_screen_key</string>
|
||||||
<string name="allow_heap_dumping_key" translatable="false">allow_heap_dumping_key</string>
|
<string name="allow_heap_dumping_key" translatable="false">allow_heap_dumping_key</string>
|
||||||
|
|
||||||
|
<string name="allow_disposed_exceptions_key" translatable="false">allow_disposed_exceptions_key</string>
|
||||||
|
|
||||||
<!-- THEMES -->
|
<!-- THEMES -->
|
||||||
<string name="theme_key" translatable="false">theme</string>
|
<string name="theme_key" translatable="false">theme</string>
|
||||||
<string name="light_theme_key" translatable="false">light_theme</string>
|
<string name="light_theme_key" translatable="false">light_theme</string>
|
||||||
|
|
|
@ -98,6 +98,7 @@
|
||||||
<string name="settings_category_popup_title">Popup</string>
|
<string name="settings_category_popup_title">Popup</string>
|
||||||
<string name="settings_category_appearance_title">Appearance</string>
|
<string name="settings_category_appearance_title">Appearance</string>
|
||||||
<string name="settings_category_other_title">Other</string>
|
<string name="settings_category_other_title">Other</string>
|
||||||
|
<string name="settings_category_debug_title">Debug</string>
|
||||||
<string name="background_player_playing_toast">Playing in background</string>
|
<string name="background_player_playing_toast">Playing in background</string>
|
||||||
<string name="popup_playing_toast">Playing in popup mode</string>
|
<string name="popup_playing_toast">Playing in popup mode</string>
|
||||||
<string name="background_player_append">Queued on background player</string>
|
<string name="background_player_append">Queued on background player</string>
|
||||||
|
@ -406,13 +407,17 @@
|
||||||
<string name="resize_fill">FILL</string>
|
<string name="resize_fill">FILL</string>
|
||||||
<string name="resize_zoom">ZOOM</string>
|
<string name="resize_zoom">ZOOM</string>
|
||||||
|
|
||||||
|
<string name="caption_auto_generated">Auto-generated</string>
|
||||||
<string name="caption_font_size_settings_title">Caption Font Size</string>
|
<string name="caption_font_size_settings_title">Caption Font Size</string>
|
||||||
<string name="smaller_caption_font_size">Smaller Font</string>
|
<string name="smaller_caption_font_size">Smaller Font</string>
|
||||||
<string name="normal_caption_font_size">Normal Font</string>
|
<string name="normal_caption_font_size">Normal Font</string>
|
||||||
<string name="larger_caption_font_size">Larger Font</string>
|
<string name="larger_caption_font_size">Larger Font</string>
|
||||||
|
|
||||||
<!-- Debug Only -->
|
<!-- Debug Settings -->
|
||||||
<string name="toggle_leak_canary">Monitor Leaks</string>
|
<string name="enable_leak_canary_title">Enable LeakCanary</string>
|
||||||
<string name="enable_leak_canary_notice">Memory leak monitoring enabled, app may become unresponsive when heap dumping</string>
|
<string name="enable_leak_canary_summary">Memory leak monitoring may cause app to become unresponsive when heap dumping</string>
|
||||||
<string name="disable_leak_canary_notice">Memory leak monitoring disabled</string>
|
|
||||||
|
<string name="enable_disposed_exceptions_title">Report Out-of-Lifecycle Errors</string>
|
||||||
|
<string name="enable_disposed_exceptions_summary">Force reporting of undeliverable Rx exceptions occurring outside of fragment or activity lifecycle after dispose</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<PreferenceScreen
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:key="general_preferences"
|
||||||
|
android:title="@string/settings_category_debug_title">
|
||||||
|
|
||||||
|
<SwitchPreference
|
||||||
|
android:defaultValue="false"
|
||||||
|
android:key="@string/allow_heap_dumping_key"
|
||||||
|
android:title="@string/enable_leak_canary_title"
|
||||||
|
android:summary="@string/enable_leak_canary_summary"/>
|
||||||
|
|
||||||
|
<SwitchPreference
|
||||||
|
android:defaultValue="true"
|
||||||
|
android:key="@string/allow_disposed_exceptions_key"
|
||||||
|
android:title="@string/enable_disposed_exceptions_title"
|
||||||
|
android:summary="@string/enable_disposed_exceptions_summary"/>
|
||||||
|
</PreferenceScreen>
|
|
@ -28,4 +28,10 @@
|
||||||
android:fragment="org.schabi.newpipe.settings.ContentSettingsFragment"
|
android:fragment="org.schabi.newpipe.settings.ContentSettingsFragment"
|
||||||
android:icon="?attr/language"
|
android:icon="?attr/language"
|
||||||
android:title="@string/content"/>
|
android:title="@string/content"/>
|
||||||
|
|
||||||
|
<PreferenceScreen
|
||||||
|
android:fragment="org.schabi.newpipe.settings.DebugSettingsFragment"
|
||||||
|
android:icon="?attr/info"
|
||||||
|
android:title="@string/settings_category_debug_title"
|
||||||
|
android:key="@string/debug_pref_screen_key"/>
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
||||||
|
|
Loading…
Reference in New Issue