diff --git a/app/build.gradle b/app/build.gradle index 0244ae4b9..ea4d5384d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -73,7 +73,7 @@ dependencies { implementation 'de.hdodenhof:circleimageview:2.2.0' implementation 'com.github.nirhart:ParallaxScroll:dd53d1f9d1' 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-urlconnection:1.5.0' diff --git a/app/src/debug/java/org/schabi/newpipe/DebugApp.java b/app/src/debug/java/org/schabi/newpipe/DebugApp.java index 1ba837cdd..aff354a69 100644 --- a/app/src/debug/java/org/schabi/newpipe/DebugApp.java +++ b/app/src/debug/java/org/schabi/newpipe/DebugApp.java @@ -15,6 +15,8 @@ import com.squareup.leakcanary.LeakCanary; import com.squareup.leakcanary.LeakDirectoryProvider; import com.squareup.leakcanary.RefWatcher; +import org.schabi.newpipe.extractor.Downloader; + import java.io.File; import java.util.concurrent.TimeUnit; @@ -33,7 +35,12 @@ public class DebugApp extends App { public void onCreate() { super.onCreate(); initStetho(); - Downloader.client = new OkHttpClient.Builder().addNetworkInterceptor(new StethoInterceptor()).readTimeout(30, TimeUnit.SECONDS).build(); + } + + @Override + protected Downloader getDownloader() { + return org.schabi.newpipe.Downloader.init(new OkHttpClient.Builder() + .addNetworkInterceptor(new StethoInterceptor())); } private void initStetho() { @@ -58,6 +65,12 @@ public class DebugApp extends App { Stetho.initialize(initializer); } + @Override + protected boolean isDisposedRxExceptionsReported() { + return PreferenceManager.getDefaultSharedPreferences(this) + .getBoolean(getString(R.string.allow_disposed_exceptions_key), false); + } + @Override protected RefWatcher installLeakCanary() { return LeakCanary.refWatcher(this) diff --git a/app/src/main/java/org/schabi/newpipe/App.java b/app/src/main/java/org/schabi/newpipe/App.java index b15a38aae..ee119dbc8 100644 --- a/app/src/main/java/org/schabi/newpipe/App.java +++ b/app/src/main/java/org/schabi/newpipe/App.java @@ -19,6 +19,7 @@ import org.acra.config.ACRAConfiguration; import org.acra.config.ACRAConfigurationException; import org.acra.config.ConfigurationBuilder; import org.acra.sender.ReportSenderFactory; +import org.schabi.newpipe.extractor.Downloader; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.report.AcraReportSenderFactory; import org.schabi.newpipe.report.ErrorActivity; @@ -30,9 +31,13 @@ import org.schabi.newpipe.util.StateSaver; import java.io.IOException; import java.io.InterruptedIOException; import java.net.SocketException; +import java.util.Collections; +import java.util.List; import io.reactivex.annotations.NonNull; import io.reactivex.exceptions.CompositeException; +import io.reactivex.exceptions.MissingBackpressureException; +import io.reactivex.exceptions.OnErrorNotImplementedException; import io.reactivex.exceptions.UndeliverableException; import io.reactivex.functions.Consumer; import io.reactivex.plugins.RxJavaPlugins; @@ -83,7 +88,7 @@ public class App extends Application { // Initialize settings first because others inits can use its values SettingsActivity.initSettings(this); - NewPipe.init(Downloader.getInstance()); + NewPipe.init(getDownloader()); NewPipeDatabase.init(this); StateSaver.init(this); initNotificationChannel(); @@ -94,36 +99,67 @@ public class App extends Application { configureRxJavaErrorHandler(); } + protected Downloader getDownloader() { + return org.schabi.newpipe.Downloader.init(null); + } + private void configureRxJavaErrorHandler() { // https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling RxJavaPlugins.setErrorHandler(new Consumer() { @Override 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) { // As UndeliverableException is a wrapper, get the cause of it to get the "real" exception throwable = throwable.getCause(); } + final List errors; if (throwable instanceof CompositeException) { - for (Throwable element : ((CompositeException) throwable).getExceptions()) { - if (checkThrowable(element)) return; + errors = ((CompositeException) throwable).getExceptions(); + } 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 Thread.currentThread().getUncaughtExceptionHandler() .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); - } }); } @@ -177,4 +213,8 @@ public class App extends Application { protected RefWatcher installLeakCanary() { return RefWatcher.DISABLED; } + + protected boolean isDisposedRxExceptionsReported() { + return false; + } } diff --git a/app/src/main/java/org/schabi/newpipe/Downloader.java b/app/src/main/java/org/schabi/newpipe/Downloader.java index 029c9c5a6..a143c5cb7 100644 --- a/app/src/main/java/org/schabi/newpipe/Downloader.java +++ b/app/src/main/java/org/schabi/newpipe/Downloader.java @@ -1,8 +1,12 @@ package org.schabi.newpipe; +import android.support.annotation.Nullable; +import android.text.TextUtils; + import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; import java.io.IOException; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -10,6 +14,7 @@ import java.util.concurrent.TimeUnit; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; +import okhttp3.ResponseBody; /* @@ -33,34 +38,38 @@ import okhttp3.Response; */ public class Downloader implements org.schabi.newpipe.extractor.Downloader { - public static final String USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:43.0) Gecko/20100101 Firefox/43.0"; - private static String mCookies = ""; - private static Downloader instance = null; + private static Downloader instance; + private String mCookies; + private OkHttpClient client; - protected static OkHttpClient client = new OkHttpClient.Builder().readTimeout(30, TimeUnit.SECONDS).build(); + private Downloader(OkHttpClient.Builder builder) { + this.client = builder + .readTimeout(30, TimeUnit.SECONDS) + //.cache(new Cache(new File(context.getExternalCacheDir(), "okhttp"), 16 * 1024 * 1024)) + .build(); + } - private Downloader() { + /** + * It's recommended to call exactly once in the entire lifetime of the application. + * + * @param builder if null, default builder will be used + */ + public static Downloader init(@Nullable OkHttpClient.Builder builder) { + return instance = new Downloader(builder != null ? builder : new OkHttpClient.Builder()); } public static Downloader getInstance() { - if (instance == null) { - synchronized (Downloader.class) { - if (instance == null) { - instance = new Downloader(); - } - } - } return instance; } - public static synchronized void setCookies(String cookies) { - Downloader.mCookies = cookies; + public String getCookies() { + return mCookies; } - public static synchronized String getCookies() { - return Downloader.mCookies; + public void setCookies(String cookies) { + mCookies = cookies; } /** @@ -89,22 +98,32 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader { */ @Override public String download(String siteUrl, Map customProperties) throws IOException, ReCaptchaException { - Request.Builder requestBuilder = new Request.Builder().url(siteUrl).addHeader("User-Agent", USER_AGENT).method("GET", null); - for (Map.Entry header : customProperties.entrySet()) { - requestBuilder = requestBuilder.addHeader(header.getKey(), header.getValue()); - } - if (getCookies().length() > 0) { - requestBuilder = requestBuilder.addHeader("Cookie", getCookies()); - } - Request request = requestBuilder.build(); + final Request.Builder requestBuilder = new Request.Builder() + .method("GET", null).url(siteUrl) + .addHeader("User-Agent", USER_AGENT); - Response response = client.newCall(request).execute(); + for (Map.Entry header : customProperties.entrySet()) { + requestBuilder.addHeader(header.getKey(), header.getValue()); + } + + if (!TextUtils.isEmpty(mCookies)) { + requestBuilder.addHeader("Cookie", mCookies); + } + + final Request request = requestBuilder.build(); + final Response response = client.newCall(request).execute(); + final ResponseBody body = response.body(); if (response.code() == 429) { throw new ReCaptchaException("reCaptcha Challenge requested"); } - return response.body().string(); + if (body == null) { + response.close(); + return null; + } + + return body.string(); } /** @@ -116,6 +135,6 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader { */ @Override public String download(String siteUrl) throws IOException, ReCaptchaException { - return download(siteUrl, new HashMap<>()); + return download(siteUrl, Collections.emptyMap()); } } diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index e696f867f..573479ea7 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -20,7 +20,6 @@ package org.schabi.newpipe; -import android.annotation.SuppressLint; import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; @@ -28,7 +27,6 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.preference.PreferenceManager; -import android.support.annotation.NonNull; import android.support.design.widget.NavigationView; import android.support.v4.app.Fragment; 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 //////////////////////////////////////////////////////////////////////////*/ @@ -301,10 +283,6 @@ public class MainActivity extends AppCompatActivity { inflater.inflate(R.menu.main_menu, menu); } - if (DEBUG) { - getMenuInflater().inflate(R.menu.debug_menu, menu); - } - ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(false); @@ -315,17 +293,6 @@ public class MainActivity extends AppCompatActivity { 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 public boolean onOptionsItemSelected(MenuItem 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: NavigationHelper.openHistory(this); return true; - case R.id.action_toggle_heap_dump: - onHeapDumpToggled(item); - return true; default: return super.onOptionsItemSelected(item); } diff --git a/app/src/main/java/org/schabi/newpipe/ReCaptchaActivity.java b/app/src/main/java/org/schabi/newpipe/ReCaptchaActivity.java index d124bc6c4..a4e6730da 100644 --- a/app/src/main/java/org/schabi/newpipe/ReCaptchaActivity.java +++ b/app/src/main/java/org/schabi/newpipe/ReCaptchaActivity.java @@ -107,7 +107,7 @@ public class ReCaptchaActivity extends AppCompatActivity { // find cookies : s_gl & goojf and Add cookies to Downloader if (find_access_cookies(cookies)) { // Give cookies to Downloader class - Downloader.setCookies(mCookies); + Downloader.getInstance().setCookies(mCookies); // Closing activity and return to parent setResult(RESULT_OK); diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index 7558f1375..222f0fad8 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -81,6 +81,10 @@ import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; 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; /** @@ -279,6 +283,11 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen if (playbackManager != null) playbackManager.dispose(); if (audioReactor != null) audioReactor.abandonAudioFocus(); if (databaseUpdateReactor != null) databaseUpdateReactor.dispose(); + + if (playQueueAdapter != null) { + playQueueAdapter.unsetSelectedListener(); + playQueueAdapter.dispose(); + } } public void destroy() { @@ -460,11 +469,11 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen final PlayQueueItem currentSourceItem = playQueue.getItem(); // Check if already playing correct window - final boolean isCurrentWindowCorrect = - simpleExoPlayer.getCurrentWindowIndex() == currentSourceIndex; + final boolean isCurrentPeriodCorrect = + simpleExoPlayer.getCurrentPeriodIndex() == currentSourceIndex; // Check if recovering - if (isCurrentWindowCorrect && currentSourceItem != null) { + if (isCurrentPeriodCorrect && currentSourceItem != null) { /* Recovering with sub-second position may cause a long buffer delay in ExoPlayer, * rounding this position to the nearest second will help alleviate this.*/ final long position = currentSourceItem.getRecoveryPosition(); @@ -605,17 +614,25 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen } @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 - final int newWindowIndex = simpleExoPlayer.getCurrentWindowIndex(); - if (DEBUG) Log.d(TAG, "onPositionDiscontinuity() called with window index = [" + newWindowIndex + "]"); + final int newPeriodIndex = simpleExoPlayer.getCurrentPeriodIndex(); - // If the user selects a new track, then the discontinuity occurs after the index is changed. - // Therefore, the only source that causes a discrepancy would be gapless transition, - // which can only offset the current track by +1. - if (newWindowIndex == playQueue.getIndex() + 1 || - (newWindowIndex == 0 && playQueue.getIndex() == playQueue.size() - 1)) { - playQueue.offsetIndex(+1); + /* Discontinuity reasons!! Thank you ExoPlayer lords */ + switch (reason) { + case DISCONTINUITY_REASON_PERIOD_TRANSITION: + if (newPeriodIndex == playQueue.getIndex()) { + registerView(); + } else { + playQueue.offsetIndex(+1); + } + break; + case DISCONTINUITY_REASON_SEEK: + case DISCONTINUITY_REASON_SEEK_ADJUSTMENT: + case DISCONTINUITY_REASON_INTERNAL: + default: + break; } playbackManager.load(); } @@ -625,6 +642,16 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen 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 //////////////////////////////////////////////////////////////////////////*/ @@ -668,19 +695,14 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen if (currentSourceIndex != playQueue.getIndex()) { Log.e(TAG, "Play Queue may be desynchronized: item index=[" + currentSourceIndex + "], 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; if (DEBUG) Log.d(TAG, "Rewinding to correct window: " + currentSourceIndex + " at: " + getTimeString((int)startPos)); simpleExoPlayer.seekTo(currentSourceIndex, startPos); } - // TODO: update exoplayer to 2.6.x in order to register view count on repeated streams - databaseUpdateReactor.add(recordManager.onViewed(currentInfo).onErrorComplete() - .subscribe( - ignored -> {/* successful */}, - error -> Log.e(TAG, "Player onViewed() failure: ", error) - )); + registerView(); initThumbnail(info == null ? item.getThumbnailUrl() : info.thumbnail_url); } @@ -814,6 +836,15 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen // 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() { if (playbackManager != null) { playbackManager.reset(); diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java index 5518357a8..1378d9a80 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -61,6 +61,9 @@ public abstract class ServicePlayerActivity extends AppCompatActivity 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 RecyclerView itemsList; @@ -211,6 +214,15 @@ public abstract class ServicePlayerActivity extends AppCompatActivity unbindService(serviceConnection); serviceBound = false; 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; } } @@ -385,7 +397,19 @@ public abstract class ServicePlayerActivity extends AppCompatActivity private ItemTouchHelper.SimpleCallback getItemTouchCallback() { return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) { @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()) { return false; } diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java index a0bc7223f..40b7df2dc 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -263,7 +263,9 @@ public abstract class VideoPlayer extends BasePlayer VideoStream videoStream = availableStreams.get(i); 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.setOnDismissListener(this); } @@ -326,7 +328,7 @@ public abstract class VideoPlayer extends BasePlayer qualityTextView.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 videos = ListHelper.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false); availableStreams = new ArrayList<>(videos); @@ -337,48 +339,62 @@ public abstract class VideoPlayer extends BasePlayer } buildQualityMenu(); - buildPlaybackSpeedMenu(); qualityTextView.setVisibility(View.VISIBLE); - playbackSpeedTextView.setVisibility(View.VISIBLE); + surfaceView.setVisibility(View.VISIBLE); + } else { + surfaceView.setVisibility(View.GONE); } + + buildPlaybackSpeedMenu(); + playbackSpeedTextView.setVisibility(View.VISIBLE); } @Override @Nullable public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) { - final List videos = ListHelper.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false); + List mediaSources = new ArrayList<>(); + // Create video stream source + final List videos = ListHelper.getSortedStreamVideosList(context, + info.video_streams, info.video_only_streams, false); final int index; - if (playbackQuality == null) { + if (videos.isEmpty()) { + index = -1; + } else if (playbackQuality == null) { index = getDefaultResolutionIndex(videos); } else { index = getOverrideResolutionIndex(videos, getPlaybackQuality()); } - if (index < 0 || index >= videos.size()) return null; - final VideoStream video = videos.get(index); - - List mediaSources = new ArrayList<>(); - // Create video stream source - final MediaSource streamSource = buildMediaSource(video.getUrl(), - MediaFormat.getSuffixById(video.getFormatId())); - mediaSources.add(streamSource); + final VideoStream video = index >= 0 && index < videos.size() ? videos.get(index) : null; + if (video != null) { + final MediaSource streamSource = buildMediaSource(video.getUrl(), + MediaFormat.getSuffixById(video.getFormatId())); + mediaSources.add(streamSource); + } // Create optional audio stream source - final AudioStream audio = ListHelper.getHighestQualityAudio(info.audio_streams); - if (video.isVideoOnly && audio != null) { - // Merge with audio stream in case if video does not contain audio + final List audioStreams = info.getAudioStreams(); + final AudioStream audio = audioStreams.isEmpty() ? null : audioStreams.get( + 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(), MediaFormat.getSuffixById(audio.getFormatId())); 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 for (final Subtitles subtitle : info.getSubtitles()) { final String mimeType = PlayerHelper.mimeTypesOf(subtitle.getFileType()); - if (mimeType == null) continue; + if (mimeType == null || context == null) continue; 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( Uri.parse(subtitle.getURL()), cacheDataSourceFactory, textFormat, TIME_UNSET); mediaSources.add(textSource); @@ -658,7 +674,9 @@ public abstract class VideoPlayer extends BasePlayer public void onDismiss(PopupMenu menu) { if (DEBUG) Log.d(TAG, "onDismiss() called with: menu = [" + menu + "]"); isSomePopupMenuVisible = false; - qualityTextView.setText(getSelectedVideoStream().resolution); + if (getSelectedVideoStream() != null) { + qualityTextView.setText(getSelectedVideoStream().resolution); + } } public void onQualitySelectorClicked() { @@ -668,8 +686,12 @@ public abstract class VideoPlayer extends BasePlayer showControls(300); final VideoStream videoStream = getSelectedVideoStream(); - final String qualityText = MediaFormat.getNameById(videoStream.format) + " " + videoStream.resolution; - qualityTextView.setText(qualityText); + if (videoStream != null) { + final String qualityText = MediaFormat.getNameById(videoStream.getFormatId()) + " " + + videoStream.resolution; + qualityTextView.setText(qualityText); + } + wasPlaying = simpleExoPlayer.getPlayWhenReady(); } @@ -864,8 +886,11 @@ public abstract class VideoPlayer extends BasePlayer return wasPlaying; } + @Nullable public VideoStream getSelectedVideoStream() { - return availableStreams.get(selectedStreamIndex); + return (selectedStreamIndex >= 0 && availableStreams != null && + availableStreams.size() > selectedStreamIndex) ? + availableStreams.get(selectedStreamIndex) : null; } public Handler getControlsVisibilityHandler() { diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java index 4e031a0dd..2c85cfc34 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java @@ -181,7 +181,9 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au public void onAudioInputFormatChanged(Format format) {} @Override - public void onAudioTrackUnderrun(int i, long l, long l1) {} + public void onAudioSinkUnderrun(int bufferSize, + long bufferSizeMs, + long elapsedSinceLastFeedMs) {} @Override public void onAudioDisabled(DecoderCounters decoderCounters) {} diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java b/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java index acc20f5b0..be7b8efde 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java @@ -2,6 +2,7 @@ package org.schabi.newpipe.player.helper; import android.content.Context; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.LoadControl; 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.DefaultAllocator; +import static com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS; + public class LoadController implements LoadControl { public static final String TAG = "LoadController"; @@ -23,16 +26,17 @@ public class LoadController implements LoadControl { public LoadController(final Context context) { this(PlayerHelper.getMinBufferMs(context), PlayerHelper.getMaxBufferMs(context), - PlayerHelper.getBufferForPlaybackMs(context), - PlayerHelper.getBufferForPlaybackAfterRebufferMs(context)); + PlayerHelper.getBufferForPlaybackMs(context)); } public LoadController(final int minBufferMs, final int maxBufferMs, - final long bufferForPlaybackMs, - final long bufferForPlaybackAfterRebufferMs) { - final DefaultAllocator allocator = new DefaultAllocator(true, 65536); - internalLoadControl = new DefaultLoadControl(allocator, minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs); + final int bufferForPlaybackMs) { + final DefaultAllocator allocator = new DefaultAllocator(true, + C.DEFAULT_BUFFER_SEGMENT_SIZE); + + internalLoadControl = new DefaultLoadControl(allocator, minBufferMs, maxBufferMs, + bufferForPlaybackMs, DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS); } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java index 476838f13..ea3f73a17 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java @@ -66,9 +66,11 @@ public class PlayerHelper { } @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()); - 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, @@ -113,12 +115,8 @@ public class PlayerHelper { return 30000; } - public static long getBufferForPlaybackMs(@NonNull final Context context) { - return 2500L; - } - - public static long getBufferForPlaybackAfterRebufferMs(@NonNull final Context context) { - return 5000L; + public static int getBufferForPlaybackMs(@NonNull final Context context) { + return 2500; } public static boolean isUsingDSP(@NonNull final Context context) { diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/DeferredMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/playback/DeferredMediaSource.java index b0990f56a..3ae744d18 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/DeferredMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/DeferredMediaSource.java @@ -114,32 +114,10 @@ public final class DeferredMediaSource implements MediaSource { Log.d(TAG, "Loading: [" + stream.getTitle() + "] with url: " + stream.getUrl()); - final Function onReceive = new Function() { - @Override - public MediaSource apply(StreamInfo streamInfo) throws Exception { - return onStreamInfoReceived(stream, streamInfo); - } - }; - - final Consumer onSuccess = new Consumer() { - @Override - public void accept(MediaSource mediaSource) throws Exception { - onMediaSourceReceived(mediaSource); - } - }; - - final Consumer onError = new Consumer() { - @Override - public void accept(Throwable throwable) throws Exception { - onStreamInfoError(throwable); - } - }; - loader = stream.getStream() - .observeOn(Schedulers.io()) - .map(onReceive) + .map(streamInfo -> onStreamInfoReceived(stream, streamInfo)) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(onSuccess, onError); + .subscribe(this::onMediaSourceReceived, this::onStreamInfoError); } private MediaSource onStreamInfoReceived(@NonNull final PlayQueueItem item, diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java index 04f1606fa..54eb4078a 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java @@ -4,7 +4,6 @@ import android.support.annotation.Nullable; import android.util.Log; import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; -import com.google.android.exoplayer2.source.MediaSource; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; @@ -21,6 +20,7 @@ import java.util.concurrent.TimeUnit; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.annotations.NonNull; +import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; import io.reactivex.disposables.SerialDisposable; import io.reactivex.functions.Consumer; @@ -48,6 +48,8 @@ public class MediaSourceManager { private Subscription playQueueReactor; private SerialDisposable syncReactor; + private PlayQueueItem syncedItem; + private boolean isBlocked; public MediaSourceManager(@NonNull final PlaybackListener listener, @@ -86,12 +88,7 @@ public class MediaSourceManager { //////////////////////////////////////////////////////////////////////////*/ private DeferredMediaSource.Callback getSourceBuilder() { - return new DeferredMediaSource.Callback() { - @Override - public MediaSource sourceOf(PlayQueueItem item, StreamInfo info) { - return playbackListener.sourceOf(item, info); - } - }; + return playbackListener::sourceOf; } /*////////////////////////////////////////////////////////////////////////// @@ -109,6 +106,7 @@ public class MediaSourceManager { playQueueReactor = null; syncReactor = null; + syncedItem = null; sources = null; } @@ -128,6 +126,8 @@ public class MediaSourceManager { * */ public void reset() { tryBlock(); + + syncedItem = null; populateSources(); } /*////////////////////////////////////////////////////////////////////////// @@ -241,22 +241,28 @@ public class MediaSourceManager { final PlayQueueItem currentItem = playQueue.getItem(); if (currentItem == null) return; - final Consumer syncPlayback = new Consumer() { - @Override - public void accept(StreamInfo streamInfo) throws Exception { - playbackListener.sync(currentItem, streamInfo); - } + final Consumer onSuccess = info -> syncInternal(currentItem, info); + final Consumer onError = throwable -> { + Log.e(TAG, "Sync error:", throwable); + syncInternal(currentItem, null); }; - final Consumer onError = new Consumer() { - @Override - public void accept(Throwable throwable) throws Exception { - Log.e(TAG, "Sync error:", throwable); - playbackListener.sync(currentItem,null); - } - }; + if (syncedItem != currentItem) { + syncedItem = currentItem; + final Disposable sync = currentItem.getStream() + .observeOn(AndroidSchedulers.mainThread()) + .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() { @@ -313,12 +319,7 @@ public class MediaSourceManager { return debouncedLoadSignal .debounce(loadDebounceMillis, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Consumer() { - @Override - public void accept(Long timestamp) throws Exception { - loadImmediate(); - } - }); + .subscribe(timestamp -> loadImmediate()); } /*////////////////////////////////////////////////////////////////////////// // Media Source List Manipulation diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java b/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java index dfed04c01..c6fdde656 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java @@ -33,6 +33,8 @@ public interface PlaybackListener { * Signals to the listener to synchronize the player's window to the manager's * window. * + * Occurs once only per play queue item change. + * * May be called only after unblock is called. * */ void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info); diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueAdapter.java b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueAdapter.java index e16693ec6..cd833c1ab 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueAdapter.java @@ -73,6 +73,10 @@ public class PlayQueueAdapter extends RecyclerView.Adapter observer = new Observer() { @Override diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItem.java b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItem.java index f8e7b8655..752dc223d 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItem.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItem.java @@ -104,17 +104,9 @@ public class PlayQueueItem implements Serializable { @NonNull private Single getInfo() { - final Consumer onError = new Consumer() { - @Override - public void accept(Throwable throwable) throws Exception { - error = throwable; - } - }; - return ExtractorHelper.getStreamInfo(this.serviceId, this.url, false) .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doOnError(onError); + .doOnError(throwable -> error = throwable); } //////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemBuilder.java b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemBuilder.java index 82277a4e7..73cdf1113 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemBuilder.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemBuilder.java @@ -53,24 +53,18 @@ public class PlayQueueItemBuilder { ImageLoader.getInstance().displayImage(item.getThumbnailUrl(), holder.itemThumbnailView, imageOptions); - holder.itemRoot.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - if (onItemClickListener != null) { - onItemClickListener.selected(item, view); - } + holder.itemRoot.setOnClickListener(view -> { + if (onItemClickListener != null) { + onItemClickListener.selected(item, view); } }); - holder.itemRoot.setOnLongClickListener(new View.OnLongClickListener() { - @Override - public boolean onLongClick(View view) { - if (onItemClickListener != null) { - onItemClickListener.held(item, view); - return true; - } - return false; + holder.itemRoot.setOnLongClickListener(view -> { + if (onItemClickListener != null) { + onItemClickListener.held(item, view); + return true; } + return false; }); holder.itemThumbnailView.setOnTouchListener(getOnTouchListener(holder)); @@ -78,26 +72,21 @@ public class PlayQueueItemBuilder { } private View.OnTouchListener getOnTouchListener(final PlayQueueItemHolder holder) { - return new View.OnTouchListener() { - @Override - public boolean onTouch(View view, MotionEvent motionEvent) { - view.performClick(); - if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) { - onItemClickListener.onStartDrag(holder); - } - return false; + return (view, motionEvent) -> { + view.performClick(); + if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN + && onItemClickListener != null) { + onItemClickListener.onStartDrag(holder); } + return false; }; } private DisplayImageOptions buildImageOptions(final int widthPx, final int heightPx) { - final BitmapProcessor bitmapProcessor = new BitmapProcessor() { - @Override - public Bitmap process(Bitmap bitmap) { - final Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, widthPx, heightPx, false); - bitmap.recycle(); - return resizedBitmap; - } + final BitmapProcessor bitmapProcessor = bitmap -> { + final Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, widthPx, heightPx, false); + bitmap.recycle(); + return resizedBitmap; }; return new DisplayImageOptions.Builder() diff --git a/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java new file mode 100644 index 000000000..0956f47d6 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java @@ -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); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java index 728da0ae5..5e07e2b12 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java @@ -3,11 +3,19 @@ package org.schabi.newpipe.settings; import android.os.Bundle; import android.support.v7.preference.Preference; +import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.R; public class MainSettingsFragment extends BasePreferenceFragment { + public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release"); + @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { addPreferencesFromResource(R.xml.main_settings); + + if (!DEBUG) { + final Preference debug = findPreference(getString(R.string.debug_pref_screen_key)); + getPreferenceScreen().removePreference(debug); + } } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java index 109466c02..92f98a9a2 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java +++ b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java @@ -64,6 +64,7 @@ public class NewPipeSettings { PreferenceManager.setDefaultValues(context, R.xml.history_settings, true); PreferenceManager.setDefaultValues(context, R.xml.main_settings, true); PreferenceManager.setDefaultValues(context, R.xml.video_audio_settings, true); + PreferenceManager.setDefaultValues(context, R.xml.debug_settings, true); getVideoDownloadFolder(context); getAudioDownloadFolder(context); diff --git a/app/src/main/res/drawable-hdpi/ic_bug_report_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_bug_report_black_24dp.png new file mode 100644 index 000000000..1bccb1d11 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_bug_report_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_bug_report_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_bug_report_white_24dp.png new file mode 100644 index 000000000..0c963e1ca Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_bug_report_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_bug_report_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_bug_report_black_24dp.png new file mode 100644 index 000000000..58aef662d Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_bug_report_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_bug_report_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_bug_report_white_24dp.png new file mode 100644 index 000000000..86e15f0d7 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_bug_report_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_bug_report_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_bug_report_black_24dp.png new file mode 100644 index 000000000..107f74a20 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_bug_report_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_bug_report_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_bug_report_white_24dp.png new file mode 100644 index 000000000..36b826bb8 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_bug_report_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_bug_report_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_bug_report_black_24dp.png new file mode 100644 index 000000000..af8c82e6e Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_bug_report_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_bug_report_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_bug_report_white_24dp.png new file mode 100644 index 000000000..766bac447 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_bug_report_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_bug_report_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_bug_report_black_24dp.png new file mode 100644 index 000000000..6eb1474e3 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_bug_report_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_bug_report_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_bug_report_white_24dp.png new file mode 100644 index 000000000..e0b5b1964 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_bug_report_white_24dp.png differ diff --git a/app/src/main/res/menu/debug_menu.xml b/app/src/main/res/menu/debug_menu.xml deleted file mode 100644 index 448f9cf23..000000000 --- a/app/src/main/res/menu/debug_menu.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 8e9821b10..171cc5ee0 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -373,5 +373,4 @@ Keine Untertitel Schriftgröße der Untertitel - "Speicherlecks nachverfolgen " diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 4d2f8a10e..25cba8191 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -395,10 +395,6 @@ Carattere normale Carattere più grande - Controllo delle perdite - Controllo delle perdite di memoria abilitato, l\'applicazione può non rispondere mentre effettua il dumping dell\'heap - Controllo delle perdite di memoria disabilitato -A breve qualcosa si troverà qui ;D + A breve qualcosa si troverà qui ;D - - + diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 9c1e21a36..c214f5bd1 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -382,8 +382,4 @@ Mindre skrift Normal skrift Større skrift - - Hold oppsyn med lekkasjer - Oppsyn med minnelekasjer påslått, programmet kan slutte å svare under haug-dumping - Oppsyn med minnelekasjer slått av diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 6ab9a5b41..8ed57a2bf 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -391,10 +391,5 @@ te openen in pop-upmodus Normaal lettertype Groter lettertype - Controleren op lekken - Controleren op geheugenlekken ingeschakeld, tijdens heapdumping kan de app tijdelijk niet reageren - Controleren op geheugenlekken uitgeschakeld -Hier zal binnenkort iets verschijnen ;D - - - + Hier zal binnenkort iets verschijnen ;D + diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 83f6ee085..5203e6b6b 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -368,8 +368,4 @@ abrir em modo popup Fonte menor Fonte normal Maior fonte - - Monitorar vazamentos de memória - Monitoramento de vazamentos de memória habilitado, o aplicativo pode ficar sem responder quando estiver descarregando pilha de memória - Monitoramento de vazamentos de memória desabilitado diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 347724f85..e620d55ba 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -391,8 +391,4 @@ otvorenie okna na popredí Menšie Písmo Normálne Písmo Väčšie Písmo - - Monitorovanie pretečenia - Monitorovanie pretečenia pamäte je povolené, pri hromadnom zbere môže aplikácia prestať reagovať - Monitorovanie pretečenia pamäte je vypnuté diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 19d47f880..a704f8c10 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -384,8 +384,4 @@ Küçük Yazı Tipi Olağan Yazı Tipi Büyük Yazı Tipi - - Sızıntıları Gözlemle - Bellek sızıntısı gözlemleme etkinleştirildi, uygulama yığın atımı sırasında yanıtsız kalabilir - Bellek sızıntısı gözlemleme devre dışı diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index c8e0907be..eb537623c 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -25,6 +25,7 @@ + diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index ee784b5f7..fc31ee02c 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -84,8 +84,11 @@ last_orientation_landscape_key + debug_pref_screen_key allow_heap_dumping_key + allow_disposed_exceptions_key + theme light_theme diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 08872694a..495842092 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -98,6 +98,7 @@ Popup Appearance Other + Debug Playing in background Playing in popup mode Queued on background player @@ -406,13 +407,17 @@ FILL ZOOM + Auto-generated Caption Font Size Smaller Font Normal Font Larger Font - - Monitor Leaks - Memory leak monitoring enabled, app may become unresponsive when heap dumping - Memory leak monitoring disabled + + Enable LeakCanary + Memory leak monitoring may cause app to become unresponsive when heap dumping + + Report Out-of-Lifecycle Errors + Force reporting of undeliverable Rx exceptions occurring outside of fragment or activity lifecycle after dispose + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index ad767835b..27b12fa62 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -20,6 +20,7 @@ @drawable/ic_thumb_up_black_24dp @drawable/ic_thumb_down_black_24dp @drawable/ic_info_outline_black_24dp + @drawable/ic_bug_report_black_24dp @drawable/ic_headset_black_24dp @drawable/ic_delete_sweep_white_24dp @drawable/ic_file_download_black_24dp @@ -74,6 +75,7 @@ @drawable/ic_thumb_down_white_24dp @drawable/ic_headset_white_24dp @drawable/ic_info_outline_white_24dp + @drawable/ic_bug_report_white_24dp @drawable/ic_delete_sweep_black_24dp @drawable/ic_file_download_white_24dp @drawable/ic_share_white_24dp diff --git a/app/src/main/res/xml/debug_settings.xml b/app/src/main/res/xml/debug_settings.xml new file mode 100644 index 000000000..c0bb1505d --- /dev/null +++ b/app/src/main/res/xml/debug_settings.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/app/src/main/res/xml/main_settings.xml b/app/src/main/res/xml/main_settings.xml index 300265557..4196a98d3 100644 --- a/app/src/main/res/xml/main_settings.xml +++ b/app/src/main/res/xml/main_settings.xml @@ -28,4 +28,10 @@ android:fragment="org.schabi.newpipe.settings.ContentSettingsFragment" android:icon="?attr/language" android:title="@string/content"/> + +