diff --git a/app/build.gradle b/app/build.gradle index bfd37e542..db9cce999 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -44,27 +44,22 @@ dependencies { exclude module: 'support-annotations' } - compile "android.arch.persistence.room:runtime:1.0.0-alpha3" - annotationProcessor "android.arch.persistence.room:compiler:1.0.0-alpha3" - + compile 'com.github.TeamNewPipe:NewPipeExtractor:97ad1a2' testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:1.10.19' - testCompile 'org.json:json:20160810' - compile 'com.android.support:appcompat-v7:26.0.0' - compile 'com.android.support:support-v4:26.0.0' - compile 'com.android.support:design:26.0.0' - compile 'com.android.support:recyclerview-v7:26.0.0' + compile 'com.android.support:appcompat-v7:26.0.1' + compile 'com.android.support:support-v4:26.0.1' + compile 'com.android.support:design:26.0.1' + compile 'com.android.support:recyclerview-v7:26.0.1' + compile 'com.android.support:preference-v14:26.0.1' compile 'com.google.code.gson:gson:2.7' - compile 'org.jsoup:jsoup:1.8.3' - compile 'org.mozilla:rhino:1.7.7' compile 'ch.acra:acra:4.9.0' - compile 'info.guardianproject.netcipher:netcipher:1.2' compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.5' - compile 'de.hdodenhof:circleimageview:2.0.0' + compile 'de.hdodenhof:circleimageview:2.1.0' compile 'com.github.nirhart:parallaxscroll:1.0' compile 'com.nononsenseapps:filepicker:3.0.0' compile 'com.google.android.exoplayer:exoplayer:r2.5.1' @@ -73,11 +68,14 @@ dependencies { debugCompile 'com.facebook.stetho:stetho-urlconnection:1.5.0' debugCompile 'com.android.support:multidex:1.0.1' - compile "android.arch.persistence.room:runtime:1.0.0-alpha8" - annotationProcessor "android.arch.persistence.room:compiler:1.0.0-alpha8" - - compile "io.reactivex.rxjava2:rxjava:2.1.2" - compile "io.reactivex.rxjava2:rxandroid:2.0.1" + compile 'io.reactivex.rxjava2:rxjava:2.1.2' + compile 'io.reactivex.rxjava2:rxandroid:2.0.1' compile 'com.jakewharton.rxbinding2:rxbinding:2.0.0' - compile "android.arch.persistence.room:rxjava2:1.0.0-alpha8" + + compile 'android.arch.persistence.room:runtime:1.0.0-alpha8' + compile 'android.arch.persistence.room:rxjava2:1.0.0-alpha8' + annotationProcessor 'android.arch.persistence.room:compiler:1.0.0-alpha8' + + compile 'frankiesardo:icepick:3.2.0' + provided 'frankiesardo:icepick-processor:3.2.0' } diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml index 614f93faf..a16d6796a 100644 --- a/app/src/debug/AndroidManifest.xml +++ b/app/src/debug/AndroidManifest.xml @@ -1,17 +1,17 @@ - - - - - - + xmlns:tools="http://schemas.android.com/tools" + package="org.schabi.newpipe"> + android:name=".DebugApp" + android:label="NewPipe Debug" + tools:replace="android:name, android:label"> + + \ No newline at end of file diff --git a/app/src/debug/java/org/schabi/newpipe/DebugApp.java b/app/src/debug/java/org/schabi/newpipe/DebugApp.java index 964d7c099..1a507b4e5 100644 --- a/app/src/debug/java/org/schabi/newpipe/DebugApp.java +++ b/app/src/debug/java/org/schabi/newpipe/DebugApp.java @@ -5,24 +5,6 @@ import android.support.multidex.MultiDex; import com.facebook.stetho.Stetho; -/** - * Copyright (C) Hans-Christoph Steiner 2016 - * App.java is part of NewPipe. - * - * NewPipe is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * NewPipe is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with NewPipe. If not, see . - */ - public class DebugApp extends App { private static final String TAG = DebugApp.class.toString(); diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c21fd6043..826ae4f44 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -16,7 +16,7 @@ android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:logo="@mipmap/ic_launcher" - android:theme="@style/AppTheme" + android:theme="@style/DarkTheme" tools:ignore="AllowBackup"> @@ -52,6 +52,15 @@ + + + + + + + android:launchMode="singleTask"/> @@ -83,6 +92,7 @@ android:label="@string/app_name" android:launchMode="singleTop" android:theme="@style/FilePickerTheme"/> + @@ -122,6 +132,8 @@ + + @@ -155,12 +167,11 @@ - + android:theme="@android:style/Theme.NoDisplay"> @@ -210,14 +221,5 @@ - - - - + \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/ActivityCommunicator.java b/app/src/main/java/org/schabi/newpipe/ActivityCommunicator.java index 2ddc2d127..da601a42f 100644 --- a/app/src/main/java/org/schabi/newpipe/ActivityCommunicator.java +++ b/app/src/main/java/org/schabi/newpipe/ActivityCommunicator.java @@ -1,6 +1,6 @@ package org.schabi.newpipe; -/** +/* * Created by Christian Schabesberger on 24.12.15. * * Copyright (C) Christian Schabesberger 2015 diff --git a/app/src/main/java/org/schabi/newpipe/App.java b/app/src/main/java/org/schabi/newpipe/App.java index 09b126104..93b4becde 100644 --- a/app/src/main/java/org/schabi/newpipe/App.java +++ b/app/src/main/java/org/schabi/newpipe/App.java @@ -5,8 +5,8 @@ import android.app.NotificationChannel; import android.app.NotificationManager; import android.content.Context; import android.os.Build; +import android.util.Log; -import com.facebook.stetho.Stetho; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.ImageLoaderConfiguration; @@ -20,12 +20,20 @@ import org.schabi.newpipe.report.AcraReportSenderFactory; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.settings.SettingsActivity; -import org.schabi.newpipe.util.ThemeHelper; +import org.schabi.newpipe.util.ExtractorHelper; +import org.schabi.newpipe.util.StateSaver; -import info.guardianproject.netcipher.NetCipher; -import info.guardianproject.netcipher.proxy.OrbotHelper; +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.SocketException; -/** +import io.reactivex.annotations.NonNull; +import io.reactivex.exceptions.CompositeException; +import io.reactivex.exceptions.UndeliverableException; +import io.reactivex.functions.Consumer; +import io.reactivex.plugins.RxJavaPlugins; + +/* * Copyright (C) Hans-Christoph Steiner 2016 * App.java is part of NewPipe. * @@ -44,80 +52,85 @@ import info.guardianproject.netcipher.proxy.OrbotHelper; */ public class App extends Application { - private static final String TAG = App.class.toString(); + protected static final String TAG = App.class.toString(); - private static boolean useTor; + @SuppressWarnings("unchecked") + private static final Class[] reportSenderFactoryClasses = new Class[]{AcraReportSenderFactory.class}; - final Class[] reportSenderFactoryClasses - = new Class[]{AcraReportSenderFactory.class}; + @Override + protected void attachBaseContext(Context base) { + super.attachBaseContext(base); + + initACRA(); + } @Override public void onCreate() { super.onCreate(); - // init crashreport - try { - final ACRAConfiguration acraConfig = new ConfigurationBuilder(this) - .setReportSenderFactoryClasses(reportSenderFactoryClasses) - .build(); - ACRA.init(this, acraConfig); - } catch(ACRAConfigurationException ace) { - ace.printStackTrace(); - ErrorActivity.reportError(this, ace, null, null, - ErrorActivity.ErrorInfo.make(UserAction.SEARCHED,"none", - "Could not initialize ACRA crash report", R.string.app_ui_crash)); - } + // Initialize settings first because others inits can use its values + SettingsActivity.initSettings(this); - NewPipeDatabase.getInstance( getApplicationContext() ); - - //init NewPipe NewPipe.init(Downloader.getInstance()); + NewPipeDatabase.init(this); + StateSaver.init(this); + initNotificationChannel(); // Initialize image loader ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(this).build(); ImageLoader.getInstance().init(config); - /* - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); - if(prefs.getBoolean(getString(R.string.use_tor_key), false)) { - OrbotHelper.requestStartTor(this); - configureTor(true); - } else { - configureTor(false); - }*/ - configureTor(false); - - // DO NOT REMOVE THIS FUNCTION!!! - // Otherwise downloadPathPreference has invalid value. - SettingsActivity.initSettings(this); - - ThemeHelper.setTheme(getApplicationContext()); - - initNotificationChannel(); + configureRxJavaErrorHandler(); } - /** - * Set the proxy settings based on whether Tor should be enabled or not. - */ - public static void configureTor(boolean enabled) { - useTor = enabled; - if (useTor) { - NetCipher.useTor(); - } else { - NetCipher.setProxy(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() + "]"); + + if (throwable instanceof UndeliverableException) { + // As UndeliverableException is a wrapper, get the cause of it to get the "real" exception + throwable = throwable.getCause(); + } + + if (throwable instanceof CompositeException) { + for (Throwable element : ((CompositeException) throwable).getExceptions()) { + if (checkThrowable(element)) return; + } + } + + if (checkThrowable(throwable)) return; + + // 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); + } + }); + } + + + private void initACRA() { + try { + final ACRAConfiguration acraConfig = new ConfigurationBuilder(this) + .setReportSenderFactoryClasses(reportSenderFactoryClasses) + .setBuildConfigClass(BuildConfig.class) + .build(); + ACRA.init(this, acraConfig); + } catch (ACRAConfigurationException ace) { + ace.printStackTrace(); + ErrorActivity.reportError(this, ace, null, null, ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none", + "Could not initialize ACRA crash report", R.string.app_ui_crash)); } } - public static void checkStartTor(Context context) { - if (useTor) { - OrbotHelper.requestStartTor(context); - } - } - - public static boolean isUsingTor() { - return useTor; - } - public void initNotificationChannel() { if (Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O) { return; diff --git a/app/src/main/java/org/schabi/newpipe/BaseFragment.java b/app/src/main/java/org/schabi/newpipe/BaseFragment.java new file mode 100644 index 000000000..186b4adc2 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/BaseFragment.java @@ -0,0 +1,121 @@ +package org.schabi.newpipe; + +import android.content.Context; +import android.content.res.TypedArray; +import android.os.Bundle; +import android.support.annotation.AttrRes; +import android.support.annotation.NonNull; +import android.support.v4.app.Fragment; +import android.support.v7.app.AppCompatActivity; +import android.util.Log; +import android.view.View; + +import com.nostra13.universalimageloader.core.DisplayImageOptions; +import com.nostra13.universalimageloader.core.ImageLoader; +import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer; + +import icepick.Icepick; + +public abstract class BaseFragment extends Fragment { + protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode()); + protected boolean DEBUG = MainActivity.DEBUG; + + protected AppCompatActivity activity; + public static final ImageLoader imageLoader = ImageLoader.getInstance(); + + /*////////////////////////////////////////////////////////////////////////// + // Fragment's Lifecycle + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onAttach(Context context) { + super.onAttach(context); + activity = (AppCompatActivity) context; + } + + @Override + public void onDetach() { + super.onDetach(); + activity = null; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]"); + super.onCreate(savedInstanceState); + Icepick.restoreInstanceState(this, savedInstanceState); + if (savedInstanceState != null) onRestoreInstanceState(savedInstanceState); + } + + + @Override + public void onViewCreated(View rootView, Bundle savedInstanceState) { + super.onViewCreated(rootView, savedInstanceState); + if (DEBUG) { + Log.d(TAG, "onViewCreated() called with: rootView = [" + rootView + "], savedInstanceState = [" + savedInstanceState + "]"); + } + initViews(rootView, savedInstanceState); + initListeners(); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + Icepick.saveInstanceState(this, outState); + } + + protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { + } + + /*////////////////////////////////////////////////////////////////////////// + // Init + //////////////////////////////////////////////////////////////////////////*/ + + protected void initViews(View rootView, Bundle savedInstanceState) { + } + + protected void initListeners() { + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + protected final int resolveResourceIdFromAttr(@AttrRes int attr) { + TypedArray a = activity.getTheme().obtainStyledAttributes(new int[]{attr}); + int attributeResourceId = a.getResourceId(0, 0); + a.recycle(); + return attributeResourceId; + } + + /*////////////////////////////////////////////////////////////////////////// + // DisplayImageOptions default configurations + //////////////////////////////////////////////////////////////////////////*/ + + public static final DisplayImageOptions BASE_OPTIONS = + new DisplayImageOptions.Builder().cacheInMemory(true).build(); + + public static final DisplayImageOptions DISPLAY_AVATAR_OPTIONS = + new DisplayImageOptions.Builder() + .cloneFrom(BASE_OPTIONS) + .showImageOnLoading(R.drawable.buddy) + .showImageForEmptyUri(R.drawable.buddy) + .showImageOnFail(R.drawable.buddy) + .build(); + + public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = + new DisplayImageOptions.Builder() + .cloneFrom(BASE_OPTIONS) + .displayer(new FadeInBitmapDisplayer(250)) + .showImageForEmptyUri(R.drawable.dummy_thumbnail) + .showImageOnFail(R.drawable.dummy_thumbnail) + .build(); + + public static final DisplayImageOptions DISPLAY_BANNER_OPTIONS = + new DisplayImageOptions.Builder() + .cloneFrom(BASE_OPTIONS) + .showImageOnLoading(R.drawable.channel_banner) + .showImageForEmptyUri(R.drawable.channel_banner) + .showImageOnFail(R.drawable.channel_banner) + .build(); +} diff --git a/app/src/main/java/org/schabi/newpipe/Downloader.java b/app/src/main/java/org/schabi/newpipe/Downloader.java index a441e8978..dede9617e 100644 --- a/app/src/main/java/org/schabi/newpipe/Downloader.java +++ b/app/src/main/java/org/schabi/newpipe/Downloader.java @@ -1,20 +1,24 @@ package org.schabi.newpipe; +import android.util.Log; + import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; +import org.schabi.newpipe.util.ExtractorHelper; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; +import java.io.InterruptedIOException; import java.net.URL; -import java.net.UnknownHostException; import java.util.HashMap; import java.util.Iterator; +import java.util.List; import java.util.Map; import javax.net.ssl.HttpsURLConnection; -/** +/* * Created by Christian Schabesberger on 28.01.16. * * Copyright (C) Christian Schabesberger 2016 @@ -35,16 +39,17 @@ import javax.net.ssl.HttpsURLConnection; */ public class Downloader implements org.schabi.newpipe.extractor.Downloader { - - private static final String USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:43.0) Gecko/20100101 Firefox/43.0"; + + 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 Downloader() {} + private Downloader() { + } public static Downloader getInstance() { - if(instance == null) { + if (instance == null) { synchronized (Downloader.class) { if (instance == null) { instance = new Downloader(); @@ -62,41 +67,66 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader { return Downloader.mCookies; } - /**Download the text file at the supplied URL as in download(String), + /** + * Download the text file at the supplied URL as in download(String), * but set the HTTP header field "Accept-Language" to the supplied string. - * @param siteUrl the URL of the text file to return the contents of + * + * @param siteUrl the URL of the text file to return the contents of * @param language the language (usually a 2-character code) to set as the preferred language - * @return the contents of the specified text file*/ + * @return the contents of the specified text file + */ + @Override public String download(String siteUrl, String language) throws IOException, ReCaptchaException { Map requestProperties = new HashMap<>(); requestProperties.put("Accept-Language", language); return download(siteUrl, requestProperties); } - - /**Download the text file at the supplied URL as in download(String), - * but set the HTTP header field "Accept-Language" to the supplied string. - * @param siteUrl the URL of the text file to return the contents of + /** + * Download the text file at the supplied URL as in download(String), + * but set the HTTP headers included in the customProperties map. + * + * @param siteUrl the URL of the text file to return the contents of * @param customProperties set request header properties * @return the contents of the specified text file - * @throws IOException*/ + * @throws IOException + */ + @Override public String download(String siteUrl, Map customProperties) throws IOException, ReCaptchaException { URL url = new URL(siteUrl); HttpsURLConnection con = (HttpsURLConnection) url.openConnection(); Iterator it = customProperties.entrySet().iterator(); - while(it.hasNext()) { - Map.Entry pair = (Map.Entry)it.next(); - con.setRequestProperty((String)pair.getKey(), (String)pair.getValue()); + while (it.hasNext()) { + Map.Entry pair = (Map.Entry) it.next(); + con.setRequestProperty((String) pair.getKey(), (String) pair.getValue()); } return dl(con); } - /**Common functionality between download(String url) and download(String url, String language)*/ + /** + * Download (via HTTP) the text file located at the supplied URL, and return its contents. + * Primarily intended for downloading web pages. + * + * @param siteUrl the URL of the text file to download + * @return the contents of the specified text file + */ + @Override + public String download(String siteUrl) throws IOException, ReCaptchaException { + URL url = new URL(siteUrl); + HttpsURLConnection con = (HttpsURLConnection) url.openConnection(); + //HttpsURLConnection con = NetCipher.getHttpsURLConnection(url); + return dl(con); + } + + /** + * Common functionality between download(String url) and download(String url, String language) + */ private static String dl(HttpsURLConnection con) throws IOException, ReCaptchaException { StringBuilder response = new StringBuilder(); BufferedReader in = null; try { + con.setReadTimeout(30 * 1000);// 30s con.setRequestMethod("GET"); con.setRequestProperty("User-Agent", USER_AGENT); @@ -104,17 +134,22 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader { con.setRequestProperty("Cookie", getCookies()); } - in = new BufferedReader( - new InputStreamReader(con.getInputStream())); + in = new BufferedReader(new InputStreamReader(con.getInputStream())); + for (Map.Entry> entry : con.getHeaderFields().entrySet()) { + System.err.println(entry.getKey() + ": " + entry.getValue()); + } String inputLine; - while((inputLine = in.readLine()) != null) { + while ((inputLine = in.readLine()) != null) { response.append(inputLine); } - } catch(UnknownHostException uhe) {//thrown when there's no internet connection - throw new IOException("unknown host or no network", uhe); - //Toast.makeText(getActivity(), uhe.getMessage(), Toast.LENGTH_LONG).show(); - } catch(Exception e) { + } catch (Exception e) { + Log.e("Downloader", "dl() ----- Exception thrown → " + e.getClass().getName()); + + if (ExtractorHelper.isInterruptedCaused(e)) { + throw new InterruptedIOException(e.getMessage()); + } + /* * HTTP 429 == Too Many Request * Receive from Youtube.com = ReCaptcha challenge request @@ -123,24 +158,14 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader { if (con.getResponseCode() == 429) { throw new ReCaptchaException("reCaptcha Challenge requested"); } - throw new IOException(e); + + throw new IOException(con.getResponseCode() + " " + con.getResponseMessage(), e); } finally { - if(in != null) { + if (in != null) { in.close(); } } return response.toString(); } - - /**Download (via HTTP) the text file located at the supplied URL, and return its contents. - * Primarily intended for downloading web pages. - * @param siteUrl the URL of the text file to download - * @return the contents of the specified text file*/ - public String download(String siteUrl) throws IOException, ReCaptchaException { - URL url = new URL(siteUrl); - HttpsURLConnection con = (HttpsURLConnection) url.openConnection(); - //HttpsURLConnection con = NetCipher.getHttpsURLConnection(url); - return dl(con); - } } diff --git a/app/src/main/java/org/schabi/newpipe/ExitActivity.java b/app/src/main/java/org/schabi/newpipe/ExitActivity.java index 6e14cfd9f..1ea3abe34 100644 --- a/app/src/main/java/org/schabi/newpipe/ExitActivity.java +++ b/app/src/main/java/org/schabi/newpipe/ExitActivity.java @@ -7,7 +7,7 @@ import android.content.Intent; import android.os.Build; import android.os.Bundle; -/** +/* * Copyright (C) Hans-Christoph Steiner 2016 * ExitActivity.java is part of NewPipe. * diff --git a/app/src/main/java/org/schabi/newpipe/ImageErrorLoadingListener.java b/app/src/main/java/org/schabi/newpipe/ImageErrorLoadingListener.java deleted file mode 100644 index eb2202fca..000000000 --- a/app/src/main/java/org/schabi/newpipe/ImageErrorLoadingListener.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.schabi.newpipe; - -import android.content.Context; -import android.graphics.Bitmap; -import android.view.View; - -import com.nostra13.universalimageloader.core.assist.FailReason; -import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; - -import org.schabi.newpipe.extractor.NewPipe; -import org.schabi.newpipe.report.ErrorActivity; -import org.schabi.newpipe.report.UserAction; - -/** - * Created by Christian Schabesberger on 01.08.16. - * - * Copyright (C) Christian Schabesberger 2015 - * StreamInfoItemViewCreator.java is part of NewPipe. - * - * NewPipe is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * NewPipe is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with NewPipe. If not, see . - */ - -public class ImageErrorLoadingListener implements ImageLoadingListener { - - private int serviceId = -1; - private Context context = null; - private View rootView = null; - - public ImageErrorLoadingListener(Context context, View rootView, int serviceId) { - this.context = context; - this.serviceId= serviceId; - this.rootView = rootView; - } - - @Override - public void onLoadingStarted(String imageUri, View view) {} - - @Override - public void onLoadingFailed(String imageUri, View view, FailReason failReason) { - ErrorActivity.reportError(context, - failReason.getCause(), null, rootView, - ErrorActivity.ErrorInfo.make(UserAction.LOAD_IMAGE, - NewPipe.getNameOfService(serviceId), imageUri, - R.string.could_not_load_image)); - } - - @Override - public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { - - } - - @Override - public void onLoadingCancelled(String imageUri, View view) {} -} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index fb381fe3c..67689d541 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -23,6 +23,8 @@ package org.schabi.newpipe; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.v4.app.Fragment; @@ -43,31 +45,29 @@ import org.schabi.newpipe.database.history.model.HistoryEntry; import org.schabi.newpipe.database.history.model.SearchHistoryEntry; import org.schabi.newpipe.database.history.model.WatchHistoryEntry; import org.schabi.newpipe.extractor.StreamingService; -import org.schabi.newpipe.extractor.stream_info.AudioStream; -import org.schabi.newpipe.extractor.stream_info.StreamInfo; -import org.schabi.newpipe.extractor.stream_info.VideoStream; +import org.schabi.newpipe.extractor.stream.AudioStream; +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.VideoStream; +import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.detail.VideoDetailFragment; -import org.schabi.newpipe.fragments.search.SearchFragment; -import org.schabi.newpipe.history.HistoryActivity; +import org.schabi.newpipe.fragments.list.search.SearchFragment; +import org.schabi.newpipe.history.HistoryListener; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.StateSaver; import org.schabi.newpipe.util.ThemeHelper; import java.util.Date; +import io.reactivex.disposables.Disposable; import io.reactivex.functions.Consumer; import io.reactivex.schedulers.Schedulers; import io.reactivex.subjects.PublishSubject; -public class MainActivity extends AppCompatActivity implements - VideoDetailFragment.OnVideoPlayListener, - SearchFragment.OnSearchListener { - public static final boolean DEBUG = false; +public class MainActivity extends AppCompatActivity implements HistoryListener { private static final String TAG = "MainActivity"; - private WatchHistoryDAO watchHistoryDAO; - private SearchHistoryDAO searchHistoryDAO; + public static final boolean DEBUG = false; private SharedPreferences sharedPreferences; - private PublishSubject historyEntrySubject; /*////////////////////////////////////////////////////////////////////////// // Activity's LifeCycle @@ -75,8 +75,7 @@ public class MainActivity extends AppCompatActivity implements @Override protected void onCreate(Bundle savedInstanceState) { - if (DEBUG) - Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]"); + if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]"); ThemeHelper.setTheme(this); super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); @@ -87,52 +86,37 @@ public class MainActivity extends AppCompatActivity implements Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); - - AppDatabase database = NewPipeDatabase.getInstance(this); - watchHistoryDAO = database.watchHistoryDAO(); - searchHistoryDAO = database.searchHistoryDAO(); sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - historyEntrySubject = PublishSubject.create(); - historyEntrySubject - .observeOn(Schedulers.io()) - .subscribe(createHistoryEntryConsumer()); - } - @NonNull - private Consumer createHistoryEntryConsumer() { - return new Consumer() { - @Override - public void accept(HistoryEntry historyEntry) throws Exception { - //noinspection unchecked - HistoryDAO historyDAO = (HistoryDAO) - (historyEntry instanceof SearchHistoryEntry ? searchHistoryDAO : watchHistoryDAO); - - HistoryEntry latestEntry = historyDAO.getLatestEntry(); - if (historyEntry.hasEqualValues(latestEntry)) { - latestEntry.setCreationDate(historyEntry.getCreationDate()); - historyDAO.update(latestEntry); - } else { - historyDAO.insert(historyEntry); - } - } - }; + initHistory(); } @Override protected void onDestroy() { super.onDestroy(); - watchHistoryDAO = null; - searchHistoryDAO = null; + if (!isChangingConfigurations()) { + StateSaver.clearStateFiles(); + } + + disposeHistory(); } @Override protected void onResume() { super.onResume(); + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); if (sharedPreferences.getBoolean(Constants.KEY_THEME_CHANGE, false)) { if (DEBUG) Log.d(TAG, "Theme has changed, recreating activity..."); sharedPreferences.edit().putBoolean(Constants.KEY_THEME_CHANGE, false).apply(); - this.recreate(); + // https://stackoverflow.com/questions/10844112/runtimeexception-performing-pause-of-activity-that-is-not-resumed + // Briefly, let the activity resume properly posting the recreate call to end of the message queue + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + MainActivity.this.recreate(); + } + }); } } @@ -144,8 +128,7 @@ public class MainActivity extends AppCompatActivity implements // Return if launched from a launcher (e.g. Nova Launcher, Pixel Launcher ...) // to not destroy the already created backstack String action = intent.getAction(); - if ((action != null && action.equals(Intent.ACTION_MAIN)) && intent.hasCategory(Intent.CATEGORY_LAUNCHER)) - return; + if ((action != null && action.equals(Intent.ACTION_MAIN)) && intent.hasCategory(Intent.CATEGORY_LAUNCHER)) return; } super.onNewIntent(intent); @@ -158,8 +141,10 @@ public class MainActivity extends AppCompatActivity implements if (DEBUG) Log.d(TAG, "onBackPressed() called"); Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder); - if (fragment instanceof VideoDetailFragment) - if (((VideoDetailFragment) fragment).onActivityBackPressed()) return; + // If current fragment implements BackPressable (i.e. can/wanna handle back press) delegate the back press to it + if (fragment instanceof BackPressable) { + if (((BackPressable) fragment).onBackPressed()) return; + } if (getSupportFragmentManager().getBackStackEntryCount() == 1) { @@ -202,23 +187,19 @@ public class MainActivity extends AppCompatActivity implements int id = item.getItemId(); switch (id) { - case android.R.id.home: { + case android.R.id.home: NavigationHelper.gotoMainFragment(getSupportFragmentManager()); return true; - } - case R.id.action_settings: { + case R.id.action_settings: NavigationHelper.openSettings(this); return true; - } - case R.id.action_show_downloads: { + case R.id.action_show_downloads: return NavigationHelper.openDownloads(this); - } case R.id.action_about: NavigationHelper.openAbout(this); return true; case R.id.action_history: - Intent intent = new Intent(this, HistoryActivity.class); - startActivity(intent); + NavigationHelper.openHistory(this); return true; default: return super.onOptionsItemSelected(item); @@ -230,6 +211,8 @@ public class MainActivity extends AppCompatActivity implements //////////////////////////////////////////////////////////////////////////*/ private void initFragments() { + if (DEBUG) Log.d(TAG, "initFragments() called"); + StateSaver.clearStateFiles(); if (getIntent() != null && getIntent().hasExtra(Constants.KEY_LINK_TYPE)) { handleIntent(getIntent()); } else NavigationHelper.gotoMainFragment(getSupportFragmentManager()); @@ -254,6 +237,9 @@ public class MainActivity extends AppCompatActivity implements case CHANNEL: NavigationHelper.openChannelFragment(getSupportFragmentManager(), serviceId, url, title); break; + case PLAYLIST: + NavigationHelper.openPlaylistFragment(getSupportFragmentManager(), serviceId, url, title); + break; } } else if (intent.hasExtra(Constants.KEY_OPEN_SEARCH)) { String searchQuery = intent.getStringExtra(Constants.KEY_QUERY); @@ -265,6 +251,50 @@ public class MainActivity extends AppCompatActivity implements } } + /*////////////////////////////////////////////////////////////////////////// + // History + //////////////////////////////////////////////////////////////////////////*/ + + private WatchHistoryDAO watchHistoryDAO; + private SearchHistoryDAO searchHistoryDAO; + private PublishSubject historyEntrySubject; + private Disposable disposable; + + private void initHistory() { + final AppDatabase database = NewPipeDatabase.getInstance(); + watchHistoryDAO = database.watchHistoryDAO(); + searchHistoryDAO = database.searchHistoryDAO(); + historyEntrySubject = PublishSubject.create(); + disposable = historyEntrySubject + .observeOn(Schedulers.io()) + .subscribe(getHistoryEntryConsumer()); + } + + private void disposeHistory() { + if (disposable != null) disposable.dispose(); + watchHistoryDAO = null; + searchHistoryDAO = null; + } + + @NonNull + private Consumer getHistoryEntryConsumer() { + return new Consumer() { + @Override + public void accept(HistoryEntry historyEntry) throws Exception { + //noinspection unchecked + HistoryDAO historyDAO = (HistoryDAO) + (historyEntry instanceof SearchHistoryEntry ? searchHistoryDAO : watchHistoryDAO); + + HistoryEntry latestEntry = historyDAO.getLatestEntry(); + if (historyEntry.hasEqualValues(latestEntry)) { + latestEntry.setCreationDate(historyEntry.getCreationDate()); + historyDAO.update(latestEntry); + } else { + historyDAO.insert(historyEntry); + } + } + }; + } private void addWatchHistoryEntry(StreamInfo streamInfo) { if (sharedPreferences.getBoolean(getString(R.string.enable_watch_history_key), true)) { @@ -274,12 +304,12 @@ public class MainActivity extends AppCompatActivity implements } @Override - public void onVideoPlayed(VideoStream videoStream, StreamInfo streamInfo) { + public void onVideoPlayed(StreamInfo streamInfo, VideoStream videoStream) { addWatchHistoryEntry(streamInfo); } @Override - public void onBackgroundPlayed(StreamInfo streamInfo, AudioStream audioStream) { + public void onAudioPlayed(StreamInfo streamInfo, AudioStream audioStream) { addWatchHistoryEntry(streamInfo); } diff --git a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java index 3e3c4d9db..7111abcf7 100644 --- a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java @@ -8,27 +8,24 @@ import org.schabi.newpipe.database.AppDatabase; import static org.schabi.newpipe.database.AppDatabase.DATABASE_NAME; -public class NewPipeDatabase { +public final class NewPipeDatabase { - private static AppDatabase sInstance; + private static AppDatabase databaseInstance; - // For Singleton instantiation - private static final Object LOCK = new Object(); + private NewPipeDatabase() { + //no instance + } + + public static void init(Context context) { + databaseInstance = Room.databaseBuilder(context.getApplicationContext(), + AppDatabase.class, DATABASE_NAME + ).build(); + } @NonNull - public synchronized static AppDatabase getInstance(Context context) { - if (sInstance == null) { - synchronized (LOCK) { - if (sInstance == null) { + public static AppDatabase getInstance() { + if (databaseInstance == null) throw new RuntimeException("Database not initialized"); - sInstance = Room.databaseBuilder( - context.getApplicationContext(), - AppDatabase.class, - DATABASE_NAME - ).build(); - } - } - } - return sInstance; + return databaseInstance; } } diff --git a/app/src/main/java/org/schabi/newpipe/PanicResponderActivity.java b/app/src/main/java/org/schabi/newpipe/PanicResponderActivity.java index 949084c57..4118070d5 100644 --- a/app/src/main/java/org/schabi/newpipe/PanicResponderActivity.java +++ b/app/src/main/java/org/schabi/newpipe/PanicResponderActivity.java @@ -6,9 +6,8 @@ import android.app.Activity; import android.content.Intent; import android.os.Build; import android.os.Bundle; -import android.media.AudioManager; -/** +/* * Copyright (C) Hans-Christoph Steiner 2016 * PanicResponderActivity.java is part of NewPipe. * diff --git a/app/src/main/java/org/schabi/newpipe/ReCaptchaActivity.java b/app/src/main/java/org/schabi/newpipe/ReCaptchaActivity.java index b97e0566d..d124bc6c4 100644 --- a/app/src/main/java/org/schabi/newpipe/ReCaptchaActivity.java +++ b/app/src/main/java/org/schabi/newpipe/ReCaptchaActivity.java @@ -16,7 +16,7 @@ import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; -/** +/* * Created by beneth on 06.12.16. * * Copyright (C) Christian Schabesberger 2015 @@ -49,7 +49,7 @@ public class ReCaptchaActivity extends AppCompatActivity { // Set return to Cancel by default setResult(RESULT_CANCELED); - Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); ActionBar actionBar = getSupportActionBar(); @@ -59,7 +59,7 @@ public class ReCaptchaActivity extends AppCompatActivity { actionBar.setDisplayShowTitleEnabled(true); } - WebView myWebView = (WebView) findViewById(R.id.reCaptchaWebView); + WebView myWebView = findViewById(R.id.reCaptchaWebView); // Enable Javascript WebSettings webSettings = myWebView.getSettings(); diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java index 65bb526fd..434a34c7e 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java @@ -1,8 +1,8 @@ package org.schabi.newpipe; -import android.app.Activity; import android.content.Intent; import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; import android.widget.Toast; import org.schabi.newpipe.util.NavigationHelper; @@ -32,7 +32,7 @@ import java.util.HashSet; * This Acitivty is designed to route share/open intents to the specified service, and * to the part of the service which can handle the url. */ -public class RouterActivity extends Activity { +public class RouterActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { @@ -40,8 +40,6 @@ public class RouterActivity extends Activity { String videoUrl = getUrl(getIntent()); handleUrl(videoUrl); - - finish(); } protected void handleUrl(String url) { @@ -50,6 +48,8 @@ public class RouterActivity extends Activity { } catch (Exception e) { Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show(); } + + finish(); } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/RouterPopupActivity.java b/app/src/main/java/org/schabi/newpipe/RouterPopupActivity.java index 1dfc4648a..1cff0ca76 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterPopupActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterPopupActivity.java @@ -6,6 +6,7 @@ import android.widget.Toast; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; +import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.player.PopupVideoPlayer; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.PermissionHelper; @@ -22,8 +23,10 @@ public class RouterPopupActivity extends RouterActivity { Toast.makeText(this, R.string.msg_popup_permission, Toast.LENGTH_LONG).show(); return; } - StreamingService service = NewPipe.getServiceByUrl(url); - if (service == null) { + StreamingService service; + try { + service = NewPipe.getServiceByUrl(url); + } catch (ExtractionException e) { Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show(); return; } @@ -40,5 +43,7 @@ public class RouterPopupActivity extends RouterActivity { callIntent.putExtra(Constants.KEY_URL, url); callIntent.putExtra(Constants.KEY_SERVICE_ID, service.getServiceId()); startService(callIntent); + + finish(); } } diff --git a/app/src/main/java/org/schabi/newpipe/about/AboutActivity.java b/app/src/main/java/org/schabi/newpipe/about/AboutActivity.java index cfabf0e63..a2fe35894 100644 --- a/app/src/main/java/org/schabi/newpipe/about/AboutActivity.java +++ b/app/src/main/java/org/schabi/newpipe/about/AboutActivity.java @@ -36,7 +36,6 @@ public class AboutActivity extends AppCompatActivity { new SoftwareComponent("Rhino", "2015", "Mozilla", "https://www.mozilla.org/rhino/", StandardLicenses.MPL2), new SoftwareComponent("ACRA", "2013", "Kevin Gaudin", "http://www.acra.ch", StandardLicenses.APACHE2), new SoftwareComponent("Universal Image Loader", "2011 - 2015", "Sergey Tarasevich", "https://github.com/nostra13/Android-Universal-Image-Loader", StandardLicenses.APACHE2), - new SoftwareComponent("Netcipher", "2015", "The Guardian Project", "https://guardianproject.info/code/netcipher/", StandardLicenses.APACHE2), new SoftwareComponent("CircleImageView", "2014 - 2017", "Henning Dodenhof", "https://github.com/hdodenhof/CircleImageView", StandardLicenses.APACHE2), new SoftwareComponent("ParalaxScrollView", "2014", "Nir Hartmann", "https://github.com/nirhart/ParallaxScroll", StandardLicenses.MIT), new SoftwareComponent("NoNonsense-FilePicker", "2016", "Jonas Kalderstam", "https://github.com/spacecowboy/NoNonsense-FilePicker", StandardLicenses.MPL2), @@ -68,7 +67,7 @@ public class AboutActivity extends AppCompatActivity { setContentView(R.layout.activity_about); - Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); getSupportActionBar().setDisplayHomeAsUpEnabled(true); // Create the adapter that will return a fragment for each of the three @@ -76,10 +75,10 @@ public class AboutActivity extends AppCompatActivity { mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager()); // Set up the ViewPager with the sections adapter. - mViewPager = (ViewPager) findViewById(R.id.container); + mViewPager = findViewById(R.id.container); mViewPager.setAdapter(mSectionsPagerAdapter); - TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs); + TabLayout tabLayout = findViewById(R.id.tabs); tabLayout.setupWithViewPager(mViewPager); } @@ -130,7 +129,7 @@ public class AboutActivity extends AppCompatActivity { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_about, container, false); - TextView version = (TextView) rootView.findViewById(R.id.app_version); + TextView version = rootView.findViewById(R.id.app_version); version.setText(BuildConfig.VERSION_NAME); View githubLink = rootView.findViewById(R.id.github_link); diff --git a/app/src/main/java/org/schabi/newpipe/about/License.java b/app/src/main/java/org/schabi/newpipe/about/License.java index a831e14ef..312ad5087 100644 --- a/app/src/main/java/org/schabi/newpipe/about/License.java +++ b/app/src/main/java/org/schabi/newpipe/about/License.java @@ -3,9 +3,6 @@ package org.schabi.newpipe.about; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; -import android.text.TextUtils; - -import java.net.URLEncoder; /** * A software license diff --git a/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.java b/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.java index a2dd68171..8b0e67d18 100644 --- a/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.java +++ b/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.java @@ -87,12 +87,12 @@ public class LicenseFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_licenses, container, false); - ViewGroup softwareComponentsView = (ViewGroup) rootView.findViewById(R.id.software_components); + ViewGroup softwareComponentsView = rootView.findViewById(R.id.software_components); for (final SoftwareComponent component : softwareComponents) { View componentView = inflater.inflate(R.layout.item_software_component, container, false); - TextView softwareName = (TextView) componentView.findViewById(R.id.name); - TextView copyright = (TextView) componentView.findViewById(R.id.copyright); + TextView softwareName = componentView.findViewById(R.id.name); + TextView copyright = componentView.findViewById(R.id.copyright); softwareName.setText(component.getName()); copyright.setText(getContext().getString(R.string.copyright, component.getYears(), diff --git a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java index 6b11de8c2..21868e3c2 100644 --- a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java @@ -4,17 +4,17 @@ import android.arch.persistence.room.Database; import android.arch.persistence.room.RoomDatabase; import android.arch.persistence.room.TypeConverters; +import org.schabi.newpipe.database.history.Converters; import org.schabi.newpipe.database.history.dao.SearchHistoryDAO; import org.schabi.newpipe.database.history.dao.WatchHistoryDAO; import org.schabi.newpipe.database.history.model.SearchHistoryEntry; import org.schabi.newpipe.database.history.model.WatchHistoryEntry; import org.schabi.newpipe.database.subscription.SubscriptionDAO; import org.schabi.newpipe.database.subscription.SubscriptionEntity; -import org.schabi.newpipe.database.history.Converters; @TypeConverters({Converters.class}) @Database(entities = {SubscriptionEntity.class, WatchHistoryEntry.class, SearchHistoryEntry.class}, version = 1, exportSchema = false) -public abstract class AppDatabase extends RoomDatabase{ +public abstract class AppDatabase extends RoomDatabase { public static final String DATABASE_NAME = "newpipe.db"; diff --git a/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java b/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java index 25ee47842..03a94508b 100644 --- a/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java @@ -27,7 +27,7 @@ public interface BasicDAO { long upsert(final Entity entity); /* Searches */ - Flowable> findAll(); + Flowable> getAll(); Flowable> listByService(int serviceId); diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java index c7d5b0ae6..921ce63a1 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java @@ -29,7 +29,7 @@ public interface SearchHistoryDAO extends HistoryDAO { @Query("SELECT * FROM " + TABLE_NAME + ORDER_BY_CREATION_DATE) @Override - Flowable> findAll(); + Flowable> getAll(); @Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE) @Override diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/WatchHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/WatchHistoryDAO.java index b81cc2e35..a01d8e46d 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/dao/WatchHistoryDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/WatchHistoryDAO.java @@ -29,7 +29,7 @@ public interface WatchHistoryDAO extends HistoryDAO { @Query("SELECT * FROM " + TABLE_NAME + ORDER_BY_CREATION_DATE) @Override - Flowable> findAll(); + Flowable> getAll(); @Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE) @Override diff --git a/app/src/main/java/org/schabi/newpipe/database/history/model/WatchHistoryEntry.java b/app/src/main/java/org/schabi/newpipe/database/history/model/WatchHistoryEntry.java index 1ed9fda39..203b3fb7a 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/model/WatchHistoryEntry.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/model/WatchHistoryEntry.java @@ -4,7 +4,7 @@ import android.arch.persistence.room.ColumnInfo; import android.arch.persistence.room.Entity; import android.arch.persistence.room.Ignore; -import org.schabi.newpipe.extractor.stream_info.StreamInfo; +import org.schabi.newpipe.extractor.stream.StreamInfo; import java.util.Date; @@ -35,9 +35,9 @@ public class WatchHistoryEntry extends HistoryEntry { private String uploader; @ColumnInfo(name = DURATION) - private int duration; + private long duration; - public WatchHistoryEntry(Date creationDate, int serviceId, String title, String url, String streamId, String thumbnailURL, String uploader, int duration) { + public WatchHistoryEntry(Date creationDate, int serviceId, String title, String url, String streamId, String thumbnailURL, String uploader, long duration) { super(creationDate, serviceId); this.title = title; this.url = url; @@ -48,8 +48,8 @@ public class WatchHistoryEntry extends HistoryEntry { } public WatchHistoryEntry(StreamInfo streamInfo) { - this(new Date(), streamInfo.service_id, streamInfo.title, streamInfo.webpage_url, - streamInfo.id, streamInfo.thumbnail_url, streamInfo.uploader, streamInfo.duration); + this(new Date(), streamInfo.service_id, streamInfo.name, streamInfo.url, + streamInfo.id, streamInfo.thumbnail_url, streamInfo.uploader_name, streamInfo.duration); } public String getUrl() { @@ -92,7 +92,7 @@ public class WatchHistoryEntry extends HistoryEntry { this.uploader = uploader; } - public int getDuration() { + public long getDuration() { return duration; } diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.java b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.java index 95eeb3fcf..fd6a83d6d 100644 --- a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.java @@ -17,7 +17,7 @@ import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCR public interface SubscriptionDAO extends BasicDAO { @Override @Query("SELECT * FROM " + SUBSCRIPTION_TABLE) - Flowable> findAll(); + Flowable> getAll(); @Override @Query("DELETE FROM " + SUBSCRIPTION_TABLE) @@ -30,5 +30,5 @@ public interface SubscriptionDAO extends BasicDAO { @Query("SELECT * FROM " + SUBSCRIPTION_TABLE + " WHERE " + SUBSCRIPTION_URL + " LIKE :url AND " + SUBSCRIPTION_SERVICE_ID + " = :serviceId") - Flowable> findAll(int serviceId, String url); + Flowable> getSubscription(int serviceId, String url); } diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java index 1e0a63bcd..567bec309 100644 --- a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java @@ -6,6 +6,8 @@ import android.arch.persistence.room.Ignore; import android.arch.persistence.room.Index; import android.arch.persistence.room.PrimaryKey; +import org.schabi.newpipe.extractor.channel.ChannelInfoItem; + import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_SERVICE_ID; import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_TABLE; import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_URL; @@ -17,8 +19,8 @@ public class SubscriptionEntity { final static String SUBSCRIPTION_TABLE = "subscriptions"; final static String SUBSCRIPTION_SERVICE_ID = "service_id"; final static String SUBSCRIPTION_URL = "url"; - final static String SUBSCRIPTION_TITLE = "title"; - final static String SUBSCRIPTION_THUMBNAIL_URL = "thumbnail_url"; + final static String SUBSCRIPTION_NAME = "name"; + final static String SUBSCRIPTION_AVATAR_URL = "avatar_url"; final static String SUBSCRIPTION_SUBSCRIBER_COUNT = "subscriber_count"; final static String SUBSCRIPTION_DESCRIPTION = "description"; @@ -31,11 +33,11 @@ public class SubscriptionEntity { @ColumnInfo(name = SUBSCRIPTION_URL) private String url; - @ColumnInfo(name = SUBSCRIPTION_TITLE) - private String title; + @ColumnInfo(name = SUBSCRIPTION_NAME) + private String name; - @ColumnInfo(name = SUBSCRIPTION_THUMBNAIL_URL) - private String thumbnailUrl; + @ColumnInfo(name = SUBSCRIPTION_AVATAR_URL) + private String avatarUrl; @ColumnInfo(name = SUBSCRIPTION_SUBSCRIBER_COUNT) private Long subscriberCount; @@ -68,20 +70,20 @@ public class SubscriptionEntity { this.url = url; } - public String getTitle() { - return title; + public String getName() { + return name; } - public void setTitle(String title) { - this.title = title; + public void setName(String name) { + this.name = name; } - public String getThumbnailUrl() { - return thumbnailUrl; + public String getAvatarUrl() { + return avatarUrl; } - public void setThumbnailUrl(String thumbnailUrl) { - this.thumbnailUrl = thumbnailUrl; + public void setAvatarUrl(String avatarUrl) { + this.avatarUrl = avatarUrl; } public Long getSubscriberCount() { @@ -101,13 +103,25 @@ public class SubscriptionEntity { } @Ignore - public void setData(final String title, - final String thumbnailUrl, + public void setData(final String name, + final String avatarUrl, final String description, final Long subscriberCount) { - this.setTitle(title); - this.setThumbnailUrl(thumbnailUrl); + this.setName(name); + this.setAvatarUrl(avatarUrl); this.setDescription(description); this.setSubscriberCount(subscriberCount); } + + @Ignore + public ChannelInfoItem toChannelInfoItem() { + ChannelInfoItem item = new ChannelInfoItem(); + item.url = getUrl(); + item.service_id = getServiceId(); + item.name = getName(); + item.thumbnail_url = getAvatarUrl(); + item.subscriber_count = getSubscriberCount(); + item.description = getDescription(); + return item; + } } diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java b/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java index fdf324526..6512f5270 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java @@ -32,7 +32,7 @@ public class DownloadActivity extends AppCompatActivity { super.onCreate(savedInstanceState); setContentView(R.layout.activity_downloader); - Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); ActionBar actionBar = getSupportActionBar(); diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index 8de10a25d..b93f66d26 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -22,15 +22,15 @@ import android.widget.TextView; import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.MediaFormat; -import org.schabi.newpipe.extractor.stream_info.AudioStream; -import org.schabi.newpipe.extractor.stream_info.StreamInfo; -import org.schabi.newpipe.extractor.stream_info.VideoStream; +import org.schabi.newpipe.extractor.stream.AudioStream; +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.fragments.detail.SpinnerToolbarAdapter; import org.schabi.newpipe.settings.NewPipeSettings; import org.schabi.newpipe.util.FilenameUtils; +import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.ThemeHelper; -import org.schabi.newpipe.util.Utils; import java.io.Serializable; import java.util.ArrayList; @@ -107,19 +107,19 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - nameEditText = ((EditText) view.findViewById(R.id.file_name)); - nameEditText.setText(FilenameUtils.createFilename(getContext(), currentInfo.title)); - selectedAudioIndex = Utils.getPreferredAudioFormat(getContext(), currentInfo.audio_streams); + nameEditText = view.findViewById(R.id.file_name); + nameEditText.setText(FilenameUtils.createFilename(getContext(), currentInfo.name)); + selectedAudioIndex = ListHelper.getDefaultAudioFormat(getContext(), currentInfo.audio_streams); - streamsSpinner = (Spinner) view.findViewById(R.id.quality_spinner); + streamsSpinner = view.findViewById(R.id.quality_spinner); streamsSpinner.setOnItemSelectedListener(this); - threadsCountTextView = (TextView) view.findViewById(R.id.threads_count); - threadsSeekBar = (SeekBar) view.findViewById(R.id.threads); - radioVideoAudioGroup = (RadioGroup) view.findViewById(R.id.video_audio_group); + threadsCountTextView = view.findViewById(R.id.threads_count); + threadsSeekBar = view.findViewById(R.id.threads); + radioVideoAudioGroup = view.findViewById(R.id.video_audio_group); radioVideoAudioGroup.setOnCheckedChangeListener(this); - initToolbar((Toolbar) view.findViewById(R.id.toolbar)); + initToolbar(view.findViewById(R.id.toolbar)); checkDownloadOptions(view); setupVideoSpinner(sortedStreamVideosList, streamsSpinner); @@ -135,12 +135,10 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck @Override public void onStartTrackingTouch(SeekBar p1) { - } @Override public void onStopTrackingTouch(SeekBar p1) { - } }); } @@ -185,7 +183,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck String[] items = new String[audioStreams.size()]; for (int i = 0; i < audioStreams.size(); i++) { AudioStream audioStream = audioStreams.get(i); - items[i] = MediaFormat.getNameById(audioStream.format) + " " + audioStream.avgBitrate + "kbps"; + items[i] = MediaFormat.getNameById(audioStream.format) + " " + audioStream.average_bitrate + "kbps"; } ArrayAdapter itemAdapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, items); @@ -241,8 +239,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck //////////////////////////////////////////////////////////////////////////*/ protected void checkDownloadOptions(View view) { - RadioButton audioButton = (RadioButton) view.findViewById(R.id.audio_button); - RadioButton videoButton = (RadioButton) view.findViewById(R.id.video_button); + RadioButton audioButton = view.findViewById(R.id.audio_button); + RadioButton videoButton = view.findViewById(R.id.video_button); if (currentInfo.audio_streams == null || currentInfo.audio_streams.size() == 0) { audioButton.setVisibility(View.GONE); @@ -258,7 +256,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck String url, location; String fileName = nameEditText.getText().toString().trim(); - if (fileName.isEmpty()) fileName = FilenameUtils.createFilename(getContext(), currentInfo.title); + if (fileName.isEmpty()) fileName = FilenameUtils.createFilename(getContext(), currentInfo.name); boolean isAudio = radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button; url = isAudio ? currentInfo.audio_streams.get(selectedAudioIndex).url : sortedStreamVideosList.get(selectedVideoIndex).url; diff --git a/app/src/main/java/org/schabi/newpipe/extractor b/app/src/main/java/org/schabi/newpipe/extractor deleted file mode 160000 index ab530381c..000000000 --- a/app/src/main/java/org/schabi/newpipe/extractor +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ab530381cfb5cc3278f1c4f63f30b33ca3d54d5d diff --git a/app/src/main/java/org/schabi/newpipe/fragments/BackPressable.java b/app/src/main/java/org/schabi/newpipe/fragments/BackPressable.java new file mode 100644 index 000000000..737db784b --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/BackPressable.java @@ -0,0 +1,13 @@ +package org.schabi.newpipe.fragments; + +/** + * Indicates that the current fragment can handle back presses + */ +public interface BackPressable { + /** + * A back press was delegated to this fragment + * + * @return if the back press was handled + */ + boolean onBackPressed(); +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/BaseFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/BaseFragment.java deleted file mode 100644 index 975c5081b..000000000 --- a/app/src/main/java/org/schabi/newpipe/fragments/BaseFragment.java +++ /dev/null @@ -1,177 +0,0 @@ -package org.schabi.newpipe.fragments; - -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Rect; -import android.os.Bundle; -import android.support.annotation.AttrRes; -import android.support.v4.app.Fragment; -import android.support.v4.view.ViewCompat; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.Toolbar; -import android.util.Log; -import android.view.Gravity; -import android.view.View; -import android.widget.Button; -import android.widget.ProgressBar; -import android.widget.TextView; -import android.widget.Toast; - -import com.nostra13.universalimageloader.core.DisplayImageOptions; -import com.nostra13.universalimageloader.core.ImageLoader; -import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer; - -import org.schabi.newpipe.MainActivity; -import org.schabi.newpipe.R; - -import java.util.concurrent.atomic.AtomicBoolean; - -import static org.schabi.newpipe.util.AnimationUtils.animateView; - -public abstract class BaseFragment extends Fragment { - protected final String TAG = "BaseFragment@" + Integer.toHexString(hashCode()); - protected static final boolean DEBUG = MainActivity.DEBUG; - - protected AppCompatActivity activity; - - protected AtomicBoolean isLoading = new AtomicBoolean(false); - protected AtomicBoolean wasLoading = new AtomicBoolean(false); - - protected static final ImageLoader imageLoader = ImageLoader.getInstance(); - protected static final DisplayImageOptions displayImageOptions = - new DisplayImageOptions.Builder().displayer(new FadeInBitmapDisplayer(400)).cacheInMemory(false).build(); - - /*////////////////////////////////////////////////////////////////////////// - // Views - //////////////////////////////////////////////////////////////////////////*/ - - protected Toolbar toolbar; - - protected View errorPanel; - protected Button errorButtonRetry; - protected TextView errorTextView; - protected ProgressBar loadingProgressBar; - //protected SwipeRefreshLayout swipeRefreshLayout; - - /*////////////////////////////////////////////////////////////////////////// - // Fragment's Lifecycle - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onAttach(Context context) { - super.onAttach(context); - if (DEBUG) Log.d(TAG, "onAttach() called with: context = [" + context + "]"); - - activity = (AppCompatActivity) context; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]"); - - isLoading.set(false); - setHasOptionsMenu(true); - } - - @Override - public void onViewCreated(View rootView, Bundle savedInstanceState) { - if (DEBUG) Log.d(TAG, "onViewCreated() called with: rootView = [" + rootView + "], savedInstanceState = [" + savedInstanceState + "]"); - initViews(rootView, savedInstanceState); - initListeners(); - wasLoading.set(false); - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - if (DEBUG) Log.d(TAG, "onDestroyView() called"); - toolbar = null; - - errorPanel = null; - errorButtonRetry = null; - errorTextView = null; - } - - /*////////////////////////////////////////////////////////////////////////// - // Init - //////////////////////////////////////////////////////////////////////////*/ - - protected void initViews(View rootView, Bundle savedInstanceState) { - toolbar = (Toolbar) activity.findViewById(R.id.toolbar); - - loadingProgressBar = (ProgressBar) rootView.findViewById(R.id.loading_progress_bar); - //swipeRefreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.swipe_refresh); - - errorPanel = rootView.findViewById(R.id.error_panel); - errorButtonRetry = (Button) rootView.findViewById(R.id.error_button_retry); - errorTextView = (TextView) rootView.findViewById(R.id.error_message_view); - } - - protected void initListeners() { - errorButtonRetry.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - onRetryButtonClicked(); - } - }); - } - - protected abstract void reloadContent(); - - protected void onRetryButtonClicked() { - if (DEBUG) Log.d(TAG, "onRetryButtonClicked() called"); - reloadContent(); - } - - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ - - protected void setErrorMessage(String message, boolean showRetryButton) { - if (errorTextView == null || activity == null) return; - - errorTextView.setText(message); - if (showRetryButton) animateView(errorButtonRetry, true, 300); - else animateView(errorButtonRetry, false, 0); - - animateView(errorPanel, true, 300); - isLoading.set(false); - - animateView(loadingProgressBar, false, 200); - } - - protected int getResourceIdFromAttr(@AttrRes int attr) { - TypedArray a = activity.getTheme().obtainStyledAttributes(new int[]{attr}); - int attributeResourceId = a.getResourceId(0, 0); - a.recycle(); - return attributeResourceId; - } - - public static void showMenuTooltip(View v, String message) { - final int[] screenPos = new int[2]; - final Rect displayFrame = new Rect(); - v.getLocationOnScreen(screenPos); - v.getWindowVisibleDisplayFrame(displayFrame); - - final Context context = v.getContext(); - final int width = v.getWidth(); - final int height = v.getHeight(); - final int midy = screenPos[1] + height / 2; - int referenceX = screenPos[0] + width / 2; - if (ViewCompat.getLayoutDirection(v) == View.LAYOUT_DIRECTION_LTR) { - final int screenWidth = context.getResources().getDisplayMetrics().widthPixels; - referenceX = screenWidth - referenceX; // mirror - } - Toast cheatSheet = Toast.makeText(context, message, Toast.LENGTH_SHORT); - if (midy < displayFrame.height()) { - // Show along the top; follow action buttons - cheatSheet.setGravity(Gravity.TOP | Gravity.END, referenceX, - screenPos[1] + height - displayFrame.top); - } else { - // Show along the bottom center - cheatSheet.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, height); - } - cheatSheet.show(); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/BaseStateFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/BaseStateFragment.java new file mode 100644 index 000000000..5a8d8dd52 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/BaseStateFragment.java @@ -0,0 +1,235 @@ +package org.schabi.newpipe.fragments; + +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.annotation.StringRes; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; + +import com.jakewharton.rxbinding2.view.RxView; + +import org.schabi.newpipe.BaseFragment; +import org.schabi.newpipe.MainActivity; +import org.schabi.newpipe.R; +import org.schabi.newpipe.ReCaptchaActivity; +import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; +import org.schabi.newpipe.report.ErrorActivity; +import org.schabi.newpipe.report.UserAction; +import org.schabi.newpipe.util.ExtractorHelper; +import org.schabi.newpipe.util.InfoCache; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import icepick.State; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.functions.Consumer; + +import static org.schabi.newpipe.util.AnimationUtils.animateView; + +public abstract class BaseStateFragment extends BaseFragment implements ViewContract { + + @State + protected AtomicBoolean wasLoading = new AtomicBoolean(); + protected AtomicBoolean isLoading = new AtomicBoolean(); + + @Nullable + protected View emptyStateView; + @Nullable + protected ProgressBar loadingProgressBar; + + protected View errorPanelRoot; + protected Button errorButtonRetry; + protected TextView errorTextView; + + @Override + public void onViewCreated(View rootView, Bundle savedInstanceState) { + super.onViewCreated(rootView, savedInstanceState); + doInitialLoadLogic(); + } + + @Override + public void onPause() { + super.onPause(); + wasLoading.set(isLoading.get()); + } + + /*////////////////////////////////////////////////////////////////////////// + // Init + //////////////////////////////////////////////////////////////////////////*/ + + + @Override + protected void initViews(View rootView, Bundle savedInstanceState) { + super.initViews(rootView, savedInstanceState); + + emptyStateView = rootView.findViewById(R.id.empty_state_view); + loadingProgressBar = rootView.findViewById(R.id.loading_progress_bar); + + errorPanelRoot = rootView.findViewById(R.id.error_panel); + errorButtonRetry = rootView.findViewById(R.id.error_button_retry); + errorTextView = rootView.findViewById(R.id.error_message_view); + } + + @Override + protected void initListeners() { + super.initListeners(); + RxView.clicks(errorButtonRetry) + .debounce(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Consumer() { + @Override + public void accept(Object o) throws Exception { + onRetryButtonClicked(); + } + }); + } + + protected void onRetryButtonClicked() { + reloadContent(); + } + + public void reloadContent() { + startLoading(true); + } + + /*////////////////////////////////////////////////////////////////////////// + // Load + //////////////////////////////////////////////////////////////////////////*/ + + protected void doInitialLoadLogic() { + startLoading(true); + } + + protected void startLoading(boolean forceLoad) { + if (DEBUG) Log.d(TAG, "startLoading() called with: forceLoad = [" + forceLoad + "]"); + showLoading(); + isLoading.set(true); + } + + /*////////////////////////////////////////////////////////////////////////// + // Contract + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void showLoading() { + if (emptyStateView != null) animateView(emptyStateView, false, 150); + if (loadingProgressBar != null) animateView(loadingProgressBar, true, 400); + animateView(errorPanelRoot, false, 150); + } + + @Override + public void hideLoading() { + if (emptyStateView != null) animateView(emptyStateView, false, 150); + if (loadingProgressBar != null) animateView(loadingProgressBar, false, 0); + animateView(errorPanelRoot, false, 150); + } + + @Override + public void showEmptyState() { + isLoading.set(false); + if (emptyStateView != null) animateView(emptyStateView, true, 200); + if (loadingProgressBar != null) animateView(loadingProgressBar, false, 0); + animateView(errorPanelRoot, false, 150); + } + + @Override + public void showError(String message, boolean showRetryButton) { + if (DEBUG) Log.d(TAG, "showError() called with: message = [" + message + "], showRetryButton = [" + showRetryButton + "]"); + isLoading.set(false); + InfoCache.getInstance().clearCache(); + hideLoading(); + + errorTextView.setText(message); + if (showRetryButton) animateView(errorButtonRetry, true, 600); + else animateView(errorButtonRetry, false, 0); + animateView(errorPanelRoot, true, 300); + } + + @Override + public void handleResult(I result) { + if (DEBUG) Log.d(TAG, "handleResult() called with: result = [" + result + "]"); + hideLoading(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Error handling + //////////////////////////////////////////////////////////////////////////*/ + + /** + * Default implementation handles some general exceptions + * + * @return if the exception was handled + */ + protected boolean onError(Throwable exception) { + if (DEBUG) Log.d(TAG, "onError() called with: exception = [" + exception + "]"); + isLoading.set(false); + + if (isDetached() || isRemoving()) { + if (DEBUG) Log.w(TAG, "onError() is detached or removing = [" + exception + "]"); + return true; + } + + if (ExtractorHelper.isInterruptedCaused(exception)) { + if (DEBUG) Log.w(TAG, "onError() isInterruptedCaused! = [" + exception + "]"); + return true; + } + + if (exception instanceof ReCaptchaException) { + onReCaptchaException(); + return true; + } else if (exception instanceof IOException) { + showError(getString(R.string.network_error), true); + return true; + } + + return false; + } + + public void onReCaptchaException() { + if (DEBUG) Log.d(TAG, "onReCaptchaException() called"); + Toast.makeText(activity, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show(); + // Starting ReCaptcha Challenge Activity + startActivityForResult(new Intent(activity, ReCaptchaActivity.class), ReCaptchaActivity.RECAPTCHA_REQUEST); + + showError(getString(R.string.recaptcha_request_toast), false); + } + + public void onUnrecoverableError(Throwable exception, UserAction userAction, String serviceName, String request, @StringRes int errorId) { + onUnrecoverableError(Collections.singletonList(exception), userAction, serviceName, request, errorId); + } + + public void onUnrecoverableError(List exception, UserAction userAction, String serviceName, String request, @StringRes int errorId) { + if (DEBUG) Log.d(TAG, "onUnrecoverableError() called with: exception = [" + exception + "]"); + + if (serviceName == null) serviceName = "none"; + if (request == null) request = "none"; + ErrorActivity.reportError(getContext(), exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(userAction, serviceName, request, errorId)); + } + + public void showSnackBarError(Throwable exception, UserAction userAction, String serviceName, String request, @StringRes int errorId) { + showSnackBarError(Collections.singletonList(exception), userAction, serviceName, request, errorId); + } + + /** + * Show a SnackBar and only call ErrorActivity#reportError IF we a find a valid view (otherwise the error screen appears) + */ + public void showSnackBarError(List exception, UserAction userAction, String serviceName, String request, @StringRes int errorId) { + if (DEBUG) { + Log.d(TAG, "showSnackBarError() called with: exception = [" + exception + "], userAction = [" + userAction + "], request = [" + request + "], errorId = [" + errorId + "]"); + } + View rootView = activity != null ? activity.findViewById(android.R.id.content) : null; + if (rootView == null && getView() != null) rootView = getView(); + if (rootView == null) return; + + ErrorActivity.reportError(getContext(), exception, MainActivity.class, rootView, ErrorActivity.ErrorInfo.make(userAction, serviceName, request, errorId)); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/BlankFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/BlankFragment.java index ef92622e6..e9e50dd69 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/BlankFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/BlankFragment.java @@ -6,6 +6,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import org.schabi.newpipe.BaseFragment; import org.schabi.newpipe.R; public class BlankFragment extends BaseFragment { @@ -14,9 +15,4 @@ public class BlankFragment extends BaseFragment { public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_blank, container, false); } - - @Override - protected void reloadContent() { - - } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/FeedFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/FeedFragment.java deleted file mode 100644 index 155f1ba00..000000000 --- a/app/src/main/java/org/schabi/newpipe/fragments/FeedFragment.java +++ /dev/null @@ -1,495 +0,0 @@ -package org.schabi.newpipe.fragments; - -import android.os.Bundle; -import android.os.Parcelable; -import android.support.annotation.Nullable; -import android.support.v7.app.ActionBar; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.View; -import android.view.ViewGroup; - -import com.jakewharton.rxbinding2.view.RxView; - -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; -import org.schabi.newpipe.MainActivity; -import org.schabi.newpipe.R; -import org.schabi.newpipe.database.subscription.SubscriptionEntity; -import org.schabi.newpipe.extractor.InfoItem; -import org.schabi.newpipe.extractor.channel.ChannelInfo; -import org.schabi.newpipe.info_list.InfoItemBuilder; -import org.schabi.newpipe.info_list.InfoListAdapter; -import org.schabi.newpipe.report.ErrorActivity; -import org.schabi.newpipe.util.NavigationHelper; - -import java.io.IOException; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; - -import io.reactivex.Flowable; -import io.reactivex.MaybeObserver; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.annotations.NonNull; -import io.reactivex.disposables.Disposable; -import io.reactivex.functions.Consumer; - -import static org.schabi.newpipe.report.UserAction.REQUESTED_CHANNEL; -import static org.schabi.newpipe.util.AnimationUtils.animateView; - -public class FeedFragment extends BaseFragment { - private static final String VIEW_STATE_KEY = "view_state_key"; - private static final String INFO_ITEMS_KEY = "info_items_key"; - - private static final int FEED_LOAD_SIZE = 4; - private static final int LOAD_ITEM_DEBOUNCE_INTERVAL = 500; - - private final String TAG = "FeedFragment@" + Integer.toHexString(hashCode()); - - private View inflatedView; - private View emptyPanel; - private View loadItemFooter; - - private InfoListAdapter infoListAdapter; - private RecyclerView resultRecyclerView; - - private Parcelable viewState; - private AtomicBoolean retainFeedItems; - - private SubscriptionService subscriptionService; - - private Disposable loadItemObserver; - private Disposable subscriptionObserver; - private Subscription feedSubscriber; - - /////////////////////////////////////////////////////////////////////////// - // Fragment LifeCycle - /////////////////////////////////////////////////////////////////////////// - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - subscriptionService = SubscriptionService.getInstance(getContext()); - - retainFeedItems = new AtomicBoolean(false); - - if (infoListAdapter == null) { - infoListAdapter = new InfoListAdapter(getActivity()); - } - - if (savedInstanceState != null) { - // Get recycler view state - viewState = savedInstanceState.getParcelable(VIEW_STATE_KEY); - - // Deserialize and get recycler adapter list - final Object[] serializedInfoItems = (Object[]) savedInstanceState.getSerializable(INFO_ITEMS_KEY); - if (serializedInfoItems != null) { - final InfoItem[] infoItems = Arrays.copyOf( - serializedInfoItems, - serializedInfoItems.length, - InfoItem[].class - ); - final List feedInfos = Arrays.asList(infoItems); - infoListAdapter.addInfoItemList( feedInfos ); - } - - // Already displayed feed items survive configuration changes - retainFeedItems.set(true); - } - } - - @Nullable - @Override - public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { - if (inflatedView == null) { - inflatedView = inflater.inflate(R.layout.fragment_subscription, container, false); - } - return inflatedView; - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - - if (resultRecyclerView != null) { - outState.putParcelable( - VIEW_STATE_KEY, - resultRecyclerView.getLayoutManager().onSaveInstanceState() - ); - } - - if (infoListAdapter != null) { - outState.putSerializable(INFO_ITEMS_KEY, infoListAdapter.getItemsList().toArray()); - } - } - - @Override - public void onDestroyView() { - // Do not monitor for updates when user is not viewing the feed fragment. - // This is a waste of bandwidth. - if (loadItemObserver != null) loadItemObserver.dispose(); - if (subscriptionObserver != null) subscriptionObserver.dispose(); - if (feedSubscriber != null) feedSubscriber.cancel(); - - loadItemObserver = null; - subscriptionObserver = null; - feedSubscriber = null; - - loadItemFooter = null; - - // Retain the already displayed items for backstack pops - retainFeedItems.set(true); - - super.onDestroyView(); - } - - @Override - public void onDestroy() { - subscriptionService = null; - - super.onDestroy(); - } - - /////////////////////////////////////////////////////////////////////////// - // Fragment Views - /////////////////////////////////////////////////////////////////////////// - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]"); - super.onCreateOptionsMenu(menu, inflater); - - ActionBar supportActionBar = activity.getSupportActionBar(); - if (supportActionBar != null) { - supportActionBar.setDisplayShowTitleEnabled(true); - supportActionBar.setDisplayHomeAsUpEnabled(true); - } - } - - private RecyclerView.OnScrollListener getOnScrollListener() { - return new RecyclerView.OnScrollListener() { - @Override - public void onScrollStateChanged(RecyclerView recyclerView, int newState) { - super.onScrollStateChanged(recyclerView, newState); - if (newState == RecyclerView.SCROLL_STATE_IDLE) { - viewState = recyclerView.getLayoutManager().onSaveInstanceState(); - } - } - }; - } - - @Override - protected void initViews(View rootView, Bundle savedInstanceState) { - super.initViews(rootView, savedInstanceState); - - if (infoListAdapter == null) return; - - animateView(errorPanel, false, 200); - animateView(loadingProgressBar, true, 200); - - emptyPanel = rootView.findViewById(R.id.empty_panel); - - resultRecyclerView = rootView.findViewById(R.id.result_list_view); - resultRecyclerView.setLayoutManager(new LinearLayoutManager(activity)); - - loadItemFooter = activity.getLayoutInflater().inflate(R.layout.load_item_footer, resultRecyclerView, false); - infoListAdapter.setFooter(loadItemFooter); - infoListAdapter.showFooter(false); - infoListAdapter.setOnStreamInfoItemSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { - @Override - public void selected(int serviceId, String url, String title) { - NavigationHelper.openVideoDetailFragment(getFragmentManager(), serviceId, url, title); - } - }); - - resultRecyclerView.setAdapter(infoListAdapter); - resultRecyclerView.addOnScrollListener(getOnScrollListener()); - - if (viewState != null) { - resultRecyclerView.getLayoutManager().onRestoreInstanceState(viewState); - viewState = null; - } - - if (activity.getSupportActionBar() != null) activity.getSupportActionBar().setTitle(R.string.fragment_whats_new); - - populateFeed(); - } - - private void resetFragment() { - if (subscriptionObserver != null) subscriptionObserver.dispose(); - if (infoListAdapter != null) infoListAdapter.clearStreamItemList(); - } - - @Override - protected void reloadContent() { - resetFragment(); - populateFeed(); - } - - @Override - protected void setErrorMessage(String message, boolean showRetryButton) { - super.setErrorMessage(message, showRetryButton); - - resetFragment(); - } - - /** - * Changes the state of the load item footer. - * - * If the current state of the feed is loaded, this displays the load item button and - * starts its reactor. - * - * Otherwise, show a spinner in place of the loader button. */ - private void setLoader(final boolean isLoaded) { - if (loadItemFooter == null) return; - - if (loadItemObserver != null) loadItemObserver.dispose(); - - if (isLoaded) { - loadItemObserver = getLoadItemObserver(loadItemFooter); - } - - loadItemFooter.findViewById(R.id.paginate_progress_bar).setVisibility(isLoaded ? View.GONE : View.VISIBLE); - loadItemFooter.findViewById(R.id.load_more_text).setVisibility(isLoaded ? View.VISIBLE : View.GONE); - } - - /////////////////////////////////////////////////////////////////////////// - // Feeds Loader - /////////////////////////////////////////////////////////////////////////// - - /** - * Responsible for reacting to subscription database updates and displaying feeds. - * - * Upon each update, the feed info list is cleared unless the fragment is - * recently recovered from a configuration change or backstack. - * - * All existing and pending feed requests are dropped. - * - * The newly received list of subscriptions is then transformed into a - * flowable, reacting to pulling requests. - * - * Pulled requests are transformed first into ChannelInfo, then Stream Info items and - * displayed on the feed fragment. - **/ - private void populateFeed() { - final Consumer> consumer = new Consumer>() { - @Override - public void accept(@NonNull List subscriptionEntities) throws Exception { - animateView(loadingProgressBar, false, 200); - - if (subscriptionEntities.isEmpty()) { - infoListAdapter.clearStreamItemList(); - emptyPanel.setVisibility(View.VISIBLE); - } else { - emptyPanel.setVisibility(View.INVISIBLE); - } - - // show progress bar on receiving a non-empty updated list of subscriptions - if (!retainFeedItems.get() && !subscriptionEntities.isEmpty()) { - infoListAdapter.clearStreamItemList(); - animateView(loadingProgressBar, true, 200); - } - - retainFeedItems.set(false); - Flowable.fromIterable(subscriptionEntities) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(getSubscriptionObserver()); - } - }; - - final Consumer onError = new Consumer() { - @Override - public void accept(@NonNull Throwable exception) throws Exception { - onRxError(exception, "Subscription Database Reactor"); - } - }; - - if (subscriptionObserver != null) subscriptionObserver.dispose(); - subscriptionObserver = subscriptionService.getSubscription() - .onErrorReturnItem(Collections.emptyList()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(consumer, onError); - } - - /** - * Responsible for reacting to user pulling request and starting a request for new feed stream. - * - * On initialization, it automatically requests the amount of feed needed to display - * a minimum amount required (FEED_LOAD_SIZE). - * - * Upon receiving a user pull, it creates a Single Observer to fetch the ChannelInfo - * containing the feed streams. - **/ - private Subscriber getSubscriptionObserver() { - return new Subscriber() { - @Override - public void onSubscribe(Subscription s) { - if (feedSubscriber != null) feedSubscriber.cancel(); - feedSubscriber = s; - - final int requestSize = FEED_LOAD_SIZE - infoListAdapter.getItemsList().size(); - if (requestSize > 0) { - requestFeed(requestSize); - } else { - setLoader(true); - } - - animateView(loadingProgressBar, false, 200); - // Footer spinner persists until subscription list is exhausted. - infoListAdapter.showFooter(true); - } - - @Override - public void onNext(SubscriptionEntity subscriptionEntity) { - setLoader(false); - - subscriptionService.getChannelInfo(subscriptionEntity) - .observeOn(AndroidSchedulers.mainThread()) - .onErrorComplete() - .subscribe(getChannelInfoObserver()); - } - - @Override - public void onError(Throwable exception) { - onRxError(exception, "Feed Pull Reactor"); - } - - @Override - public void onComplete() { - infoListAdapter.showFooter(false); - } - }; - } - - /** - * On each request, a subscription item from the updated table is transformed - * into a ChannelInfo, containing the latest streams from the channel. - * - * Currently, the feed uses the first into from the list of streams. - * - * If chosen feed already displayed, then we request another feed from another - * subscription, until the subscription table runs out of new items. - * - * This Observer is self-contained and will dispose itself when complete. However, this - * does not obey the fragment lifecycle and may continue running in the background - * until it is complete. This is done due to RxJava2 no longer propagate errors once - * an observer is unsubscribed while the thread process is still running. - * - * To solve the above issue, we can either set a global RxJava Error Handler, or - * manage exceptions case by case. This should be done if the current implementation is - * too costly when dealing with larger subscription sets. - **/ - private MaybeObserver getChannelInfoObserver() { - return new MaybeObserver() { - Disposable observer; - @Override - public void onSubscribe(Disposable d) { - observer = d; - } - - // Called only when response is non-empty - @Override - public void onSuccess(ChannelInfo channelInfo) { - emptyPanel.setVisibility(View.INVISIBLE); - - if (infoListAdapter == null || channelInfo.related_streams.isEmpty()) return; - - final InfoItem item = channelInfo.related_streams.get(0); - // Keep requesting new items if the current one already exists - if (!doesItemExist(infoListAdapter.getItemsList(), item)) { - infoListAdapter.addInfoItem(item); - } else { - requestFeed(1); - } - onDone(); - } - - @Override - public void onError(Throwable exception) { - onRxError(exception, "Feed Display Reactor"); - onDone(); - } - - // Called only when response is empty - @Override - public void onComplete() { - onDone(); - } - - private void onDone() { - setLoader(true); - - observer.dispose(); - observer = null; - } - }; - } - - private boolean doesItemExist(final List items, final InfoItem item) { - for (final InfoItem existingItem: items) { - if (existingItem.infoType() == item.infoType() && - existingItem.getTitle().equals(item.getTitle()) && - existingItem.getLink().equals(item.getLink())) return true; - } - return false; - } - - private void requestFeed(final int count) { - if (feedSubscriber == null) return; - - feedSubscriber.request(count); - } - - private Disposable getLoadItemObserver(@NonNull final View itemLoader) { - final Consumer onNext = new Consumer() { - @Override - public void accept(Object o) throws Exception { - requestFeed(FEED_LOAD_SIZE); - } - }; - - final Consumer onError = new Consumer() { - @Override - public void accept(Throwable throwable) throws Exception { - onRxError(throwable, "Load Button Reactor"); - } - }; - - return RxView.clicks(itemLoader) - .debounce(LOAD_ITEM_DEBOUNCE_INTERVAL, TimeUnit.MILLISECONDS) - .subscribe(onNext, onError); - } - - /////////////////////////////////////////////////////////////////////////// - // Fragment Error Handling - /////////////////////////////////////////////////////////////////////////// - - private void onRxError(final Throwable exception, final String tag) { - if (exception instanceof IOException) { - onRecoverableError(R.string.network_error); - } else { - onUnrecoverableError(exception, tag); - } - } - - private void onRecoverableError(int messageId) { - if (!this.isAdded()) return; - - if (DEBUG) Log.d(TAG, "onError() called with: messageId = [" + messageId + "]"); - setErrorMessage(getString(messageId), true); - } - - private void onUnrecoverableError(Throwable exception, final String tag) { - if (DEBUG) Log.d(TAG, "onUnrecoverableError() called with: exception = [" + exception + "]"); - ErrorActivity.reportError(getContext(), exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(REQUESTED_CHANNEL, "Feed", tag, R.string.general_error)); - - activity.finish(); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java index f7a59c1c9..236f95968 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java @@ -1,6 +1,5 @@ package org.schabi.newpipe.fragments; -import android.content.Context; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.design.widget.TabLayout; @@ -9,7 +8,6 @@ import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentPagerAdapter; import android.support.v4.view.ViewPager; import android.support.v7.app.ActionBar; -import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; @@ -18,43 +16,35 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import org.schabi.newpipe.MainActivity; +import org.schabi.newpipe.BaseFragment; import org.schabi.newpipe.R; +import org.schabi.newpipe.fragments.subscription.SubscriptionFragment; import org.schabi.newpipe.util.NavigationHelper; -public class MainFragment extends Fragment implements TabLayout.OnTabSelectedListener { - private final String TAG = "MainFragment@" + Integer.toHexString(hashCode()); - private static final boolean DEBUG = MainActivity.DEBUG; - - private AppCompatActivity activity; - +public class MainFragment extends BaseFragment implements TabLayout.OnTabSelectedListener { private ViewPager viewPager; /*////////////////////////////////////////////////////////////////////////// // Fragment's LifeCycle //////////////////////////////////////////////////////////////////////////*/ - @Override - public void onAttach(Context context) { - super.onAttach(context); - if (DEBUG) Log.d(TAG, "onAttach() called with: context = [" + context + "]"); - activity = ((AppCompatActivity) context); - } - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]"); setHasOptionsMenu(true); } @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - if (DEBUG) Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]"); - View inflatedView = inflater.inflate(R.layout.fragment_main, container, false); + return inflater.inflate(R.layout.fragment_main, container, false); + } - TabLayout tabLayout = (TabLayout) inflatedView.findViewById(R.id.main_tab_layout); - viewPager = (ViewPager) inflatedView.findViewById(R.id.pager); + @Override + protected void initViews(View rootView, Bundle savedInstanceState) { + super.initViews(rootView, savedInstanceState); + + TabLayout tabLayout = rootView.findViewById(R.id.main_tab_layout); + viewPager = rootView.findViewById(R.id.pager); /* Nested fragment, use child fragment here to maintain backstack in view pager. */ PagerAdapter adapter = new PagerAdapter(getChildFragmentManager()); @@ -62,8 +52,6 @@ public class MainFragment extends Fragment implements TabLayout.OnTabSelectedLis viewPager.setOffscreenPageLimit(adapter.getCount()); tabLayout.setupWithViewPager(viewPager); - - return inflatedView; } /*////////////////////////////////////////////////////////////////////////// @@ -93,16 +81,22 @@ public class MainFragment extends Fragment implements TabLayout.OnTabSelectedLis return super.onOptionsItemSelected(item); } + /*////////////////////////////////////////////////////////////////////////// + // Tabs + //////////////////////////////////////////////////////////////////////////*/ + @Override public void onTabSelected(TabLayout.Tab tab) { viewPager.setCurrentItem(tab.getPosition()); } @Override - public void onTabUnselected(TabLayout.Tab tab) {} + public void onTabUnselected(TabLayout.Tab tab) { + } @Override - public void onTabReselected(TabLayout.Tab tab) {} + public void onTabReselected(TabLayout.Tab tab) { + } private class PagerAdapter extends FragmentPagerAdapter { @@ -117,7 +111,7 @@ public class MainFragment extends Fragment implements TabLayout.OnTabSelectedLis @Override public Fragment getItem(int position) { - switch ( position ) { + switch (position) { case 1: return new SubscriptionFragment(); default: diff --git a/app/src/main/java/org/schabi/newpipe/fragments/OnScrollBelowItemsListener.java b/app/src/main/java/org/schabi/newpipe/fragments/OnScrollBelowItemsListener.java new file mode 100644 index 000000000..774e6cc03 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/OnScrollBelowItemsListener.java @@ -0,0 +1,43 @@ +package org.schabi.newpipe.fragments; + +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.StaggeredGridLayoutManager; + +/** + * Recycler view scroll listener which calls the method {@link #onScrolledDown(RecyclerView)} + * if the view is scrolled below the last item. + */ +public abstract class OnScrollBelowItemsListener extends RecyclerView.OnScrollListener { + + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + super.onScrolled(recyclerView, dx, dy); + if (dy > 0) { + int pastVisibleItems = 0, visibleItemCount, totalItemCount; + RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); + + visibleItemCount = layoutManager.getChildCount(); + totalItemCount = layoutManager.getItemCount(); + + // Already covers the GridLayoutManager case + if (layoutManager instanceof LinearLayoutManager) { + pastVisibleItems = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition(); + } else if (layoutManager instanceof StaggeredGridLayoutManager) { + int[] positions = ((StaggeredGridLayoutManager) layoutManager).findFirstVisibleItemPositions(null); + if (positions != null && positions.length > 0) pastVisibleItems = positions[0]; + } + + if ((visibleItemCount + pastVisibleItems) >= totalItemCount) { + onScrolledDown(recyclerView); + } + } + } + + /** + * Called when the recycler view is scrolled below the last item. + * + * @param recyclerView the recycler view + */ + public abstract void onScrolledDown(RecyclerView recyclerView); +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/SubscriptionFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/SubscriptionFragment.java deleted file mode 100644 index f2db9018d..000000000 --- a/app/src/main/java/org/schabi/newpipe/fragments/SubscriptionFragment.java +++ /dev/null @@ -1,278 +0,0 @@ -package org.schabi.newpipe.fragments; - -import android.os.Bundle; -import android.os.Parcelable; -import android.support.annotation.Nullable; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import org.schabi.newpipe.MainActivity; -import org.schabi.newpipe.R; -import org.schabi.newpipe.database.subscription.SubscriptionEntity; -import org.schabi.newpipe.extractor.InfoItem; -import org.schabi.newpipe.extractor.channel.ChannelInfoItem; -import org.schabi.newpipe.info_list.InfoItemBuilder; -import org.schabi.newpipe.info_list.InfoListAdapter; -import org.schabi.newpipe.report.ErrorActivity; -import org.schabi.newpipe.util.NavigationHelper; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - -import io.reactivex.Observer; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; - -import static org.schabi.newpipe.report.UserAction.REQUESTED_CHANNEL; -import static org.schabi.newpipe.util.AnimationUtils.animateView; - -public class SubscriptionFragment extends BaseFragment { - private static final String VIEW_STATE_KEY = "view_state_key"; - private final String TAG = "SubscriptionFragment@" + Integer.toHexString(hashCode()); - - private View inflatedView; - private View emptyPanel; - private View headerRootLayout; - private View whatsNewView; - - private InfoListAdapter infoListAdapter; - private RecyclerView resultRecyclerView; - private Parcelable viewState; - - /* Used for independent events */ - private CompositeDisposable disposables; - private SubscriptionService subscriptionService; - - /////////////////////////////////////////////////////////////////////////// - // Fragment LifeCycle - /////////////////////////////////////////////////////////////////////////// - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - disposables = new CompositeDisposable(); - subscriptionService = SubscriptionService.getInstance( getContext() ); - - if (savedInstanceState != null) { - viewState = savedInstanceState.getParcelable(VIEW_STATE_KEY); - } - } - - @Nullable - @Override - public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { - if (inflatedView == null) { - inflatedView = inflater.inflate(R.layout.fragment_subscription, container, false); - } - return inflatedView; - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - - outState.putParcelable(VIEW_STATE_KEY, viewState); - } - - @Override - public void onDestroyView() { - if (disposables != null) disposables.clear(); - - headerRootLayout = null; - whatsNewView = null; - - super.onDestroyView(); - } - - @Override - public void onDestroy() { - if (disposables != null) disposables.dispose(); - disposables = null; - - subscriptionService = null; - - super.onDestroy(); - } - - /////////////////////////////////////////////////////////////////////////// - // Fragment Views - /////////////////////////////////////////////////////////////////////////// - - private RecyclerView.OnScrollListener getOnScrollListener() { - return new RecyclerView.OnScrollListener() { - @Override - public void onScrollStateChanged(RecyclerView recyclerView, int newState) { - super.onScrollStateChanged(recyclerView, newState); - if (newState == RecyclerView.SCROLL_STATE_IDLE) { - viewState = recyclerView.getLayoutManager().onSaveInstanceState(); - } - } - }; - } - - private View.OnClickListener getWhatsNewOnClickListener() { - return new View.OnClickListener() { - @Override - public void onClick(View view) { - NavigationHelper.openWhatsNewFragment(getParentFragment().getFragmentManager()); - } - }; - } - - @Override - protected void initViews(View rootView, Bundle savedInstanceState) { - super.initViews(rootView, savedInstanceState); - - emptyPanel = rootView.findViewById(R.id.empty_panel); - - resultRecyclerView = rootView.findViewById(R.id.result_list_view); - resultRecyclerView.setLayoutManager(new LinearLayoutManager(activity)); - resultRecyclerView.addOnScrollListener(getOnScrollListener()); - - if (infoListAdapter == null) { - infoListAdapter = new InfoListAdapter(getActivity()); - infoListAdapter.setFooter(activity.getLayoutInflater().inflate(R.layout.pignate_footer, resultRecyclerView, false)); - infoListAdapter.showFooter(false); - infoListAdapter.setOnChannelInfoItemSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { - @Override - public void selected(int serviceId, String url, String title) { - /* Requires the parent fragment to find holder for fragment replacement */ - NavigationHelper.openChannelFragment(getParentFragment().getFragmentManager(), serviceId, url, title); - } - }); - } - - headerRootLayout = activity.getLayoutInflater().inflate(R.layout.subscription_header, resultRecyclerView, false); - infoListAdapter.setHeader(headerRootLayout); - - whatsNewView = headerRootLayout.findViewById(R.id.whatsNew); - whatsNewView.setOnClickListener(getWhatsNewOnClickListener()); - - resultRecyclerView.setAdapter(infoListAdapter); - - populateView(); - } - - @Override - protected void reloadContent() { - populateView(); - } - - @Override - protected void setErrorMessage(String message, boolean showRetryButton) { - super.setErrorMessage(message, showRetryButton); - resetFragment(); - } - - private void resetFragment() { - if (disposables != null) disposables.clear(); - if (infoListAdapter != null) infoListAdapter.clearStreamItemList(); - } - - /////////////////////////////////////////////////////////////////////////// - // Subscriptions Loader - /////////////////////////////////////////////////////////////////////////// - - private void populateView() { - resetFragment(); - - animateView(loadingProgressBar, true, 200); - animateView(errorPanel, false, 200); - - subscriptionService.getSubscription().toObservable() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(getSubscriptionObserver()); - } - - private Observer> getSubscriptionObserver() { - return new Observer>() { - @Override - public void onSubscribe(Disposable d) { - animateView(loadingProgressBar, true, 200); - - disposables.add( d ); - } - - @Override - public void onNext(List subscriptions) { - animateView(loadingProgressBar, true, 200); - - infoListAdapter.clearStreamItemList(); - infoListAdapter.addInfoItemList( getSubscriptionItems(subscriptions) ); - - animateView(loadingProgressBar, false, 200); - - emptyPanel.setVisibility(subscriptions.isEmpty() ? View.VISIBLE : View.INVISIBLE); - - if (viewState != null && resultRecyclerView != null) { - resultRecyclerView.getLayoutManager().onRestoreInstanceState(viewState); - } - } - - @Override - public void onError(Throwable exception) { - if (exception instanceof IOException) { - onRecoverableError(R.string.network_error); - } else { - onUnrecoverableError(exception); - } - } - - @Override - public void onComplete() { - - } - }; - } - - private List getSubscriptionItems(List subscriptions) { - List items = new ArrayList<>(); - for (final SubscriptionEntity subscription: subscriptions) { - ChannelInfoItem item = new ChannelInfoItem(); - item.webPageUrl = subscription.getUrl(); - item.serviceId = subscription.getServiceId(); - item.channelName = subscription.getTitle(); - item.thumbnailUrl = subscription.getThumbnailUrl(); - item.subscriberCount = subscription.getSubscriberCount(); - item.description = subscription.getDescription(); - - items.add( item ); - } - Collections.sort(items, new Comparator() { - @Override - public int compare(InfoItem o1, InfoItem o2) { - return o1.getTitle().compareToIgnoreCase(o2.getTitle()); - } - }); - - return items; - } - - /////////////////////////////////////////////////////////////////////////// - // Fragment Error Handling - /////////////////////////////////////////////////////////////////////////// - - private void onRecoverableError(int messageId) { - if (!this.isAdded()) return; - - if (DEBUG) Log.d(TAG, "onError() called with: messageId = [" + messageId + "]"); - setErrorMessage(getString(messageId), true); - } - - private void onUnrecoverableError(Throwable exception) { - if (DEBUG) Log.d(TAG, "onUnrecoverableError() called with: exception = [" + exception + "]"); - ErrorActivity.reportError(getContext(), exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(REQUESTED_CHANNEL, "unknown", "unknown", R.string.general_error)); - activity.finish(); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/ViewContract.java b/app/src/main/java/org/schabi/newpipe/fragments/ViewContract.java new file mode 100644 index 000000000..4ce09b000 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/ViewContract.java @@ -0,0 +1,10 @@ +package org.schabi.newpipe.fragments; + +public interface ViewContract { + void showLoading(); + void hideLoading(); + void showEmptyState(); + void showError(String message, boolean showRetryButton); + + void handleResult(I result); +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/channel/ChannelFragment.java deleted file mode 100644 index d5964b7ac..000000000 --- a/app/src/main/java/org/schabi/newpipe/fragments/channel/ChannelFragment.java +++ /dev/null @@ -1,570 +0,0 @@ -package org.schabi.newpipe.fragments.channel; - -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; -import android.support.v4.content.ContextCompat; -import android.support.v7.app.ActionBar; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.text.TextUtils; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.ImageView; -import android.widget.TextView; -import android.widget.Toast; - -import com.jakewharton.rxbinding2.view.RxView; - -import org.schabi.newpipe.ImageErrorLoadingListener; -import org.schabi.newpipe.R; -import org.schabi.newpipe.database.subscription.SubscriptionEntity; -import org.schabi.newpipe.extractor.InfoItem; -import org.schabi.newpipe.extractor.channel.ChannelInfo; -import org.schabi.newpipe.fragments.BaseFragment; -import org.schabi.newpipe.fragments.SubscriptionService; -import org.schabi.newpipe.fragments.search.OnScrollBelowItemsListener; -import org.schabi.newpipe.info_list.InfoItemBuilder; -import org.schabi.newpipe.info_list.InfoListAdapter; -import org.schabi.newpipe.util.Constants; -import org.schabi.newpipe.util.NavigationHelper; -import org.schabi.newpipe.workers.ChannelExtractorWorker; - -import java.io.Serializable; -import java.text.NumberFormat; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import io.reactivex.Observer; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.annotations.NonNull; -import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.disposables.Disposable; -import io.reactivex.functions.Action; -import io.reactivex.functions.Consumer; -import io.reactivex.functions.Function; -import io.reactivex.schedulers.Schedulers; - -import static org.schabi.newpipe.util.AnimationUtils.animateView; - -public class ChannelFragment extends BaseFragment implements ChannelExtractorWorker.OnChannelInfoReceive { -private final String TAG = "ChannelFragment@" + Integer.toHexString(hashCode()); - - private static final String INFO_LIST_KEY = "info_list_key"; - private static final String CHANNEL_INFO_KEY = "channel_info_key"; - private static final String PAGE_NUMBER_KEY = "page_number_key"; - - private static final int BUTTON_DEBOUNCE_INTERVAL = 100; - - private InfoListAdapter infoListAdapter; - - private ChannelExtractorWorker currentChannelWorker; - private ChannelInfo currentChannelInfo; - private int serviceId = -1; - private String channelName = ""; - private String channelUrl = ""; - private String feedUrl = ""; - private int pageNumber = 0; - private boolean hasNextPage = true; - - private SubscriptionService subscriptionService; - - private CompositeDisposable disposables; - private Disposable subscribeButtonMonitor; - - /*////////////////////////////////////////////////////////////////////////// - // Views - //////////////////////////////////////////////////////////////////////////*/ - - private RecyclerView channelVideosList; - - private View headerRootLayout; - private ImageView headerChannelBanner; - private ImageView headerAvatarView; - private TextView headerTitleView; - private TextView headerSubscribersTextView; - private Button headerSubscribeButton; - - /*////////////////////////////////////////////////////////////////////////*/ - - public ChannelFragment() { - } - - public static Fragment getInstance(int serviceId, String channelUrl, String name) { - ChannelFragment instance = new ChannelFragment(); - instance.setChannel(serviceId, channelUrl, name); - return instance; - } - - /*////////////////////////////////////////////////////////////////////////// - // Fragment's LifeCycle - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onCreate(Bundle savedInstanceState) { - if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]"); - super.onCreate(savedInstanceState); - setHasOptionsMenu(true); - if (savedInstanceState != null) { - channelUrl = savedInstanceState.getString(Constants.KEY_URL); - channelName = savedInstanceState.getString(Constants.KEY_TITLE); - serviceId = savedInstanceState.getInt(Constants.KEY_SERVICE_ID, -1); - - pageNumber = savedInstanceState.getInt(PAGE_NUMBER_KEY, 0); - Serializable serializable = savedInstanceState.getSerializable(CHANNEL_INFO_KEY); - if (serializable instanceof ChannelInfo) currentChannelInfo = (ChannelInfo) serializable; - } - } - - @Override - public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - if (DEBUG) Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]"); - return inflater.inflate(R.layout.fragment_channel, container, false); - } - - @Override - public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - if (currentChannelInfo == null) loadPage(0); - else handleChannelInfo(currentChannelInfo, false, false); - } - - @Override - public void onDestroyView() { - if (DEBUG) Log.d(TAG, "onDestroyView() called"); - headerAvatarView.setImageBitmap(null); - headerChannelBanner.setImageBitmap(null); - channelVideosList.removeAllViews(); - - channelVideosList = null; - headerRootLayout = null; - headerChannelBanner = null; - headerAvatarView = null; - headerTitleView = null; - headerSubscribersTextView = null; - headerSubscribeButton = null; - - if (disposables != null) disposables.dispose(); - if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose(); - disposables = null; - subscribeButtonMonitor = null; - subscriptionService = null; - - super.onDestroyView(); - } - - @Override - public void onResume() { - if (DEBUG) Log.d(TAG, "onResume() called"); - super.onResume(); - if (wasLoading.getAndSet(false) && (currentChannelWorker == null || !currentChannelWorker.isRunning())) { - loadPage(pageNumber); - } - } - - @Override - public void onStop() { - if (DEBUG) Log.d(TAG, "onStop() called"); - super.onStop(); - wasLoading.set(currentChannelWorker != null && currentChannelWorker.isRunning()); - if (currentChannelWorker != null && currentChannelWorker.isRunning()) currentChannelWorker.cancel(); - } - - @Override - public void onSaveInstanceState(Bundle outState) { - if (DEBUG) Log.d(TAG, "onSaveInstanceState() called with: outState = [" + outState + "]"); - super.onSaveInstanceState(outState); - outState.putString(Constants.KEY_URL, channelUrl); - outState.putString(Constants.KEY_TITLE, channelName); - outState.putInt(Constants.KEY_SERVICE_ID, serviceId); - - outState.putSerializable(INFO_LIST_KEY, infoListAdapter.getItemsList()); - outState.putSerializable(CHANNEL_INFO_KEY, currentChannelInfo); - outState.putInt(PAGE_NUMBER_KEY, pageNumber); - } - - /*////////////////////////////////////////////////////////////////////////// - // Menu - //////////////////////////////////////////////////////////////////////////*/ - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]"); - super.onCreateOptionsMenu(menu, inflater); - inflater.inflate(R.menu.menu_channel, menu); - - ActionBar supportActionBar = activity.getSupportActionBar(); - if (supportActionBar != null) { - supportActionBar.setDisplayShowTitleEnabled(true); - supportActionBar.setDisplayHomeAsUpEnabled(true); - } - menu.findItem(R.id.menu_item_rss).setVisible( !TextUtils.isEmpty(feedUrl) ); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (DEBUG) Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]"); - super.onOptionsItemSelected(item); - switch (item.getItemId()) { - case R.id.menu_item_openInBrowser: { - Intent intent = new Intent(); - intent.setAction(Intent.ACTION_VIEW); - intent.setData(Uri.parse(channelUrl)); - startActivity(Intent.createChooser(intent, getString(R.string.choose_browser))); - return true; - } - case R.id.menu_item_rss: { - Intent intent = new Intent(); - intent.setAction(Intent.ACTION_VIEW); - intent.setData(Uri.parse(currentChannelInfo.feed_url)); - startActivity(intent); - return true; - } - case R.id.menu_item_share: { - Intent intent = new Intent(); - intent.setAction(Intent.ACTION_SEND); - intent.putExtra(Intent.EXTRA_TEXT, channelUrl); - intent.setType("text/plain"); - startActivity(Intent.createChooser(intent, getString(R.string.share_dialog_title))); - return true; - } - default: - return super.onOptionsItemSelected(item); - } - } - - /*////////////////////////////////////////////////////////////////////////// - // Init's - //////////////////////////////////////////////////////////////////////////*/ - - @Override - protected void initViews(View rootView, Bundle savedInstanceState) { - super.initViews(rootView, savedInstanceState); - - channelVideosList = (RecyclerView) rootView.findViewById(R.id.channel_streams_view); - - channelVideosList.setLayoutManager(new LinearLayoutManager(activity)); - if (infoListAdapter == null) { - infoListAdapter = new InfoListAdapter(activity); - if (savedInstanceState != null) { - //noinspection unchecked - ArrayList serializable = (ArrayList) savedInstanceState.getSerializable(INFO_LIST_KEY); - infoListAdapter.addInfoItemList(serializable); - } - } - - channelVideosList.setAdapter(infoListAdapter); - headerRootLayout = activity.getLayoutInflater().inflate(R.layout.channel_header, channelVideosList, false); - infoListAdapter.setHeader(headerRootLayout); - infoListAdapter.setFooter(activity.getLayoutInflater().inflate(R.layout.pignate_footer, channelVideosList, false)); - - headerChannelBanner = (ImageView) headerRootLayout.findViewById(R.id.channel_banner_image); - headerAvatarView = (ImageView) headerRootLayout.findViewById(R.id.channel_avatar_view); - headerTitleView = (TextView) headerRootLayout.findViewById(R.id.channel_title_view); - headerSubscribersTextView = (TextView) headerRootLayout.findViewById(R.id.channel_subscriber_view); - headerSubscribeButton = (Button) headerRootLayout.findViewById(R.id.channel_subscribe_button); - - disposables = new CompositeDisposable(); - subscriptionService = SubscriptionService.getInstance( getContext() ); - } - - protected void initListeners() { - super.initListeners(); - - infoListAdapter.setOnStreamInfoItemSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { - @Override - public void selected(int serviceId, String url, String title) { - if (DEBUG) Log.d(TAG, "selected() called with: serviceId = [" + serviceId + "], url = [" + url + "], title = [" + title + "]"); - NavigationHelper.openVideoDetailFragment(getFragmentManager(), serviceId, url, title); - } - }); - - channelVideosList.clearOnScrollListeners(); - channelVideosList.addOnScrollListener(new OnScrollBelowItemsListener() { - @Override - public void onScrolledDown(RecyclerView recyclerView) { - if ((currentChannelWorker == null || !currentChannelWorker.isRunning()) && hasNextPage && !isLoading.get()) { - pageNumber++; - loadMoreVideos(); - } - } - }); - } - - - @Override - protected void reloadContent() { - if (DEBUG) Log.d(TAG, "reloadContent() called"); - currentChannelInfo = null; - infoListAdapter.clearStreamItemList(); - loadPage(0); - } - - /*////////////////////////////////////////////////////////////////////////// - // Channel Subscription - //////////////////////////////////////////////////////////////////////////*/ - - private void monitorSubscription(final int serviceId, - final String channelUrl, - final ChannelInfo info) { - subscriptionService.subscriptionTable().findAll(serviceId, channelUrl) - .toObservable() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(getSubscribeButtonMonitor(serviceId, channelUrl, info)); - } - - private Function mapOnSubscribe(final SubscriptionEntity subscription) { - return new Function() { - @Override - public Object apply(@NonNull Object o) throws Exception { - subscriptionService.subscriptionTable().insert( subscription ); - return o; - } - }; - } - - private Function mapOnUnsubscribe(final SubscriptionEntity subscription) { - return new Function() { - @Override - public Object apply(@NonNull Object o) throws Exception { - subscriptionService.subscriptionTable().delete( subscription ); - return o; - } - }; - } - - private Observer> getSubscribeButtonMonitor(final int serviceId, - final String channelUrl, - final ChannelInfo info) { - return new Observer>() { - @Override - public void onSubscribe(Disposable d) { - disposables.add( d ); - } - - @Override - public void onNext(List subscriptionEntities) { - if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose(); - - if (subscriptionEntities.isEmpty()) { - if (DEBUG) Log.d(TAG, "No subscription to this channel!"); - SubscriptionEntity channel = new SubscriptionEntity(); - channel.setServiceId( serviceId ); - channel.setUrl( channelUrl ); - channel.setData(info.channel_name, info.avatar_url, "", info.subscriberCount); - - subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnSubscribe(channel)); - - headerSubscribeButton.setText(R.string.subscribe_button_title); - } else { - if (DEBUG) Log.d(TAG, "Found subscription to this channel!"); - final SubscriptionEntity subscription = subscriptionEntities.get(0); - subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnUnsubscribe(subscription)); - - headerSubscribeButton.setText(R.string.subscribed_button_title); - } - - headerSubscribeButton.setVisibility(View.VISIBLE); - } - - @Override - public void onError(Throwable throwable) { - Log.e(TAG, "Status get failed", throwable); - headerSubscribeButton.setVisibility(View.INVISIBLE); - } - - @Override - public void onComplete() {} - }; - } - - private Disposable monitorSubscribeButton(final Button subscribeButton, - final Function action) { - final Consumer onNext = new Consumer() { - @Override - public void accept(@NonNull Object o) throws Exception { - if (DEBUG) Log.d(TAG, "Changed subscription status to this channel!"); - } - }; - - final Consumer onError = new Consumer() { - @Override - public void accept(@NonNull Throwable throwable) throws Exception { - if (DEBUG) Log.e(TAG, "Subscription Fatal Error: ", throwable.getCause()); - Toast.makeText(getContext(), R.string.subscription_change_failed, Toast.LENGTH_SHORT).show(); - } - }; - - /* Emit clicks from main thread unto io thread */ - return RxView.clicks(subscribeButton) - .subscribeOn(AndroidSchedulers.mainThread()) - .observeOn(Schedulers.io()) - .debounce(BUTTON_DEBOUNCE_INTERVAL, TimeUnit.MILLISECONDS) // Ignore rapid clicks - .map(action) - .subscribe(onNext, onError); - } - - private Disposable updateSubscription(final int serviceId, - final String channelUrl, - final ChannelInfo info) { - final Action onComplete = new Action() { - @Override - public void run() throws Exception { - if (DEBUG) Log.d(TAG, "Updated subscription: " + channelUrl); - } - }; - - final Consumer onError = new Consumer() { - @Override - public void accept(@NonNull Throwable throwable) throws Exception { - Log.e(TAG, "Subscription Update Fatal Error: ", throwable); - Toast.makeText(getContext(), R.string.subscription_update_failed, Toast.LENGTH_SHORT).show(); - } - }; - - return subscriptionService.updateChannelInfo(serviceId, channelUrl, info) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(onComplete, onError); - } - - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ - - private String buildSubscriberString(long count) { - String out = NumberFormat.getNumberInstance().format(count); - out += " " + getString(count > 1 ? R.string.subscriber_plural : R.string.subscriber); - return out; - } - - private void loadPage(int page) { - if (DEBUG) Log.d(TAG, "loadPage() called with: page = [" + page + "]"); - if (currentChannelWorker != null && currentChannelWorker.isRunning()) currentChannelWorker.cancel(); - isLoading.set(true); - pageNumber = page; - infoListAdapter.showFooter(false); - - animateView(loadingProgressBar, true, 200); - animateView(errorPanel, false, 200); - - imageLoader.cancelDisplayTask(headerChannelBanner); - imageLoader.cancelDisplayTask(headerAvatarView); - - headerSubscribeButton.setVisibility(View.GONE); - headerSubscribersTextView.setVisibility(View.GONE); - - headerTitleView.setText(channelName != null ? channelName : ""); - headerChannelBanner.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.channel_banner)); - headerAvatarView.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.buddy)); - if (activity.getSupportActionBar() != null) activity.getSupportActionBar().setTitle(channelName != null ? channelName : ""); - - currentChannelWorker = new ChannelExtractorWorker(activity, serviceId, channelUrl, page, false, this); - currentChannelWorker.start(); - } - - private void loadMoreVideos() { - if (DEBUG) Log.d(TAG, "loadMoreVideos() called"); - if (currentChannelWorker != null && currentChannelWorker.isRunning()) currentChannelWorker.cancel(); - isLoading.set(true); - currentChannelWorker = new ChannelExtractorWorker(activity, serviceId, channelUrl, pageNumber, true, this); - currentChannelWorker.start(); - } - - private void setChannel(int serviceId, String channelUrl, String name) { - this.serviceId = serviceId; - this.channelUrl = channelUrl; - this.channelName = name; - } - - private void handleChannelInfo(ChannelInfo info, boolean onlyVideos, boolean addVideos) { - currentChannelInfo = info; - - animateView(errorPanel, false, 300); - animateView(channelVideosList, true, 200); - animateView(loadingProgressBar, false, 200); - - if (!onlyVideos) { - feedUrl = info.feed_url; - if (activity.getSupportActionBar() != null) activity.getSupportActionBar().invalidateOptionsMenu(); - - headerRootLayout.setVisibility(View.VISIBLE); - //animateView(loadingProgressBar, false, 200, null); - - if (!TextUtils.isEmpty(info.channel_name)) { - if (activity.getSupportActionBar() != null) activity.getSupportActionBar().setTitle(info.channel_name); - headerTitleView.setText(info.channel_name); - channelName = info.channel_name; - } else channelName = ""; - - if (!TextUtils.isEmpty(info.banner_url)) { - imageLoader.displayImage(info.banner_url, headerChannelBanner, displayImageOptions, new ImageErrorLoadingListener(activity, getView(), info.service_id)); - } - - if (!TextUtils.isEmpty(info.avatar_url)) { - headerAvatarView.setVisibility(View.VISIBLE); - imageLoader.displayImage(info.avatar_url, headerAvatarView, displayImageOptions, new ImageErrorLoadingListener(activity, getView(), info.service_id)); - } - - if (info.subscriberCount != -1) { - headerSubscribersTextView.setText(buildSubscriberString(info.subscriberCount)); - headerSubscribersTextView.setVisibility(View.VISIBLE); - } else headerSubscribersTextView.setVisibility(View.GONE); - - if (disposables != null) disposables.clear(); - if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose(); - disposables.add( updateSubscription(serviceId, channelUrl, info) ); - monitorSubscription(serviceId, channelUrl, info); - - infoListAdapter.showFooter(true); - } - - hasNextPage = info.hasNextPage; - if (!hasNextPage) infoListAdapter.showFooter(false); - - //if (!listRestored) { - if (addVideos) infoListAdapter.addInfoItemList(info.related_streams); - //} - } - - @Override - protected void setErrorMessage(String message, boolean showRetryButton) { - super.setErrorMessage(message, showRetryButton); - - animateView(channelVideosList, false, 200); - currentChannelInfo = null; - } - - /*////////////////////////////////////////////////////////////////////////// - // OnChannelInfoReceiveListener - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onReceive(ChannelInfo info, boolean onlyVideos) { - if (DEBUG) Log.d(TAG, "onReceive() called with: info = [" + info + "]"); - if (info == null || isRemoving() || !isVisible()) return; - - handleChannelInfo(info, onlyVideos, true); - isLoading.set(false); - } - - @Override - public void onError(int messageId) { - if (DEBUG) Log.d(TAG, "onError() called with: messageId = [" + messageId + "]"); - setErrorMessage(getString(messageId), true); - } - - @Override - public void onUnrecoverableError(Exception exception) { - if (DEBUG) Log.d(TAG, "onUnrecoverableError() called with: exception = [" + exception + "]"); - activity.finish(); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/ActionBarHandler.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/ActionBarHandler.java index 77896b475..27bffca2d 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/ActionBarHandler.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/ActionBarHandler.java @@ -12,12 +12,12 @@ import android.widget.AdapterView; import android.widget.Spinner; import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.stream_info.VideoStream; -import org.schabi.newpipe.util.Utils; +import org.schabi.newpipe.extractor.stream.VideoStream; +import org.schabi.newpipe.util.ListHelper; import java.util.List; -/** +/* * Created by Christian Schabesberger on 18.08.15. *

* Copyright (C) Christian Schabesberger 2015 @@ -68,7 +68,7 @@ class ActionBarHandler { public void setupStreamList(final List videoStreams, Spinner toolbarSpinner) { if (activity == null) return; - selectedVideoStream = Utils.getDefaultResolution(activity, videoStreams); + selectedVideoStream = ListHelper.getDefaultResolutionIndex(activity, videoStreams); boolean isExternalPlayerEnabled = PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(activity.getString(R.string.use_external_video_player_key), false); toolbarSpinner.setAdapter(new SpinnerToolbarAdapter(activity, videoStreams, isExternalPlayerEnabled)); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/SpinnerToolbarAdapter.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/SpinnerToolbarAdapter.java index f93512fc7..94fe2cf5b 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/SpinnerToolbarAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/SpinnerToolbarAdapter.java @@ -11,7 +11,7 @@ import android.widget.TextView; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.MediaFormat; -import org.schabi.newpipe.extractor.stream_info.VideoStream; +import org.schabi.newpipe.extractor.stream.VideoStream; import java.util.List; @@ -57,8 +57,8 @@ public class SpinnerToolbarAdapter extends BaseAdapter { convertView = LayoutInflater.from(context).inflate(R.layout.resolutions_spinner_item, parent, false); } - ImageView woSoundIcon = (ImageView) convertView.findViewById(R.id.wo_sound_icon); - TextView text = (TextView) convertView.findViewById(android.R.id.text1); + ImageView woSoundIcon = convertView.findViewById(R.id.wo_sound_icon); + TextView text = convertView.findViewById(android.R.id.text1); VideoStream item = (VideoStream) getItem(position); text.setText(MediaFormat.getNameById(item.format) + " " + item.resolution); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/StackItem.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/StackItem.java index 647637fec..f50f805c2 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/StackItem.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/StackItem.java @@ -1,24 +1,25 @@ package org.schabi.newpipe.fragments.detail; -import org.schabi.newpipe.extractor.stream_info.StreamInfo; - import java.io.Serializable; - -@SuppressWarnings("WeakerAccess") -public class StackItem implements Serializable { +class StackItem implements Serializable { + private int serviceId; private String title, url; - private StreamInfo info; - public StackItem(String url, String title) { - this.title = title; + StackItem(int serviceId, String url, String title) { + this.serviceId = serviceId; this.url = url; + this.title = title; } public void setTitle(String title) { this.title = title; } + public int getServiceId() { + return serviceId; + } + public String getTitle() { return title; } @@ -27,16 +28,8 @@ public class StackItem implements Serializable { return url; } - public void setInfo(StreamInfo info) { - this.info = info; - } - - public StreamInfo getInfo() { - return info; - } - @Override public String toString() { - return getUrl() + " > " + getTitle(); + return getServiceId() + ":" + getUrl() + " > " + getTitle(); } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/StreamInfoCache.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/StreamInfoCache.java deleted file mode 100644 index c7bf80245..000000000 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/StreamInfoCache.java +++ /dev/null @@ -1,85 +0,0 @@ -package org.schabi.newpipe.fragments.detail; - -import android.support.annotation.NonNull; -import android.text.TextUtils; -import android.util.Log; - -import org.schabi.newpipe.MainActivity; -import org.schabi.newpipe.extractor.stream_info.StreamInfo; - -import java.util.Iterator; -import java.util.LinkedHashMap; - - -@SuppressWarnings("WeakerAccess") -public class StreamInfoCache { - private static String TAG = "StreamInfoCache@"; - private static final boolean DEBUG = MainActivity.DEBUG; - private static final StreamInfoCache instance = new StreamInfoCache(); - private static final int MAX_ITEMS_ON_CACHE = 20; - - private final LinkedHashMap myCache = new LinkedHashMap<>(); - - private StreamInfoCache() { - TAG += "" + Integer.toHexString(hashCode()); - } - - public static StreamInfoCache getInstance() { - if (DEBUG) Log.d(TAG, "getInstance() called"); - return instance; - } - - public boolean hasKey(@NonNull String url) { - if (DEBUG) Log.d(TAG, "hasKey() called with: url = [" + url + "]"); - return !TextUtils.isEmpty(url) && myCache.containsKey(url) && myCache.get(url) != null; - } - - public StreamInfo getFromKey(@NonNull String url) { - if (DEBUG) Log.d(TAG, "getFromKey() called with: url = [" + url + "]"); - return myCache.get(url); - } - - public void putInfo(@NonNull StreamInfo info) { - if (DEBUG) Log.d(TAG, "putInfo() called with: info = [" + info + "]"); - putInfo(info.webpage_url, info); - } - - public void putInfo(@NonNull String url, @NonNull StreamInfo info) { - if (DEBUG) Log.d(TAG, "putInfo() called with: url = [" + url + "], info = [" + info + "]"); - myCache.put(url, info); - } - - public void removeInfo(@NonNull StreamInfo info) { - if (DEBUG) Log.d(TAG, "removeInfo() called with: info = [" + info + "]"); - myCache.remove(info.webpage_url); - } - - public void removeInfo(@NonNull String url) { - if (DEBUG) Log.d(TAG, "removeInfo() called with: url = [" + url + "]"); - myCache.remove(url); - } - - @SuppressWarnings("unused") - public void clearCache() { - if (DEBUG) Log.d(TAG, "clearCache() called"); - myCache.clear(); - } - - public void removeOldEntries() { - if (DEBUG) Log.d(TAG, "removeOldEntries() called , size = " + getSize()); - if (getSize() > MAX_ITEMS_ON_CACHE) { - Iterator iterator = myCache.keySet().iterator(); - while (iterator.hasNext()) { - iterator.next(); - iterator.remove(); - if (DEBUG) Log.d(TAG, "getSize() = " + getSize()); - if (getSize() <= MAX_ITEMS_ON_CACHE) break; - } - } - } - - public int getSize() { - return myCache.size(); - } - -} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 2f5a3fc5c..5f954cad2 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -1,18 +1,14 @@ package org.schabi.newpipe.fragments.detail; import android.app.Activity; -import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Looper; -import android.os.Message; import android.preference.PreferenceManager; +import android.support.annotation.DrawableRes; import android.support.annotation.FloatRange; import android.support.annotation.NonNull; import android.support.v4.content.ContextCompat; @@ -24,6 +20,7 @@ import android.text.Spanned; import android.text.TextUtils; import android.text.method.LinkMovementMethod; import android.text.util.Linkify; +import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; import android.view.Gravity; @@ -46,76 +43,82 @@ import com.nirhart.parallaxscroll.views.ParallaxScrollView; import com.nostra13.universalimageloader.core.assist.FailReason; import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener; -import org.schabi.newpipe.ImageErrorLoadingListener; import org.schabi.newpipe.R; import org.schabi.newpipe.ReCaptchaActivity; import org.schabi.newpipe.download.DownloadDialog; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.NewPipe; -import org.schabi.newpipe.extractor.stream_info.AudioStream; -import org.schabi.newpipe.extractor.stream_info.StreamInfo; -import org.schabi.newpipe.extractor.stream_info.VideoStream; -import org.schabi.newpipe.fragments.BaseFragment; +import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; +import org.schabi.newpipe.extractor.exceptions.ParsingException; +import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor; +import org.schabi.newpipe.extractor.stream.AudioStream; +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.extractor.stream.VideoStream; +import org.schabi.newpipe.fragments.BackPressable; +import org.schabi.newpipe.fragments.BaseStateFragment; +import org.schabi.newpipe.history.HistoryListener; import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.player.MainVideoPlayer; -import org.schabi.newpipe.player.PlayVideoActivity; import org.schabi.newpipe.player.PopupVideoPlayer; +import org.schabi.newpipe.player.old.PlayVideoActivity; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; -import org.schabi.newpipe.util.Constants; +import org.schabi.newpipe.util.ExtractorHelper; +import org.schabi.newpipe.util.InfoCache; +import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PermissionHelper; -import org.schabi.newpipe.util.Utils; -import org.schabi.newpipe.workers.StreamExtractorWorker; import java.io.Serializable; import java.util.ArrayList; -import java.util.Stack; +import java.util.Collection; +import java.util.LinkedList; + +import icepick.State; +import io.reactivex.Single; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Consumer; +import io.reactivex.functions.Function; +import io.reactivex.schedulers.Schedulers; import static org.schabi.newpipe.util.AnimationUtils.animateView; -public class VideoDetailFragment extends BaseFragment implements StreamExtractorWorker.OnStreamInfoReceivedListener, SharedPreferences.OnSharedPreferenceChangeListener, View.OnClickListener { - private final String TAG = "VideoDetailFragment@" + Integer.toHexString(hashCode()); +public class VideoDetailFragment extends BaseStateFragment implements BackPressable, SharedPreferences.OnSharedPreferenceChangeListener, View.OnClickListener { + public static final String AUTO_PLAY = "auto_play"; // Amount of videos to show on start private static final int INITIAL_RELATED_VIDEOS = 8; - private static final String KORE_PACKET = "org.xbmc.kore"; - private static final String STACK_KEY = "stack_key"; - private static final String INFO_KEY = "info_key"; - private static final String WAS_RELATED_EXPANDED_KEY = "was_related_expanded_key"; - public static final String AUTO_PLAY = "auto_play"; - - private String thousand; - private String million; - private String billion; - - private ArrayList sortedStreamVideosList; private ActionBarHandler actionBarHandler; + private ArrayList sortedStreamVideosList; private InfoItemBuilder infoItemBuilder = null; - private StreamInfo currentStreamInfo = null; - private StreamExtractorWorker curExtractorWorker; - - private String videoTitle; - private String videoUrl; - private int serviceId = -1; + private int updateFlags = 0; private static final int RELATED_STREAMS_UPDATE_FLAG = 0x1; private static final int RESOLUTIONS_MENU_UPDATE_FLAG = 0x2; private static final int TOOLBAR_ITEMS_UPDATE_FLAG = 0x4; - private int updateFlags = 0; private boolean autoPlayEnabled; private boolean showRelatedStreams; private boolean wasRelatedStreamsExpanded = false; - private Handler uiHandler; - private Handler backgroundHandler; - private HandlerThread backgroundHandlerThread; + @State + protected int serviceId = -1; + @State + protected String name; + @State + protected String url; + + private StreamInfo currentInfo; + private Disposable currentWorker; + private CompositeDisposable disposables = new CompositeDisposable(); /*////////////////////////////////////////////////////////////////////////// // Views @@ -156,24 +159,15 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor private LinearLayout relatedStreamRootLayout; private LinearLayout relatedStreamsView; private ImageButton relatedStreamExpandButton; - private OnVideoPlayListener onVideoPlayedListener; /*////////////////////////////////////////////////////////////////////////*/ - public static VideoDetailFragment getInstance(int serviceId, String url) { - return getInstance(serviceId, url, ""); - } - - public static VideoDetailFragment getInstance(int serviceId, String videoUrl, String videoTitle) { - VideoDetailFragment instance = getInstance(); - instance.selectVideo(serviceId, videoUrl, videoTitle); + public static VideoDetailFragment getInstance(int serviceId, String videoUrl, String name) { + VideoDetailFragment instance = new VideoDetailFragment(); + instance.setInitialData(serviceId, videoUrl, name); return instance; } - public static VideoDetailFragment getInstance() { - return new VideoDetailFragment(); - } - /*////////////////////////////////////////////////////////////////////////// // Fragment's Lifecycle //////////////////////////////////////////////////////////////////////////*/ @@ -181,179 +175,69 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]"); - - if (savedInstanceState != null) { - videoTitle = savedInstanceState.getString(Constants.KEY_TITLE); - videoUrl = savedInstanceState.getString(Constants.KEY_URL); - serviceId = savedInstanceState.getInt(Constants.KEY_SERVICE_ID, 0); - wasRelatedStreamsExpanded = savedInstanceState.getBoolean(WAS_RELATED_EXPANDED_KEY, false); - Serializable serializable = savedInstanceState.getSerializable(STACK_KEY); - if (serializable instanceof Stack) { - //noinspection unchecked - Stack list = (Stack) serializable; - stack.clear(); - stack.addAll(list); - } - - Serializable serial = savedInstanceState.getSerializable(INFO_KEY); - if (serial instanceof StreamInfo) currentStreamInfo = (StreamInfo) serial; - } + setHasOptionsMenu(true); showRelatedStreams = PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(getString(R.string.show_next_video_key), true); PreferenceManager.getDefaultSharedPreferences(activity).registerOnSharedPreferenceChangeListener(this); - - thousand = getString(R.string.short_thousand); - million = getString(R.string.short_million); - billion = getString(R.string.short_billion); - - if (uiHandler == null) { - uiHandler = new Handler(Looper.getMainLooper(), new UICallback()); - } - if (backgroundHandler == null) { - HandlerThread handlerThread = new HandlerThread("VideoDetailFragment-BG"); - handlerThread.start(); - backgroundHandlerThread = handlerThread; - backgroundHandler = new Handler(handlerThread.getLooper(), new BackgroundCallback(uiHandler, getContext())); - } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - if (DEBUG) Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]"); return inflater.inflate(R.layout.fragment_video_detail, container, false); } @Override - public void onViewCreated(View rootView, Bundle savedInstanceState) { - super.onViewCreated(rootView, savedInstanceState); - if (currentStreamInfo == null) selectAndLoadVideo(serviceId, videoUrl, videoTitle); - else prepareAndLoad(currentStreamInfo, false); - } - - @Override - public void onAttach(Context context) { - super.onAttach(context); - onVideoPlayedListener = (OnVideoPlayListener) context; - } - - @Override - public void onDetach() { - super.onDetach(); - onVideoPlayedListener = null; + public void onPause() { + super.onPause(); + if (currentWorker != null) currentWorker.dispose(); } @Override public void onResume() { super.onResume(); - // Currently only used for enable/disable related videos - // but can be extended for other live settings changes if (updateFlags != 0) { - if (!isLoading.get() && currentStreamInfo != null) { - if ((updateFlags & RELATED_STREAMS_UPDATE_FLAG) != 0) initRelatedVideos(currentStreamInfo); - if ((updateFlags & RESOLUTIONS_MENU_UPDATE_FLAG) != 0) setupActionBarHandler(currentStreamInfo); + if (!isLoading.get() && currentInfo != null) { + if ((updateFlags & RELATED_STREAMS_UPDATE_FLAG) != 0) initRelatedVideos(currentInfo); + if ((updateFlags & RESOLUTIONS_MENU_UPDATE_FLAG) != 0) setupActionBarHandler(currentInfo); } if ((updateFlags & TOOLBAR_ITEMS_UPDATE_FLAG) != 0 && actionBarHandler != null) actionBarHandler.updateItemsVisibility(); updateFlags = 0; } - // Check if it was loading when the activity was stopped/paused, - // because when this happen, the curExtractorWorker is cancelled - if (wasLoading.getAndSet(false)) selectAndLoadVideo(serviceId, videoUrl, videoTitle); - } - - @Override - public void onStop() { - super.onStop(); - wasLoading.set(curExtractorWorker != null && curExtractorWorker.isRunning()); - if (curExtractorWorker != null && curExtractorWorker.isRunning()) curExtractorWorker.cancel(); - StreamInfoCache.getInstance().removeOldEntries(); + // Check if it was loading when the fragment was stopped/paused, + if (wasLoading.getAndSet(false)) { + selectAndLoadVideo(serviceId, url, name); + } } @Override public void onDestroy() { super.onDestroy(); - if (backgroundHandlerThread != null) { - backgroundHandlerThread.quit(); - } - backgroundHandlerThread = null; - backgroundHandler = null; PreferenceManager.getDefaultSharedPreferences(activity).unregisterOnSharedPreferenceChangeListener(this); + + if (currentWorker != null) currentWorker.dispose(); + if (disposables != null) disposables.clear(); + currentWorker = null; + disposables = null; } @Override public void onDestroyView() { if (DEBUG) Log.d(TAG, "onDestroyView() called"); - thumbnailImageView.setImageBitmap(null); - relatedStreamsView.removeAllViews(); spinnerToolbar.setOnItemSelectedListener(null); - - spinnerToolbar = null; - - parallaxScrollRootView = null; - contentRootLayoutHiding = null; - - thumbnailBackgroundButton = null; - thumbnailImageView = null; - thumbnailPlayButton = null; - - videoTitleRoot = null; - videoTitleTextView = null; - videoTitleToggleArrow = null; - videoCountView = null; - - detailControlsBackground = null; - detailControlsPopup = null; - - videoDescriptionRootLayout = null; - videoUploadDateView = null; - videoDescriptionView = null; - - uploaderRootLayout = null; - uploaderTextView = null; - uploaderThumb = null; - - thumbsUpTextView = null; - thumbsUpImageView = null; - thumbsDownTextView = null; - thumbsDownImageView = null; - thumbsDisabledTextView = null; - - nextStreamTitle = null; - relatedStreamRootLayout = null; - relatedStreamsView = null; - relatedStreamExpandButton = null; - + spinnerToolbar.setAdapter(null); super.onDestroyView(); } - @Override - public void onSaveInstanceState(Bundle outState) { - if (DEBUG) Log.d(TAG, "onSaveInstanceState() called with: outState = [" + outState + "]"); - outState.putString(Constants.KEY_URL, videoUrl); - outState.putString(Constants.KEY_TITLE, videoTitle); - outState.putInt(Constants.KEY_SERVICE_ID, serviceId); - outState.putSerializable(STACK_KEY, stack); - - int nextCount = currentStreamInfo != null && currentStreamInfo.next_video != null ? 2 : 0; - if (relatedStreamsView != null && relatedStreamsView.getChildCount() > INITIAL_RELATED_VIDEOS + nextCount) { - outState.putSerializable(WAS_RELATED_EXPANDED_KEY, true); - } - - if (!isLoading.get() && (curExtractorWorker == null || !curExtractorWorker.isRunning())) { - outState.putSerializable(INFO_KEY, currentStreamInfo); - } - } - @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); switch (requestCode) { case ReCaptchaActivity.RECAPTCHA_REQUEST: if (resultCode == Activity.RESULT_OK) { - NavigationHelper.openVideoDetailFragment(getFragmentManager(), serviceId, videoUrl, videoTitle); + NavigationHelper.openVideoDetailFragment(getFragmentManager(), serviceId, url, name); } else Log.e(TAG, "ReCaptcha failed"); break; default: @@ -367,7 +251,7 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor if (key.equals(getString(R.string.show_next_video_key))) { showRelatedStreams = sharedPreferences.getBoolean(key, true); updateFlags |= RELATED_STREAMS_UPDATE_FLAG; - } else if (key.equals(getString(R.string.preferred_video_format_key)) + } else if (key.equals(getString(R.string.default_video_format_key)) || key.equals(getString(R.string.default_resolution_key)) || key.equals(getString(R.string.show_higher_resolutions_key)) || key.equals(getString(R.string.use_external_video_player_key))) { @@ -377,114 +261,85 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor } } + /*////////////////////////////////////////////////////////////////////////// + // State Saving + //////////////////////////////////////////////////////////////////////////*/ + + private static final String INFO_KEY = "info_key"; + private static final String STACK_KEY = "stack_key"; + private static final String WAS_RELATED_EXPANDED_KEY = "was_related_expanded_key"; + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + // Check if the next video label and video is visible, + // if it is, include the two elements in the next check + int nextCount = currentInfo != null && currentInfo.next_video != null ? 2 : 0; + if (relatedStreamsView != null && relatedStreamsView.getChildCount() > INITIAL_RELATED_VIDEOS + nextCount) { + outState.putSerializable(WAS_RELATED_EXPANDED_KEY, true); + } + + if (!isLoading.get() && currentInfo != null && isVisible()) { + outState.putSerializable(INFO_KEY, currentInfo); + } + + outState.putSerializable(STACK_KEY, stack); + } + + @Override + protected void onRestoreInstanceState(@NonNull Bundle savedState) { + super.onRestoreInstanceState(savedState); + + wasRelatedStreamsExpanded = savedState.getBoolean(WAS_RELATED_EXPANDED_KEY, false); + Serializable serializable = savedState.getSerializable(INFO_KEY); + if (serializable instanceof StreamInfo) { + //noinspection unchecked + currentInfo = (StreamInfo) serializable; + InfoCache.getInstance().putInfo(currentInfo); + } + + serializable = savedState.getSerializable(STACK_KEY); + if (serializable instanceof Collection) { + //noinspection unchecked + stack.addAll((Collection) serializable); + } + } + /*////////////////////////////////////////////////////////////////////////// // OnClick //////////////////////////////////////////////////////////////////////////*/ @Override public void onClick(View v) { - if (isLoading.get() || currentStreamInfo == null) return; + if (isLoading.get() || currentInfo == null) return; switch (v.getId()) { case R.id.detail_controls_background: - openInBackground(); + openBackgroundPlayer(); break; case R.id.detail_controls_popup: - openInPopup(); + openPopupPlayer(); break; case R.id.detail_uploader_root_layout: - if (currentStreamInfo.channel_url == null || currentStreamInfo.channel_url.isEmpty()) { + if (currentInfo.uploader_url == null || currentInfo.uploader_url.isEmpty()) { Log.w(TAG, "Can't open channel because we got no channel URL"); } else { - NavigationHelper.openChannelFragment(getFragmentManager(), currentStreamInfo.service_id, currentStreamInfo.channel_url, currentStreamInfo.uploader); + NavigationHelper.openChannelFragment(getFragmentManager(), currentInfo.service_id, currentInfo.uploader_url, currentInfo.uploader_name); } break; case R.id.detail_thumbnail_root_layout: - playVideo(currentStreamInfo); + openVideoPlayer(); break; case R.id.detail_title_root_layout: toggleTitleAndDescription(); break; case R.id.detail_related_streams_expand: - toggleExpandRelatedVideos(currentStreamInfo); + toggleExpandRelatedVideos(currentInfo); break; } } - @Override - protected void reloadContent() { - if (DEBUG) Log.d(TAG, "reloadContent() called"); - if (currentStreamInfo != null) StreamInfoCache.getInstance().removeInfo(currentStreamInfo); - currentStreamInfo = null; - for (StackItem stackItem : stack) if (stackItem.getUrl().equals(videoUrl)) stackItem.setInfo(null); - prepareAndLoad(null, true); - } - - private void openInBackground() { - if (isLoading.get()) return; - - boolean useExternalAudioPlayer = PreferenceManager.getDefaultSharedPreferences(activity) - .getBoolean(activity.getString(R.string.use_external_audio_player_key), false); - Intent intent; - AudioStream audioStream = currentStreamInfo.audio_streams.get(Utils.getPreferredAudioFormat(activity, currentStreamInfo.audio_streams)); - onVideoPlayedListener.onBackgroundPlayed(currentStreamInfo, audioStream); - - if (!useExternalAudioPlayer && android.os.Build.VERSION.SDK_INT >= 16) { - activity.startService(NavigationHelper.getOpenBackgroundPlayerIntent(activity, currentStreamInfo, audioStream)); - Toast.makeText(activity, R.string.background_player_playing_toast, Toast.LENGTH_SHORT).show(); - } else { - intent = new Intent(); - try { - intent.setAction(Intent.ACTION_VIEW); - intent.setDataAndType(Uri.parse(audioStream.url), - MediaFormat.getMimeById(audioStream.format)); - intent.putExtra(Intent.EXTRA_TITLE, currentStreamInfo.title); - intent.putExtra("title", currentStreamInfo.title); - // HERE !!! - activity.startActivity(intent); - } catch (Exception e) { - e.printStackTrace(); - AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setMessage(R.string.no_player_found) - .setPositiveButton(R.string.install, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - Intent intent = new Intent(); - intent.setAction(Intent.ACTION_VIEW); - intent.setData(Uri.parse(activity.getString(R.string.fdroid_vlc_url))); - activity.startActivity(intent); - } - }) - .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - Log.i(TAG, "You unlocked a secret unicorn."); - } - }); - builder.create().show(); - Log.e(TAG, "Either no Streaming player for audio was installed, or something important crashed:"); - e.printStackTrace(); - } - } - } - - private void openInPopup() { - if (isLoading.get()) return; - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !PermissionHelper.checkSystemAlertWindowPermission(activity)) { - Toast toast = Toast.makeText(activity, R.string.msg_popup_permission, Toast.LENGTH_LONG); - TextView messageView = (TextView) toast.getView().findViewById(android.R.id.message); - if (messageView != null) messageView.setGravity(Gravity.CENTER); - toast.show(); - return; - } - - onVideoPlayedListener.onVideoPlayed(getSelectedVideoStream(), currentStreamInfo); - Toast.makeText(activity, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show(); - Intent mIntent = NavigationHelper.getOpenVideoPlayerIntent(activity, PopupVideoPlayer.class, currentStreamInfo, actionBarHandler.getSelectedVideoStream()); - activity.startService(mIntent); - } - private void toggleTitleAndDescription() { if (videoDescriptionRootLayout.getVisibility() == View.VISIBLE) { videoTitleTextView.setMaxLines(1); @@ -506,7 +361,7 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor if (relatedStreamsView.getChildCount() > initialCount) { relatedStreamsView.removeViews(initialCount, relatedStreamsView.getChildCount() - (initialCount)); - relatedStreamExpandButton.setImageDrawable(ContextCompat.getDrawable(activity, getResourceIdFromAttr(R.attr.expand))); + relatedStreamExpandButton.setImageDrawable(ContextCompat.getDrawable(activity, resolveResourceIdFromAttr(R.attr.expand))); return; } @@ -516,71 +371,70 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor //Log.d(TAG, "i = " + i); relatedStreamsView.addView(infoItemBuilder.buildView(relatedStreamsView, item)); } - relatedStreamExpandButton.setImageDrawable(ContextCompat.getDrawable(activity, getResourceIdFromAttr(R.attr.collapse))); + relatedStreamExpandButton.setImageDrawable(ContextCompat.getDrawable(activity, resolveResourceIdFromAttr(R.attr.collapse))); } /*////////////////////////////////////////////////////////////////////////// // Init //////////////////////////////////////////////////////////////////////////*/ + @Override protected void initViews(View rootView, Bundle savedInstanceState) { super.initViews(rootView, savedInstanceState); - spinnerToolbar = (Spinner) toolbar.findViewById(R.id.toolbar_spinner); + spinnerToolbar = activity.findViewById(R.id.toolbar).findViewById(R.id.toolbar_spinner); - parallaxScrollRootView = (ParallaxScrollView) rootView.findViewById(R.id.detail_main_content); + parallaxScrollRootView = rootView.findViewById(R.id.detail_main_content); thumbnailBackgroundButton = rootView.findViewById(R.id.detail_thumbnail_root_layout); - thumbnailImageView = (ImageView) rootView.findViewById(R.id.detail_thumbnail_image_view); - thumbnailPlayButton = (ImageView) rootView.findViewById(R.id.detail_thumbnail_play_button); + thumbnailImageView = rootView.findViewById(R.id.detail_thumbnail_image_view); + thumbnailPlayButton = rootView.findViewById(R.id.detail_thumbnail_play_button); - contentRootLayoutHiding = (LinearLayout) rootView.findViewById(R.id.detail_content_root_hiding); + contentRootLayoutHiding = rootView.findViewById(R.id.detail_content_root_hiding); videoTitleRoot = rootView.findViewById(R.id.detail_title_root_layout); - videoTitleTextView = (TextView) rootView.findViewById(R.id.detail_video_title_view); - videoTitleToggleArrow = (ImageView) rootView.findViewById(R.id.detail_toggle_description_view); - videoCountView = (TextView) rootView.findViewById(R.id.detail_view_count_view); + videoTitleTextView = rootView.findViewById(R.id.detail_video_title_view); + videoTitleToggleArrow = rootView.findViewById(R.id.detail_toggle_description_view); + videoCountView = rootView.findViewById(R.id.detail_view_count_view); - detailControlsBackground = (TextView) rootView.findViewById(R.id.detail_controls_background); - detailControlsPopup = (TextView) rootView.findViewById(R.id.detail_controls_popup); + detailControlsBackground = rootView.findViewById(R.id.detail_controls_background); + detailControlsPopup = rootView.findViewById(R.id.detail_controls_popup); - videoDescriptionRootLayout = (LinearLayout) rootView.findViewById(R.id.detail_description_root_layout); - videoUploadDateView = (TextView) rootView.findViewById(R.id.detail_upload_date_view); - videoDescriptionView = (TextView) rootView.findViewById(R.id.detail_description_view); + videoDescriptionRootLayout = rootView.findViewById(R.id.detail_description_root_layout); + videoUploadDateView = rootView.findViewById(R.id.detail_upload_date_view); + videoDescriptionView = rootView.findViewById(R.id.detail_description_view); + videoDescriptionView.setMovementMethod(LinkMovementMethod.getInstance()); + videoDescriptionView.setAutoLinkMask(Linkify.WEB_URLS); - //thumbsRootLayout = (LinearLayout) rootView.findViewById(R.id.detail_thumbs_root_layout); - thumbsUpTextView = (TextView) rootView.findViewById(R.id.detail_thumbs_up_count_view); - thumbsUpImageView = (ImageView) rootView.findViewById(R.id.detail_thumbs_up_img_view); - thumbsDownTextView = (TextView) rootView.findViewById(R.id.detail_thumbs_down_count_view); - thumbsDownImageView = (ImageView) rootView.findViewById(R.id.detail_thumbs_down_img_view); - thumbsDisabledTextView = (TextView) rootView.findViewById(R.id.detail_thumbs_disabled_view); + //thumbsRootLayout = rootView.findViewById(R.id.detail_thumbs_root_layout); + thumbsUpTextView = rootView.findViewById(R.id.detail_thumbs_up_count_view); + thumbsUpImageView = rootView.findViewById(R.id.detail_thumbs_up_img_view); + thumbsDownTextView = rootView.findViewById(R.id.detail_thumbs_down_count_view); + thumbsDownImageView = rootView.findViewById(R.id.detail_thumbs_down_img_view); + thumbsDisabledTextView = rootView.findViewById(R.id.detail_thumbs_disabled_view); uploaderRootLayout = rootView.findViewById(R.id.detail_uploader_root_layout); - uploaderTextView = (TextView) rootView.findViewById(R.id.detail_uploader_text_view); - uploaderThumb = (ImageView) rootView.findViewById(R.id.detail_uploader_thumbnail_view); + uploaderTextView = rootView.findViewById(R.id.detail_uploader_text_view); + uploaderThumb = rootView.findViewById(R.id.detail_uploader_thumbnail_view); - relatedStreamRootLayout = (LinearLayout) rootView.findViewById(R.id.detail_related_streams_root_layout); - nextStreamTitle = (TextView) rootView.findViewById(R.id.detail_next_stream_title); - relatedStreamsView = (LinearLayout) rootView.findViewById(R.id.detail_related_streams_view); + relatedStreamRootLayout = rootView.findViewById(R.id.detail_related_streams_root_layout); + nextStreamTitle = rootView.findViewById(R.id.detail_next_stream_title); + relatedStreamsView = rootView.findViewById(R.id.detail_related_streams_view); - relatedStreamExpandButton = ((ImageButton) rootView.findViewById(R.id.detail_related_streams_expand)); + relatedStreamExpandButton = rootView.findViewById(R.id.detail_related_streams_expand); actionBarHandler = new ActionBarHandler(activity); - videoDescriptionView.setAutoLinkMask(Linkify.WEB_URLS); - videoDescriptionView.setMovementMethod(LinkMovementMethod.getInstance()); - infoItemBuilder = new InfoItemBuilder(activity); - setHeightThumbnail(); } + @Override protected void initListeners() { super.initListeners(); - infoItemBuilder.setOnStreamInfoItemSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { + infoItemBuilder.setOnStreamSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { @Override - public void selected(int serviceId, String url, String title) { - //NavigationHelper.openVideoDetail(activity, url, serviceId); - selectAndLoadVideo(serviceId, url, title); + public void selected(StreamInfoItem selectedItem) { + selectAndLoadVideo(selectedItem.service_id, selectedItem.url, selectedItem.name); } }); @@ -593,18 +447,18 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor } private void initThumbnailViews(StreamInfo info) { + thumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark); if (info.thumbnail_url != null && !info.thumbnail_url.isEmpty()) { - imageLoader.displayImage(info.thumbnail_url, thumbnailImageView, displayImageOptions, new SimpleImageLoadingListener() { + imageLoader.displayImage(info.thumbnail_url, thumbnailImageView, DISPLAY_THUMBNAIL_OPTIONS, new SimpleImageLoadingListener() { @Override public void onLoadingFailed(String imageUri, View view, FailReason failReason) { - ErrorActivity.reportError(activity, failReason.getCause(), null, activity.findViewById(android.R.id.content), ErrorActivity.ErrorInfo.make(UserAction.LOAD_IMAGE, NewPipe.getNameOfService(currentStreamInfo.service_id), imageUri, R.string.could_not_load_thumbnails)); + ErrorActivity.reportError(activity, failReason.getCause(), null, activity.findViewById(android.R.id.content), ErrorActivity.ErrorInfo.make(UserAction.LOAD_IMAGE, NewPipe.getNameOfService(currentInfo.service_id), imageUri, R.string.could_not_load_thumbnails)); } }); - } else thumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark); + } - if (info.uploader_thumbnail_url != null && !info.uploader_thumbnail_url.isEmpty()) { - imageLoader.displayImage(info.uploader_thumbnail_url, uploaderThumb, displayImageOptions, - new ImageErrorLoadingListener(activity, activity.findViewById(android.R.id.content), info.service_id)); + if (info.uploader_avatar_url != null && !info.uploader_avatar_url.isEmpty()) { + imageLoader.displayImage(info.uploader_avatar_url, uploaderThumb, DISPLAY_AVATAR_OPTIONS); } } @@ -632,7 +486,7 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor relatedStreamRootLayout.setVisibility(View.VISIBLE); relatedStreamExpandButton.setVisibility(View.VISIBLE); - relatedStreamExpandButton.setImageDrawable(ContextCompat.getDrawable(activity, getResourceIdFromAttr(R.attr.expand))); + relatedStreamExpandButton.setImageDrawable(ContextCompat.getDrawable(activity, resolveResourceIdFromAttr(R.attr.expand))); } else { if (info.next_video == null) relatedStreamRootLayout.setVisibility(View.GONE); relatedStreamExpandButton.setVisibility(View.GONE); @@ -655,20 +509,19 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor @Override public boolean onOptionsItemSelected(MenuItem item) { - return actionBarHandler.onItemSelected(item) || super.onOptionsItemSelected(item); + return (!isLoading.get() && actionBarHandler.onItemSelected(item)) || super.onOptionsItemSelected(item); } private void setupActionBarHandler(final StreamInfo info) { - sortedStreamVideosList = Utils.getSortedStreamVideosList(activity, info.video_streams, info.video_only_streams, false); + if (DEBUG) Log.d(TAG, "setupActionBarHandler() called with: info = [" + info + "]"); + sortedStreamVideosList = new ArrayList<>(ListHelper.getSortedStreamVideosList(activity, info.video_streams, info.video_only_streams, false)); actionBarHandler.setupStreamList(sortedStreamVideosList, spinnerToolbar); actionBarHandler.setOnShareListener(new ActionBarHandler.OnActionListener() { @Override public void onActionSelected(int selectedStreamId) { - if (isLoading.get()) return; - Intent intent = new Intent(); intent.setAction(Intent.ACTION_SEND); - intent.putExtra(Intent.EXTRA_TEXT, info.webpage_url); + intent.putExtra(Intent.EXTRA_TEXT, info.url); intent.setType("text/plain"); startActivity(Intent.createChooser(intent, activity.getString(R.string.share_dialog_title))); } @@ -677,11 +530,9 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor actionBarHandler.setOnOpenInBrowserListener(new ActionBarHandler.OnActionListener() { @Override public void onActionSelected(int selectedStreamId) { - if (isLoading.get()) return; - Intent intent = new Intent(); intent.setAction(Intent.ACTION_VIEW); - intent.setData(Uri.parse(info.webpage_url)); + intent.setData(Uri.parse(info.url)); startActivity(Intent.createChooser(intent, activity.getString(R.string.choose_browser))); } }); @@ -689,12 +540,10 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor actionBarHandler.setOnPlayWithKodiListener(new ActionBarHandler.OnActionListener() { @Override public void onActionSelected(int selectedStreamId) { - if (isLoading.get()) return; - try { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setPackage(KORE_PACKET); - intent.setData(Uri.parse(info.webpage_url.replace("https", "http"))); + intent.setData(Uri.parse(info.url.replace("https", "http"))); activity.startActivity(intent); } catch (Exception e) { e.printStackTrace(); @@ -723,8 +572,7 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor actionBarHandler.setOnDownloadListener(new ActionBarHandler.OnActionListener() { @Override public void onActionSelected(int selectedStreamId) { - - if (isLoading.get() || !PermissionHelper.checkStoragePermissions(activity)) { + if (!PermissionHelper.checkStoragePermissions(activity)) { return; } @@ -747,58 +595,257 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor * Stack that contains the "navigation history".
* The peek is the current video. */ - private final Stack stack = new Stack<>(); + protected LinkedList stack = new LinkedList<>(); public void clearHistory() { stack.clear(); } - public void pushToStack(String videoUrl, String videoTitle) { - if (DEBUG) Log.d(TAG, "pushToStack() called with: videoUrl = [" + videoUrl + "], videoTitle = [" + videoTitle + "]"); - if (stack.size() > 0 && stack.peek().getUrl().equals(videoUrl)) return; - stack.push(new StackItem(videoUrl, videoTitle)); + public void pushToStack(int serviceId, String videoUrl, String name) { + if (DEBUG) { + Log.d(TAG, "pushToStack() called with: serviceId = [" + serviceId + "], videoUrl = [" + videoUrl + "], name = [" + name + "]"); + } + + if (stack.size() > 0 && stack.peek().getServiceId() == serviceId && stack.peek().getUrl().equals(videoUrl)) { + Log.d(TAG, "pushToStack() called with: serviceId == peek.serviceId = [" + serviceId + "], videoUrl == peek.getUrl = [" + videoUrl + "]"); + return; + } else { + Log.d(TAG, "pushToStack() wasn't equal"); + } + + stack.push(new StackItem(serviceId, videoUrl, name)); } - public void setTitleToUrl(String videoUrl, String videoTitle) { - if (videoTitle != null && !videoTitle.isEmpty()) { + public void setTitleToUrl(int serviceId, String videoUrl, String name) { + if (name != null && !name.isEmpty()) { for (StackItem stackItem : stack) { - if (stackItem.getUrl().equals(videoUrl)) stackItem.setTitle(videoTitle); + if (stack.peek().getServiceId() == serviceId && stackItem.getUrl().equals(videoUrl)) stackItem.setTitle(name); } } } - public void setStreamInfoToUrl(String videoUrl, StreamInfo info) { - if (info != null) { - for (StackItem stackItem : stack) { - if (stackItem.getUrl().equals(videoUrl)) stackItem.setInfo(info); - } - } - } - - public boolean onActivityBackPressed() { - if (DEBUG) Log.d(TAG, "onActivityBackPressed() called"); + @Override + public boolean onBackPressed() { + if (DEBUG) Log.d(TAG, "onBackPressed() called"); // That means that we are on the start of the stack, // return false to let the MainActivity handle the onBack - if (stack.size() == 1) return false; + if (stack.size() <= 1) return false; // Remove top stack.pop(); - // Get url from the new top + // Get stack item from the new top StackItem peek = stack.peek(); - if (peek.getInfo() != null) { - final StreamInfo streamInfo = peek.getInfo(); - getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - selectAndHandleInfo(streamInfo); - } - }); - } else { - selectAndLoadVideo(0, peek.getUrl(), !TextUtils.isEmpty(peek.getTitle()) ? peek.getTitle() : ""); - } + selectAndLoadVideo(peek.getServiceId(), peek.getUrl(), !TextUtils.isEmpty(peek.getTitle()) ? peek.getTitle() : ""); return true; } + /*////////////////////////////////////////////////////////////////////////// + // Info loading and handling + //////////////////////////////////////////////////////////////////////////*/ + + @Override + protected void doInitialLoadLogic() { + if (currentInfo == null) prepareAndLoadInfo(); + else prepareAndHandleInfo(currentInfo, false); + } + + public void selectAndLoadVideo(int serviceId, String videoUrl, String name) { + setInitialData(serviceId, videoUrl, name); + prepareAndLoadInfo(); + } + + public void prepareAndHandleInfo(final StreamInfo info, boolean scrollToTop) { + if (DEBUG) Log.d(TAG, "prepareAndHandleInfo() called with: info = [" + info + "], scrollToTop = [" + scrollToTop + "]"); + + setInitialData(info.service_id, info.url, info.name); + pushToStack(serviceId, url, name); + showLoading(); + + Log.d(TAG, "prepareAndHandleInfo() called parallaxScrollRootView.getScrollY(): " + parallaxScrollRootView.getScrollY()); + final boolean greaterThanThreshold = parallaxScrollRootView.getScrollY() > (int) + (getResources().getDisplayMetrics().heightPixels * .1f); + + if (scrollToTop) parallaxScrollRootView.smoothScrollTo(0, 0); + animateView(contentRootLayoutHiding, false, greaterThanThreshold ? 250 : 0, 0, new Runnable() { + @Override + public void run() { + handleResult(info); + showContentWithAnimation(120, 0, .01f); + } + }); + } + + protected void prepareAndLoadInfo() { + parallaxScrollRootView.smoothScrollTo(0, 0); + pushToStack(serviceId, url, name); + startLoading(false); + } + + @Override + public void startLoading(boolean forceLoad) { + super.startLoading(forceLoad); + + currentInfo = null; + if (currentWorker != null) currentWorker.dispose(); + + currentWorker = ExtractorHelper.getStreamInfo(serviceId, url, forceLoad) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Consumer() { + @Override + public void accept(@NonNull StreamInfo result) throws Exception { + isLoading.set(false); + currentInfo = result; + showContentWithAnimation(120, 0, 0); + handleResult(result); + } + }, new Consumer() { + @Override + public void accept(@NonNull Throwable throwable) throws Exception { + isLoading.set(false); + onError(throwable); + } + }); + } + + /*////////////////////////////////////////////////////////////////////////// + // Play Utils + //////////////////////////////////////////////////////////////////////////*/ + + private void openBackgroundPlayer() { + AudioStream audioStream = currentInfo.audio_streams.get(ListHelper.getDefaultAudioFormat(activity, currentInfo.audio_streams)); + + if (activity instanceof HistoryListener) { + ((HistoryListener) activity).onAudioPlayed(currentInfo, audioStream); + } + + boolean useExternalAudioPlayer = PreferenceManager.getDefaultSharedPreferences(activity) + .getBoolean(activity.getString(R.string.use_external_audio_player_key), false); + + if (!useExternalAudioPlayer && android.os.Build.VERSION.SDK_INT >= 16) { + openNormalBackgroundPlayer(audioStream); + } else { + openExternalBackgroundPlayer(audioStream); + } + } + + private void openPopupPlayer() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !PermissionHelper.checkSystemAlertWindowPermission(activity)) { + Toast toast = Toast.makeText(activity, R.string.msg_popup_permission, Toast.LENGTH_LONG); + TextView messageView = toast.getView().findViewById(android.R.id.message); + if (messageView != null) messageView.setGravity(Gravity.CENTER); + toast.show(); + return; + } + + if (activity instanceof HistoryListener) { + ((HistoryListener) activity).onVideoPlayed(currentInfo, getSelectedVideoStream()); + } + + Toast.makeText(activity, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show(); + Intent mIntent = NavigationHelper.getOpenVideoPlayerIntent(activity, PopupVideoPlayer.class, currentInfo, actionBarHandler.getSelectedVideoStream()); + activity.startService(mIntent); + } + + private void openVideoPlayer() { + VideoStream selectedVideoStream = getSelectedVideoStream(); + + if (activity instanceof HistoryListener) { + ((HistoryListener) activity).onVideoPlayed(currentInfo, selectedVideoStream); + } + + if (PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(this.getString(R.string.use_external_video_player_key), false)) { + openExternalVideoPlayer(selectedVideoStream); + } else { + openNormalPlayer(selectedVideoStream); + } + } + + + private void openNormalBackgroundPlayer(AudioStream audioStream) { + activity.startService(NavigationHelper.getOpenBackgroundPlayerIntent(activity, currentInfo, audioStream)); + Toast.makeText(activity, R.string.background_player_playing_toast, Toast.LENGTH_SHORT).show(); + } + + private void openExternalBackgroundPlayer(AudioStream audioStream) { + Intent intent; + intent = new Intent(); + try { + intent.setAction(Intent.ACTION_VIEW); + intent.setDataAndType(Uri.parse(audioStream.url), MediaFormat.getMimeById(audioStream.format)); + intent.putExtra(Intent.EXTRA_TITLE, currentInfo.name); + intent.putExtra("title", currentInfo.name); + activity.startActivity(intent); + } catch (Exception e) { + e.printStackTrace(); + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setMessage(R.string.no_player_found) + .setPositiveButton(R.string.install, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Intent intent = new Intent(); + intent.setAction(Intent.ACTION_VIEW); + intent.setData(Uri.parse(activity.getString(R.string.fdroid_vlc_url))); + activity.startActivity(intent); + } + }) + .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Log.i(TAG, "You unlocked a secret unicorn."); + } + }); + builder.create().show(); + Log.e(TAG, "Either no Streaming player for audio was installed, or something important crashed:"); + e.printStackTrace(); + } + } + + private void openNormalPlayer(VideoStream selectedVideoStream) { + Intent mIntent; + boolean useOldPlayer = PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(getString(R.string.use_old_player_key), false) + || (Build.VERSION.SDK_INT < 16); + if (!useOldPlayer) { + // ExoPlayer + mIntent = NavigationHelper.getOpenVideoPlayerIntent(activity, MainVideoPlayer.class, currentInfo, actionBarHandler.getSelectedVideoStream()); + } else { + // Internal Player + mIntent = new Intent(activity, PlayVideoActivity.class) + .putExtra(PlayVideoActivity.VIDEO_TITLE, currentInfo.name) + .putExtra(PlayVideoActivity.STREAM_URL, selectedVideoStream.url) + .putExtra(PlayVideoActivity.VIDEO_URL, currentInfo.url) + .putExtra(PlayVideoActivity.START_POSITION, currentInfo.start_position); + } + startActivity(mIntent); + } + + private void openExternalVideoPlayer(VideoStream selectedVideoStream) { + // External Player + Intent intent = new Intent(); + try { + intent.setAction(Intent.ACTION_VIEW) + .setDataAndType(Uri.parse(selectedVideoStream.url), MediaFormat.getMimeById(selectedVideoStream.format)) + .putExtra(Intent.EXTRA_TITLE, currentInfo.name) + .putExtra("title", currentInfo.name); + this.startActivity(intent); + } catch (Exception e) { + e.printStackTrace(); + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setMessage(R.string.no_player_found) + .setPositiveButton(R.string.install, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Intent intent = new Intent() + .setAction(Intent.ACTION_VIEW) + .setData(Uri.parse(getString(R.string.fdroid_vlc_url))); + startActivity(intent); + } + }) + .setNegativeButton(R.string.cancel, null); + builder.create().show(); + } + } + /*////////////////////////////////////////////////////////////////////////// // Utils //////////////////////////////////////////////////////////////////////////*/ @@ -807,247 +854,38 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor this.autoPlayEnabled = autoplay; } - public void selectVideo(int serviceId, String videoUrl, String videoTitle) { - this.videoUrl = videoUrl; - this.videoTitle = videoTitle; - this.serviceId = serviceId; - } - - public void selectAndHandleInfo(StreamInfo info) { - selectAndHandleInfo(info, true); - } - - public void selectAndHandleInfo(StreamInfo info, boolean scrollToTop) { - if (DEBUG) Log.d(TAG, "selectAndHandleInfo() called with: info = [" + info + "], scrollToTop = [" + scrollToTop + "]"); - selectVideo(info.service_id, info.webpage_url, info.title); - prepareAndLoad(info, scrollToTop); - } - - public void selectAndLoadVideo(int serviceId, String videoUrl, String videoTitle) { - selectAndLoadVideo(serviceId, videoUrl, videoTitle, true); - } - - public void selectAndLoadVideo(int serviceId, String videoUrl, String videoTitle, boolean scrollToTop) { - if (DEBUG) { - Log.d(TAG, "selectAndLoadVideo() called with: serviceId = [" + serviceId + "], videoUrl = [" + videoUrl + "], videoTitle = [" + videoTitle + "], scrollToTop = [" + scrollToTop + "]"); - } - - selectVideo(serviceId, videoUrl, videoTitle); - prepareAndLoad(null, scrollToTop); - } - - /** - * Prepare the UI for loading the info.
- * If the argument info is not null, it'll be passed in {@link #handleStreamInfo(StreamInfo, boolean)}.
- * If it is, check if the cache contains the info already.
- * If the cache doesn't have the info, load from the network. - * - * @param info info to prepare and load, can be null - * @param scrollToTop whether or not scroll the scrollView to y = 0 - */ - public void prepareAndLoad(StreamInfo info, boolean scrollToTop) { - if (DEBUG) Log.d(TAG, "prepareAndLoad() called with: info = [" + info + "]"); - isLoading.set(true); - - // Only try to get from the cache if the passed info IS null - if (info == null && StreamInfoCache.getInstance().hasKey(videoUrl)) { - info = StreamInfoCache.getInstance().getFromKey(videoUrl); - } - - if (info != null) selectVideo(info.service_id, info.webpage_url, info.title); - pushToStack(videoUrl, videoTitle); - - if (curExtractorWorker != null && curExtractorWorker.isRunning()) curExtractorWorker.cancel(); - animateView(spinnerToolbar, false, 200); - animateView(errorPanel, false, 200); - - videoTitleTextView.setText(videoTitle != null ? videoTitle : ""); - videoTitleTextView.setMaxLines(1); - animateView(videoTitleTextView, true, 0); - - videoDescriptionRootLayout.setVisibility(View.GONE); - videoTitleToggleArrow.setImageResource(R.drawable.arrow_down); - videoTitleToggleArrow.setVisibility(View.GONE); - videoTitleRoot.setClickable(false); - - animateView(thumbnailPlayButton, false, 50); - imageLoader.cancelDisplayTask(thumbnailImageView); - imageLoader.cancelDisplayTask(uploaderThumb); - thumbnailImageView.setImageBitmap(null); - uploaderThumb.setImageBitmap(null); - - if (info != null) { - final StreamInfo infoFinal = info; - final boolean greaterThanThreshold = parallaxScrollRootView.getScrollY() > - (int) (getResources().getDisplayMetrics().heightPixels * .1f); - - if (scrollToTop) { - if (greaterThanThreshold) parallaxScrollRootView.smoothScrollTo(0, 0); - else parallaxScrollRootView.scrollTo(0, 0); - } - - animateView(contentRootLayoutHiding, false, greaterThanThreshold ? 250 : 0, 0, new Runnable() { - @Override - public void run() { - handleStreamInfo(infoFinal, false); - isLoading.set(false); - showContentWithAnimation(greaterThanThreshold ? 120 : 200, 0, .02f); - } - }); - } else { - if (scrollToTop) parallaxScrollRootView.smoothScrollTo(0, 0); - curExtractorWorker = new StreamExtractorWorker(activity, serviceId, videoUrl, this); - curExtractorWorker.start(); - animateView(loadingProgressBar, true, 200); - animateView(contentRootLayoutHiding, false, 200); - } - } - - private void handleStreamInfo(@NonNull final StreamInfo info, boolean fromNetwork) { - if (DEBUG) Log.d(TAG, "handleStreamInfo() called with: info = [" + info + "]"); - currentStreamInfo = info; - selectVideo(info.service_id, info.webpage_url, info.title); - - loadingProgressBar.setVisibility(View.GONE); - animateView(thumbnailPlayButton, true, 200); - - // Since newpipe is designed to work even if certain information is not available, - // the UI has to react on missing information. - if (fromNetwork) videoTitleTextView.setText(info.title); - - if (!TextUtils.isEmpty(info.uploader)) uploaderTextView.setText(info.uploader); - uploaderTextView.setVisibility(!TextUtils.isEmpty(info.uploader) ? View.VISIBLE : View.GONE); - uploaderThumb.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.buddy)); - - if (info.view_count >= 0) videoCountView.setText(Localization.localizeViewCount(info.view_count, activity)); - videoCountView.setVisibility(info.view_count >= 0 ? View.VISIBLE : View.GONE); - - if (info.dislike_count == -1 && info.like_count == -1) { - thumbsDownImageView.setVisibility(View.VISIBLE); - thumbsUpImageView.setVisibility(View.VISIBLE); - thumbsUpTextView.setVisibility(View.GONE); - thumbsDownTextView.setVisibility(View.GONE); - - thumbsDisabledTextView.setVisibility(View.VISIBLE); - } else { - thumbsDisabledTextView.setVisibility(View.GONE); - - if (info.dislike_count >= 0) thumbsDownTextView.setText(getShortCount((long) info.dislike_count)); - thumbsDownTextView.setVisibility(info.dislike_count >= 0 ? View.VISIBLE : View.GONE); - thumbsDownImageView.setVisibility(info.dislike_count >= 0 ? View.VISIBLE : View.GONE); - - if (info.like_count >= 0) thumbsUpTextView.setText(getShortCount((long) info.like_count)); - thumbsUpTextView.setVisibility(info.like_count >= 0 ? View.VISIBLE : View.GONE); - thumbsUpImageView.setVisibility(info.like_count >= 0 ? View.VISIBLE : View.GONE); - } - - - videoDescriptionView.setVisibility(View.GONE); - videoDescriptionRootLayout.setVisibility(View.GONE); - videoTitleToggleArrow.setImageResource(R.drawable.arrow_down); - videoTitleToggleArrow.setVisibility(View.VISIBLE); - videoTitleRoot.setClickable(true); - - animateView(spinnerToolbar, true, 500); - setupActionBarHandler(info); - initThumbnailViews(info); - initRelatedVideos(info); - if (wasRelatedStreamsExpanded) { - toggleExpandRelatedVideos(currentStreamInfo); - wasRelatedStreamsExpanded = false; - } - setTitleToUrl(info.webpage_url, info.title); - setStreamInfoToUrl(info.webpage_url, info); - - prepareDescription(info.description); - prepareUploadDate(info.upload_date); - - if (autoPlayEnabled) { - playVideo(info); - // Only auto play in the first open - autoPlayEnabled = false; - } - } - - private void prepareUploadDate(final String uploadDate) { - // Hide until date is prepared or forever if no date is supplied - videoUploadDateView.setVisibility(View.GONE); - if (!TextUtils.isEmpty(uploadDate)) { - backgroundHandler.sendMessage(Message.obtain(backgroundHandler, BackgroundCallback.MESSAGE_UPLOADER_DATE, uploadDate)); - } - } - - private void prepareDescription(final String descriptionHtml) { - // Send the unparsed description to the handler as a message - if (!TextUtils.isEmpty(descriptionHtml)) { - backgroundHandler.sendMessage(Message.obtain(backgroundHandler, BackgroundCallback.MESSAGE_DESCRIPTION, descriptionHtml)); - } - } - - /** - * Get the currently selected video stream - * @return the selected video stream - */ private VideoStream getSelectedVideoStream() { return sortedStreamVideosList.get(actionBarHandler.getSelectedVideoStream()); } - public void playVideo(StreamInfo info) { - // ----------- THE MAGIC MOMENT --------------- - VideoStream selectedVideoStream = getSelectedVideoStream(); - - if(onVideoPlayedListener != null) { - onVideoPlayedListener.onVideoPlayed(selectedVideoStream, info); + private void prepareDescription(final String descriptionHtml) { + if (TextUtils.isEmpty(descriptionHtml)) { + return; } - if (PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(this.getString(R.string.use_external_video_player_key), false)) { - - // External Player - Intent intent = new Intent(); - try { - intent.setAction(Intent.ACTION_VIEW) - .setDataAndType(Uri.parse(selectedVideoStream.url), MediaFormat.getMimeById(selectedVideoStream.format)) - .putExtra(Intent.EXTRA_TITLE, info.title) - .putExtra("title", info.title); - this.startActivity(intent); - } catch (Exception e) { - e.printStackTrace(); - AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setMessage(R.string.no_player_found) - .setPositiveButton(R.string.install, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - Intent intent = new Intent() - .setAction(Intent.ACTION_VIEW) - .setData(Uri.parse(getString(R.string.fdroid_vlc_url))); - startActivity(intent); - } - }) - .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - } - }); - builder.create().show(); - } - } else { - Intent mIntent; - boolean useOldPlayer = PreferenceManager.getDefaultSharedPreferences(activity) - .getBoolean(getString(R.string.use_old_player_key), false) - || (Build.VERSION.SDK_INT < 16); - if (!useOldPlayer) { - // ExoPlayer - mIntent = NavigationHelper.getOpenVideoPlayerIntent(activity, MainVideoPlayer.class, info, actionBarHandler.getSelectedVideoStream()); - } else { - // Internal Player - mIntent = new Intent(activity, PlayVideoActivity.class) - .putExtra(PlayVideoActivity.VIDEO_TITLE, info.title) - .putExtra(PlayVideoActivity.STREAM_URL, selectedVideoStream.url) - .putExtra(PlayVideoActivity.VIDEO_URL, info.webpage_url) - .putExtra(PlayVideoActivity.START_POSITION, info.start_position); - } - startActivity(mIntent); - } + disposables.add(Single.just(descriptionHtml) + .map(new Function() { + @Override + public Spanned apply(@io.reactivex.annotations.NonNull String description) throws Exception { + Spanned parsedDescription; + if (Build.VERSION.SDK_INT >= 24) { + parsedDescription = Html.fromHtml(description, 0); + } else { + //noinspection deprecation + parsedDescription = Html.fromHtml(description); + } + return parsedDescription; + } + }) + .subscribeOn(Schedulers.computation()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Consumer() { + @Override + public void accept(@io.reactivex.annotations.NonNull Spanned spanned) throws Exception { + videoDescriptionView.setText(spanned); + videoDescriptionView.setVisibility(View.VISIBLE); + } + })); } private View getSeparatorView() { @@ -1059,36 +897,24 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor separator.setLayoutParams(params); TypedValue typedValue = new TypedValue(); - activity.getTheme().resolveAttribute(R.attr.separatorColor, typedValue, true); + activity.getTheme().resolveAttribute(R.attr.separator_color, typedValue, true); separator.setBackgroundColor(typedValue.data); return separator; } private void setHeightThumbnail() { - boolean isPortrait = getResources().getDisplayMetrics().heightPixels > getResources().getDisplayMetrics().widthPixels; - int height = isPortrait ? (int) (getResources().getDisplayMetrics().widthPixels / (16.0f / 9.0f)) - : (int) (getResources().getDisplayMetrics().heightPixels / 2f); + final DisplayMetrics metrics = getResources().getDisplayMetrics(); + boolean isPortrait = metrics.heightPixels > metrics.widthPixels; + int height = isPortrait ? (int) (metrics.widthPixels / (16.0f / 9.0f)) : (int) (metrics.heightPixels / 2f); thumbnailImageView.setScaleType(isPortrait ? ImageView.ScaleType.CENTER_CROP : ImageView.ScaleType.FIT_CENTER); thumbnailImageView.setLayoutParams(new FrameLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, height)); thumbnailImageView.setMinimumHeight(height); } - public String getShortCount(Long viewCount) { - if (viewCount >= 1000000000) { - return Long.toString(viewCount / 1000000000) + billion; - } else if (viewCount >= 1000000) { - return Long.toString(viewCount / 1000000) + million; - } else if (viewCount >= 1000) { - return Long.toString(viewCount / 1000) + thousand; - } else { - return Long.toString(viewCount); - } - } - private void showContentWithAnimation(long duration, long delay, @FloatRange(from = 0.0f, to = 1.0f) float translationPercent) { int translationY = (int) (getResources().getDisplayMetrics().heightPixels * - (translationPercent > 0.0f ? translationPercent : .12f)); + (translationPercent > 0.0f ? translationPercent : .06f)); contentRootLayoutHiding.animate().setListener(null).cancel(); contentRootLayoutHiding.setAlpha(0f); @@ -1114,8 +940,15 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor } } + protected void setInitialData(int serviceId, String url, String name) { + this.serviceId = serviceId; + this.url = url; + this.name = !TextUtils.isEmpty(name) ? name : ""; + } + private void setErrorImage(final int imageResource) { if (thumbnailImageView == null || activity == null) return; + thumbnailImageView.setImageDrawable(ContextCompat.getDrawable(activity, imageResource)); animateView(thumbnailImageView, false, 0, 0, new Runnable() { @Override @@ -1126,51 +959,133 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor } @Override - protected void setErrorMessage(String message, boolean showRetryButton) { - super.setErrorMessage(message, showRetryButton); + public void showError(String message, boolean showRetryButton) { + showError(message, showRetryButton, R.drawable.not_available_monkey); + } - if (!TextUtils.isEmpty(videoUrl)) StreamInfoCache.getInstance().removeInfo(videoUrl); - currentStreamInfo = null; + protected void showError(String message, boolean showRetryButton, @DrawableRes int imageError) { + super.showError(message, showRetryButton); + setErrorImage(imageError); } /*////////////////////////////////////////////////////////////////////////// - // OnStreamInfoReceivedListener callbacks + // Contract //////////////////////////////////////////////////////////////////////////*/ @Override - public void onReceive(StreamInfo info) { - if (DEBUG) Log.d(TAG, "onReceive() called with: info = [" + info + "]"); - if (info == null || isRemoving() || !isVisible()) return; + public void showLoading() { + super.showLoading(); - handleStreamInfo(info, true); - showContentWithAnimation(300, 0, 0); - animateView(loadingProgressBar, false, 200); + animateView(contentRootLayoutHiding, false, 200); + animateView(spinnerToolbar, false, 200); + animateView(thumbnailPlayButton, false, 50); - StreamInfoCache.getInstance().putInfo(info); - isLoading.set(false); + videoTitleTextView.setText(name != null ? name : ""); + videoTitleTextView.setMaxLines(1); + animateView(videoTitleTextView, true, 0); + videoDescriptionRootLayout.setVisibility(View.GONE); + videoTitleToggleArrow.setImageResource(R.drawable.arrow_down); + videoTitleToggleArrow.setVisibility(View.GONE); + videoTitleRoot.setClickable(false); + + imageLoader.cancelDisplayTask(thumbnailImageView); + imageLoader.cancelDisplayTask(uploaderThumb); + thumbnailImageView.setImageBitmap(null); + uploaderThumb.setImageBitmap(null); } @Override - public void onError(int messageId) { - if (DEBUG) Log.d(TAG, "onError() called with: messageId = [" + messageId + "]"); - //Toast.makeText(activity, messageId, Toast.LENGTH_LONG).show(); - setErrorImage(R.drawable.not_available_monkey); - setErrorMessage(getString(messageId), true); + public void handleResult(@NonNull StreamInfo info) { + super.handleResult(info); + + setInitialData(info.service_id, info.url, info.name); + pushToStack(serviceId, url, name); + + animateView(thumbnailPlayButton, true, 200); + videoTitleTextView.setText(name); + + if (!TextUtils.isEmpty(info.uploader_name)) uploaderTextView.setText(info.uploader_name); + uploaderTextView.setVisibility(!TextUtils.isEmpty(info.uploader_name) ? View.VISIBLE : View.GONE); + uploaderThumb.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.buddy)); + + if (info.view_count >= 0) videoCountView.setText(Localization.localizeViewCount(activity, info.view_count)); + videoCountView.setVisibility(info.view_count >= 0 ? View.VISIBLE : View.GONE); + + if (info.dislike_count == -1 && info.like_count == -1) { + thumbsDownImageView.setVisibility(View.VISIBLE); + thumbsUpImageView.setVisibility(View.VISIBLE); + thumbsUpTextView.setVisibility(View.GONE); + thumbsDownTextView.setVisibility(View.GONE); + + thumbsDisabledTextView.setVisibility(View.VISIBLE); + } else { + if (info.dislike_count >= 0) thumbsDownTextView.setText(Localization.shortCount(activity, info.dislike_count)); + thumbsDownTextView.setVisibility(info.dislike_count >= 0 ? View.VISIBLE : View.GONE); + thumbsDownImageView.setVisibility(info.dislike_count >= 0 ? View.VISIBLE : View.GONE); + + if (info.like_count >= 0) thumbsUpTextView.setText(Localization.shortCount(activity, info.like_count)); + thumbsUpTextView.setVisibility(info.like_count >= 0 ? View.VISIBLE : View.GONE); + thumbsUpImageView.setVisibility(info.like_count >= 0 ? View.VISIBLE : View.GONE); + + thumbsDisabledTextView.setVisibility(View.GONE); + } + + videoTitleRoot.setClickable(true); + videoTitleToggleArrow.setVisibility(View.VISIBLE); + videoTitleToggleArrow.setImageResource(R.drawable.arrow_down); + videoDescriptionView.setVisibility(View.GONE); + videoDescriptionRootLayout.setVisibility(View.GONE); + if (!TextUtils.isEmpty(info.upload_date)) { + videoUploadDateView.setText(Localization.localizeDate(activity, info.upload_date)); + } + prepareDescription(info.description); + + animateView(spinnerToolbar, true, 500); + setupActionBarHandler(info); + initThumbnailViews(info); + initRelatedVideos(info); + if (wasRelatedStreamsExpanded) { + toggleExpandRelatedVideos(currentInfo); + wasRelatedStreamsExpanded = false; + } + setTitleToUrl(info.service_id, info.url, info.name); + + if (!info.errors.isEmpty()) { + showSnackBarError(info.errors, UserAction.REQUESTED_STREAM, NewPipe.getNameOfService(info.service_id), info.url, 0); + } + + if (autoPlayEnabled) { + openVideoPlayer(); + // Only auto play in the first open + autoPlayEnabled = false; + } } - @Override - public void onReCaptchaException() { - Toast.makeText(activity, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show(); - // Starting ReCaptcha Challenge Activity - startActivityForResult(new Intent(activity, ReCaptchaActivity.class), ReCaptchaActivity.RECAPTCHA_REQUEST); + /*////////////////////////////////////////////////////////////////////////// + // Stream Results + //////////////////////////////////////////////////////////////////////////*/ - setErrorMessage(getString(R.string.recaptcha_request_toast), false); + @Override + protected boolean onError(Throwable exception) { + if (super.onError(exception)) return true; + + if (exception instanceof YoutubeStreamExtractor.GemaException) { + onBlockedByGemaError(); + } else if (exception instanceof YoutubeStreamExtractor.LiveStreamException) { + showError(getString(R.string.live_streams_not_supported), false); + } else if (exception instanceof ContentNotAvailableException) { + showError(getString(R.string.content_not_available), false); + } else { + int errorId = exception instanceof YoutubeStreamExtractor.DecryptException ? R.string.youtube_signature_decryption_error : + exception instanceof ParsingException ? R.string.parsing_error : R.string.general_error; + onUnrecoverableError(exception, UserAction.REQUESTED_STREAM, NewPipe.getNameOfService(serviceId), url, errorId); + } + + return true; } - @Override public void onBlockedByGemaError() { - thumbnailBackgroundButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -1181,109 +1096,6 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor } }); - setErrorImage(R.drawable.gruese_die_gema); - setErrorMessage(getString(R.string.blocked_by_gema), false); + showError(getString(R.string.blocked_by_gema), false, R.drawable.gruese_die_gema); } - - @Override - public void onContentErrorWithMessage(int messageId) { - setErrorImage(R.drawable.not_available_monkey); - setErrorMessage(getString(messageId), false); - } - - @Override - public void onContentError() { - setErrorImage(R.drawable.not_available_monkey); - setErrorMessage(getString(R.string.content_not_available), false); - } - - @Override - public void onUnrecoverableError(Exception exception) { - activity.finish(); - } - - /*////////////////////////////////////////////////////////////////////////// - // Background handling - //////////////////////////////////////////////////////////////////////////*/ - - private static class BackgroundCallback implements Handler.Callback { - private static final int MESSAGE_DESCRIPTION = 1; - public static final int MESSAGE_UPLOADER_DATE = 2; - private final Handler uiHandler; - private final Context context; - - BackgroundCallback(Handler uiHandler, Context context) { - this.uiHandler = uiHandler; - this.context = context; - } - - @Override - public boolean handleMessage(Message msg) { - switch (msg.what) { - case MESSAGE_DESCRIPTION: - handleDescription((String) msg.obj); - return true; - case MESSAGE_UPLOADER_DATE: - handleUploadDate((String) msg.obj); - return true; - } - return false; - } - - private void handleUploadDate(String uploadDate) { - String localizedDate = Localization.localizeDate(uploadDate, context); - uiHandler.sendMessage(Message.obtain(uiHandler, MESSAGE_UPLOADER_DATE, localizedDate)); - } - - private void handleDescription(String description) { - Spanned parsedDescription; - if (TextUtils.isEmpty(description)) { - return; - } - if (Build.VERSION.SDK_INT >= 24) { - parsedDescription = Html.fromHtml(description, 0); - } else { - //noinspection deprecation - parsedDescription = Html.fromHtml(description); - } - uiHandler.sendMessage(Message.obtain(uiHandler, MESSAGE_DESCRIPTION, parsedDescription)); - } - } - - private class UICallback implements Handler.Callback { - @Override - public boolean handleMessage(Message msg) { - switch (msg.what) { - case BackgroundCallback.MESSAGE_DESCRIPTION: - if (videoDescriptionView != null) { - videoDescriptionView.setText((Spanned) msg.obj); - videoDescriptionView.setVisibility(View.VISIBLE); - } - return true; - case BackgroundCallback.MESSAGE_UPLOADER_DATE: - if (videoUploadDateView != null) { - videoUploadDateView.setText((String) msg.obj); - videoUploadDateView.setVisibility(View.VISIBLE); - } - return true; - } - return false; - } - } - - public interface OnVideoPlayListener { - /** - * Called when a video is played - * @param videoStream the video stream that is played - * @param streamInfo the stream info - */ - void onVideoPlayed(VideoStream videoStream, StreamInfo streamInfo); - - /** - * Called when the audio is played in the background - * @param streamInfo the stream info - * @param audioStream the audio stream that is played - */ - void onBackgroundPlayed(StreamInfo streamInfo, AudioStream audioStream); - } -} +} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java new file mode 100644 index 000000000..4501ab859 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java @@ -0,0 +1,239 @@ +package org.schabi.newpipe.fragments.list; + +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v7.app.ActionBar; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.View; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.extractor.channel.ChannelInfoItem; +import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.fragments.BaseStateFragment; +import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; +import org.schabi.newpipe.info_list.InfoItemBuilder; +import org.schabi.newpipe.info_list.InfoListAdapter; +import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.StateSaver; + +import java.util.List; +import java.util.Queue; + +import static org.schabi.newpipe.util.AnimationUtils.animateView; + +public abstract class BaseListFragment extends BaseStateFragment implements ListViewContract, StateSaver.WriteRead { + + /*////////////////////////////////////////////////////////////////////////// + // Views + //////////////////////////////////////////////////////////////////////////*/ + + protected InfoListAdapter infoListAdapter; + protected RecyclerView itemsList; + + /*////////////////////////////////////////////////////////////////////////// + // LifeCycle + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onAttach(Context context) { + super.onAttach(context); + infoListAdapter = new InfoListAdapter(activity); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public void onDestroy() { + super.onDestroy(); + StateSaver.onDestroy(savedState); + } + + /*////////////////////////////////////////////////////////////////////////// + // State Saving + //////////////////////////////////////////////////////////////////////////*/ + + protected StateSaver.SavedState savedState; + + @Override + public String generateSuffix() { + // Naive solution, but it's good for now (the items don't change) + return "." + infoListAdapter.getItemsList().size() + ".list"; + } + + @Override + public void writeTo(Queue objectsToSave) { + objectsToSave.add(infoListAdapter.getItemsList()); + } + + @Override + @SuppressWarnings("unchecked") + public void readFrom(@NonNull Queue savedObjects) throws Exception { + infoListAdapter.getItemsList().clear(); + infoListAdapter.getItemsList().addAll((List) savedObjects.poll()); + } + + @Override + public void onSaveInstanceState(Bundle bundle) { + super.onSaveInstanceState(bundle); + savedState = StateSaver.tryToSave(activity.isChangingConfigurations(), savedState, bundle, this); + } + + @Override + protected void onRestoreInstanceState(@NonNull Bundle bundle) { + super.onRestoreInstanceState(bundle); + savedState = StateSaver.tryToRestore(bundle, this); + } + + /*////////////////////////////////////////////////////////////////////////// + // Init + //////////////////////////////////////////////////////////////////////////*/ + + protected View getListHeader() { + return null; + } + + protected View getListFooter() { + return activity.getLayoutInflater().inflate(R.layout.pignate_footer, itemsList, false); + } + + protected RecyclerView.LayoutManager getListLayoutManager() { + return new LinearLayoutManager(activity); + } + + @Override + protected void initViews(View rootView, Bundle savedInstanceState) { + super.initViews(rootView, savedInstanceState); + + itemsList = rootView.findViewById(R.id.items_list); + itemsList.setLayoutManager(getListLayoutManager()); + + infoListAdapter.setFooter(getListFooter()); + infoListAdapter.setHeader(getListHeader()); + + itemsList.setAdapter(infoListAdapter); + } + + protected void onItemSelected(InfoItem selectedItem) { + if (DEBUG) Log.d(TAG, "onItemSelected() called with: selectedItem = [" + selectedItem + "]"); + } + + @Override + protected void initListeners() { + super.initListeners(); + infoListAdapter.setOnStreamSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { + @Override + public void selected(StreamInfoItem selectedItem) { + onItemSelected(selectedItem); + NavigationHelper.openVideoDetailFragment(getFragmentManager(), selectedItem.service_id, selectedItem.url, selectedItem.name); + } + }); + + infoListAdapter.setOnChannelSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { + @Override + public void selected(ChannelInfoItem selectedItem) { + onItemSelected(selectedItem); + NavigationHelper.openChannelFragment(getFragmentManager(), selectedItem.service_id, selectedItem.url, selectedItem.name); + } + }); + + infoListAdapter.setOnPlaylistSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { + @Override + public void selected(PlaylistInfoItem selectedItem) { + onItemSelected(selectedItem); + NavigationHelper.openPlaylistFragment(getFragmentManager(), selectedItem.service_id, selectedItem.url, selectedItem.name); + } + }); + + itemsList.clearOnScrollListeners(); + itemsList.addOnScrollListener(new OnScrollBelowItemsListener() { + @Override + public void onScrolledDown(RecyclerView recyclerView) { + onScrollToBottom(); + } + }); + } + + protected void onScrollToBottom() { + if (hasMoreItems() && !isLoading.get()) { + loadMoreItems(); + } + } + + /*////////////////////////////////////////////////////////////////////////// + // Menu + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]"); + super.onCreateOptionsMenu(menu, inflater); + ActionBar supportActionBar = activity.getSupportActionBar(); + if (supportActionBar != null) { + supportActionBar.setDisplayShowTitleEnabled(true); + supportActionBar.setDisplayHomeAsUpEnabled(true); + } + } + + /*////////////////////////////////////////////////////////////////////////// + // Load and handle + //////////////////////////////////////////////////////////////////////////*/ + + protected abstract void loadMoreItems(); + + protected abstract boolean hasMoreItems(); + + /*////////////////////////////////////////////////////////////////////////// + // Contract + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void showLoading() { + super.showLoading(); + // animateView(itemsList, false, 400); + } + + @Override + public void hideLoading() { + super.hideLoading(); + animateView(itemsList, true, 300); + } + + @Override + public void showError(String message, boolean showRetryButton) { + super.showError(message, showRetryButton); + showListFooter(false); + animateView(itemsList, false, 200); + } + + @Override + public void showEmptyState() { + super.showEmptyState(); + showListFooter(false); + } + + @Override + public void showListFooter(final boolean show) { + itemsList.post(new Runnable() { + @Override + public void run() { + infoListAdapter.showFooter(show); + } + }); + } + + @Override + public void handleNextItems(N result) { + isLoading.set(false); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java new file mode 100644 index 000000000..34fcaf873 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java @@ -0,0 +1,216 @@ +package org.schabi.newpipe.fragments.list; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; + +import org.schabi.newpipe.extractor.ListExtractor; +import org.schabi.newpipe.extractor.ListInfo; + +import java.util.Queue; + +import icepick.State; +import io.reactivex.Single; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Consumer; +import io.reactivex.schedulers.Schedulers; + +public abstract class BaseListInfoFragment extends BaseListFragment { + + @State + protected int serviceId = -1; + @State + protected String name; + @State + protected String url; + + protected I currentInfo; + protected String currentNextItemsUrl; + protected Disposable currentWorker; + + @Override + protected void initViews(View rootView, Bundle savedInstanceState) { + super.initViews(rootView, savedInstanceState); + setTitle(name); + showListFooter(hasMoreItems()); + } + + @Override + public void onPause() { + super.onPause(); + if (currentWorker != null) currentWorker.dispose(); + } + + @Override + public void onResume() { + super.onResume(); + // Check if it was loading when the fragment was stopped/paused, + if (wasLoading.getAndSet(false)) { + if (hasMoreItems() && infoListAdapter.getItemsList().size() > 0) { + loadMoreItems(); + } else { + doInitialLoadLogic(); + } + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (currentWorker != null) currentWorker.dispose(); + currentWorker = null; + } + + /*////////////////////////////////////////////////////////////////////////// + // State Saving + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void writeTo(Queue objectsToSave) { + super.writeTo(objectsToSave); + objectsToSave.add(currentInfo); + objectsToSave.add(currentNextItemsUrl); + } + + @Override + @SuppressWarnings("unchecked") + public void readFrom(@NonNull Queue savedObjects) throws Exception { + super.readFrom(savedObjects); + currentInfo = (I) savedObjects.poll(); + currentNextItemsUrl = (String) savedObjects.poll(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + public void setTitle(String title) { + Log.d(TAG, "setTitle() called with: title = [" + title + "]"); + if (activity.getSupportActionBar() != null) { + activity.getSupportActionBar().setTitle(title); + } + } + + /*////////////////////////////////////////////////////////////////////////// + // Load and handle + //////////////////////////////////////////////////////////////////////////*/ + + protected void doInitialLoadLogic() { + if (DEBUG) Log.d(TAG, "doInitialLoadLogic() called"); + if (currentInfo == null) { + startLoading(false); + } else handleResult(currentInfo); + } + + /** + * Implement the logic to load the info from the network.
+ * You can use the default implementations from {@link org.schabi.newpipe.util.ExtractorHelper}. + * + * @param forceLoad allow or disallow the result to come from the cache + */ + protected abstract Single loadResult(boolean forceLoad); + + @Override + public void startLoading(boolean forceLoad) { + super.startLoading(forceLoad); + + showListFooter(false); + currentInfo = null; + if (currentWorker != null) currentWorker.dispose(); + currentWorker = loadResult(forceLoad) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Consumer() { + @Override + public void accept(@NonNull I result) throws Exception { + isLoading.set(false); + currentInfo = result; + currentNextItemsUrl = result.next_streams_url; + handleResult(result); + } + }, new Consumer() { + @Override + public void accept(@NonNull Throwable throwable) throws Exception { + onError(throwable); + } + }); + } + + /** + * Implement the logic to load more items
+ * You can use the default implementations from {@link org.schabi.newpipe.util.ExtractorHelper} + */ + protected abstract Single loadMoreItemsLogic(); + + protected void loadMoreItems() { + isLoading.set(true); + + if (currentWorker != null) currentWorker.dispose(); + currentWorker = loadMoreItemsLogic() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Consumer() { + @Override + public void accept(@io.reactivex.annotations.NonNull ListExtractor.NextItemsResult nextItemsResult) throws Exception { + isLoading.set(false); + handleNextItems(nextItemsResult); + } + }, new Consumer() { + @Override + public void accept(@io.reactivex.annotations.NonNull Throwable throwable) throws Exception { + isLoading.set(false); + onError(throwable); + } + }); + } + + @Override + public void handleNextItems(ListExtractor.NextItemsResult result) { + super.handleNextItems(result); + currentNextItemsUrl = result.nextItemsUrl; + infoListAdapter.addInfoItemList(result.nextItemsList); + + showListFooter(hasMoreItems()); + } + + @Override + protected boolean hasMoreItems() { + return !TextUtils.isEmpty(currentNextItemsUrl); + } + + /*////////////////////////////////////////////////////////////////////////// + // Contract + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void handleResult(@NonNull I result) { + super.handleResult(result); + + url = result.url; + name = result.name; + setTitle(name); + + if (infoListAdapter.getItemsList().size() == 0) { + if (result.related_streams.size() > 0) { + infoListAdapter.addInfoItemList(result.related_streams); + showListFooter(hasMoreItems()); + } else { + infoListAdapter.clearStreamItemList(); + showEmptyState(); + } + } + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + protected void setInitialData(int serviceId, String url, String name) { + this.serviceId = serviceId; + this.url = url; + this.name = !TextUtils.isEmpty(name) ? name : ""; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/ListViewContract.java b/app/src/main/java/org/schabi/newpipe/fragments/list/ListViewContract.java new file mode 100644 index 000000000..161d5d524 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/ListViewContract.java @@ -0,0 +1,9 @@ +package org.schabi.newpipe.fragments.list; + +import org.schabi.newpipe.fragments.ViewContract; + +public interface ListViewContract extends ViewContract { + void showListFooter(boolean show); + + void handleNextItems(N result); +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java new file mode 100644 index 000000000..8645c94d6 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -0,0 +1,383 @@ +package org.schabi.newpipe.fragments.list.channel; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.content.ContextCompat; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.TextView; + +import com.jakewharton.rxbinding2.view.RxView; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.subscription.SubscriptionEntity; +import org.schabi.newpipe.extractor.ListExtractor; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.channel.ChannelInfo; +import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import org.schabi.newpipe.fragments.list.BaseListInfoFragment; +import org.schabi.newpipe.fragments.subscription.SubscriptionService; +import org.schabi.newpipe.report.UserAction; +import org.schabi.newpipe.util.AnimationUtils; +import org.schabi.newpipe.util.ExtractorHelper; +import org.schabi.newpipe.util.Localization; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import io.reactivex.Observable; +import io.reactivex.Single; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Action; +import io.reactivex.functions.Consumer; +import io.reactivex.functions.Function; +import io.reactivex.schedulers.Schedulers; + +import static org.schabi.newpipe.util.AnimationUtils.animateBackgroundColor; +import static org.schabi.newpipe.util.AnimationUtils.animateTextColor; +import static org.schabi.newpipe.util.AnimationUtils.animateView; + +public class ChannelFragment extends BaseListInfoFragment { + + private CompositeDisposable disposables = new CompositeDisposable(); + private Disposable subscribeButtonMonitor; + private SubscriptionService subscriptionService; + + /*////////////////////////////////////////////////////////////////////////// + // Views + //////////////////////////////////////////////////////////////////////////*/ + + private View headerRootLayout; + private ImageView headerChannelBanner; + private ImageView headerAvatarView; + private TextView headerTitleView; + private TextView headerSubscribersTextView; + private Button headerSubscribeButton; + + private MenuItem menuRssButton; + + public static ChannelFragment getInstance(int serviceId, String url, String name) { + ChannelFragment instance = new ChannelFragment(); + instance.setInitialData(serviceId, url, name); + return instance; + } + + /*////////////////////////////////////////////////////////////////////////// + // LifeCycle + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onAttach(Context context) { + super.onAttach(context); + subscriptionService = SubscriptionService.getInstance(); + } + + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_channel, container, false); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (disposables != null) disposables.clear(); + if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Init + //////////////////////////////////////////////////////////////////////////*/ + + protected View getListHeader() { + headerRootLayout = activity.getLayoutInflater().inflate(R.layout.channel_header, itemsList, false); + headerChannelBanner = headerRootLayout.findViewById(R.id.channel_banner_image); + headerAvatarView = headerRootLayout.findViewById(R.id.channel_avatar_view); + headerTitleView = headerRootLayout.findViewById(R.id.channel_title_view); + headerSubscribersTextView = headerRootLayout.findViewById(R.id.channel_subscriber_view); + headerSubscribeButton = headerRootLayout.findViewById(R.id.channel_subscribe_button); + return headerRootLayout; + } + + /*////////////////////////////////////////////////////////////////////////// + // Menu + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]"); + super.onCreateOptionsMenu(menu, inflater); + inflater.inflate(R.menu.menu_channel, menu); + + menuRssButton = menu.findItem(R.id.menu_item_rss); + if (currentInfo != null) { + menuRssButton.setVisible(!TextUtils.isEmpty(currentInfo.feed_url)); + } + + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_item_rss: { + Intent intent = new Intent(); + intent.setAction(Intent.ACTION_VIEW); + intent.setData(Uri.parse(currentInfo.feed_url)); + startActivity(intent); + return true; + } + default: + return super.onOptionsItemSelected(item); + } + } + + /*////////////////////////////////////////////////////////////////////////// + // Channel Subscription + //////////////////////////////////////////////////////////////////////////*/ + + private static final int BUTTON_DEBOUNCE_INTERVAL = 100; + + private void monitorSubscription(final ChannelInfo info) { + final Consumer onError = new Consumer() { + @Override + public void accept(Throwable throwable) throws Exception { + animateView(headerSubscribeButton, false, 100); + showSnackBarError(throwable, UserAction.SUBSCRIPTION, NewPipe.getNameOfService(currentInfo.service_id), "Get subscription status", 0); + } + }; + + final Observable> observable = subscriptionService.subscriptionTable() + .getSubscription(info.service_id, info.url) + .toObservable(); + + disposables.add(observable + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(getSubscribeUpdateMonitor(info), onError)); + + disposables.add(observable + // Some updates are very rapid (when calling the updateSubscription(info), for example) + // so only update the UI for the latest emission ("sync" the subscribe button's state) + .debounce(100, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Consumer>() { + @Override + public void accept(List subscriptionEntities) throws Exception { + updateSubscribeButton(!subscriptionEntities.isEmpty()); + } + }, onError)); + + } + + private Function mapOnSubscribe(final SubscriptionEntity subscription) { + return new Function() { + @Override + public Object apply(@NonNull Object o) throws Exception { + subscriptionService.subscriptionTable().insert(subscription); + return o; + } + }; + } + + private Function mapOnUnsubscribe(final SubscriptionEntity subscription) { + return new Function() { + @Override + public Object apply(@NonNull Object o) throws Exception { + subscriptionService.subscriptionTable().delete(subscription); + return o; + } + }; + } + + private void updateSubscription(final ChannelInfo info) { + if (DEBUG) Log.d(TAG, "updateSubscription() called with: info = [" + info + "]"); + final Action onComplete = new Action() { + @Override + public void run() throws Exception { + if (DEBUG) Log.d(TAG, "Updated subscription: " + info.url); + } + }; + + final Consumer onError = new Consumer() { + @Override + public void accept(@NonNull Throwable throwable) throws Exception { + onUnrecoverableError(throwable, UserAction.SUBSCRIPTION, NewPipe.getNameOfService(info.service_id), "Updating Subscription for " + info.url, R.string.subscription_update_failed); + } + }; + + disposables.add(subscriptionService.updateChannelInfo(info) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(onComplete, onError)); + } + + private Disposable monitorSubscribeButton(final Button subscribeButton, final Function action) { + final Consumer onNext = new Consumer() { + @Override + public void accept(@NonNull Object o) throws Exception { + if (DEBUG) Log.d(TAG, "Changed subscription status to this channel!"); + } + }; + + final Consumer onError = new Consumer() { + @Override + public void accept(@NonNull Throwable throwable) throws Exception { + onUnrecoverableError(throwable, UserAction.SUBSCRIPTION, NewPipe.getNameOfService(currentInfo.service_id), "Subscription Change", R.string.subscription_change_failed); + } + }; + + /* Emit clicks from main thread unto io thread */ + return RxView.clicks(subscribeButton) + .subscribeOn(AndroidSchedulers.mainThread()) + .observeOn(Schedulers.io()) + .debounce(BUTTON_DEBOUNCE_INTERVAL, TimeUnit.MILLISECONDS) // Ignore rapid clicks + .map(action) + .subscribe(onNext, onError); + } + + private Consumer> getSubscribeUpdateMonitor(final ChannelInfo info) { + return new Consumer>() { + @Override + public void accept(List subscriptionEntities) throws Exception { + if (DEBUG) + Log.d(TAG, "subscriptionService.subscriptionTable.doOnNext() called with: subscriptionEntities = [" + subscriptionEntities + "]"); + if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose(); + + if (subscriptionEntities.isEmpty()) { + if (DEBUG) Log.d(TAG, "No subscription to this channel!"); + SubscriptionEntity channel = new SubscriptionEntity(); + channel.setServiceId(info.service_id); + channel.setUrl(info.url); + channel.setData(info.name, info.avatar_url, info.description, info.subscriber_count); + subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnSubscribe(channel)); + } else { + if (DEBUG) Log.d(TAG, "Found subscription to this channel!"); + final SubscriptionEntity subscription = subscriptionEntities.get(0); + subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnUnsubscribe(subscription)); + } + } + }; + } + + private void updateSubscribeButton(boolean isSubscribed) { + if (DEBUG) Log.d(TAG, "updateSubscribeButton() called with: isSubscribed = [" + isSubscribed + "]"); + + boolean isButtonVisible = headerSubscribeButton.getVisibility() == View.VISIBLE; + int backgroundDuration = isButtonVisible ? 300 : 0; + int textDuration = isButtonVisible ? 200 : 0; + + int subscribeBackground = ContextCompat.getColor(activity, R.color.subscribe_background_color); + int subscribeText = ContextCompat.getColor(activity, R.color.subscribe_text_color); + int subscribedBackground = ContextCompat.getColor(activity, R.color.subscribed_background_color); + int subscribedText = ContextCompat.getColor(activity, R.color.subscribed_text_color); + + if (!isSubscribed) { + headerSubscribeButton.setText(R.string.subscribe_button_title); + animateBackgroundColor(headerSubscribeButton, backgroundDuration, subscribedBackground, subscribeBackground); + animateTextColor(headerSubscribeButton, textDuration, subscribedText, subscribeText); + } else { + headerSubscribeButton.setText(R.string.subscribed_button_title); + animateBackgroundColor(headerSubscribeButton, backgroundDuration, subscribeBackground, subscribedBackground); + animateTextColor(headerSubscribeButton, textDuration, subscribeText, subscribedText); + } + + animateView(headerSubscribeButton, AnimationUtils.Type.LIGHT_SCALE_AND_ALPHA, true, 100); + } + + /*////////////////////////////////////////////////////////////////////////// + // Load and handle + //////////////////////////////////////////////////////////////////////////*/ + + @Override + protected Single loadMoreItemsLogic() { + return ExtractorHelper.getMoreChannelItems(serviceId, currentNextItemsUrl); + } + + @Override + protected Single loadResult(boolean forceLoad) { + return ExtractorHelper.getChannelInfo(serviceId, url, forceLoad); + } + + /*////////////////////////////////////////////////////////////////////////// + // Contract + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void showLoading() { + super.showLoading(); + + imageLoader.cancelDisplayTask(headerChannelBanner); + imageLoader.cancelDisplayTask(headerAvatarView); + animateView(headerSubscribeButton, false, 100); + } + + @Override + public void handleResult(@NonNull ChannelInfo result) { + super.handleResult(result); + + headerRootLayout.setVisibility(View.VISIBLE); + imageLoader.displayImage(result.banner_url, headerChannelBanner, DISPLAY_BANNER_OPTIONS); + imageLoader.displayImage(result.avatar_url, headerAvatarView, DISPLAY_AVATAR_OPTIONS); + + if (result.subscriber_count != -1) { + headerSubscribersTextView.setText(Localization.localizeSubscribersCount(activity, result.subscriber_count)); + headerSubscribersTextView.setVisibility(View.VISIBLE); + } else headerSubscribersTextView.setVisibility(View.GONE); + + if (menuRssButton != null) menuRssButton.setVisible(!TextUtils.isEmpty(result.feed_url)); + + if (!result.errors.isEmpty()) { + showSnackBarError(result.errors, UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(result.service_id), result.url, 0); + } + + if (disposables != null) disposables.clear(); + if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose(); + updateSubscription(result); + monitorSubscription(result); + } + + @Override + public void handleNextItems(ListExtractor.NextItemsResult result) { + super.handleNextItems(result); + + if (!result.errors.isEmpty()) { + showSnackBarError(result.errors, UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(serviceId), + "Get next page of: " + url, R.string.general_error); + } + } + + /*////////////////////////////////////////////////////////////////////////// + // OnError + //////////////////////////////////////////////////////////////////////////*/ + + @Override + protected boolean onError(Throwable exception) { + if (super.onError(exception)) return true; + + int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error; + onUnrecoverableError(exception, UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(serviceId), url, errorId); + return true; + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void setTitle(String title) { + super.setTitle(title); + headerTitleView.setText(title); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/feed/FeedFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/feed/FeedFragment.java new file mode 100644 index 000000000..2af9a9270 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/feed/FeedFragment.java @@ -0,0 +1,445 @@ +package org.schabi.newpipe.fragments.list.feed; + +import android.os.Bundle; +import android.os.Handler; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v7.app.ActionBar; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.View; +import android.view.ViewGroup; + +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.subscription.SubscriptionEntity; +import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.channel.ChannelInfo; +import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import org.schabi.newpipe.fragments.list.BaseListFragment; +import org.schabi.newpipe.fragments.subscription.SubscriptionService; +import org.schabi.newpipe.report.UserAction; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import io.reactivex.Flowable; +import io.reactivex.MaybeObserver; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Action; +import io.reactivex.functions.Consumer; +import io.reactivex.functions.Predicate; +import io.reactivex.schedulers.Schedulers; + +public class FeedFragment extends BaseListFragment, Void> { + + private static final int OFF_SCREEN_ITEMS_COUNT = 3; + private static final int MIN_ITEMS_INITIAL_LOAD = 8; + private int FEED_LOAD_COUNT = MIN_ITEMS_INITIAL_LOAD; + + private int subscriptionPoolSize; + + private SubscriptionService subscriptionService; + + private AtomicBoolean allItemsLoaded = new AtomicBoolean(false); + private HashSet itemsLoaded = new HashSet<>(); + private final AtomicInteger requestLoadedAtomic = new AtomicInteger(); + + private CompositeDisposable compositeDisposable = new CompositeDisposable(); + private Disposable subscriptionObserver; + private Subscription feedSubscriber; + + /*////////////////////////////////////////////////////////////////////////// + // Fragment LifeCycle + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + subscriptionService = SubscriptionService.getInstance(); + + FEED_LOAD_COUNT = howManyItemsToLoad(); + } + + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_feed, container, false); + } + + @Override + public void onPause() { + super.onPause(); + disposeEverything(); + } + + @Override + public void onResume() { + super.onResume(); + if (wasLoading.get()) doInitialLoadLogic(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + + disposeEverything(); + subscriptionService = null; + compositeDisposable = null; + subscriptionObserver = null; + feedSubscriber = null; + } + + @Override + public void onDestroyView() { + // Do not monitor for updates when user is not viewing the feed fragment. + // This is a waste of bandwidth. + disposeEverything(); + super.onDestroyView(); + } + + /*@Override + protected RecyclerView.LayoutManager getListLayoutManager() { + boolean isPortrait = getResources().getDisplayMetrics().heightPixels > getResources().getDisplayMetrics().widthPixels; + return new GridLayoutManager(activity, isPortrait ? 1 : 2); + }*/ + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + + ActionBar supportActionBar = activity.getSupportActionBar(); + if (supportActionBar != null) { + supportActionBar.setTitle(R.string.fragment_whats_new); + } + } + + @Override + public void reloadContent() { + resetFragment(); + super.reloadContent(); + } + + /*////////////////////////////////////////////////////////////////////////// + // StateSaving + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void writeTo(Queue objectsToSave) { + super.writeTo(objectsToSave); + objectsToSave.add(allItemsLoaded); + objectsToSave.add(itemsLoaded); + } + + @Override + @SuppressWarnings("unchecked") + public void readFrom(@NonNull Queue savedObjects) throws Exception { + super.readFrom(savedObjects); + allItemsLoaded = (AtomicBoolean) savedObjects.poll(); + itemsLoaded = (HashSet) savedObjects.poll(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Feed Loader + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void startLoading(boolean forceLoad) { + if (DEBUG) Log.d(TAG, "startLoading() called with: forceLoad = [" + forceLoad + "]"); + if (subscriptionObserver != null) subscriptionObserver.dispose(); + + if (allItemsLoaded.get()) { + if (infoListAdapter.getItemsList().size() == 0) { + showEmptyState(); + } else { + showListFooter(false); + hideLoading(); + } + + isLoading.set(false); + return; + } + + isLoading.set(true); + showLoading(); + showListFooter(true); + subscriptionObserver = subscriptionService.getSubscription() + .onErrorReturnItem(Collections.emptyList()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Consumer>() { + @Override + public void accept(List subscriptionEntities) throws Exception { + handleResult(subscriptionEntities); + } + }, new Consumer() { + @Override + public void accept(Throwable throwable) throws Exception { + onError(throwable); + } + }); + } + + @Override + public void handleResult(@android.support.annotation.NonNull List result) { + super.handleResult(result); + + if (result.isEmpty()) { + infoListAdapter.clearStreamItemList(); + showEmptyState(); + return; + } + + subscriptionPoolSize = result.size(); + Flowable.fromIterable(result) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(getSubscriptionObserver()); + } + + /** + * Responsible for reacting to user pulling request and starting a request for new feed stream. + *

+ * On initialization, it automatically requests the amount of feed needed to display + * a minimum amount required (FEED_LOAD_SIZE). + *

+ * Upon receiving a user pull, it creates a Single Observer to fetch the ChannelInfo + * containing the feed streams. + **/ + private Subscriber getSubscriptionObserver() { + return new Subscriber() { + @Override + public void onSubscribe(Subscription s) { + if (feedSubscriber != null) feedSubscriber.cancel(); + feedSubscriber = s; + + int requestSize = FEED_LOAD_COUNT - infoListAdapter.getItemsList().size(); + if (wasLoading.getAndSet(false)) requestSize = FEED_LOAD_COUNT; + + boolean hasToLoad = requestSize > 0; + if (hasToLoad) { + requestLoadedAtomic.set(infoListAdapter.getItemsList().size()); + requestFeed(requestSize); + } + isLoading.set(hasToLoad); + } + + @Override + public void onNext(SubscriptionEntity subscriptionEntity) { + if (!itemsLoaded.contains(subscriptionEntity.getServiceId() + subscriptionEntity.getUrl())) { + subscriptionService.getChannelInfo(subscriptionEntity) + .observeOn(AndroidSchedulers.mainThread()) + .onErrorComplete(new Predicate() { + @Override + public boolean test(@io.reactivex.annotations.NonNull Throwable throwable) throws Exception { + return FeedFragment.super.onError(throwable); + } + }) + .subscribe(getChannelInfoObserver(subscriptionEntity.getServiceId(), subscriptionEntity.getUrl())); + } else { + requestFeed(1); + } + } + + @Override + public void onError(Throwable exception) { + FeedFragment.this.onError(exception); + } + + @Override + public void onComplete() { + if (DEBUG) Log.d(TAG, "getSubscriptionObserver > onComplete() called"); + } + }; + } + + /** + * On each request, a subscription item from the updated table is transformed + * into a ChannelInfo, containing the latest streams from the channel. + *

+ * Currently, the feed uses the first into from the list of streams. + *

+ * If chosen feed already displayed, then we request another feed from another + * subscription, until the subscription table runs out of new items. + *

+ * This Observer is self-contained and will dispose itself when complete. However, this + * does not obey the fragment lifecycle and may continue running in the background + * until it is complete. This is done due to RxJava2 no longer propagate errors once + * an observer is unsubscribed while the thread process is still running. + *

+ * To solve the above issue, we can either set a global RxJava Error Handler, or + * manage exceptions case by case. This should be done if the current implementation is + * too costly when dealing with larger subscription sets. + * + * @param url + serviceId to put in {@link #allItemsLoaded} to signal that this specific entity has been loaded. + */ + private MaybeObserver getChannelInfoObserver(final int serviceId, final String url) { + return new MaybeObserver() { + private Disposable observer; + + @Override + public void onSubscribe(Disposable d) { + observer = d; + compositeDisposable.add(d); + isLoading.set(true); + } + + // Called only when response is non-empty + @Override + public void onSuccess(final ChannelInfo channelInfo) { + if (infoListAdapter == null || channelInfo.related_streams.isEmpty()) { + onDone(); + return; + } + + final InfoItem item = channelInfo.related_streams.get(0); + // Keep requesting new items if the current one already exists + boolean itemExists = doesItemExist(infoListAdapter.getItemsList(), item); + if (!itemExists) { + infoListAdapter.addInfoItem(item); + //updateSubscription(channelInfo); + } else { + requestFeed(1); + } + onDone(); + } + + @Override + public void onError(Throwable exception) { + showSnackBarError(exception, UserAction.SUBSCRIPTION, NewPipe.getNameOfService(serviceId), url, 0); + requestFeed(1); + onDone(); + } + + // Called only when response is empty + @Override + public void onComplete() { + onDone(); + } + + private void onDone() { + if (observer.isDisposed()) { + return; + } + + itemsLoaded.add(serviceId + url); + compositeDisposable.remove(observer); + + int loaded = requestLoadedAtomic.incrementAndGet(); + if (loaded >= Math.min(FEED_LOAD_COUNT, subscriptionPoolSize)) { + requestLoadedAtomic.set(0); + isLoading.set(false); + } + + if (itemsLoaded.size() == subscriptionPoolSize) { + if (DEBUG) Log.d(TAG, "getChannelInfoObserver > All Items Loaded"); + allItemsLoaded.set(true); + showListFooter(false); + isLoading.set(false); + hideLoading(); + if (infoListAdapter.getItemsList().size() == 0) { + showEmptyState(); + } + } + } + }; + } + + @Override + protected void loadMoreItems() { + isLoading.set(true); + delayHandler.removeCallbacksAndMessages(null); + // Add a little of a delay when requesting more items because the cache is so fast, + // that the view seems stuck to the user when he scroll to the bottom + delayHandler.postDelayed(new Runnable() { + @Override + public void run() { + requestFeed(FEED_LOAD_COUNT); + } + }, 300); + } + + @Override + protected boolean hasMoreItems() { + return !allItemsLoaded.get(); + } + + private final Handler delayHandler = new Handler(); + + private void requestFeed(final int count) { + if (DEBUG) Log.d(TAG, "requestFeed() called with: count = [" + count + "], feedSubscriber = [" + feedSubscriber + "]"); + if (feedSubscriber == null) return; + + isLoading.set(true); + delayHandler.removeCallbacksAndMessages(null); + feedSubscriber.request(count); + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + private void resetFragment() { + if (DEBUG) Log.d(TAG, "resetFragment() called"); + if (subscriptionObserver != null) subscriptionObserver.dispose(); + if (compositeDisposable != null) compositeDisposable.clear(); + if (infoListAdapter != null) infoListAdapter.clearStreamItemList(); + + delayHandler.removeCallbacksAndMessages(null); + requestLoadedAtomic.set(0); + allItemsLoaded.set(false); + showListFooter(false); + itemsLoaded.clear(); + } + + private void disposeEverything() { + if (subscriptionObserver != null) subscriptionObserver.dispose(); + if (compositeDisposable != null) compositeDisposable.clear(); + if (feedSubscriber != null) feedSubscriber.cancel(); + delayHandler.removeCallbacksAndMessages(null); + } + + private boolean doesItemExist(final List items, final InfoItem item) { + for (final InfoItem existingItem : items) { + if (existingItem.info_type == item.info_type && + existingItem.service_id == item.service_id && + existingItem.name.equals(item.name) && + existingItem.url.equals(item.url)) return true; + } + return false; + } + + private int howManyItemsToLoad() { + int heightPixels = getResources().getDisplayMetrics().heightPixels; + int itemHeightPixels = activity.getResources().getDimensionPixelSize(R.dimen.video_item_search_height); + + int items = itemHeightPixels > 0 ? heightPixels / itemHeightPixels + OFF_SCREEN_ITEMS_COUNT : MIN_ITEMS_INITIAL_LOAD; + return Math.max(MIN_ITEMS_INITIAL_LOAD, items); + } + + /*////////////////////////////////////////////////////////////////////////// + // Fragment Error Handling + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void showError(String message, boolean showRetryButton) { + resetFragment(); + super.showError(message, showRetryButton); + } + + @Override + protected boolean onError(Throwable exception) { + if (super.onError(exception)) return true; + + int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error; + onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, "none", "Requesting feed", errorId); + return true; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java new file mode 100644 index 000000000..1ba700fd6 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java @@ -0,0 +1,174 @@ +package org.schabi.newpipe.fragments.list.playlist; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.ListExtractor; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import org.schabi.newpipe.extractor.playlist.PlaylistInfo; +import org.schabi.newpipe.fragments.list.BaseListInfoFragment; +import org.schabi.newpipe.report.UserAction; +import org.schabi.newpipe.util.ExtractorHelper; +import org.schabi.newpipe.util.NavigationHelper; + +import io.reactivex.Single; + +import static org.schabi.newpipe.util.AnimationUtils.animateView; + +public class PlaylistFragment extends BaseListInfoFragment { + + /*////////////////////////////////////////////////////////////////////////// + // Views + //////////////////////////////////////////////////////////////////////////*/ + + private View headerRootLayout; + private TextView headerTitleView; + private View headerUploaderLayout; + private TextView headerUploaderName; + private ImageView headerUploaderAvatar; + private TextView headerStreamCount; + + public static PlaylistFragment getInstance(int serviceId, String url, String name) { + PlaylistFragment instance = new PlaylistFragment(); + instance.setInitialData(serviceId, url, name); + return instance; + } + + /*////////////////////////////////////////////////////////////////////////// + // LifeCycle + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_playlist, container, false); + } + + /*////////////////////////////////////////////////////////////////////////// + // Init + //////////////////////////////////////////////////////////////////////////*/ + + protected View getListHeader() { + headerRootLayout = activity.getLayoutInflater().inflate(R.layout.playlist_header, itemsList, false); + headerTitleView = headerRootLayout.findViewById(R.id.playlist_title_view); + headerUploaderLayout = headerRootLayout.findViewById(R.id.uploader_layout); + headerUploaderName = headerRootLayout.findViewById(R.id.uploader_name); + headerUploaderAvatar = headerRootLayout.findViewById(R.id.uploader_avatar_view); + headerStreamCount = headerRootLayout.findViewById(R.id.playlist_stream_count); + + return headerRootLayout; + } + + @Override + protected void initViews(View rootView, Bundle savedInstanceState) { + super.initViews(rootView, savedInstanceState); + + infoListAdapter.useMiniItemVariants(true); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]"); + super.onCreateOptionsMenu(menu, inflater); + inflater.inflate(R.menu.menu_playlist, menu); + } + + /*////////////////////////////////////////////////////////////////////////// + // Load and handle + //////////////////////////////////////////////////////////////////////////*/ + + @Override + protected Single loadMoreItemsLogic() { + return ExtractorHelper.getMorePlaylistItems(serviceId, currentNextItemsUrl); + } + + @Override + protected Single loadResult(boolean forceLoad) { + return ExtractorHelper.getPlaylistInfo(serviceId, url, forceLoad); + } + + /*////////////////////////////////////////////////////////////////////////// + // Contract + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void showLoading() { + super.showLoading(); + animateView(headerRootLayout, false, 200); + animateView(itemsList, false, 100); + + imageLoader.cancelDisplayTask(headerUploaderAvatar); + animateView(headerUploaderLayout, false, 200); + } + + @Override + public void handleResult(@NonNull final PlaylistInfo result) { + super.handleResult(result); + + animateView(headerRootLayout, true, 100); + animateView(headerUploaderLayout, true, 300); + headerUploaderLayout.setOnClickListener(null); + if (!TextUtils.isEmpty(result.uploader_name)) { + headerUploaderName.setText(result.uploader_name); + if (!TextUtils.isEmpty(result.uploader_url)) { + headerUploaderLayout.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + NavigationHelper.openChannelFragment(getFragmentManager(), result.service_id, result.uploader_url, result.uploader_name); + } + }); + } + } + + imageLoader.displayImage(result.uploader_avatar_url, headerUploaderAvatar, DISPLAY_AVATAR_OPTIONS); + headerStreamCount.setText(result.stream_count + " videos"); + + if (!result.errors.isEmpty()) { + showSnackBarError(result.errors, UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(result.service_id), result.url, 0); + } + } + + @Override + public void handleNextItems(ListExtractor.NextItemsResult result) { + super.handleNextItems(result); + + if (!result.errors.isEmpty()) { + showSnackBarError(result.errors, UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(serviceId) + , "Get next page of: " + url, 0); + } + } + + /*////////////////////////////////////////////////////////////////////////// + // OnError + //////////////////////////////////////////////////////////////////////////*/ + + @Override + protected boolean onError(Throwable exception) { + if (super.onError(exception)) return true; + + int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error; + onUnrecoverableError(exception, UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(serviceId), url, errorId); + return true; + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void setTitle(String title) { + super.setTitle(title); + headerTitleView.setText(title); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java new file mode 100644 index 000000000..653c50109 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java @@ -0,0 +1,695 @@ +package org.schabi.newpipe.fragments.list.search; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Build; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v7.app.ActionBar; +import android.support.v7.widget.TooltipCompat; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.util.Log; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.DecelerateInterpolator; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.AdapterView; +import android.widget.AutoCompleteTextView; +import android.widget.TextView; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.ReCaptchaActivity; +import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.extractor.ListExtractor; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.exceptions.ParsingException; +import org.schabi.newpipe.extractor.search.SearchEngine; +import org.schabi.newpipe.extractor.search.SearchResult; +import org.schabi.newpipe.fragments.list.BaseListFragment; +import org.schabi.newpipe.history.HistoryListener; +import org.schabi.newpipe.report.UserAction; +import org.schabi.newpipe.util.ExtractorHelper; +import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.StateSaver; + +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.TimeUnit; + +import icepick.State; +import io.reactivex.Notification; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Consumer; +import io.reactivex.functions.Function; +import io.reactivex.functions.Predicate; +import io.reactivex.schedulers.Schedulers; +import io.reactivex.subjects.PublishSubject; + +import static org.schabi.newpipe.util.AnimationUtils.animateView; + +public class SearchFragment extends BaseListFragment { + + /*////////////////////////////////////////////////////////////////////////// + // Search + //////////////////////////////////////////////////////////////////////////*/ + + /** + * The suggestions will appear only if the query meet this threshold (>=). + */ + private static final int THRESHOLD_SUGGESTION = 3; + + /** + * How much time have to pass without emitting a item (i.e. the user stop typing) to fetch/show the suggestions, in milliseconds. + */ + private static final int SUGGESTIONS_DEBOUNCE = 150; //ms + + @State + protected int filterItemCheckedId = -1; + private SearchEngine.Filter filter = SearchEngine.Filter.ANY; + + @State + protected int serviceId = -1; + @State + protected String searchQuery = ""; + @State + protected boolean wasSearchFocused = false; + + private int currentPage = 0; + private int currentNextPage = 0; + private String searchLanguage; + private boolean showSuggestions = true; + + private PublishSubject suggestionPublisher = PublishSubject.create(); + private Disposable searchDisposable; + private Disposable suggestionWorkerDisposable; + + private SuggestionListAdapter suggestionListAdapter; + + /*////////////////////////////////////////////////////////////////////////// + // Views + //////////////////////////////////////////////////////////////////////////*/ + + private View searchToolbarContainer; + private AutoCompleteTextView searchEditText; + private View searchClear; + + /*////////////////////////////////////////////////////////////////////////*/ + + public static SearchFragment getInstance(int serviceId, String query) { + SearchFragment searchFragment = new SearchFragment(); + searchFragment.setQuery(serviceId, query); + searchFragment.searchOnResume(); + return searchFragment; + } + + /** + * Set wasLoading to true so when the fragment onResume is called, the initial search is done. + * (it will only start searching if the query is not null or empty) + */ + private void searchOnResume() { + if (!TextUtils.isEmpty(searchQuery)) { + wasLoading.set(true); + } + } + + /*////////////////////////////////////////////////////////////////////////// + // Fragment's LifeCycle + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onAttach(Context context) { + super.onAttach(context); + suggestionListAdapter = new SuggestionListAdapter(activity); + } + + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_search, container, false); + } + + @Override + public void onPause() { + super.onPause(); + wasSearchFocused = searchEditText.hasFocus(); + + if (searchDisposable != null) searchDisposable.dispose(); + if (suggestionWorkerDisposable != null) suggestionWorkerDisposable.dispose(); + hideSoftKeyboard(searchEditText); + } + + @Override + public void onResume() { + if (DEBUG) Log.d(TAG, "onResume() called"); + super.onResume(); + + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity); + showSuggestions = preferences.getBoolean(getString(R.string.show_search_suggestions_key), true); + searchLanguage = preferences.getString(getString(R.string.search_language_key), getString(R.string.default_language_value)); + + if (!TextUtils.isEmpty(searchQuery)) { + if (wasLoading.getAndSet(false)) { + if (currentNextPage > currentPage) loadMoreItems(); + else search(searchQuery); + } else if (infoListAdapter.getItemsList().size() == 0) { + if (savedState == null) { + search(searchQuery); + } else if (!isLoading.get() && !wasSearchFocused) { + infoListAdapter.clearStreamItemList(); + showEmptyState(); + } + } + } + + if (suggestionWorkerDisposable == null || suggestionWorkerDisposable.isDisposed()) initSuggestionObserver(); + } + + @Override + public void onDestroyView() { + if (DEBUG) Log.d(TAG, "onDestroyView() called"); + unsetSearchListeners(); + super.onDestroyView(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (!activity.isChangingConfigurations()) StateSaver.onDestroy(savedState); + + if (searchDisposable != null) searchDisposable.dispose(); + if (suggestionWorkerDisposable != null) suggestionWorkerDisposable.dispose(); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case ReCaptchaActivity.RECAPTCHA_REQUEST: + if (resultCode == Activity.RESULT_OK && searchQuery.length() != 0) { + search(searchQuery); + } else Log.e(TAG, "ReCaptcha failed"); + break; + + default: + Log.e(TAG, "Request code from activity not supported [" + requestCode + "]"); + break; + } + } + + /*////////////////////////////////////////////////////////////////////////// + // State Saving + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void writeTo(Queue objectsToSave) { + super.writeTo(objectsToSave); + objectsToSave.add(currentPage); + objectsToSave.add(currentNextPage); + } + + @Override + public void readFrom(@NonNull Queue savedObjects) throws Exception { + super.readFrom(savedObjects); + currentPage = (int) savedObjects.poll(); + currentNextPage = (int) savedObjects.poll(); + } + + @Override + public void onSaveInstanceState(Bundle bundle) { + searchQuery = searchEditText != null && !TextUtils.isEmpty(searchEditText.getText().toString()) + ? searchEditText.getText().toString() : searchQuery; + super.onSaveInstanceState(bundle); + } + + /*////////////////////////////////////////////////////////////////////////// + // Init's + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void reloadContent() { + if (!TextUtils.isEmpty(searchQuery) || (searchEditText != null && !TextUtils.isEmpty(searchEditText.getText()))) { + search(!TextUtils.isEmpty(searchQuery) ? searchQuery : searchEditText.getText().toString()); + } else { + if (searchEditText != null) { + searchEditText.setText(""); + showSoftKeyboard(searchEditText); + } + animateView(errorPanelRoot, false, 200); + } + } + + /*////////////////////////////////////////////////////////////////////////// + // Menu + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + + ActionBar supportActionBar = activity.getSupportActionBar(); + if (supportActionBar != null) { + supportActionBar.setDisplayShowTitleEnabled(false); + supportActionBar.setDisplayHomeAsUpEnabled(true); + } + + inflater.inflate(R.menu.menu_search, menu); + + searchToolbarContainer = activity.findViewById(R.id.toolbar_search_container); + searchEditText = searchToolbarContainer.findViewById(R.id.toolbar_search_edit_text); + searchClear = searchToolbarContainer.findViewById(R.id.toolbar_search_clear); + setupSearchView(); + + restoreFilterChecked(menu, filterItemCheckedId); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_filter_all: + case R.id.menu_filter_video: + case R.id.menu_filter_channel: + case R.id.menu_filter_playlist: + changeFilter(item, getFilterFromMenuId(item.getItemId())); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + private void restoreFilterChecked(Menu menu, int itemId) { + if (itemId != -1) { + MenuItem item = menu.findItem(itemId); + if (item == null) return; + + item.setChecked(true); + filter = getFilterFromMenuId(itemId); + } + } + + private SearchEngine.Filter getFilterFromMenuId(int itemId) { + switch (itemId) { + case R.id.menu_filter_all: + return SearchEngine.Filter.ANY; + case R.id.menu_filter_video: + return SearchEngine.Filter.STREAM; + case R.id.menu_filter_channel: + return SearchEngine.Filter.CHANNEL; + case R.id.menu_filter_playlist: + return SearchEngine.Filter.PLAYLIST; + default: + return SearchEngine.Filter.ANY; + } + } + + /*////////////////////////////////////////////////////////////////////////// + // Search + //////////////////////////////////////////////////////////////////////////*/ + + private TextWatcher textWatcher; + + private void setupSearchView() { + searchEditText.setText(searchQuery != null ? searchQuery : ""); + searchEditText.setAdapter(suggestionListAdapter); + + if (TextUtils.isEmpty(searchQuery) || TextUtils.isEmpty(searchEditText.getText())) { + searchToolbarContainer.setTranslationX(100); + searchToolbarContainer.setAlpha(0f); + searchToolbarContainer.setVisibility(View.VISIBLE); + searchToolbarContainer.animate().translationX(0).alpha(1f).setDuration(200).setInterpolator(new DecelerateInterpolator()).start(); + } else { + searchToolbarContainer.setTranslationX(0); + searchToolbarContainer.setAlpha(1f); + searchToolbarContainer.setVisibility(View.VISIBLE); + } + + initSearchListeners(); + + if (TextUtils.isEmpty(searchQuery) || wasSearchFocused) showSoftKeyboard(searchEditText); + else hideSoftKeyboard(searchEditText); + wasSearchFocused = false; + } + + private void initSearchListeners() { + searchClear.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]"); + if (TextUtils.isEmpty(searchEditText.getText())) { + NavigationHelper.gotoMainFragment(getFragmentManager()); + return; + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + searchEditText.setText("", false); + } else searchEditText.setText(""); + suggestionListAdapter.updateAdapter(new ArrayList()); + showSoftKeyboard(searchEditText); + } + }); + + TooltipCompat.setTooltipText(searchClear, getString(R.string.clear)); + + searchEditText.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]"); + searchEditText.showDropDown(); + } + }); + + searchEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + if (DEBUG) Log.d(TAG, "onFocusChange() called with: v = [" + v + "], hasFocus = [" + hasFocus + "]"); + if (hasFocus) searchEditText.showDropDown(); + } + }); + + searchEditText.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + if (DEBUG) { + Log.d(TAG, "onItemClick() called with: parent = [" + parent + "], view = [" + view + "], position = [" + position + "], id = [" + id + "]"); + } + String s = suggestionListAdapter.getSuggestion(position); + if (DEBUG) Log.d(TAG, "onItemClick text = " + s); + submitQuery(s); + } + }); + searchEditText.setThreshold(THRESHOLD_SUGGESTION); + + if (textWatcher != null) searchEditText.removeTextChangedListener(textWatcher); + textWatcher = new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(Editable s) { + String newText = searchEditText.getText().toString(); + if (!TextUtils.isEmpty(newText)) suggestionPublisher.onNext(newText); + } + }; + searchEditText.addTextChangedListener(textWatcher); + + searchEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (DEBUG) + Log.d(TAG, "onEditorAction() called with: v = [" + v + "], actionId = [" + actionId + "], event = [" + event + "]"); + if (event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER || event.getAction() == EditorInfo.IME_ACTION_SEARCH)) { + submitQuery(searchEditText.getText().toString()); + return true; + } + return false; + } + }); + + if (suggestionWorkerDisposable == null || suggestionWorkerDisposable.isDisposed()) initSuggestionObserver(); + } + + private void unsetSearchListeners() { + searchClear.setOnClickListener(null); + searchClear.setOnLongClickListener(null); + searchEditText.setOnClickListener(null); + searchEditText.setOnItemClickListener(null); + searchEditText.setOnFocusChangeListener(null); + searchEditText.setOnEditorActionListener(null); + + if (textWatcher != null) searchEditText.removeTextChangedListener(textWatcher); + textWatcher = null; + } + + private void showSoftKeyboard(View view) { + if (DEBUG) Log.d(TAG, "showSoftKeyboard() called with: view = [" + view + "]"); + if (view == null) return; + + if (view.requestFocus()) { + InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); + } + } + + private void hideSoftKeyboard(View view) { + if (DEBUG) Log.d(TAG, "hideSoftKeyboard() called with: view = [" + view + "]"); + if (view == null) return; + + InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); + + view.clearFocus(); + } + + public void giveSearchEditTextFocus() { + showSoftKeyboard(searchEditText); + } + + private void initSuggestionObserver() { + if (suggestionWorkerDisposable != null) suggestionWorkerDisposable.dispose(); + final Predicate checkEnabledAndLength = new Predicate() { + @Override + public boolean test(@io.reactivex.annotations.NonNull String s) throws Exception { + boolean lengthCheck = s.length() >= THRESHOLD_SUGGESTION; + // Clear the suggestions adapter if the length check fails + if (!lengthCheck && !suggestionListAdapter.isEmpty()) { + suggestionListAdapter.updateAdapter(new ArrayList()); + } + // Only pass through if suggestions is enabled and the query length is equal or greater than THRESHOLD_SUGGESTION + return showSuggestions && lengthCheck; + } + }; + + suggestionWorkerDisposable = suggestionPublisher + .debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS) + .startWith(!TextUtils.isEmpty(searchQuery) ? searchQuery : "") + .filter(checkEnabledAndLength) + .switchMap(new Function>>>() { + @Override + public Observable>> apply(@io.reactivex.annotations.NonNull String query) throws Exception { + return ExtractorHelper.suggestionsFor(serviceId, query, searchLanguage).toObservable().materialize(); + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Consumer>>() { + @Override + public void accept(@io.reactivex.annotations.NonNull Notification> listNotification) throws Exception { + if (listNotification.isOnNext()) { + handleSuggestions(listNotification.getValue()); + if (errorPanelRoot.getVisibility() == View.VISIBLE) { + hideLoading(); + } + } else if (listNotification.isOnError()) { + Throwable error = listNotification.getError(); + if (!ExtractorHelper.isInterruptedCaused(error)) { + onSuggestionError(error); + } + } + } + }); + } + + @Override + protected void doInitialLoadLogic() { + // no-op + } + + private void search(final String query) { + if (DEBUG) Log.d(TAG, "search() called with: query = [" + query + "]"); + + hideSoftKeyboard(searchEditText); + this.searchQuery = query; + this.currentPage = 0; + infoListAdapter.clearStreamItemList(); + + if (activity instanceof HistoryListener) { + ((HistoryListener) activity).onSearch(serviceId, query); + } + + final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + final String searchLanguageKey = getContext().getString(R.string.search_language_key); + searchLanguage = sharedPreferences.getString(searchLanguageKey, getContext().getString(R.string.default_language_value)); + startLoading(false); + } + + @Override + public void startLoading(boolean forceLoad) { + super.startLoading(forceLoad); + if (searchDisposable != null) searchDisposable.dispose(); + searchDisposable = ExtractorHelper.searchFor(serviceId, searchQuery, currentPage, searchLanguage, filter) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Consumer() { + @Override + public void accept(@NonNull SearchResult result) throws Exception { + isLoading.set(false); + handleResult(result); + } + }, new Consumer() { + @Override + public void accept(@NonNull Throwable throwable) throws Exception { + isLoading.set(false); + onError(throwable); + } + }); + } + + @Override + protected void loadMoreItems() { + isLoading.set(true); + showListFooter(true); + if (searchDisposable != null) searchDisposable.dispose(); + currentNextPage = currentPage + 1; + searchDisposable = ExtractorHelper.getMoreSearchItems(serviceId, searchQuery, currentNextPage, searchLanguage, filter) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Consumer() { + @Override + public void accept(@NonNull ListExtractor.NextItemsResult result) throws Exception { + isLoading.set(false); + handleNextItems(result); + } + }, new Consumer() { + @Override + public void accept(@NonNull Throwable throwable) throws Exception { + isLoading.set(false); + onError(throwable); + } + }); + } + + @Override + protected boolean hasMoreItems() { + // TODO: No way to tell if search has more items in the moment + return true; + } + + @Override + protected void onItemSelected(InfoItem selectedItem) { + super.onItemSelected(selectedItem); + hideSoftKeyboard(searchEditText); + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + private void changeFilter(MenuItem item, SearchEngine.Filter filter) { + this.filter = filter; + this.filterItemCheckedId = item.getItemId(); + item.setChecked(true); + if (searchQuery != null && !searchQuery.isEmpty()) search(searchQuery); + } + + private void submitQuery(String query) { + if (DEBUG) Log.d(TAG, "submitQuery() called with: query = [" + query + "]"); + if (query.isEmpty()) return; + search(query); + } + + private void setQuery(int serviceId, String searchQuery) { + this.serviceId = serviceId; + this.searchQuery = searchQuery; + } + + @Override + public void showError(String message, boolean showRetryButton) { + super.showError(message, showRetryButton); + hideSoftKeyboard(searchEditText); + } + + /*////////////////////////////////////////////////////////////////////////// + // Suggestion Results + //////////////////////////////////////////////////////////////////////////*/ + + public void handleSuggestions(@NonNull List suggestions) { + if (DEBUG) Log.d(TAG, "handleSuggestions() called with: suggestions = [" + suggestions + "]"); + suggestionListAdapter.updateAdapter(suggestions); + } + + public void onSuggestionError(Throwable exception) { + if (DEBUG) Log.d(TAG, "onSuggestionError() called with: exception = [" + exception + "]"); + if (super.onError(exception)) return; + + int errorId = exception instanceof ParsingException ? R.string.parsing_error : R.string.general_error; + onUnrecoverableError(exception, UserAction.GET_SUGGESTIONS, NewPipe.getNameOfService(serviceId), searchQuery, errorId); + } + + /*////////////////////////////////////////////////////////////////////////// + // Contract + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void hideLoading() { + super.hideLoading(); + showListFooter(false); + } + + /*////////////////////////////////////////////////////////////////////////// + // Search Results + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void handleResult(@NonNull SearchResult result) { + if (!result.errors.isEmpty()) { + showSnackBarError(result.errors, UserAction.SEARCHED, NewPipe.getNameOfService(serviceId), searchQuery, 0); + } + + if (infoListAdapter.getItemsList().size() == 0) { + if (result.resultList.size() > 0) { + infoListAdapter.addInfoItemList(result.resultList); + } else { + infoListAdapter.clearStreamItemList(); + showEmptyState(); + return; + } + } + + super.handleResult(result); + } + + @Override + public void handleNextItems(ListExtractor.NextItemsResult result) { + showListFooter(false); + currentPage = Integer.parseInt(result.nextItemsUrl); + infoListAdapter.addInfoItemList(result.nextItemsList); + + if (!result.errors.isEmpty()) { + showSnackBarError(result.errors, UserAction.SEARCHED, NewPipe.getNameOfService(serviceId) + , "\"" + searchQuery + "\" → page " + currentPage, 0); + } + super.handleNextItems(result); + } + + @Override + protected boolean onError(Throwable exception) { + if (super.onError(exception)) return true; + + if (exception instanceof SearchEngine.NothingFoundException) { + infoListAdapter.clearStreamItemList(); + showEmptyState(); + } else { + int errorId = exception instanceof ParsingException ? R.string.parsing_error : R.string.general_error; + onUnrecoverableError(exception, UserAction.SEARCHED, NewPipe.getNameOfService(serviceId), searchQuery, errorId); + } + + return true; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/search/SuggestionListAdapter.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java similarity index 95% rename from app/src/main/java/org/schabi/newpipe/fragments/search/SuggestionListAdapter.java rename to app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java index dda698a78..0a7e3d613 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/search/SuggestionListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.fragments.search; +package org.schabi.newpipe.fragments.list.search; import android.content.Context; import android.database.Cursor; @@ -9,7 +9,7 @@ import android.widget.TextView; import java.util.List; -/** +/* * Created by Christian Schabesberger on 02.08.16. * * Copyright (C) Christian Schabesberger 2016 @@ -83,7 +83,7 @@ public class SuggestionListAdapter extends ResourceCursorAdapter { private class ViewHolder { private final TextView suggestionTitle; private ViewHolder(View view) { - this.suggestionTitle = (TextView) view.findViewById(android.R.id.text1); + this.suggestionTitle = view.findViewById(android.R.id.text1); } } } \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/fragments/search/OnScrollBelowItemsListener.java b/app/src/main/java/org/schabi/newpipe/fragments/search/OnScrollBelowItemsListener.java deleted file mode 100644 index 51ef88706..000000000 --- a/app/src/main/java/org/schabi/newpipe/fragments/search/OnScrollBelowItemsListener.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.schabi.newpipe.fragments.search; - -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; - -/** - * Recycler view scroll listener which calls the method {@link #onScrolledDown(RecyclerView)} - * if the view is scrolled below the last item. - */ -public abstract class OnScrollBelowItemsListener extends RecyclerView.OnScrollListener { - - @Override - public void onScrolled(RecyclerView recyclerView, int dx, int dy) { - super.onScrolled(recyclerView, dx, dy); - //check for scroll down - if (dy > 0) { - int pastVisibleItems, visibleItemCount, totalItemCount; - LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager(); - visibleItemCount = recyclerView.getLayoutManager().getChildCount(); - totalItemCount = recyclerView.getLayoutManager().getItemCount(); - pastVisibleItems = layoutManager.findFirstVisibleItemPosition(); - if ((visibleItemCount + pastVisibleItems) >= totalItemCount) { - onScrolledDown(recyclerView); - } - } - } - - /** - * Called when the recycler view is scrolled below the last item. - * @param recyclerView the recycler view - */ - public abstract void onScrolledDown(RecyclerView recyclerView); -} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/search/SearchFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/search/SearchFragment.java deleted file mode 100644 index 8ec3e7709..000000000 --- a/app/src/main/java/org/schabi/newpipe/fragments/search/SearchFragment.java +++ /dev/null @@ -1,642 +0,0 @@ -package org.schabi.newpipe.fragments.search; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.os.Build; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v7.app.ActionBar; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.text.Editable; -import android.text.TextUtils; -import android.text.TextWatcher; -import android.util.Log; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.view.animation.DecelerateInterpolator; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputMethodManager; -import android.widget.AdapterView; -import android.widget.AutoCompleteTextView; -import android.widget.TextView; -import android.widget.Toast; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.ReCaptchaActivity; -import org.schabi.newpipe.extractor.InfoItem; -import org.schabi.newpipe.extractor.search.SearchEngine; -import org.schabi.newpipe.extractor.search.SearchResult; -import org.schabi.newpipe.fragments.BaseFragment; -import org.schabi.newpipe.info_list.InfoItemBuilder; -import org.schabi.newpipe.info_list.InfoListAdapter; -import org.schabi.newpipe.util.Constants; -import org.schabi.newpipe.util.NavigationHelper; -import org.schabi.newpipe.workers.SearchWorker; -import org.schabi.newpipe.workers.SuggestionWorker; - -import java.util.ArrayList; -import java.util.EnumSet; -import java.util.List; - -import static org.schabi.newpipe.util.AnimationUtils.animateView; - -public class SearchFragment extends BaseFragment implements SuggestionWorker.OnSuggestionResult, SearchWorker.OnSearchResult { - private final String TAG = "SearchFragment@" + Integer.toHexString(hashCode()); - // savedInstanceBundle arguments - private static final String QUERY_KEY = "query_key"; - private static final String PAGE_NUMBER_KEY = "page_number_key"; - private static final String INFO_LIST_KEY = "info_list_key"; - private static final String WAS_LOADING_KEY = "was_loading_key"; - private static final String ERROR_KEY = "error_key"; - private static final String FILTER_CHECKED_ID_KEY = "filter_checked_id_key"; - - /*////////////////////////////////////////////////////////////////////////// - // Search - //////////////////////////////////////////////////////////////////////////*/ - - private int filterItemCheckedId = -1; - private EnumSet filter = EnumSet.of(SearchEngine.Filter.CHANNEL, SearchEngine.Filter.STREAM); - - private int serviceId = -1; - private String searchQuery = ""; - private int pageNumber = 0; - private boolean showSuggestions = true; - - private SearchWorker curSearchWorker; - private SuggestionWorker curSuggestionWorker; - private SuggestionListAdapter suggestionListAdapter; - private InfoListAdapter infoListAdapter; - - /*////////////////////////////////////////////////////////////////////////// - // Views - //////////////////////////////////////////////////////////////////////////*/ - - private View searchToolbarContainer; - private AutoCompleteTextView searchEditText; - private View searchClear; - - private RecyclerView resultRecyclerView; - private OnSearchListener onSearchListener; - - /*////////////////////////////////////////////////////////////////////////*/ - - public static SearchFragment getInstance(int serviceId, String query) { - SearchFragment searchFragment = new SearchFragment(); - searchFragment.setQuery(serviceId, query); - if(!TextUtils.isEmpty(query)) { - searchFragment.wasLoading.set(true); - } - return searchFragment; - } - - /*////////////////////////////////////////////////////////////////////////// - // Fragment's LifeCycle - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]"); - setHasOptionsMenu(true); - if (savedInstanceState != null) { - searchQuery = savedInstanceState.getString(QUERY_KEY); - serviceId = savedInstanceState.getInt(Constants.KEY_SERVICE_ID, 0); - pageNumber = savedInstanceState.getInt(PAGE_NUMBER_KEY, 0); - wasLoading.set(savedInstanceState.getBoolean(WAS_LOADING_KEY, false)); - filterItemCheckedId = savedInstanceState.getInt(FILTER_CHECKED_ID_KEY, 0); - } - } - - @Override - public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - if (DEBUG) Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]"); - return inflater.inflate(R.layout.fragment_search, container, false); - } - - @Override - public void onViewCreated(View rootView, @Nullable Bundle savedInstanceState) { - final boolean wasLoadingPreserved = wasLoading.get(); - super.onViewCreated(rootView, savedInstanceState); - wasLoading.set(wasLoadingPreserved); - if (DEBUG) Log.d(TAG, "onViewCreated() called with: rootView = [" + rootView + "], savedInstanceState = [" + savedInstanceState + "]"); - - if (savedInstanceState != null && savedInstanceState.getBoolean(ERROR_KEY, false)) { - search(searchQuery, 0, true); - } - } - - @Override - public void onAttach(Context context) { - super.onAttach(context); - onSearchListener = (OnSearchListener) context; - } - - @Override - public void onDetach() { - super.onDetach(); - onSearchListener = null; - } - - @Override - public void onResume() { - super.onResume(); - if (DEBUG) Log.d(TAG, "onResume() called"); - if (wasLoading.getAndSet(false) && !TextUtils.isEmpty(searchQuery)) { - if (pageNumber > 0) search(searchQuery, pageNumber); - else search(searchQuery, 0, true); - } - - showSuggestions = PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(getString(R.string.show_search_suggestions_key), true); - } - - @Override - public void onStop() { - super.onStop(); - if (DEBUG) Log.d(TAG, "onStop() called"); - - hideSoftKeyboard(searchEditText); - - wasLoading.set(curSearchWorker != null && curSearchWorker.isRunning()); - if (curSearchWorker != null && curSearchWorker.isRunning()) curSearchWorker.cancel(); - if (curSuggestionWorker != null && curSuggestionWorker.isRunning()) curSuggestionWorker.cancel(); - } - - @Override - public void onDestroyView() { - if (DEBUG) Log.d(TAG, "onDestroyView() called"); - unsetSearchListeners(); - - resultRecyclerView.removeAllViews(); - - searchToolbarContainer = null; - searchEditText = null; - searchClear = null; - - resultRecyclerView = null; - - super.onDestroyView(); - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - if (DEBUG) Log.d(TAG, "onSaveInstanceState() called with: outState = [" + outState + "]"); - - String query = searchEditText != null && !TextUtils.isEmpty(searchEditText.getText().toString()) - ? searchEditText.getText().toString() : searchQuery; - outState.putString(QUERY_KEY, query); - outState.putInt(Constants.KEY_SERVICE_ID, serviceId); - outState.putInt(PAGE_NUMBER_KEY, pageNumber); - outState.putSerializable(INFO_LIST_KEY, infoListAdapter.getItemsList()); - outState.putBoolean(WAS_LOADING_KEY, curSearchWorker != null && curSearchWorker.isRunning()); - - if (errorPanel != null && errorPanel.getVisibility() == View.VISIBLE) { - outState.putBoolean(ERROR_KEY, true); - } - if (filterItemCheckedId != -1) outState.putInt(FILTER_CHECKED_ID_KEY, filterItemCheckedId); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - switch (requestCode) { - case ReCaptchaActivity.RECAPTCHA_REQUEST: - if (resultCode == Activity.RESULT_OK && searchQuery.length() != 0) { - search(searchQuery, pageNumber, true); - } else Log.e(TAG, "ReCaptcha failed"); - break; - - default: - Log.e(TAG, "Request code from activity not supported [" + requestCode + "]"); - break; - } - } - - /*////////////////////////////////////////////////////////////////////////// - // Init's - //////////////////////////////////////////////////////////////////////////*/ - - @Override - protected void initViews(View rootView, Bundle savedInstanceState) { - super.initViews(rootView, savedInstanceState); - resultRecyclerView = ((RecyclerView) rootView.findViewById(R.id.result_list_view)); - resultRecyclerView.setLayoutManager(new LinearLayoutManager(activity)); - - if (infoListAdapter == null) { - infoListAdapter = new InfoListAdapter(getActivity()); - if (savedInstanceState != null) { - //noinspection unchecked - ArrayList serializable = (ArrayList) savedInstanceState.getSerializable(INFO_LIST_KEY); - infoListAdapter.addInfoItemList(serializable); - } - - infoListAdapter.setFooter(activity.getLayoutInflater().inflate(R.layout.pignate_footer, resultRecyclerView, false)); - infoListAdapter.showFooter(false); - infoListAdapter.setOnStreamInfoItemSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { - @Override - public void selected(int serviceId, String url, String title) { - NavigationHelper.openVideoDetailFragment(getFragmentManager(), serviceId, url, title); - } - }); - infoListAdapter.setOnChannelInfoItemSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { - @Override - public void selected(int serviceId, String url, String title) { - NavigationHelper.openChannelFragment(getFragmentManager(), serviceId, url, title); - } - }); - } - - resultRecyclerView.setAdapter(infoListAdapter); - } - - @Override - protected void initListeners() { - super.initListeners(); - resultRecyclerView.clearOnScrollListeners(); - resultRecyclerView.addOnScrollListener(new OnScrollBelowItemsListener() { - @Override - public void onScrolledDown(RecyclerView recyclerView) { - if(!isLoading.get()) { - pageNumber++; - recyclerView.post(new Runnable() { - @Override - public void run() { - infoListAdapter.showFooter(true); - } - }); - search(searchQuery, pageNumber); - } - } - }); - } - - - @Override - protected void reloadContent() { - if (DEBUG) Log.d(TAG, "reloadContent() called"); - if (!TextUtils.isEmpty(searchQuery) || (searchEditText != null && !TextUtils.isEmpty(searchEditText.getText()))) { - search(!TextUtils.isEmpty(searchQuery) ? searchQuery : searchEditText.getText().toString(), 0, true); - } else { - if (searchEditText != null) { - searchEditText.setText(""); - showSoftKeyboard(searchEditText); - } - animateView(errorPanel, false, 200); - } - } - - /*////////////////////////////////////////////////////////////////////////// - // Menu - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - super.onCreateOptionsMenu(menu, inflater); - if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]"); - inflater.inflate(R.menu.search_menu, menu); - - ActionBar supportActionBar = activity.getSupportActionBar(); - if (supportActionBar != null) { - supportActionBar.setDisplayShowTitleEnabled(false); - supportActionBar.setDisplayHomeAsUpEnabled(true); - } - - searchToolbarContainer = activity.findViewById(R.id.toolbar_search_container); - searchEditText = (AutoCompleteTextView) searchToolbarContainer.findViewById(R.id.toolbar_search_edit_text); - searchClear = searchToolbarContainer.findViewById(R.id.toolbar_search_clear); - setupSearchView(); - - restoreFilterChecked(menu, filterItemCheckedId); - } - - private void restoreFilterChecked(Menu menu, int itemId) { - if (itemId != -1) { - MenuItem item = menu.findItem(itemId); - if (item == null) return; - - item.setChecked(true); - switch (itemId) { - case R.id.menu_filter_all: - filter = EnumSet.of(SearchEngine.Filter.STREAM, SearchEngine.Filter.CHANNEL); - break; - case R.id.menu_filter_video: - filter = EnumSet.of(SearchEngine.Filter.STREAM); - break; - case R.id.menu_filter_channel: - filter = EnumSet.of(SearchEngine.Filter.CHANNEL); - break; - } - } - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (DEBUG) Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]"); - - switch (item.getItemId()) { - case R.id.menu_filter_all: - changeFilter(item, EnumSet.of(SearchEngine.Filter.STREAM, SearchEngine.Filter.CHANNEL)); - return true; - case R.id.menu_filter_video: - changeFilter(item, EnumSet.of(SearchEngine.Filter.STREAM)); - return true; - case R.id.menu_filter_channel: - changeFilter(item, EnumSet.of(SearchEngine.Filter.CHANNEL)); - return true; - default: - return false; - } - } - - /*////////////////////////////////////////////////////////////////////////// - // Search - //////////////////////////////////////////////////////////////////////////*/ - - private TextWatcher textWatcher; - - private void setupSearchView() { - searchEditText.setText(searchQuery != null ? searchQuery : ""); - searchEditText.setHint(getString(R.string.search) + "..."); - ////searchEditText.setCursorVisible(true); - - suggestionListAdapter = new SuggestionListAdapter(activity); - searchEditText.setAdapter(suggestionListAdapter); - - - if (TextUtils.isEmpty(searchQuery) || TextUtils.isEmpty(searchEditText.getText())) { - searchToolbarContainer.setTranslationX(100); - searchToolbarContainer.setAlpha(0f); - searchToolbarContainer.setVisibility(View.VISIBLE); - searchToolbarContainer.animate().translationX(0).alpha(1f).setDuration(400).setInterpolator(new DecelerateInterpolator()).start(); - } else { - searchToolbarContainer.setTranslationX(0); - searchToolbarContainer.setAlpha(1f); - searchToolbarContainer.setVisibility(View.VISIBLE); - } - - // - initSearchListeners(); - - if (TextUtils.isEmpty(searchQuery)) showSoftKeyboard(searchEditText); - else hideSoftKeyboard(searchEditText); - - if (!TextUtils.isEmpty(searchQuery) && searchQuery.length() > 2 && suggestionListAdapter != null && suggestionListAdapter.isEmpty()) { - searchSuggestions(searchQuery); - } - } - - private void initSearchListeners() { - searchClear.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]"); - if (TextUtils.isEmpty(searchEditText.getText())) { - NavigationHelper.gotoMainFragment(getFragmentManager()); - return; - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - searchEditText.setText("", false); - } else searchEditText.setText(""); - suggestionListAdapter.updateAdapter(new ArrayList()); - showSoftKeyboard(searchEditText); - } - }); - - searchClear.setOnLongClickListener(new View.OnLongClickListener() { - @Override - public boolean onLongClick(View v) { - if (DEBUG) Log.d(TAG, "onLongClick() called with: v = [" + v + "]"); - showMenuTooltip(v, getString(R.string.clear)); - return true; - } - }); - - searchEditText.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - searchEditText.showDropDown(); - } - }); - - searchEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View v, boolean hasFocus) { - if (hasFocus) searchEditText.showDropDown(); - } - }); - - searchEditText.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - if (DEBUG) Log.d(TAG, "onItemClick() called with: parent = [" + parent + "], view = [" + view + "], position = [" + position + "], id = [" + id + "]"); - String s = suggestionListAdapter.getSuggestion(position); - if (DEBUG) Log.d(TAG, "onItemClick text = " + s); - submitQuery(s); - } - }); - - - if (textWatcher != null) searchEditText.removeTextChangedListener(textWatcher); - textWatcher = new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - } - - @Override - public void afterTextChanged(Editable s) { - String newText = searchEditText.getText().toString(); - if (!TextUtils.isEmpty(newText) && newText.length() > 1) onQueryTextChange(newText); - } - }; - searchEditText.addTextChangedListener(textWatcher); - - searchEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() { - @Override - public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { - if (DEBUG) Log.d(TAG, "onEditorAction() called with: v = [" + v + "], actionId = [" + actionId + "], event = [" + event + "]"); - if (event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER || event.getAction() == EditorInfo.IME_ACTION_SEARCH)) { - submitQuery(searchEditText.getText().toString()); - return true; - } - return false; - } - }); - } - - private void unsetSearchListeners() { - searchClear.setOnClickListener(null); - searchClear.setOnLongClickListener(null); - if (textWatcher != null) searchEditText.removeTextChangedListener(textWatcher); - searchEditText.setOnClickListener(null); - searchEditText.setOnItemClickListener(null); - searchEditText.setOnFocusChangeListener(null); - searchEditText.setOnEditorActionListener(null); - - textWatcher = null; - } - - public void showSoftKeyboard(View view) { - if (DEBUG) Log.d(TAG, "showSoftKeyboard() called with: view = [" + view + "]"); - if (view == null) return; - - if (view.requestFocus()) { - InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); - } - } - - public void hideSoftKeyboard(View view) { - if (DEBUG) Log.d(TAG, "hideSoftKeyboard() called with: view = [" + view + "]"); - if (view == null) return; - - InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); - - view.clearFocus(); - } - - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ - - private void changeFilter(MenuItem item, EnumSet filter) { - this.filter = filter; - this.filterItemCheckedId = item.getItemId(); - item.setChecked(true); - if (searchQuery != null && !searchQuery.isEmpty()) search(searchQuery, 0, true); - } - - public void submitQuery(String query) { - if (DEBUG) Log.d(TAG, "submitQuery() called with: query = [" + query + "]"); - if (query.isEmpty()) return; - search(query, 0, true); - searchQuery = query; - } - - public void onQueryTextChange(String newText) { - if (DEBUG) Log.d(TAG, "onQueryTextChange() called with: newText = [" + newText + "]"); - if (!newText.isEmpty()) searchSuggestions(newText); - } - - private void setQuery(int serviceId, String searchQuery) { - this.serviceId = serviceId; - this.searchQuery = searchQuery; - } - - private void searchSuggestions(String query) { - if (!showSuggestions) { - if (DEBUG) Log.d(TAG, "searchSuggestions() showSuggestions is disabled"); - return; - } - - if (DEBUG) Log.d(TAG, "searchSuggestions() called with: query = [" + query + "]"); - if (curSuggestionWorker != null && curSuggestionWorker.isRunning()) curSuggestionWorker.cancel(); - curSuggestionWorker = SuggestionWorker.startForQuery(activity, serviceId, query, this); - } - - private void search(String query, int pageNumber) { - if (DEBUG) Log.d(TAG, "search() called with: query = [" + query + "], pageNumber = [" + pageNumber + "]"); - search(query, pageNumber, false); - } - - private void search(String query, int pageNumber, boolean clearList) { - if (DEBUG) Log.d(TAG, "search() called with: query = [" + query + "], pageNumber = [" + pageNumber + "], clearList = [" + clearList + "]"); - isLoading.set(true); - hideSoftKeyboard(searchEditText); - if(pageNumber == 0) { - if(onSearchListener != null) { - onSearchListener.onSearch(serviceId, query); - } - } - searchQuery = query; - this.pageNumber = pageNumber; - - if (clearList) { - animateView(resultRecyclerView, false, 50); - infoListAdapter.clearStreamItemList(); - infoListAdapter.showFooter(false); - animateView(loadingProgressBar, true, 200); - } - animateView(errorPanel, false, 200); - - if (curSearchWorker != null && curSearchWorker.isRunning()) curSearchWorker.cancel(); - curSearchWorker = SearchWorker.startForQuery(activity, serviceId, query, pageNumber, filter, this); - } - - protected void setErrorMessage(String message, boolean showRetryButton) { - super.setErrorMessage(message, showRetryButton); - - animateView(resultRecyclerView, false, 400); - hideSoftKeyboard(searchEditText); - } - - /*////////////////////////////////////////////////////////////////////////// - // OnSuggestionResult - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onSuggestionResult(@NonNull List suggestions) { - if (DEBUG) Log.d(TAG, "onSuggestionResult() called with: suggestions = [" + suggestions + "]"); - suggestionListAdapter.updateAdapter(suggestions); - } - - @Override - public void onSuggestionError(int messageId) { - if (DEBUG) Log.d(TAG, "onSuggestionError() called with: messageId = [" + messageId + "]"); - setErrorMessage(getString(messageId), true); - } - - /*////////////////////////////////////////////////////////////////////////// - // SearchWorkerResultListener - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onSearchResult(SearchResult result) { - if (DEBUG) Log.d(TAG, "onSearchResult() called with: result = [" + result + "]"); - infoListAdapter.addInfoItemList(result.resultList); - animateView(resultRecyclerView, true, 400); - animateView(loadingProgressBar, false, 200); - isLoading.set(false); - } - - @Override - public void onNothingFound(String message) { - if (DEBUG) Log.d(TAG, "onNothingFound() called with: messageId = [" + message + "]"); - setErrorMessage(message, false); - } - - @Override - public void onSearchError(int messageId) { - if (DEBUG) Log.d(TAG, "onSearchError() called with: messageId = [" + messageId + "]"); - //Toast.makeText(getActivity(), messageId, Toast.LENGTH_LONG).show(); - setErrorMessage(getString(messageId), true); - } - - @Override - public void onReCaptchaChallenge() { - if (DEBUG) Log.d(TAG, "onReCaptchaChallenge() called"); - Toast.makeText(getActivity(), R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show(); - setErrorMessage(getString(R.string.recaptcha_request_toast), false); - - // Starting ReCaptcha Challenge Activity - startActivityForResult(new Intent(getActivity(), ReCaptchaActivity.class), ReCaptchaActivity.RECAPTCHA_REQUEST); - } - - public interface OnSearchListener { - void onSearch(int serviceId, String query); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/subscription/SubscriptionFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/subscription/SubscriptionFragment.java new file mode 100644 index 000000000..646fe597e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/subscription/SubscriptionFragment.java @@ -0,0 +1,240 @@ +package org.schabi.newpipe.fragments.subscription; + +import android.content.Context; +import android.os.Bundle; +import android.os.Parcelable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.subscription.SubscriptionEntity; +import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.extractor.channel.ChannelInfoItem; +import org.schabi.newpipe.fragments.BaseStateFragment; +import org.schabi.newpipe.info_list.InfoItemBuilder; +import org.schabi.newpipe.info_list.InfoListAdapter; +import org.schabi.newpipe.report.UserAction; +import org.schabi.newpipe.util.NavigationHelper; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import icepick.State; +import io.reactivex.Observer; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; + +import static org.schabi.newpipe.util.AnimationUtils.animateView; + +public class SubscriptionFragment extends BaseStateFragment> { + private View headerRootLayout; + + private InfoListAdapter infoListAdapter; + private RecyclerView itemsList; + + @State + protected Parcelable itemsListState; + + /* Used for independent events */ + private CompositeDisposable disposables = new CompositeDisposable(); + private SubscriptionService subscriptionService; + + /////////////////////////////////////////////////////////////////////////// + // Fragment LifeCycle + /////////////////////////////////////////////////////////////////////////// + + @Override + public void onAttach(Context context) { + super.onAttach(context); + infoListAdapter = new InfoListAdapter(activity); + subscriptionService = SubscriptionService.getInstance(); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_subscription, container, false); + } + + @Override + public void onPause() { + super.onPause(); + itemsListState = itemsList.getLayoutManager().onSaveInstanceState(); + } + + @Override + public void onDestroyView() { + if (disposables != null) disposables.clear(); + + super.onDestroyView(); + } + + @Override + public void onDestroy() { + if (disposables != null) disposables.dispose(); + disposables = null; + subscriptionService = null; + + super.onDestroy(); + } + + /////////////////////////////////////////////////////////////////////////// + // Fragment Views + /////////////////////////////////////////////////////////////////////////// + + @Override + protected void initViews(View rootView, Bundle savedInstanceState) { + super.initViews(rootView, savedInstanceState); + + infoListAdapter = new InfoListAdapter(getActivity()); + itemsList = rootView.findViewById(R.id.items_list); + itemsList.setLayoutManager(new LinearLayoutManager(activity)); + + infoListAdapter.setHeader(headerRootLayout = activity.getLayoutInflater().inflate(R.layout.subscription_header, itemsList, false)); + infoListAdapter.useMiniItemVariants(true); + + itemsList.setAdapter(infoListAdapter); + } + + @Override + protected void initListeners() { + super.initListeners(); + + infoListAdapter.setOnChannelSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { + @Override + public void selected(ChannelInfoItem selectedItem) { + // Requires the parent fragment to find holder for fragment replacement + NavigationHelper.openChannelFragment(getParentFragment().getFragmentManager(), selectedItem.service_id, selectedItem.url, selectedItem.name); + + } + }); + + headerRootLayout.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + NavigationHelper.openWhatsNewFragment(getParentFragment().getFragmentManager()); + } + }); + } + + private void resetFragment() { + if (disposables != null) disposables.clear(); + if (infoListAdapter != null) infoListAdapter.clearStreamItemList(); + } + + /////////////////////////////////////////////////////////////////////////// + // Subscriptions Loader + /////////////////////////////////////////////////////////////////////////// + + @Override + public void startLoading(boolean forceLoad) { + super.startLoading(forceLoad); + resetFragment(); + + subscriptionService.getSubscription().toObservable() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(getSubscriptionObserver()); + } + + private Observer> getSubscriptionObserver() { + return new Observer>() { + @Override + public void onSubscribe(Disposable d) { + showLoading(); + disposables.add(d); + } + + @Override + public void onNext(List subscriptions) { + handleResult(subscriptions); + } + + @Override + public void onError(Throwable exception) { + SubscriptionFragment.this.onError(exception); + } + + @Override + public void onComplete() { + } + }; + } + + @Override + public void handleResult(@NonNull List result) { + super.handleResult(result); + + infoListAdapter.clearStreamItemList(); + + if (result.isEmpty()) { + showEmptyState(); + } else { + infoListAdapter.addInfoItemList(getSubscriptionItems(result)); + if (itemsListState != null) { + itemsList.getLayoutManager().onRestoreInstanceState(itemsListState); + itemsListState = null; + } + + hideLoading(); + } + } + + + private List getSubscriptionItems(List subscriptions) { + List items = new ArrayList<>(); + for (final SubscriptionEntity subscription : subscriptions) items.add(subscription.toChannelInfoItem()); + + Collections.sort(items, new Comparator() { + @Override + public int compare(InfoItem o1, InfoItem o2) { + return o1.name.compareToIgnoreCase(o2.name); + } + }); + return items; + } + + /*////////////////////////////////////////////////////////////////////////// + // Contract + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void showLoading() { + super.showLoading(); + animateView(itemsList, false, 100); + } + + @Override + public void hideLoading() { + super.hideLoading(); + animateView(itemsList, true, 200); + } + + @Override + public void showEmptyState() { + super.showEmptyState(); + animateView(itemsList, false, 200); + } + + /////////////////////////////////////////////////////////////////////////// + // Fragment Error Handling + /////////////////////////////////////////////////////////////////////////// + + @Override + protected boolean onError(Throwable exception) { + resetFragment(); + if (super.onError(exception)) return true; + + onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, "none", "Subscriptions", R.string.general_error); + return true; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/SubscriptionService.java b/app/src/main/java/org/schabi/newpipe/fragments/subscription/SubscriptionService.java similarity index 50% rename from app/src/main/java/org/schabi/newpipe/fragments/SubscriptionService.java rename to app/src/main/java/org/schabi/newpipe/fragments/subscription/SubscriptionService.java index 369d65664..21d4ea2d3 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/SubscriptionService.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/subscription/SubscriptionService.java @@ -1,19 +1,16 @@ -package org.schabi.newpipe.fragments; +package org.schabi.newpipe.fragments.subscription; -import android.content.Context; +import android.util.Log; +import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.database.AppDatabase; import org.schabi.newpipe.database.subscription.SubscriptionDAO; import org.schabi.newpipe.database.subscription.SubscriptionEntity; -import org.schabi.newpipe.extractor.NewPipe; -import org.schabi.newpipe.extractor.StreamingService; -import org.schabi.newpipe.extractor.channel.ChannelExtractor; import org.schabi.newpipe.extractor.channel.ChannelInfo; -import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import org.schabi.newpipe.util.ExtractorHelper; import java.util.List; -import java.util.concurrent.Callable; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -27,28 +24,22 @@ import io.reactivex.annotations.NonNull; import io.reactivex.functions.Function; import io.reactivex.schedulers.Schedulers; -/** Subscription Service singleton: - * Provides a basis for channel Subscriptions. - * Provides access to subscription table in database as well as - * up-to-date observations on the subscribed channels - * */ +/** + * Subscription Service singleton: + * Provides a basis for channel Subscriptions. + * Provides access to subscription table in database as well as + * up-to-date observations on the subscribed channels + */ public class SubscriptionService { - private static SubscriptionService sInstance; - private static final Object LOCK = new Object(); + private static final SubscriptionService sInstance = new SubscriptionService(); - public static SubscriptionService getInstance(Context context) { - if (sInstance == null) { - synchronized (LOCK) { - if (sInstance == null) { - sInstance = new SubscriptionService(context); - } - } - } + public static SubscriptionService getInstance() { return sInstance; } protected final String TAG = "SubscriptionService@" + Integer.toHexString(hashCode()); + protected static final boolean DEBUG = MainActivity.DEBUG; private static final int SUBSCRIPTION_DEBOUNCE_INTERVAL = 500; private static final int SUBSCRIPTION_THREAD_POOL_SIZE = 4; @@ -57,19 +48,21 @@ public class SubscriptionService { private Scheduler subscriptionScheduler; - private SubscriptionService(Context context) { - db = NewPipeDatabase.getInstance( context ); + private SubscriptionService() { + db = NewPipeDatabase.getInstance(); subscription = getSubscriptionInfos(); final Executor subscriptionExecutor = Executors.newFixedThreadPool(SUBSCRIPTION_THREAD_POOL_SIZE); subscriptionScheduler = Schedulers.from(subscriptionExecutor); } - /** Part of subscription observation pipeline + /** + * Part of subscription observation pipeline + * * @see SubscriptionService#getSubscription() */ private Flowable> getSubscriptionInfos() { - return subscriptionTable().findAll() + return subscriptionTable().getAll() // Wait for a period of infrequent updates and return the latest update .debounce(SUBSCRIPTION_DEBOUNCE_INTERVAL, TimeUnit.MILLISECONDS) .share() // Share allows multiple subscribers on the same observable @@ -79,62 +72,47 @@ public class SubscriptionService { /** * Provides an observer to the latest update to the subscription table. - * - * This observer may be subscribed multiple times, where each subscriber obtains - * the latest synchronized changes available, effectively share the same data - * across all subscribers. - * - * This observer has a debounce cooldown, meaning if multiple updates are observed - * in the cooldown interval, only the latest changes are emitted to the subscribers. - * This reduces the amount of observations caused by frequent updates to the database. - * */ + *

+ * This observer may be subscribed multiple times, where each subscriber obtains + * the latest synchronized changes available, effectively share the same data + * across all subscribers. + *

+ * This observer has a debounce cooldown, meaning if multiple updates are observed + * in the cooldown interval, only the latest changes are emitted to the subscribers. + * This reduces the amount of observations caused by frequent updates to the database. + */ @android.support.annotation.NonNull public Flowable> getSubscription() { return subscription; } public Maybe getChannelInfo(final SubscriptionEntity subscriptionEntity) { - final StreamingService service = getService(subscriptionEntity.getServiceId()); - if (service == null) return Maybe.empty(); + if (DEBUG) Log.d(TAG, "getChannelInfo() called with: subscriptionEntity = [" + subscriptionEntity + "]"); - final String url = subscriptionEntity.getUrl(); - final Callable callable = new Callable() { - @Override - public ChannelInfo call() throws Exception { - final ChannelExtractor extractor = service.getChannelExtractorInstance(url, 0); - return ChannelInfo.getInfo(extractor); - } - }; - - return Maybe.fromCallable(callable).subscribeOn(subscriptionScheduler); + return Maybe.fromSingle(ExtractorHelper + .getChannelInfo(subscriptionEntity.getServiceId(), subscriptionEntity.getUrl(), false)) + .subscribeOn(subscriptionScheduler); } - private StreamingService getService(final int serviceId) { - try { - return NewPipe.getService(serviceId); - } catch (ExtractionException e) { - return null; - } - } - - /** Returns the database access interface for subscription table. */ + /** + * Returns the database access interface for subscription table. + */ public SubscriptionDAO subscriptionTable() { return db.subscriptionDAO(); } - public Completable updateChannelInfo(final int serviceId, - final String channelUrl, - final ChannelInfo info) { + public Completable updateChannelInfo(final ChannelInfo info) { final Function, CompletableSource> update = new Function, CompletableSource>() { @Override public CompletableSource apply(@NonNull List subscriptionEntities) throws Exception { + if (DEBUG) Log.d(TAG, "updateChannelInfo() called with: subscriptionEntities = [" + subscriptionEntities + "]"); if (subscriptionEntities.size() == 1) { SubscriptionEntity subscription = subscriptionEntities.get(0); // Subscriber count changes very often, making this check almost unnecessary. // Consider removing it later. - if (isSubscriptionUpToDate(channelUrl, info, subscription)) { - subscription.setData(info.channel_name, info.avatar_url, "", info.subscriberCount); + if (!isSubscriptionUpToDate(info, subscription)) { + subscription.setData(info.name, info.avatar_url, info.description, info.subscriber_count); return update(subscription); } @@ -144,7 +122,7 @@ public class SubscriptionService { } }; - return subscriptionTable().findAll(serviceId, channelUrl) + return subscriptionTable().getSubscription(info.service_id, info.url) .firstOrError() .flatMapCompletable(update); } @@ -158,13 +136,12 @@ public class SubscriptionService { }); } - private boolean isSubscriptionUpToDate(final String channelUrl, - final ChannelInfo info, - final SubscriptionEntity entity) { - return channelUrl.equals( entity.getUrl() ) && + private boolean isSubscriptionUpToDate(final ChannelInfo info, final SubscriptionEntity entity) { + return info.url.equals(entity.getUrl()) && info.service_id == entity.getServiceId() && - info.channel_name.equals( entity.getTitle() ) && - info.avatar_url.equals( entity.getThumbnailUrl() ) && - info.subscriberCount == entity.getSubscriberCount(); + info.name.equals(entity.getName()) && + info.avatar_url.equals(entity.getAvatarUrl()) && + info.description.equals(entity.getDescription()) && + info.subscriber_count == entity.getSubscriberCount(); } } diff --git a/app/src/main/java/org/schabi/newpipe/history/HistoryActivity.java b/app/src/main/java/org/schabi/newpipe/history/HistoryActivity.java index 26f91a06b..8d8e4ef16 100644 --- a/app/src/main/java/org/schabi/newpipe/history/HistoryActivity.java +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryActivity.java @@ -2,22 +2,18 @@ package org.schabi.newpipe.history; import android.content.Intent; import android.os.Bundle; -import android.support.annotation.Nullable; import android.support.design.widget.FloatingActionButton; -import android.support.design.widget.Snackbar; import android.support.design.widget.TabLayout; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentPagerAdapter; +import android.support.v4.app.FragmentStatePagerAdapter; import android.support.v4.view.ViewPager; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.util.Log; -import android.util.SparseArray; import android.view.Menu; import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; import com.jakewharton.rxbinding2.view.RxView; @@ -25,11 +21,8 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.settings.SettingsActivity; import org.schabi.newpipe.util.ThemeHelper; -import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.functions.Consumer; -import io.reactivex.functions.Function; -import io.reactivex.schedulers.Schedulers; public class HistoryActivity extends AppCompatActivity { @@ -72,30 +65,17 @@ public class HistoryActivity extends AppCompatActivity { final FloatingActionButton fab = findViewById(R.id.fab); RxView.clicks(fab) - .observeOn(Schedulers.io()) - .flatMap(new Function>() { - @Override - public Observable apply(Object o) { - int currentItem = mViewPager.getCurrentItem(); - HistoryFragment fragment = (HistoryFragment) mSectionsPagerAdapter.getFragment(currentItem); - if(fragment == null) { - Log.w(TAG, "Couldn't find current fragment"); - return Observable.empty(); - } else { - fragment.onClearHistory(); - return Observable.just(fragment); - } - } - }) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Consumer() { + .subscribe(new Consumer() { @Override - public void accept(HistoryFragment historyFragment) { - View view = historyFragment.getView(); - if(view != null) { - Snackbar.make(view, R.string.history_cleared, Snackbar.LENGTH_LONG).show(); + public void accept(Object o) { + int currentItem = mViewPager.getCurrentItem(); + HistoryFragment fragment = (HistoryFragment) mSectionsPagerAdapter.instantiateItem(mViewPager, currentItem); + if(fragment != null) { + fragment.onHistoryCleared(); + } else { + Log.w(TAG, "Couldn't find current fragment"); } - historyFragment.onHistoryCleared(); } }); } @@ -125,15 +105,12 @@ public class HistoryActivity extends AppCompatActivity { * A {@link FragmentPagerAdapter} that returns a fragment corresponding to * one of the sections/tabs/pages. */ - public class SectionsPagerAdapter extends FragmentPagerAdapter { - - private SparseArray fragments = new SparseArray<>(); + public class SectionsPagerAdapter extends FragmentStatePagerAdapter { public SectionsPagerAdapter(FragmentManager fm) { super(fm); } - @Override public Fragment getItem(int position) { Fragment fragment; @@ -147,21 +124,9 @@ public class HistoryActivity extends AppCompatActivity { default: throw new IllegalArgumentException("position: " + position); } - fragments.put(position, fragment); return fragment; } - @Override - public void destroyItem(ViewGroup container, int position, Object object) { - super.destroyItem(container, position, object); - fragments.remove(position); - } - - @Nullable - public Fragment getFragment(int position) { - return fragments.get(position); - } - @Override public CharSequence getPageTitle(int position) { switch (position) { diff --git a/app/src/main/java/org/schabi/newpipe/history/HistoryEntryAdapter.java b/app/src/main/java/org/schabi/newpipe/history/HistoryEntryAdapter.java index c0ee464e5..d56469a7e 100644 --- a/app/src/main/java/org/schabi/newpipe/history/HistoryEntryAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryEntryAdapter.java @@ -11,9 +11,7 @@ import org.schabi.newpipe.database.history.model.HistoryEntry; import java.text.DateFormat; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.Date; -import java.util.List; /** @@ -42,6 +40,10 @@ public abstract class HistoryEntryAdapter getItems() { + return mEntries; + } + public void clear() { mEntries.clear(); notifyDataSetChanged(); diff --git a/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java b/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java index 3e22f603e..03657d264 100644 --- a/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java @@ -1,16 +1,17 @@ package org.schabi.newpipe.history; -import android.content.Context; import android.content.SharedPreferences; +import android.graphics.Color; import android.os.Bundle; +import android.os.Parcelable; import android.preference.PreferenceManager; import android.support.annotation.CallSuper; import android.support.annotation.MainThread; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.StringRes; -import android.support.v4.app.Fragment; +import android.support.design.widget.Snackbar; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.helper.ItemTouchHelper; @@ -18,12 +19,17 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import org.schabi.newpipe.BaseFragment; import org.schabi.newpipe.R; import org.schabi.newpipe.database.history.dao.HistoryDAO; import org.schabi.newpipe.database.history.model.HistoryEntry; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.List; +import icepick.State; import io.reactivex.Observer; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; @@ -33,20 +39,27 @@ import io.reactivex.subjects.PublishSubject; import static org.schabi.newpipe.util.AnimationUtils.animateView; -public abstract class HistoryFragment extends Fragment +public abstract class HistoryFragment extends BaseFragment implements HistoryEntryAdapter.OnHistoryItemClickListener { + private SharedPreferences mSharedPreferences; + private String mHistoryIsEnabledKey; private boolean mHistoryIsEnabled; private HistoryIsEnabledChangeListener mHistoryIsEnabledChangeListener; - private String mHistoryIsEnabledKey; - private SharedPreferences mSharedPreferences; - private RecyclerView mRecyclerView; + private View mDisabledView; - private HistoryDAO mHistoryDataSource; - private HistoryEntryAdapter mHistoryAdapter; private View mEmptyHistoryView; + + @State + Parcelable mRecyclerViewState; + private RecyclerView mRecyclerView; + private HistoryEntryAdapter mHistoryAdapter; private ItemTouchHelper.SimpleCallback mHistoryItemSwipeCallback; - private PublishSubject mHistoryEntryDeleteSubject; + private int allowedSwipeToDeleteDirections = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT; + + private HistoryDAO mHistoryDataSource; + private PublishSubject> mHistoryEntryDeleteSubject; + private PublishSubject> mHistoryEntryInsertSubject; @StringRes abstract int getEnabledConfigKey(); @@ -64,19 +77,29 @@ public abstract class HistoryFragment extends Fragment // Register history enabled listener mSharedPreferences.registerOnSharedPreferenceChangeListener(mHistoryIsEnabledChangeListener); - mHistoryDataSource = createHistoryDAO(getContext()); + mHistoryDataSource = createHistoryDAO(); mHistoryEntryDeleteSubject = PublishSubject.create(); mHistoryEntryDeleteSubject .observeOn(Schedulers.io()) - .subscribe(new Consumer() { + .subscribe(new Consumer>() { @Override - public void accept(E historyEntry) throws Exception { - mHistoryDataSource.delete(historyEntry); + public void accept(Collection historyEntries) throws Exception { + mHistoryDataSource.delete(historyEntries); } }); - mHistoryItemSwipeCallback = new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) { + mHistoryEntryInsertSubject = PublishSubject.create(); + mHistoryEntryInsertSubject + .observeOn(Schedulers.io()) + .subscribe(new Consumer>() { + @Override + public void accept(Collection historyEntries) throws Exception { + mHistoryDataSource.insertAll(historyEntries); + } + }); + + mHistoryItemSwipeCallback = new ItemTouchHelper.SimpleCallback(0, allowedSwipeToDeleteDirections) { @Override public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { return false; @@ -85,8 +108,20 @@ public abstract class HistoryFragment extends Fragment @Override public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) { if (mHistoryAdapter != null) { - E historyEntry = mHistoryAdapter.removeItemAt(viewHolder.getAdapterPosition()); - mHistoryEntryDeleteSubject.onNext(historyEntry); + final E historyEntry = mHistoryAdapter.removeItemAt(viewHolder.getAdapterPosition()); + mHistoryEntryDeleteSubject.onNext(Collections.singletonList(historyEntry)); + + View view = getActivity().findViewById(R.id.main_content); + if (view == null) view = mRecyclerView.getRootView(); + + Snackbar.make(view, R.string.item_deleted, 5 * 1000) + .setActionTextColor(Color.WHITE) + .setAction(R.string.undo, new View.OnClickListener() { + @Override + public void onClick(View v) { + mHistoryEntryInsertSubject.onNext(Collections.singletonList(historyEntry)); + } + }).show(); } } }; @@ -98,7 +133,7 @@ public abstract class HistoryFragment extends Fragment @Override public void onResume() { super.onResume(); - mHistoryDataSource.findAll() + mHistoryDataSource.getAll() .toObservable() .observeOn(AndroidSchedulers.mainThread()) .subscribe(getHistoryListConsumer()); @@ -121,9 +156,14 @@ public abstract class HistoryFragment extends Fragment if (!historyEntries.isEmpty()) { mHistoryAdapter.setEntries(historyEntries); animateView(mEmptyHistoryView, false, 200); + + if (mRecyclerViewState != null) { + mRecyclerView.getLayoutManager().onRestoreInstanceState(mRecyclerViewState); + mRecyclerViewState = null; + } } else { mHistoryAdapter.clear(); - onEmptyHistory(); + showEmptyHistory(); } } @@ -148,11 +188,33 @@ public abstract class HistoryFragment extends Fragment */ @MainThread public void onHistoryCleared() { + final Parcelable stateBeforeClear = mRecyclerView.getLayoutManager().onSaveInstanceState(); + final Collection itemsToDelete = new ArrayList<>(mHistoryAdapter.getItems()); + mHistoryEntryDeleteSubject.onNext(itemsToDelete); + + View view = getActivity().findViewById(R.id.main_content); + if (view == null) view = mRecyclerView.getRootView(); + + if (!itemsToDelete.isEmpty()) { + Snackbar.make(view, R.string.history_cleared, 5 * 1000) + .setActionTextColor(Color.WHITE) + .setAction(R.string.undo, new View.OnClickListener() { + @Override + public void onClick(View v) { + mRecyclerViewState = stateBeforeClear; + mHistoryEntryInsertSubject.onNext(itemsToDelete); + } + }).show(); + } else { + Snackbar.make(view, R.string.history_cleared, Snackbar.LENGTH_LONG).show(); + } + mHistoryAdapter.clear(); - onEmptyHistory(); + showEmptyHistory(); + } - private void onEmptyHistory() { + private void showEmptyHistory() { if (mHistoryIsEnabled) { animateView(mEmptyHistoryView, true, 200); } @@ -196,12 +258,14 @@ public abstract class HistoryFragment extends Fragment mHistoryDataSource = null; } - /** - * Called when the history is cleared - */ - @CallSuper - public void onClearHistory() { - mHistoryDataSource.deleteAll(); + @Override + public void onPause() { + super.onPause(); + mRecyclerViewState = mRecyclerView.getLayoutManager().onSaveInstanceState(); + } + + public void setAllowedSwipeToDeleteDirections(int allowedSwipeToDeleteDirections) { + this.allowedSwipeToDeleteDirections = allowedSwipeToDeleteDirections; } /** @@ -228,11 +292,10 @@ public abstract class HistoryFragment extends Fragment /** * Creates a new history DAO * - * @param context the fragments context * @return the history DAO */ @NonNull - protected abstract HistoryDAO createHistoryDAO(Context context); + protected abstract HistoryDAO createHistoryDAO(); private class HistoryIsEnabledChangeListener implements SharedPreferences.OnSharedPreferenceChangeListener { @Override diff --git a/app/src/main/java/org/schabi/newpipe/history/HistoryListener.java b/app/src/main/java/org/schabi/newpipe/history/HistoryListener.java new file mode 100644 index 000000000..8b6c91328 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryListener.java @@ -0,0 +1,31 @@ +package org.schabi.newpipe.history; + +import org.schabi.newpipe.extractor.stream.AudioStream; +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.VideoStream; + +public interface HistoryListener { + /** + * Called when a video is played + * + * @param streamInfo the stream info + * @param videoStream the video stream that is played + */ + void onVideoPlayed(StreamInfo streamInfo, VideoStream videoStream); + + /** + * Called when the audio is played in the background + * + * @param streamInfo the stream info + * @param audioStream the audio stream that is played + */ + void onAudioPlayed(StreamInfo streamInfo, AudioStream audioStream); + + /** + * Called when the user searched for something + * + * @param serviceId which service the search was done + * @param query what the user searched for + */ + void onSearch(int serviceId, String query); +} diff --git a/app/src/main/java/org/schabi/newpipe/history/SearchHistoryFragment.java b/app/src/main/java/org/schabi/newpipe/history/SearchHistoryFragment.java index 5904dd986..888086a83 100644 --- a/app/src/main/java/org/schabi/newpipe/history/SearchHistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/history/SearchHistoryFragment.java @@ -11,7 +11,6 @@ import android.widget.TextView; import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; -import org.schabi.newpipe.database.AppDatabase; import org.schabi.newpipe.database.history.dao.HistoryDAO; import org.schabi.newpipe.database.history.model.SearchHistoryEntry; import org.schabi.newpipe.util.NavigationHelper; @@ -37,8 +36,8 @@ public class SearchHistoryFragment extends HistoryFragment { @NonNull @Override - protected HistoryDAO createHistoryDAO(Context context) { - return NewPipeDatabase.getInstance(context).searchHistoryDAO(); + protected HistoryDAO createHistoryDAO() { + return NewPipeDatabase.getInstance().searchHistoryDAO(); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/history/WatchedHistoryFragment.java b/app/src/main/java/org/schabi/newpipe/history/WatchedHistoryFragment.java index f096d7c63..086528af7 100644 --- a/app/src/main/java/org/schabi/newpipe/history/WatchedHistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/history/WatchedHistoryFragment.java @@ -19,9 +19,10 @@ import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.database.history.dao.HistoryDAO; import org.schabi.newpipe.database.history.model.WatchHistoryEntry; +import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder; +import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; -import static org.schabi.newpipe.info_list.InfoItemBuilder.getDurationString; public class WatchedHistoryFragment extends HistoryFragment { @@ -50,8 +51,8 @@ public class WatchedHistoryFragment extends HistoryFragment { @NonNull @Override - protected HistoryDAO createHistoryDAO(Context context) { - return NewPipeDatabase.getInstance(context).watchHistoryDAO(); + protected HistoryDAO createHistoryDAO() { + return NewPipeDatabase.getInstance().watchHistoryDAO(); } @Override @@ -71,7 +72,7 @@ public class WatchedHistoryFragment extends HistoryFragment { @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { LayoutInflater inflater = LayoutInflater.from(parent.getContext()); - View itemView = inflater.inflate(R.layout.stream_item, parent, false); + View itemView = inflater.inflate(R.layout.list_stream_item, parent, false); return new ViewHolder(itemView); } @@ -87,9 +88,9 @@ public class WatchedHistoryFragment extends HistoryFragment { holder.date.setText(getFormattedDate(entry.getCreationDate())); holder.streamTitle.setText(entry.getTitle()); holder.uploader.setText(entry.getUploader()); - holder.duration.setText(getDurationString(entry.getDuration())); + holder.duration.setText(Localization.getDurationString(entry.getDuration())); ImageLoader.getInstance() - .displayImage(entry.getThumbnailURL(), holder.thumbnailView); + .displayImage(entry.getThumbnailURL(), holder.thumbnailView, StreamInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS); } } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/ChannelInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/ChannelInfoItemHolder.java deleted file mode 100644 index 5543907e5..000000000 --- a/app/src/main/java/org/schabi/newpipe/info_list/ChannelInfoItemHolder.java +++ /dev/null @@ -1,52 +0,0 @@ -package org.schabi.newpipe.info_list; - -import android.view.View; -import android.widget.TextView; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.InfoItem; - -import de.hdodenhof.circleimageview.CircleImageView; - -/** - * Created by Christian Schabesberger on 12.02.17. - * - * Copyright (C) Christian Schabesberger 2016 - * ChannelInfoItemHolder .java is part of NewPipe. - * - * NewPipe is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * NewPipe is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with NewPipe. If not, see . - */ - -public class ChannelInfoItemHolder extends InfoItemHolder { - public final CircleImageView itemThumbnailView; - public final TextView itemChannelTitleView; - public final TextView itemAdditionalDetailView; - public final TextView itemChannelDescriptionView; - - public final View itemRoot; - - ChannelInfoItemHolder(View v) { - super(v); - itemRoot = v.findViewById(R.id.itemRoot); - itemThumbnailView = (CircleImageView) v.findViewById(R.id.itemThumbnailView); - itemChannelTitleView = (TextView) v.findViewById(R.id.itemChannelTitleView); - itemAdditionalDetailView = (TextView) v.findViewById(R.id.itemAdditionalDetails); - itemChannelDescriptionView = (TextView) v.findViewById(R.id.itemChannelDescriptionView); - } - - @Override - public InfoItem.InfoType infoType() { - return InfoItem.InfoType.CHANNEL; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java index 8033d281b..c9a772fc1 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java @@ -1,25 +1,25 @@ package org.schabi.newpipe.info_list; import android.content.Context; -import android.text.TextUtils; +import android.support.annotation.NonNull; import android.util.Log; -import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.ImageLoader; -import org.schabi.newpipe.ImageErrorLoadingListener; -import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.AbstractStreamInfo; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.channel.ChannelInfoItem; -import org.schabi.newpipe.extractor.stream_info.StreamInfoItem; +import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder; +import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder; +import org.schabi.newpipe.info_list.holder.InfoItemHolder; +import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder; +import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder; +import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder; -import java.util.Locale; - -/** +/* * Created by Christian Schabesberger on 26.09.16. *

* Copyright (C) Christian Schabesberger 2016 @@ -40,250 +40,77 @@ import java.util.Locale; */ public class InfoItemBuilder { - - private final String viewsS; - private final String videosS; - private final String subsS; - private final String subsPluralS; - - private final String thousand; - private final String million; - private final String billion; - private static final String TAG = InfoItemBuilder.class.toString(); - public interface OnInfoItemSelectedListener { - void selected(int serviceId, String url, String title); + public interface OnInfoItemSelectedListener { + void selected(T selectedItem); } + private final Context context; private ImageLoader imageLoader = ImageLoader.getInstance(); - /** Base display options */ - private static final DisplayImageOptions DISPLAY_IMAGE_OPTIONS = - new DisplayImageOptions.Builder() - .cacheInMemory(true) - .build(); - - /** Display options for stream thumbnails */ - private static final DisplayImageOptions DISPLAY_STREAM_THUMBNAIL_OPTIONS = - new DisplayImageOptions.Builder() - .cloneFrom(DISPLAY_IMAGE_OPTIONS) - .showImageOnFail(R.drawable.dummy_thumbnail) - .showImageForEmptyUri(R.drawable.dummy_thumbnail) - .showImageOnLoading(R.drawable.dummy_thumbnail) - .build(); - - /** Display options for channel thumbnails */ - private static final DisplayImageOptions DISPLAY_CHANNEL_THUMBNAIL_OPTIONS = - new DisplayImageOptions.Builder() - .cloneFrom(DISPLAY_IMAGE_OPTIONS) - .showImageOnLoading(R.drawable.buddy_channel_item) - .showImageForEmptyUri(R.drawable.buddy_channel_item) - .showImageOnFail(R.drawable.buddy_channel_item) - .build(); - private OnInfoItemSelectedListener onStreamInfoItemSelectedListener; - private OnInfoItemSelectedListener onChannelInfoItemSelectedListener; + private OnInfoItemSelectedListener onStreamSelectedListener; + private OnInfoItemSelectedListener onChannelSelectedListener; + private OnInfoItemSelectedListener onPlaylistSelectedListener; public InfoItemBuilder(Context context) { - viewsS = context.getString(R.string.views); - videosS = context.getString(R.string.videos); - subsS = context.getString(R.string.subscriber); - subsPluralS = context.getString(R.string.subscriber_plural); - thousand = context.getString(R.string.short_thousand); - million = context.getString(R.string.short_million); - billion = context.getString(R.string.short_billion); + this.context = context; } - public void setOnStreamInfoItemSelectedListener( - OnInfoItemSelectedListener listener) { - this.onStreamInfoItemSelectedListener = listener; + public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem) { + return buildView(parent, infoItem, false); } - public void setOnChannelInfoItemSelectedListener( - OnInfoItemSelectedListener listener) { - this.onChannelInfoItemSelectedListener = listener; + public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem, boolean useMiniVariant) { + InfoItemHolder holder = holderFromInfoType(parent, infoItem.info_type, useMiniVariant); + holder.updateFromItem(infoItem); + return holder.itemView; } - public void buildByHolder(InfoItemHolder holder, final InfoItem i) { - if (i.infoType() != holder.infoType()) - return; - switch (i.infoType()) { + private InfoItemHolder holderFromInfoType(@NonNull ViewGroup parent, @NonNull InfoItem.InfoType infoType, boolean useMiniVariant) { + switch (infoType) { case STREAM: - buildStreamInfoItem((StreamInfoItemHolder) holder, (StreamInfoItem) i); - break; + return useMiniVariant ? new StreamMiniInfoItemHolder(this, parent) : new StreamInfoItemHolder(this, parent); case CHANNEL: - buildChannelInfoItem((ChannelInfoItemHolder) holder, (ChannelInfoItem) i); - break; + return useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent) : new ChannelInfoItemHolder(this, parent); case PLAYLIST: - Log.e(TAG, "Not yet implemented"); - break; + return new PlaylistInfoItemHolder(this, parent); default: Log.e(TAG, "Trollolo"); + throw new RuntimeException("InfoType not expected = " + infoType.name()); } } - public View buildView(ViewGroup parent, final InfoItem info) { - View itemView = null; - InfoItemHolder holder = null; - LayoutInflater inflater = LayoutInflater.from(parent.getContext()); - switch (info.infoType()) { - case STREAM: - //long start = System.nanoTime(); - itemView = inflater.inflate(R.layout.stream_item, parent, false); - //Log.d(TAG, "time to inflate: " + ((System.nanoTime() - start) / 1000000L) + "ms"); - holder = new StreamInfoItemHolder(itemView); - break; - case CHANNEL: - itemView = inflater.inflate(R.layout.channel_item, parent, false); - holder = new ChannelInfoItemHolder(itemView); - break; - case PLAYLIST: - Log.e(TAG, "Not yet implemented"); - default: - Log.e(TAG, "Trollolo"); - } - buildByHolder(holder, info); - return itemView; + public Context getContext() { + return context; } - - private String getStreamInfoDetailLine(final StreamInfoItem info) { - String viewsAndDate = ""; - if(info.view_count >= 0) { - viewsAndDate = shortViewCount(info.view_count); - } - if(!TextUtils.isEmpty(info.upload_date)) { - if(viewsAndDate.isEmpty()) { - viewsAndDate = info.upload_date; - } else { - viewsAndDate += " • " + info.upload_date; - } - } - return viewsAndDate; + public ImageLoader getImageLoader() { + return imageLoader; } - private void buildStreamInfoItem(StreamInfoItemHolder holder, final StreamInfoItem info) { - if (info.infoType() != InfoItem.InfoType.STREAM) { - Log.e("InfoItemBuilder", "Info type not yet supported"); - } - // fill holder with information - if (!TextUtils.isEmpty(info.title)) holder.itemVideoTitleView.setText(info.title); - - if (!TextUtils.isEmpty(info.uploader)) holder.itemUploaderView.setText(info.uploader); - else holder.itemUploaderView.setVisibility(View.INVISIBLE); - - if (info.duration > 0) { - holder.itemDurationView.setText(getDurationString(info.duration)); - } else { - if (info.stream_type == AbstractStreamInfo.StreamType.LIVE_STREAM) { - holder.itemDurationView.setText(R.string.duration_live); - } else { - holder.itemDurationView.setVisibility(View.GONE); - } - } - - holder.itemAdditionalDetails.setText(getStreamInfoDetailLine(info)); - - // Default thumbnail is shown on error, while loading and if the url is empty - imageLoader.displayImage(info.thumbnail_url, - holder.itemThumbnailView, - DISPLAY_STREAM_THUMBNAIL_OPTIONS, - new ImageErrorLoadingListener(holder.itemRoot.getContext(), holder.itemRoot.getRootView(), info.service_id)); - - - holder.itemRoot.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - if(onStreamInfoItemSelectedListener != null) { - onStreamInfoItemSelectedListener.selected(info.service_id, info.webpage_url, info.getTitle()); - } - } - }); + public OnInfoItemSelectedListener getOnStreamSelectedListener() { + return onStreamSelectedListener; } - private String getChannelInfoDetailLine(final ChannelInfoItem info) { - String details = ""; - if(info.subscriberCount >= 0) { - details = shortSubscriber(info.subscriberCount); - } - if(info.videoAmount >= 0) { - String formattedVideoAmount = info.videoAmount + " " + videosS; - if(!details.isEmpty()) { - details += " • " + formattedVideoAmount; - } else { - details = formattedVideoAmount; - } - } - return details; + public void setOnStreamSelectedListener(OnInfoItemSelectedListener listener) { + this.onStreamSelectedListener = listener; } - private void buildChannelInfoItem(ChannelInfoItemHolder holder, final ChannelInfoItem info) { - if (!TextUtils.isEmpty(info.getTitle())) holder.itemChannelTitleView.setText(info.getTitle()); - holder.itemAdditionalDetailView.setText(getChannelInfoDetailLine(info)); - if (!TextUtils.isEmpty(info.description)) holder.itemChannelDescriptionView.setText(info.description); - - imageLoader.displayImage(info.thumbnailUrl, - holder.itemThumbnailView, - DISPLAY_CHANNEL_THUMBNAIL_OPTIONS, - new ImageErrorLoadingListener(holder.itemRoot.getContext(), holder.itemRoot.getRootView(), info.serviceId)); - - - holder.itemRoot.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - if(onChannelInfoItemSelectedListener != null) { - onChannelInfoItemSelectedListener.selected(info.serviceId, info.getLink(), info.channelName); - } - } - }); + public OnInfoItemSelectedListener getOnChannelSelectedListener() { + return onChannelSelectedListener; } - public String shortViewCount(Long viewCount) { - if (viewCount >= 1000000000) { - return Long.toString(viewCount / 1000000000) + billion + " " + viewsS; - } else if (viewCount >= 1000000) { - return Long.toString(viewCount / 1000000) + million + " " + viewsS; - } else if (viewCount >= 1000) { - return Long.toString(viewCount / 1000) + thousand + " " + viewsS; - } else { - return Long.toString(viewCount) + " " + viewsS; - } + public void setOnChannelSelectedListener(OnInfoItemSelectedListener listener) { + this.onChannelSelectedListener = listener; } - public String shortSubscriber(Long count) { - String curSubString = count > 1 ? subsPluralS : subsS; - - if (count >= 1000000000) { - return Long.toString(count / 1000000000) + billion + " " + curSubString; - } else if (count >= 1000000) { - return Long.toString(count / 1000000) + million + " " + curSubString; - } else if (count >= 1000) { - return Long.toString(count / 1000) + thousand + " " + curSubString; - } else { - return Long.toString(count) + " " + curSubString; - } + public OnInfoItemSelectedListener getOnPlaylistSelectedListener() { + return onPlaylistSelectedListener; } - public static String getDurationString(int duration) { - if(duration < 0) { - duration = 0; - } - String output; - int days = duration / (24 * 60 * 60); /* greater than a day */ - duration %= (24 * 60 * 60); - int hours = duration / (60 * 60); /* greater than an hour */ - duration %= (60 * 60); - int minutes = duration / 60; - int seconds = duration % 60; - - //handle days - if (days > 0) { - output = String.format(Locale.US, "%d:%02d:%02d:%02d", days, hours, minutes, seconds); - } else if(hours > 0) { - output = String.format(Locale.US, "%d:%02d:%02d", hours, minutes, seconds); - } else { - output = String.format(Locale.US, "%d:%02d", minutes, seconds); - } - return output; + public void setOnPlaylistSelectedListener(OnInfoItemSelectedListener listener) { + this.onPlaylistSelectedListener = listener; } + } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemHolder.java deleted file mode 100644 index c1fab069b..000000000 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemHolder.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.schabi.newpipe.info_list; - -import android.support.v7.widget.RecyclerView; -import android.view.View; - -import org.schabi.newpipe.extractor.InfoItem; - -/** - * Created by Christian Schabesberger on 12.02.17. - * - * Copyright (C) Christian Schabesberger 2016 - * InfoItemHolder.java is part of NewPipe. - * - * NewPipe is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * NewPipe is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with NewPipe. If not, see . - */ - -public abstract class InfoItemHolder extends RecyclerView.ViewHolder { - public InfoItemHolder(View v) { - super(v); - } - public abstract InfoItem.InfoType infoType(); -} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java index 5230b2772..806b348d7 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java @@ -2,20 +2,26 @@ package org.schabi.newpipe.info_list; import android.app.Activity; import android.support.v7.widget.RecyclerView; -import android.text.Layout; import android.util.Log; -import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.channel.ChannelInfoItem; +import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.info_list.InfoItemBuilder.OnInfoItemSelectedListener; +import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder; +import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder; +import org.schabi.newpipe.info_list.holder.InfoItemHolder; +import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder; +import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder; +import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder; import java.util.ArrayList; import java.util.List; -/** +/* * Created by Christian Schabesberger on 01.08.16. * * Copyright (C) Christian Schabesberger 2016 @@ -36,25 +42,32 @@ import java.util.List; */ public class InfoListAdapter extends RecyclerView.Adapter { - private static final String TAG = InfoListAdapter.class.toString(); + private static final String TAG = InfoListAdapter.class.getSimpleName(); + private static final boolean DEBUG = false; + + private static final int HEADER_TYPE = 0; + private static final int FOOTER_TYPE = 1; + + private static final int MINI_STREAM_HOLDER_TYPE = 0x100; + private static final int STREAM_HOLDER_TYPE = 0x101; + private static final int MINI_CHANNEL_HOLDER_TYPE = 0x200; + private static final int CHANNEL_HOLDER_TYPE = 0x201; + private static final int PLAYLIST_HOLDER_TYPE = 0x301; private final InfoItemBuilder infoItemBuilder; private final ArrayList infoItemList; + private boolean useMiniVariant = false; private boolean showFooter = false; private View header = null; private View footer = null; public class HFHolder extends RecyclerView.ViewHolder { + public View view; + public HFHolder(View v) { super(v); view = v; } - public View view; - } - - public void showFooter(boolean show) { - showFooter = show; - notifyDataSetChanged(); } public InfoListAdapter(Activity a) { @@ -62,32 +75,71 @@ public class InfoListAdapter extends RecyclerView.Adapter(); } - public void setOnStreamInfoItemSelectedListener - (InfoItemBuilder.OnInfoItemSelectedListener listener) { - infoItemBuilder.setOnStreamInfoItemSelectedListener(listener); + public void setOnStreamSelectedListener(OnInfoItemSelectedListener listener) { + infoItemBuilder.setOnStreamSelectedListener(listener); } - public void setOnChannelInfoItemSelectedListener - (InfoItemBuilder.OnInfoItemSelectedListener listener) { - infoItemBuilder.setOnChannelInfoItemSelectedListener(listener); + public void setOnChannelSelectedListener(OnInfoItemSelectedListener listener) { + infoItemBuilder.setOnChannelSelectedListener(listener); + } + + public void setOnPlaylistSelectedListener(OnInfoItemSelectedListener listener) { + infoItemBuilder.setOnPlaylistSelectedListener(listener); + } + + public void useMiniItemVariants(boolean useMiniVariant) { + this.useMiniVariant = useMiniVariant; } public void addInfoItemList(List data) { - if(data != null) { + if (data != null) { + if (DEBUG) { + Log.d(TAG, "addInfoItemList() before > infoItemList.size() = " + infoItemList.size() + ", data.size() = " + data.size()); + } + + int offsetStart = sizeConsideringHeaderOffset(); infoItemList.addAll(data); - notifyDataSetChanged(); + + if (DEBUG) { + Log.d(TAG, "addInfoItemList() after > offsetStart = " + offsetStart + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter); + } + + notifyItemRangeInserted(offsetStart, data.size()); + + if (footer != null && showFooter) { + int footerNow = sizeConsideringHeaderOffset(); + notifyItemMoved(offsetStart, footerNow); + + if (DEBUG) Log.d(TAG, "addInfoItemList() footer from " + offsetStart + " to " + footerNow); + } } } public void addInfoItem(InfoItem data) { if (data != null) { - infoItemList.add( data ); - notifyDataSetChanged(); + if (DEBUG) { + Log.d(TAG, "addInfoItem() before > infoItemList.size() = " + infoItemList.size() + ", thread = " + Thread.currentThread()); + } + + int positionInserted = sizeConsideringHeaderOffset(); + infoItemList.add(data); + + if (DEBUG) { + Log.d(TAG, "addInfoItem() after > position = " + positionInserted + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter); + } + notifyItemInserted(positionInserted); + + if (footer != null && showFooter) { + int footerNow = sizeConsideringHeaderOffset(); + notifyItemMoved(positionInserted, footerNow); + + if (DEBUG) Log.d(TAG, "addInfoItem() footer from " + positionInserted + " to " + footerNow); + } } } public void clearStreamItemList() { - if(infoItemList.isEmpty()) { + if (infoItemList.isEmpty()) { return; } infoItemList.clear(); @@ -95,13 +147,29 @@ public class InfoListAdapter extends RecyclerView.Adapter getItemsList() { @@ -111,30 +179,35 @@ public class InfoListAdapter extends RecyclerView.Adapter - * Copyright (C) Christian Schabesberger 2016 - * StreamInfoItemHolder.java is part of NewPipe. - *

- * NewPipe is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - *

- * NewPipe is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - *

- * You should have received a copy of the GNU General Public License - * along with NewPipe. If not, see . - */ - -public class StreamInfoItemHolder extends InfoItemHolder { - - public final ImageView itemThumbnailView; - public final TextView itemVideoTitleView, - itemUploaderView, - itemDurationView, - itemAdditionalDetails; - public final View itemRoot; - - public StreamInfoItemHolder(View v) { - super(v); - itemRoot = v.findViewById(R.id.itemRoot); - itemThumbnailView = (ImageView) v.findViewById(R.id.itemThumbnailView); - itemVideoTitleView = (TextView) v.findViewById(R.id.itemVideoTitleView); - itemUploaderView = (TextView) v.findViewById(R.id.itemUploaderView); - itemDurationView = (TextView) v.findViewById(R.id.itemDurationView); - itemAdditionalDetails = (TextView) v.findViewById(R.id.itemAdditionalDetails); - } - - @Override - public InfoItem.InfoType infoType() { - return InfoItem.InfoType.STREAM; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelInfoItemHolder.java new file mode 100644 index 000000000..15c9c46e0 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelInfoItemHolder.java @@ -0,0 +1,65 @@ +package org.schabi.newpipe.info_list.holder; + +import android.view.ViewGroup; +import android.widget.TextView; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.extractor.channel.ChannelInfoItem; +import org.schabi.newpipe.info_list.InfoItemBuilder; +import org.schabi.newpipe.util.Localization; + +/* + * Created by Christian Schabesberger on 12.02.17. + * + * Copyright (C) Christian Schabesberger 2016 + * ChannelInfoItemHolder .java is part of NewPipe. + * + * NewPipe is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * NewPipe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with NewPipe. If not, see . + */ + +public class ChannelInfoItemHolder extends ChannelMiniInfoItemHolder { + public final TextView itemChannelDescriptionView; + + public ChannelInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) { + super(infoItemBuilder, R.layout.list_channel_item, parent); + itemChannelDescriptionView = itemView.findViewById(R.id.itemChannelDescriptionView); + } + + @Override + public void updateFromItem(final InfoItem infoItem) { + super.updateFromItem(infoItem); + + if (!(infoItem instanceof ChannelInfoItem)) return; + final ChannelInfoItem item = (ChannelInfoItem) infoItem; + + itemChannelDescriptionView.setText(item.description); + } + + @Override + protected String getDetailLine(final ChannelInfoItem item) { + String details = super.getDetailLine(item); + + if (item.stream_count >= 0) { + String formattedVideoAmount = Localization.localizeStreamCount(itemBuilder.getContext(), item.stream_count); + + if (!details.isEmpty()) { + details += " • " + formattedVideoAmount; + } else { + details = formattedVideoAmount; + } + } + return details; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java new file mode 100644 index 000000000..9aef6dbd2 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java @@ -0,0 +1,73 @@ +package org.schabi.newpipe.info_list.holder; + +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.nostra13.universalimageloader.core.DisplayImageOptions; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.extractor.channel.ChannelInfoItem; +import org.schabi.newpipe.info_list.InfoItemBuilder; +import org.schabi.newpipe.util.Localization; + +import de.hdodenhof.circleimageview.CircleImageView; + +public class ChannelMiniInfoItemHolder extends InfoItemHolder { + public final CircleImageView itemThumbnailView; + public final TextView itemTitleView; + public final TextView itemAdditionalDetailView; + + ChannelMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) { + super(infoItemBuilder, layoutId, parent); + + itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView); + itemTitleView = itemView.findViewById(R.id.itemTitleView); + itemAdditionalDetailView = itemView.findViewById(R.id.itemAdditionalDetails); + } + + public ChannelMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) { + this(infoItemBuilder, R.layout.list_channel_mini_item, parent); + } + + @Override + public void updateFromItem(final InfoItem infoItem) { + if (!(infoItem instanceof ChannelInfoItem)) return; + final ChannelInfoItem item = (ChannelInfoItem) infoItem; + + itemTitleView.setText(item.name); + itemAdditionalDetailView.setText(getDetailLine(item)); + + itemBuilder.getImageLoader() + .displayImage(item.thumbnail_url, itemThumbnailView, ChannelInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS); + + itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (itemBuilder.getOnChannelSelectedListener() != null) { + itemBuilder.getOnChannelSelectedListener().selected(item); + } + } + }); + } + + protected String getDetailLine(final ChannelInfoItem item) { + String details = ""; + if (item.subscriber_count >= 0) { + details += Localization.shortSubscriberCount(itemBuilder.getContext(), item.subscriber_count); + } + return details; + } + + /** + * Display options for channel thumbnails + */ + public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = + new DisplayImageOptions.Builder() + .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) + .showImageOnLoading(R.drawable.buddy_channel_item) + .showImageForEmptyUri(R.drawable.buddy_channel_item) + .showImageOnFail(R.drawable.buddy_channel_item) + .build(); +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/InfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/InfoItemHolder.java new file mode 100644 index 000000000..fb5aa2b7c --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/InfoItemHolder.java @@ -0,0 +1,53 @@ +package org.schabi.newpipe.info_list.holder; + +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import com.nostra13.universalimageloader.core.DisplayImageOptions; + +import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.info_list.InfoItemBuilder; + +/* + * Created by Christian Schabesberger on 12.02.17. + * + * Copyright (C) Christian Schabesberger 2016 + * InfoItemHolder.java is part of NewPipe. + * + * NewPipe is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * NewPipe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with NewPipe. If not, see . + */ + +public abstract class InfoItemHolder extends RecyclerView.ViewHolder { + protected final InfoItemBuilder itemBuilder; + + public InfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) { + super(LayoutInflater.from(infoItemBuilder.getContext()).inflate(layoutId, parent, false)); + this.itemBuilder = infoItemBuilder; + } + + public abstract void updateFromItem(final InfoItem infoItem); + + /*////////////////////////////////////////////////////////////////////////// + // ImageLoaderOptions + //////////////////////////////////////////////////////////////////////////*/ + + /** + * Base display options + */ + public static final DisplayImageOptions BASE_DISPLAY_IMAGE_OPTIONS = + new DisplayImageOptions.Builder() + .cacheInMemory(true) + .build(); +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistInfoItemHolder.java new file mode 100644 index 000000000..3c29a4b76 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistInfoItemHolder.java @@ -0,0 +1,62 @@ +package org.schabi.newpipe.info_list.holder; + +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import com.nostra13.universalimageloader.core.DisplayImageOptions; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; +import org.schabi.newpipe.info_list.InfoItemBuilder; + +public class PlaylistInfoItemHolder extends InfoItemHolder { + public final ImageView itemThumbnailView; + public final TextView itemStreamCountView; + public final TextView itemTitleView; + public final TextView itemUploaderView; + + public PlaylistInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) { + super(infoItemBuilder, R.layout.list_playlist_item, parent); + + itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView); + itemTitleView = itemView.findViewById(R.id.itemTitleView); + itemStreamCountView = itemView.findViewById(R.id.itemStreamCountView); + itemUploaderView = itemView.findViewById(R.id.itemUploaderView); + } + + @Override + public void updateFromItem(final InfoItem infoItem) { + if (!(infoItem instanceof PlaylistInfoItem)) return; + final PlaylistInfoItem item = (PlaylistInfoItem) infoItem; + + itemTitleView.setText(item.name); + itemStreamCountView.setText(item.stream_count + ""); + itemUploaderView.setText(item.uploader_name); + + itemBuilder.getImageLoader() + .displayImage(item.thumbnail_url, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS); + + itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (itemBuilder.getOnPlaylistSelectedListener() != null) { + itemBuilder.getOnPlaylistSelectedListener().selected(item); + } + } + }); + } + + /** + * Display options for playlist thumbnails + */ + public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = + new DisplayImageOptions.Builder() + .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) + .showImageOnLoading(R.drawable.dummy_thumbnail_playlist) + .showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist) + .showImageOnFail(R.drawable.dummy_thumbnail_playlist) + .build(); +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamInfoItemHolder.java new file mode 100644 index 000000000..78954a2ee --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamInfoItemHolder.java @@ -0,0 +1,66 @@ +package org.schabi.newpipe.info_list.holder; + +import android.text.TextUtils; +import android.view.ViewGroup; +import android.widget.TextView; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.info_list.InfoItemBuilder; +import org.schabi.newpipe.util.Localization; + +/* + * Created by Christian Schabesberger on 01.08.16. + *

+ * Copyright (C) Christian Schabesberger 2016 + * StreamInfoItemHolder.java is part of NewPipe. + *

+ * NewPipe is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + *

+ * NewPipe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *

+ * You should have received a copy of the GNU General Public License + * along with NewPipe. If not, see . + */ + +public class StreamInfoItemHolder extends StreamMiniInfoItemHolder { + + public final TextView itemAdditionalDetails; + + public StreamInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) { + super(infoItemBuilder, R.layout.list_stream_item, parent); + itemAdditionalDetails = itemView.findViewById(R.id.itemAdditionalDetails); + } + + @Override + public void updateFromItem(final InfoItem infoItem) { + super.updateFromItem(infoItem); + + if (!(infoItem instanceof StreamInfoItem)) return; + final StreamInfoItem item = (StreamInfoItem) infoItem; + + itemAdditionalDetails.setText(getStreamInfoDetailLine(item)); + } + + private String getStreamInfoDetailLine(final StreamInfoItem infoItem) { + String viewsAndDate = ""; + if (infoItem.view_count >= 0) { + viewsAndDate = Localization.shortViewCount(itemBuilder.getContext(), infoItem.view_count); + } + if (!TextUtils.isEmpty(infoItem.upload_date)) { + if (viewsAndDate.isEmpty()) { + viewsAndDate = infoItem.upload_date; + } else { + viewsAndDate += " • " + infoItem.upload_date; + } + } + return viewsAndDate; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java new file mode 100644 index 000000000..138503d39 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java @@ -0,0 +1,82 @@ +package org.schabi.newpipe.info_list.holder; + +import android.support.v4.content.ContextCompat; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import com.nostra13.universalimageloader.core.DisplayImageOptions; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.extractor.stream.StreamType; +import org.schabi.newpipe.info_list.InfoItemBuilder; +import org.schabi.newpipe.util.Localization; + +public class StreamMiniInfoItemHolder extends InfoItemHolder { + + public final ImageView itemThumbnailView; + public final TextView itemVideoTitleView; + public final TextView itemUploaderView; + public final TextView itemDurationView; + + StreamMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) { + super(infoItemBuilder, layoutId, parent); + + itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView); + itemVideoTitleView = itemView.findViewById(R.id.itemVideoTitleView); + itemUploaderView = itemView.findViewById(R.id.itemUploaderView); + itemDurationView = itemView.findViewById(R.id.itemDurationView); + } + + public StreamMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) { + this(infoItemBuilder, R.layout.list_stream_mini_item, parent); + } + + @Override + public void updateFromItem(final InfoItem infoItem) { + if (!(infoItem instanceof StreamInfoItem)) return; + final StreamInfoItem item = (StreamInfoItem) infoItem; + + itemVideoTitleView.setText(item.name); + itemUploaderView.setText(item.uploader_name); + + if (item.duration > 0) { + itemDurationView.setText(Localization.getDurationString(item.duration)); + itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), R.color.duration_background_color)); + itemDurationView.setVisibility(View.VISIBLE); + } else if (item.stream_type == StreamType.LIVE_STREAM) { + itemDurationView.setText(R.string.duration_live); + itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), R.color.live_duration_background_color)); + itemDurationView.setVisibility(View.VISIBLE); + } else { + itemDurationView.setVisibility(View.GONE); + } + + // Default thumbnail is shown on error, while loading and if the url is empty + itemBuilder.getImageLoader() + .displayImage(item.thumbnail_url, itemThumbnailView, StreamInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS); + + itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (itemBuilder.getOnStreamSelectedListener() != null) { + itemBuilder.getOnStreamSelectedListener().selected(item); + } + } + }); + } + + /** + * Display options for stream thumbnails + */ + public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = + new DisplayImageOptions.Builder() + .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) + .showImageOnFail(R.drawable.dummy_thumbnail) + .showImageForEmptyUri(R.drawable.dummy_thumbnail) + .showImageOnLoading(R.drawable.dummy_thumbnail) + .build(); +} diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java index b52c2e09d..be9247569 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java @@ -1,6 +1,24 @@ +/* + * Copyright 2017 Mauricio Colli + * BackgroundPlayer.java is part of NewPipe + * + * License: GPL-3.0+ + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package org.schabi.newpipe.player; -import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; @@ -22,7 +40,7 @@ import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.StreamingService; -import org.schabi.newpipe.extractor.stream_info.AudioStream; +import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.ThemeHelper; @@ -66,7 +84,6 @@ public class BackgroundPlayer extends Service { private RemoteViews bigNotRemoteView; private final String setAlphaMethodName = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) ? "setImageAlpha" : "setAlpha"; - /*////////////////////////////////////////////////////////////////////////// // Service's LifeCycle //////////////////////////////////////////////////////////////////////////*/ @@ -159,7 +176,7 @@ public class BackgroundPlayer extends Service { //if (videoThumbnail != null) remoteViews.setImageViewBitmap(R.id.notificationCover, videoThumbnail); ///else remoteViews.setImageViewResource(R.id.notificationCover, R.drawable.dummy_thumbnail); remoteViews.setTextViewText(R.id.notificationSongName, basePlayerImpl.getVideoTitle()); - remoteViews.setTextViewText(R.id.notificationArtist, basePlayerImpl.getChannelName()); + remoteViews.setTextViewText(R.id.notificationArtist, basePlayerImpl.getUploaderName()); remoteViews.setOnClickPendingIntent(R.id.notificationPlayPause, PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT)); 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 2b837cc7e..4b0604bb0 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -1,3 +1,22 @@ +/* + * Copyright 2017 Mauricio Colli + * BasePlayer.java is part of NewPipe + * + * License: GPL-3.0+ + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package org.schabi.newpipe.player; import android.animation.Animator; @@ -49,6 +68,7 @@ import com.google.android.exoplayer2.util.Util; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener; +import org.schabi.newpipe.Downloader; import org.schabi.newpipe.R; import java.io.File; @@ -65,6 +85,8 @@ import java.util.concurrent.atomic.AtomicBoolean; */ @SuppressWarnings({"WeakerAccess", "unused"}) public abstract class BasePlayer implements Player.EventListener, AudioManager.OnAudioFocusChangeListener { + // TODO: Check api version for deprecated audio manager methods + public static final boolean DEBUG = false; public static final String TAG = "BasePlayer"; @@ -90,8 +112,8 @@ public abstract class BasePlayer implements Player.EventListener, AudioManager.O protected String videoUrl = ""; protected String videoTitle = ""; protected String videoThumbnailUrl = ""; - protected int videoStartPos = -1; - protected String channelName = ""; + protected long videoStartPos = -1; + protected String uploaderName = ""; /*////////////////////////////////////////////////////////////////////////// // Player @@ -139,7 +161,7 @@ public abstract class BasePlayer implements Player.EventListener, AudioManager.O private void initExoPlayerCache() { if (cacheDataSourceFactory == null) { - DefaultDataSourceFactory dataSourceFactory = new DefaultDataSourceFactory(context, Util.getUserAgent(context, context.getPackageName()), bandwidthMeter); + DefaultDataSourceFactory dataSourceFactory = new DefaultDataSourceFactory(context, Downloader.USER_AGENT, bandwidthMeter); File cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME); if (!cacheDir.exists()) { //noinspection ResultOfMethodCallIgnored @@ -183,8 +205,8 @@ public abstract class BasePlayer implements Player.EventListener, AudioManager.O videoUrl = intent.getStringExtra(VIDEO_URL); videoTitle = intent.getStringExtra(VIDEO_TITLE); videoThumbnailUrl = intent.getStringExtra(VIDEO_THUMBNAIL_URL); - videoStartPos = intent.getIntExtra(START_POSITION, -1); - channelName = intent.getStringExtra(CHANNEL_NAME); + videoStartPos = intent.getLongExtra(START_POSITION, -1L); + uploaderName = intent.getStringExtra(CHANNEL_NAME); setPlaybackSpeed(intent.getFloatExtra(PLAYBACK_SPEED, getPlaybackSpeed())); initThumbnail(); @@ -200,7 +222,8 @@ public abstract class BasePlayer implements Player.EventListener, AudioManager.O @Override public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { if (simpleExoPlayer == null) return; - if (DEBUG) Log.d(TAG, "onLoadingComplete() called with: imageUri = [" + imageUri + "], view = [" + view + "], loadedImage = [" + loadedImage + "]"); + if (DEBUG) + Log.d(TAG, "onLoadingComplete() called with: imageUri = [" + imageUri + "], view = [" + view + "], loadedImage = [" + loadedImage + "]"); videoThumbnail = loadedImage; onThumbnailReceived(loadedImage); } @@ -236,6 +259,10 @@ public abstract class BasePlayer implements Player.EventListener, AudioManager.O simpleExoPlayer.release(); } if (progressLoop != null && isProgressLoopRunning.get()) stopProgressLoop(); + if (audioManager != null) { + audioManager.abandonAudioFocus(this); + audioManager = null; + } } public void destroy() { @@ -327,12 +354,8 @@ public abstract class BasePlayer implements Player.EventListener, AudioManager.O } private boolean isResumeAfterAudioFocusGain() { - if (this.sharedPreferences == null || this.context == null) return false; - - return this.sharedPreferences.getBoolean( - this.context.getString(R.string.resume_on_audio_focus_gain_key), - false - ); + return sharedPreferences != null && context != null + && sharedPreferences.getBoolean(context.getString(R.string.resume_on_audio_focus_gain_key), false); } protected void onAudioFocusGain() { @@ -473,7 +496,8 @@ public abstract class BasePlayer implements Player.EventListener, AudioManager.O @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (DEBUG) Log.d(TAG, "onPlayerStateChanged() called with: playWhenReady = [" + playWhenReady + "], playbackState = [" + playbackState + "]"); + if (DEBUG) + Log.d(TAG, "onPlayerStateChanged() called with: playWhenReady = [" + playWhenReady + "], playbackState = [" + playbackState + "]"); if (getCurrentState() == STATE_PAUSED_SEEK) { if (DEBUG) Log.d(TAG, "onPlayerStateChanged() currently on PausedSeek"); return; @@ -516,7 +540,9 @@ public abstract class BasePlayer implements Player.EventListener, AudioManager.O // General Player //////////////////////////////////////////////////////////////////////////*/ - public abstract void onError(Exception exception); + public void onError(Exception exception){ + destroy(); + } public void onPrepared(boolean playWhenReady) { if (DEBUG) Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]"); @@ -565,7 +591,8 @@ public abstract class BasePlayer implements Player.EventListener, AudioManager.O public void seekBy(int milliSeconds) { if (DEBUG) Log.d(TAG, "seekBy() called with: milliSeconds = [" + milliSeconds + "]"); - if (simpleExoPlayer == null || (isCompleted() && milliSeconds > 0) || ((milliSeconds < 0 && simpleExoPlayer.getCurrentPosition() == 0))) return; + if (simpleExoPlayer == null || (isCompleted() && milliSeconds > 0) || ((milliSeconds < 0 && simpleExoPlayer.getCurrentPosition() == 0))) + return; int progress = (int) (simpleExoPlayer.getCurrentPosition() + milliSeconds); if (progress < 0) progress = 0; simpleExoPlayer.seekTo(progress); @@ -693,11 +720,11 @@ public abstract class BasePlayer implements Player.EventListener, AudioManager.O this.videoUrl = videoUrl; } - public int getVideoStartPos() { + public long getVideoStartPos() { return videoStartPos; } - public void setVideoStartPos(int videoStartPos) { + public void setVideoStartPos(long videoStartPos) { this.videoStartPos = videoStartPos; } @@ -709,12 +736,12 @@ public abstract class BasePlayer implements Player.EventListener, AudioManager.O this.videoTitle = videoTitle; } - public String getChannelName() { - return channelName; + public String getUploaderName() { + return uploaderName; } - public void setChannelName(String channelName) { - this.channelName = channelName; + public void setUploaderName(String uploaderName) { + this.uploaderName = uploaderName; } public boolean isCompleted() { diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index 6706ab4ef..301200dfc 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -1,3 +1,22 @@ +/* + * Copyright 2017 Mauricio Colli + * MainVideoPlayer.java is part of NewPipe + * + * License: GPL-3.0+ + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package org.schabi.newpipe.player; import android.app.Activity; @@ -169,14 +188,14 @@ public class MainVideoPlayer extends Activity { @Override public void initViews(View rootView) { super.initViews(rootView); - this.titleTextView = (TextView) rootView.findViewById(R.id.titleTextView); - this.channelTextView = (TextView) rootView.findViewById(R.id.channelTextView); - this.volumeTextView = (TextView) rootView.findViewById(R.id.volumeTextView); - this.brightnessTextView = (TextView) rootView.findViewById(R.id.brightnessTextView); - this.repeatButton = (ImageButton) rootView.findViewById(R.id.repeatButton); + this.titleTextView = rootView.findViewById(R.id.titleTextView); + this.channelTextView = rootView.findViewById(R.id.channelTextView); + this.volumeTextView = rootView.findViewById(R.id.volumeTextView); + this.brightnessTextView = rootView.findViewById(R.id.brightnessTextView); + this.repeatButton = rootView.findViewById(R.id.repeatButton); - this.screenRotationButton = (ImageButton) rootView.findViewById(R.id.screenRotationButton); - this.playPauseButton = (ImageButton) rootView.findViewById(R.id.playPauseButton); + this.screenRotationButton = rootView.findViewById(R.id.screenRotationButton); + this.playPauseButton = rootView.findViewById(R.id.playPauseButton); // Due to a bug on lower API, lets set the alpha instead of using a drawable if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) repeatButton.setImageAlpha(77); @@ -205,7 +224,7 @@ public class MainVideoPlayer extends Activity { public void handleIntent(Intent intent) { super.handleIntent(intent); titleTextView.setText(getVideoTitle()); - channelTextView.setText(getChannelName()); + channelTextView.setText(getUploaderName()); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index f40843726..b022cd003 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -1,3 +1,22 @@ +/* + * Copyright 2017 Mauricio Colli + * PopupVideoPlayer.java is part of NewPipe + * + * License: GPL-3.0+ + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package org.schabi.newpipe.player; import android.annotation.SuppressLint; @@ -15,6 +34,7 @@ import android.os.Build; import android.os.Handler; import android.os.IBinder; import android.preference.PreferenceManager; +import android.support.annotation.NonNull; import android.support.v4.app.NotificationCompat; import android.util.DisplayMetrics; import android.util.Log; @@ -38,13 +58,29 @@ import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; import org.schabi.newpipe.ReCaptchaActivity; import org.schabi.newpipe.extractor.MediaFormat; +import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; -import org.schabi.newpipe.extractor.stream_info.StreamInfo; +import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; +import org.schabi.newpipe.extractor.exceptions.ParsingException; +import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; +import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor; +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.player.old.PlayVideoActivity; +import org.schabi.newpipe.report.ErrorActivity; +import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.Constants; +import org.schabi.newpipe.util.ExtractorHelper; +import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.ThemeHelper; -import org.schabi.newpipe.util.Utils; -import org.schabi.newpipe.workers.StreamExtractorWorker; + +import java.io.IOException; +import java.util.ArrayList; + +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Consumer; +import io.reactivex.schedulers.Schedulers; import static org.schabi.newpipe.util.AnimationUtils.animateView; @@ -87,7 +123,7 @@ public class PopupVideoPlayer extends Service { private DisplayImageOptions displayImageOptions = new DisplayImageOptions.Builder().cacheInMemory(true).build(); private VideoPlayerImpl playerImpl; - private StreamExtractorWorker currentExtractorWorker; + private Disposable currentWorker; /*////////////////////////////////////////////////////////////////////////// // Service LifeCycle @@ -105,15 +141,33 @@ public class PopupVideoPlayer extends Service { @Override @SuppressWarnings("unchecked") public int onStartCommand(final Intent intent, int flags, int startId) { - if (DEBUG) Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "], flags = [" + flags + "], startId = [" + startId + "]"); + if (DEBUG) + Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "], flags = [" + flags + "], startId = [" + startId + "]"); if (playerImpl.getPlayer() == null) initPopup(); if (!playerImpl.isPlaying()) playerImpl.getPlayer().setPlayWhenReady(true); if (imageLoader != null) imageLoader.clearMemoryCache(); if (intent.getStringExtra(Constants.KEY_URL) != null) { + final int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0); + final String url = intent.getStringExtra(Constants.KEY_URL); + playerImpl.setStartedFromNewPipe(false); - currentExtractorWorker = new StreamExtractorWorker(this, 0, intent.getStringExtra(Constants.KEY_URL), new FetcherRunnable(this)); - currentExtractorWorker.start(); + + final FetcherHandler fetcherRunnable = new FetcherHandler(this, serviceId, url); + currentWorker = ExtractorHelper.getStreamInfo(serviceId,url,false) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Consumer() { + @Override + public void accept(@NonNull StreamInfo info) throws Exception { + fetcherRunnable.onReceive(info); + } + }, new Consumer() { + @Override + public void accept(@NonNull Throwable throwable) throws Exception { + fetcherRunnable.onError(throwable); + } + }); } else { playerImpl.setStartedFromNewPipe(true); playerImpl.handleIntent(intent); @@ -137,11 +191,7 @@ public class PopupVideoPlayer extends Service { if (playerImpl.getRootView() != null) windowManager.removeView(playerImpl.getRootView()); } if (notificationManager != null) notificationManager.cancel(NOTIFICATION_ID); - if (currentExtractorWorker != null) { - currentExtractorWorker.cancel(); - currentExtractorWorker = null; - } - + if (currentWorker != null) currentWorker.dispose(); savePositionAndSize(); } @@ -204,7 +254,7 @@ public class PopupVideoPlayer extends Service { else notRemoteView.setImageViewBitmap(R.id.notificationCover, playerImpl.getVideoThumbnail()); notRemoteView.setTextViewText(R.id.notificationSongName, playerImpl.getVideoTitle()); - notRemoteView.setTextViewText(R.id.notificationArtist, playerImpl.getChannelName()); + notRemoteView.setTextViewText(R.id.notificationArtist, playerImpl.getUploaderName()); notRemoteView.setOnClickPendingIntent(R.id.notificationPlayPause, PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT)); @@ -275,9 +325,11 @@ public class PopupVideoPlayer extends Service { //////////////////////////////////////////////////////////////////////////*/ private void checkPositionBounds() { - if (windowLayoutParams.x > screenWidth - windowLayoutParams.width) windowLayoutParams.x = (int) (screenWidth - windowLayoutParams.width); + if (windowLayoutParams.x > screenWidth - windowLayoutParams.width) + windowLayoutParams.x = (int) (screenWidth - windowLayoutParams.width); if (windowLayoutParams.x < 0) windowLayoutParams.x = 0; - if (windowLayoutParams.y > screenHeight - windowLayoutParams.height) windowLayoutParams.y = (int) (screenHeight - windowLayoutParams.height); + if (windowLayoutParams.y > screenHeight - windowLayoutParams.height) + windowLayoutParams.y = (int) (screenHeight - windowLayoutParams.height); if (windowLayoutParams.y < 0) windowLayoutParams.y = 0; } @@ -352,7 +404,7 @@ public class PopupVideoPlayer extends Service { @Override public void initViews(View rootView) { super.initViews(rootView); - resizingIndicator = (TextView) rootView.findViewById(R.id.resizing_indicator); + resizingIndicator = rootView.findViewById(R.id.resizing_indicator); } @Override @@ -431,6 +483,7 @@ public class PopupVideoPlayer extends Service { hideControls(100, 0); } } + /*////////////////////////////////////////////////////////////////////////// // Broadcast Receiver //////////////////////////////////////////////////////////////////////////*/ @@ -527,7 +580,8 @@ public class PopupVideoPlayer extends Service { @Override public boolean onDoubleTap(MotionEvent e) { - if (DEBUG) Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY()); + if (DEBUG) + Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY()); if (!playerImpl.isPlaying()) return false; if (e.getX() > popupWidth / 2) playerImpl.onFastForward(); else playerImpl.onFastRewind(); @@ -621,7 +675,8 @@ public class PopupVideoPlayer extends Service { } if (event.getAction() == MotionEvent.ACTION_UP) { - if (DEBUG) Log.d(TAG, "onTouch() ACTION_UP > v = [" + v + "], e1.getRaw = [" + event.getRawX() + ", " + event.getRawY() + "]"); + if (DEBUG) + Log.d(TAG, "onTouch() ACTION_UP > v = [" + v + "], e1.getRaw = [" + event.getRawX() + ", " + event.getRawY() + "]"); if (isMoving) { isMoving = false; onScrollEnd(); @@ -640,32 +695,36 @@ public class PopupVideoPlayer extends Service { } /** - * Fetcher used if open by a link out of NewPipe + * Fetcher handler used if open by a link out of NewPipe */ - private class FetcherRunnable implements StreamExtractorWorker.OnStreamInfoReceivedListener { + private class FetcherHandler { + private final int serviceId; + private final String url; + private final Context context; private final Handler mainHandler; - FetcherRunnable(Context context) { + FetcherHandler(Context context, int serviceId, String url) { this.mainHandler = new Handler(PopupVideoPlayer.this.getMainLooper()); this.context = context; + this.url = url; + this.serviceId = serviceId; } - @Override public void onReceive(StreamInfo info) { - playerImpl.setVideoTitle(info.title); - playerImpl.setVideoUrl(info.webpage_url); + playerImpl.setVideoTitle(info.name); + playerImpl.setVideoUrl(info.url); playerImpl.setVideoThumbnailUrl(info.thumbnail_url); - playerImpl.setChannelName(info.uploader); + playerImpl.setUploaderName(info.uploader_name); - playerImpl.setVideoStreamsList(Utils.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false)); - playerImpl.setAudioStream(Utils.getHighestQualityAudio(info.audio_streams)); + playerImpl.setVideoStreamsList(new ArrayList<>(ListHelper.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false))); + playerImpl.setAudioStream(ListHelper.getHighestQualityAudio(info.audio_streams)); - int defaultResolution = Utils.getPopupDefaultResolution(context, playerImpl.getVideoStreamsList()); + int defaultResolution = ListHelper.getPopupDefaultResolutionIndex(context, playerImpl.getVideoStreamsList()); playerImpl.setSelectedIndexStream(defaultResolution); if (DEBUG) { - Log.d(TAG, "FetcherRunnable.StreamExtractor: chosen = " + Log.d(TAG, "FetcherHandler.StreamExtractor: chosen = " + MediaFormat.getNameById(info.video_streams.get(defaultResolution).format) + " " + info.video_streams.get(defaultResolution).resolution + " > " + info.video_streams.get(defaultResolution).url); @@ -686,7 +745,7 @@ public class PopupVideoPlayer extends Service { @Override public void onLoadingComplete(final String imageUri, View view, final Bitmap loadedImage) { if (playerImpl == null || playerImpl.getPlayer() == null) return; - if (DEBUG) Log.d(TAG, "FetcherRunnable.imageLoader.onLoadingComplete() called with: imageUri = [" + imageUri + "]"); + if (DEBUG) Log.d(TAG, "FetcherHandler.imageLoader.onLoadingComplete() called with: imageUri = [" + imageUri + "]"); mainHandler.post(new Runnable() { @Override public void run() { @@ -699,70 +758,40 @@ public class PopupVideoPlayer extends Service { }); } - @Override - public void onError(final int messageId) { + protected void onError(final Throwable exception) { + if (DEBUG) Log.d(TAG, "onError() called with: exception = [" + exception + "]"); + exception.printStackTrace(); mainHandler.post(new Runnable() { @Override public void run() { - Toast.makeText(context, messageId, Toast.LENGTH_LONG).show(); + if (exception instanceof ReCaptchaException) { + onReCaptchaException(); + } else if (exception instanceof IOException) { + Toast.makeText(context, R.string.network_error, Toast.LENGTH_LONG).show(); + } else if (exception instanceof YoutubeStreamExtractor.GemaException) { + Toast.makeText(context, R.string.blocked_by_gema, Toast.LENGTH_LONG).show(); + } else if (exception instanceof YoutubeStreamExtractor.LiveStreamException) { + Toast.makeText(context, R.string.live_streams_not_supported, Toast.LENGTH_LONG).show(); + } else if (exception instanceof ContentNotAvailableException) { + Toast.makeText(context, R.string.content_not_available, Toast.LENGTH_LONG).show(); + } else { + int errorId = exception instanceof YoutubeStreamExtractor.DecryptException ? R.string.youtube_signature_decryption_error : + exception instanceof ParsingException ? R.string.parsing_error : R.string.general_error; + ErrorActivity.reportError(mainHandler, context, exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(UserAction.REQUESTED_STREAM, NewPipe.getNameOfService(serviceId), url, errorId)); + } } }); stopSelf(); } - @Override public void onReCaptchaException() { - mainHandler.post(new Runnable() { - @Override - public void run() { - Toast.makeText(context, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show(); - } - }); + Toast.makeText(context, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show(); // Starting ReCaptcha Challenge Activity Intent intent = new Intent(context, ReCaptchaActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); stopSelf(); } - - @Override - public void onBlockedByGemaError() { - mainHandler.post(new Runnable() { - @Override - public void run() { - Toast.makeText(context, R.string.blocked_by_gema, Toast.LENGTH_LONG).show(); - } - }); - stopSelf(); - } - - @Override - public void onContentErrorWithMessage(final int messageId) { - mainHandler.post(new Runnable() { - @Override - public void run() { - Toast.makeText(context, messageId, Toast.LENGTH_LONG).show(); - } - }); - stopSelf(); - } - - @Override - public void onContentError() { - mainHandler.post(new Runnable() { - @Override - public void run() { - Toast.makeText(context, R.string.content_not_available, Toast.LENGTH_LONG).show(); - } - }); - stopSelf(); - } - - @Override - public void onUnrecoverableError(Exception exception) { - exception.printStackTrace(); - stopSelf(); - } } } \ No newline at end of file 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 39e94471f..fa25cc957 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -1,3 +1,22 @@ +/* + * Copyright 2017 Mauricio Colli + * VideoPlayer.java is part of NewPipe + * + * License: GPL-3.0+ + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package org.schabi.newpipe.player; import android.animation.Animator; @@ -26,7 +45,6 @@ import android.widget.ProgressBar; import android.widget.SeekBar; import android.widget.TextView; -import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.source.ExtractorMediaSource; @@ -36,8 +54,8 @@ import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.MediaFormat; -import org.schabi.newpipe.extractor.stream_info.AudioStream; -import org.schabi.newpipe.extractor.stream_info.VideoStream; +import org.schabi.newpipe.extractor.stream.AudioStream; +import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.util.AnimationUtils; import java.io.Serializable; @@ -74,7 +92,7 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer. // Player //////////////////////////////////////////////////////////////////////////*/ - public static final int DEFAULT_CONTROLS_HIDE_TIME = 3000; // 3 Seconds + public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds private static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f}; private boolean startedFromNewPipe = true; @@ -133,26 +151,27 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer. public void initViews(View rootView) { this.rootView = rootView; - this.aspectRatioFrameLayout = (AspectRatioFrameLayout) rootView.findViewById(R.id.aspectRatioLayout); - this.surfaceView = (SurfaceView) rootView.findViewById(R.id.surfaceView); + this.aspectRatioFrameLayout = rootView.findViewById(R.id.aspectRatioLayout); + this.surfaceView = rootView.findViewById(R.id.surfaceView); this.surfaceForeground = rootView.findViewById(R.id.surfaceForeground); this.loadingPanel = rootView.findViewById(R.id.loading_panel); - this.endScreen = (ImageView) rootView.findViewById(R.id.endScreen); - this.controlAnimationView = (ImageView) rootView.findViewById(R.id.controlAnimationView); + this.endScreen = rootView.findViewById(R.id.endScreen); + this.controlAnimationView = rootView.findViewById(R.id.controlAnimationView); this.controlsRoot = rootView.findViewById(R.id.playbackControlRoot); - this.currentDisplaySeek = (TextView) rootView.findViewById(R.id.currentDisplaySeek); - this.playbackSeekBar = (SeekBar) rootView.findViewById(R.id.playbackSeekBar); - this.playbackCurrentTime = (TextView) rootView.findViewById(R.id.playbackCurrentTime); - this.playbackEndTime = (TextView) rootView.findViewById(R.id.playbackEndTime); - this.playbackSpeed = (TextView) rootView.findViewById(R.id.playbackSpeed); + this.currentDisplaySeek = rootView.findViewById(R.id.currentDisplaySeek); + this.playbackSeekBar = rootView.findViewById(R.id.playbackSeekBar); + this.playbackCurrentTime = rootView.findViewById(R.id.playbackCurrentTime); + this.playbackEndTime = rootView.findViewById(R.id.playbackEndTime); + this.playbackSpeed = rootView.findViewById(R.id.playbackSpeed); this.bottomControlsRoot = rootView.findViewById(R.id.bottomControls); this.topControlsRoot = rootView.findViewById(R.id.topControls); - this.qualityTextView = (TextView) rootView.findViewById(R.id.qualityTextView); - this.fullScreenButton = (ImageButton) rootView.findViewById(R.id.fullScreenButton); + this.qualityTextView = rootView.findViewById(R.id.qualityTextView); + this.fullScreenButton = rootView.findViewById(R.id.fullScreenButton); //this.aspectRatioFrameLayout.setAspectRatio(16.0f / 9.0f); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) + playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN); this.playbackSeekBar.getProgressDrawable().setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY); this.qualityPopupMenu = new PopupMenu(context, qualityTextView); @@ -226,7 +245,7 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer. @Override public MediaSource buildMediaSource(String url, String overrideExtension) { MediaSource mediaSource = super.buildMediaSource(url, overrideExtension); - if (!getSelectedVideoStream().isVideoOnly) return mediaSource; + if (!getSelectedVideoStream().isVideoOnly || videoOnlyAudioStream == null) return mediaSource; Uri audioUri = Uri.parse(videoOnlyAudioStream.url); return new MergingMediaSource(mediaSource, new ExtractorMediaSource(audioUri, cacheDataSourceFactory, extractorsFactory, null, null)); @@ -269,7 +288,8 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer. playbackSeekBar.setProgress(0); // Bug on lower api, disabling and enabling the seekBar resets the thumb color -.-, so sets the color again - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) + playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN); animateView(endScreen, false, 0); loadingPanel.setBackgroundColor(Color.BLACK); @@ -324,7 +344,8 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer. playbackEndTime.setText(getTimeString(playbackSeekBar.getMax())); playbackCurrentTime.setText(playbackEndTime.getText()); // Bug on lower api, disabling and enabling the seekBar resets the thumb color -.-, so sets the color again - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) + playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN); animateView(surfaceForeground, true, 100); @@ -360,8 +381,8 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer. if (DEBUG) Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]"); if (videoStartPos > 0) { - playbackSeekBar.setProgress(videoStartPos); - playbackCurrentTime.setText(getTimeString(videoStartPos)); + playbackSeekBar.setProgress((int) videoStartPos); + playbackCurrentTime.setText(getTimeString((int) videoStartPos)); videoStartPos = -1; } @@ -444,11 +465,12 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer. */ @Override public boolean onMenuItemClick(MenuItem menuItem) { - if (DEBUG) Log.d(TAG, "onMenuItemClick() called with: menuItem = [" + menuItem + "], menuItem.getItemId = [" + menuItem.getItemId() + "]"); + if (DEBUG) + Log.d(TAG, "onMenuItemClick() called with: menuItem = [" + menuItem + "], menuItem.getItemId = [" + menuItem.getItemId() + "]"); if (qualityPopupMenuGroupId == menuItem.getGroupId()) { if (selectedIndexStream == menuItem.getItemId()) return true; - setVideoStartPos((int) simpleExoPlayer.getCurrentPosition()); + setVideoStartPos(simpleExoPlayer.getCurrentPosition()); selectedIndexStream = menuItem.getItemId(); if (!(getCurrentState() == STATE_COMPLETED)) play(wasPlaying); diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayVideoActivity.java b/app/src/main/java/org/schabi/newpipe/player/old/PlayVideoActivity.java similarity index 98% rename from app/src/main/java/org/schabi/newpipe/player/PlayVideoActivity.java rename to app/src/main/java/org/schabi/newpipe/player/old/PlayVideoActivity.java index 49da537ac..092f82aad 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PlayVideoActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/old/PlayVideoActivity.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.player; +package org.schabi.newpipe.player.old; import android.content.Context; import android.content.Intent; @@ -29,7 +29,7 @@ import android.widget.VideoView; import org.schabi.newpipe.R; -/** +/* * Copyright (C) Christian Schabesberger 2015 * PlayVideoActivity.java is part of NewPipe. * @@ -122,8 +122,8 @@ public class PlayVideoActivity extends AppCompatActivity { position = intent.getIntExtra(START_POSITION, 0)*1000;//convert from seconds to milliseconds - videoView = (VideoView) findViewById(R.id.video_view); - progressBar = (ProgressBar) findViewById(R.id.play_video_progress_bar); + videoView = findViewById(R.id.video_view); + progressBar = findViewById(R.id.play_video_progress_bar); try { videoView.setMediaController(mediaController); videoView.setVideoURI(Uri.parse(intent.getStringExtra(STREAM_URL))); @@ -146,7 +146,7 @@ public class PlayVideoActivity extends AppCompatActivity { }); videoUrl = intent.getStringExtra(VIDEO_URL); - Button button = (Button) findViewById(R.id.content_button); + Button button = findViewById(R.id.content_button); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { diff --git a/app/src/main/java/org/schabi/newpipe/report/AcraReportSender.java b/app/src/main/java/org/schabi/newpipe/report/AcraReportSender.java index 12d0f634d..2d3226ab6 100644 --- a/app/src/main/java/org/schabi/newpipe/report/AcraReportSender.java +++ b/app/src/main/java/org/schabi/newpipe/report/AcraReportSender.java @@ -8,7 +8,7 @@ import org.acra.sender.ReportSender; import org.acra.sender.ReportSenderException; import org.schabi.newpipe.R; -/** +/* * Created by Christian Schabesberger on 13.09.16. * * Copyright (C) Christian Schabesberger 2015 diff --git a/app/src/main/java/org/schabi/newpipe/report/AcraReportSenderFactory.java b/app/src/main/java/org/schabi/newpipe/report/AcraReportSenderFactory.java index 52ba5ad3c..89f0ec614 100644 --- a/app/src/main/java/org/schabi/newpipe/report/AcraReportSenderFactory.java +++ b/app/src/main/java/org/schabi/newpipe/report/AcraReportSenderFactory.java @@ -6,9 +6,8 @@ import android.support.annotation.NonNull; import org.acra.config.ACRAConfiguration; import org.acra.sender.ReportSender; import org.acra.sender.ReportSenderFactory; -import org.schabi.newpipe.report.AcraReportSender; -/** +/* * Created by Christian Schabesberger on 13.09.16. * * Copyright (C) Christian Schabesberger 2015 diff --git a/app/src/main/java/org/schabi/newpipe/report/ErrorActivity.java b/app/src/main/java/org/schabi/newpipe/report/ErrorActivity.java index f0fb8714a..b62b63510 100644 --- a/app/src/main/java/org/schabi/newpipe/report/ErrorActivity.java +++ b/app/src/main/java/org/schabi/newpipe/report/ErrorActivity.java @@ -36,7 +36,7 @@ import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.Downloader; import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.Parser; +import org.schabi.newpipe.extractor.utils.Parser; import org.schabi.newpipe.util.ThemeHelper; import java.io.PrintWriter; @@ -47,7 +47,7 @@ import java.util.List; import java.util.TimeZone; import java.util.Vector; -/** +/* * Created by Christian Schabesberger on 24.10.15. * * Copyright (C) Christian Schabesberger 2016 @@ -95,37 +95,34 @@ public class ErrorActivity extends AppCompatActivity { public static void reportError(final Context context, final List el, final Class returnActivity, View rootView, final ErrorInfo errorInfo) { - if (rootView != null) { - Snackbar.make(rootView, R.string.error_snackbar_message, Snackbar.LENGTH_LONG) + Snackbar.make(rootView, R.string.error_snackbar_message, 15 * 1000) .setActionTextColor(Color.YELLOW) .setAction(R.string.error_snackbar_action, new View.OnClickListener() { @Override public void onClick(View v) { - ActivityCommunicator ac = ActivityCommunicator.getCommunicator(); - ac.returnActivity = returnActivity; - Intent intent = new Intent(context, ErrorActivity.class); - intent.putExtra(ERROR_INFO, errorInfo); - intent.putExtra(ERROR_LIST, elToSl(el)); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); + startErrorActivity(returnActivity, context, errorInfo, el); } }).show(); } else { - ActivityCommunicator ac = ActivityCommunicator.getCommunicator(); - ac.returnActivity = returnActivity; - Intent intent = new Intent(context, ErrorActivity.class); - intent.putExtra(ERROR_INFO, errorInfo); - intent.putExtra(ERROR_LIST, elToSl(el)); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); + startErrorActivity(returnActivity, context, errorInfo, el); } } + private static void startErrorActivity(Class returnActivity, Context context, ErrorInfo errorInfo, List el) { + ActivityCommunicator ac = ActivityCommunicator.getCommunicator(); + ac.returnActivity = returnActivity; + Intent intent = new Intent(context, ErrorActivity.class); + intent.putExtra(ERROR_INFO, errorInfo); + intent.putExtra(ERROR_LIST, elToSl(el)); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } + public static void reportError(final Context context, final Throwable e, final Class returnActivity, View rootView, final ErrorInfo errorInfo) { List el = null; - if(e != null) { + if (e != null) { el = new Vector<>(); el.add(e); } @@ -137,7 +134,7 @@ public class ErrorActivity extends AppCompatActivity { final Class returnActivity, final View rootView, final ErrorInfo errorInfo) { List el = null; - if(e != null) { + if (e != null) { el = new Vector<>(); el.add(e); } @@ -158,12 +155,12 @@ public class ErrorActivity extends AppCompatActivity { public static void reportError(final Context context, final CrashReportData report, final ErrorInfo errorInfo) { // get key first (don't ask about this solution) ReportField key = null; - for(ReportField k : report.keySet()) { - if(k.toString().equals("STACK_TRACE")) { + for (ReportField k : report.keySet()) { + if (k.toString().equals("STACK_TRACE")) { key = k; } } - String[] el = new String[] { report.get(key) }; + String[] el = new String[]{report.get(key)}; Intent intent = new Intent(context, ErrorActivity.class); intent.putExtra(ERROR_INFO, errorInfo); @@ -196,7 +193,7 @@ public class ErrorActivity extends AppCompatActivity { Intent intent = getIntent(); - Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); ActionBar actionBar = getSupportActionBar(); @@ -206,11 +203,11 @@ public class ErrorActivity extends AppCompatActivity { actionBar.setDisplayShowTitleEnabled(true); } - reportButton = (Button) findViewById(R.id.errorReportButton); - userCommentBox = (EditText) findViewById(R.id.errorCommentBox); - errorView = (TextView) findViewById(R.id.errorView); - infoView = (TextView) findViewById(R.id.errorInfosView); - errorMessageView = (TextView) findViewById(R.id.errorMessageView); + reportButton = findViewById(R.id.errorReportButton); + userCommentBox = findViewById(R.id.errorCommentBox); + errorView = findViewById(R.id.errorView); + infoView = findViewById(R.id.errorInfosView); + errorMessageView = findViewById(R.id.errorMessageView); ActivityCommunicator ac = ActivityCommunicator.getCommunicator(); returnActivity = ac.returnActivity; @@ -240,7 +237,7 @@ public class ErrorActivity extends AppCompatActivity { // normal bugreport buildInfo(errorInfo); - if(errorInfo.message != 0) { + if (errorInfo.message != 0) { errorMessageView.setText(errorInfo.message); } else { errorMessageView.setVisibility(View.GONE); @@ -250,7 +247,7 @@ public class ErrorActivity extends AppCompatActivity { errorView.setText(formErrorText(errorList)); //print stack trace once again for debugging: - for(String e : errorList) { + for (String e : errorList) { Log.e(TAG, e); } } @@ -283,7 +280,7 @@ public class ErrorActivity extends AppCompatActivity { private String formErrorText(String[] el) { String text = ""; - if(el != null) { + if (el != null) { for (String e : el) { text += "-------------------------------------\n" + e; @@ -295,13 +292,14 @@ public class ErrorActivity extends AppCompatActivity { /** * Get the checked activity. + * * @param returnActivity the activity to return to * @return the casted return activity or null */ @Nullable static Class getReturnActivity(Class returnActivity) { Class checkedReturnActivity = null; - if (returnActivity != null){ + if (returnActivity != null) { if (Activity.class.isAssignableFrom(returnActivity)) { checkedReturnActivity = returnActivity.asSubclass(Activity.class); } else { @@ -323,8 +321,8 @@ public class ErrorActivity extends AppCompatActivity { } private void buildInfo(ErrorInfo info) { - TextView infoLabelView = (TextView) findViewById(R.id.errorInfoLabelsView); - TextView infoView = (TextView) findViewById(R.id.errorInfosView); + TextView infoLabelView = findViewById(R.id.errorInfoLabelsView); + TextView infoView = findViewById(R.id.errorInfosView); String text = ""; infoLabelView.setText(getString(R.string.info_labels).replace("\\n", "\n")); @@ -356,7 +354,7 @@ public class ErrorActivity extends AppCompatActivity { .put("ip_range", globIpRange); JSONArray exceptionArray = new JSONArray(); - if(errorList != null) { + if (errorList != null) { for (String e : errorList) { exceptionArray.put(e); } @@ -375,7 +373,7 @@ public class ErrorActivity extends AppCompatActivity { } private String getUserActionString(UserAction userAction) { - if(userAction == null) { + if (userAction == null) { return "Your description is in another castle."; } else { return userAction.getMessage(); @@ -397,7 +395,7 @@ public class ErrorActivity extends AppCompatActivity { private void addGuruMeditaion() { //just an easter egg - TextView sorryView = (TextView) findViewById(R.id.errorSorryView); + TextView sorryView = findViewById(R.id.errorSorryView); String text = sorryView.getText().toString(); text += "\n" + getString(R.string.guru_meditation); sorryView.setText(text); @@ -467,6 +465,7 @@ public class ErrorActivity extends AppCompatActivity { private class IpRangeRequester implements Runnable { Handler h = new Handler(); + public void run() { String ipRange = "none"; try { @@ -475,7 +474,7 @@ public class ErrorActivity extends AppCompatActivity { ipRange = Parser.matchGroup1("([0-9]*\\.[0-9]*\\.)[0-9]*\\.[0-9]*", ip) + "0.0"; - } catch(Throwable e) { + } catch (Throwable e) { Log.w(TAG, "Error while error: could not get iprange", e); } finally { h.post(new IpRangeReturnRunnable(ipRange)); @@ -485,12 +484,14 @@ public class ErrorActivity extends AppCompatActivity { private class IpRangeReturnRunnable implements Runnable { String ipRange; + public IpRangeReturnRunnable(String ipRange) { this.ipRange = ipRange; } + public void run() { globIpRange = ipRange; - if(infoView != null) { + if (infoView != null) { String text = infoView.getText().toString(); text += "\n" + globIpRange; infoView.setText(text); diff --git a/app/src/main/java/org/schabi/newpipe/report/UserAction.java b/app/src/main/java/org/schabi/newpipe/report/UserAction.java index 48dcb5752..b97080170 100644 --- a/app/src/main/java/org/schabi/newpipe/report/UserAction.java +++ b/app/src/main/java/org/schabi/newpipe/report/UserAction.java @@ -4,14 +4,16 @@ package org.schabi.newpipe.report; * The user actions that can cause an error. */ public enum UserAction { - SEARCHED("searched"), - REQUESTED_STREAM("requested stream"), - GET_SUGGESTIONS("get suggestions"), - SOMETHING_ELSE("something"), USER_REPORT("user report"), - LOAD_IMAGE("load image"), UI_ERROR("ui error"), - REQUESTED_CHANNEL("requested channel"); + SUBSCRIPTION("subscription"), + LOAD_IMAGE("load image"), + SOMETHING_ELSE("something"), + SEARCHED("searched"), + GET_SUGGESTIONS("get suggestions"), + REQUESTED_STREAM("requested stream"), + REQUESTED_CHANNEL("requested channel"), + REQUESTED_PLAYLIST("requested playlist"); private final String message; diff --git a/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java new file mode 100644 index 000000000..70b6e25ed --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java @@ -0,0 +1,42 @@ +package org.schabi.newpipe.settings; + +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v7.preference.Preference; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.util.Constants; + +public class AppearanceSettingsFragment extends BasePreferenceFragment { + /** + * Theme that was applied when the settings was opened (or recreated after a theme change) + */ + private String startThemeKey; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + String themeKey = getString(R.string.theme_key); + startThemeKey = defaultPreferences.getString(themeKey, getString(R.string.default_theme_value)); + findPreference(themeKey).setOnPreferenceChangeListener(themePreferenceChange); + } + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.appearance_settings); + } + + private Preference.OnPreferenceChangeListener themePreferenceChange = new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + defaultPreferences.edit().putBoolean(Constants.KEY_THEME_CHANGE, true).apply(); + defaultPreferences.edit().putString(getString(R.string.theme_key), newValue.toString()).apply(); + + if (!newValue.equals(startThemeKey)) { // If it's not the current theme + getActivity().recreate(); + } + + return false; + } + }; +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java b/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java new file mode 100644 index 000000000..a16f7dd79 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java @@ -0,0 +1,45 @@ +package org.schabi.newpipe.settings; + +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.support.annotation.Nullable; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.preference.PreferenceFragmentCompat; +import android.view.View; + +import org.schabi.newpipe.MainActivity; + +public abstract class BasePreferenceFragment extends PreferenceFragmentCompat { + protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode()); + protected boolean DEBUG = MainActivity.DEBUG; + + protected SharedPreferences defaultPreferences; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + defaultPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + setDivider(null); + updateTitle(); + } + + @Override + public void onResume() { + super.onResume(); + updateTitle(); + } + + private void updateTitle() { + if (getActivity() instanceof AppCompatActivity) { + ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); + if (actionBar != null) actionBar.setTitle(getPreferenceScreen().getTitle()); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java new file mode 100644 index 000000000..6021b40fd --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java @@ -0,0 +1,12 @@ +package org.schabi.newpipe.settings; + +import android.os.Bundle; + +import org.schabi.newpipe.R; + +public class ContentSettingsFragment extends BasePreferenceFragment { + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.content_settings); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java new file mode 100644 index 000000000..f93134a5d --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java @@ -0,0 +1,79 @@ +package org.schabi.newpipe.settings; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v7.preference.Preference; +import android.util.Log; + +import com.nononsenseapps.filepicker.FilePickerActivity; + +import org.schabi.newpipe.R; + +public class DownloadSettingsFragment extends BasePreferenceFragment { + private static final int REQUEST_DOWNLOAD_PATH = 0x1235; + private static final int REQUEST_DOWNLOAD_AUDIO_PATH = 0x1236; + + private String DOWNLOAD_PATH_PREFERENCE; + private String DOWNLOAD_PATH_AUDIO_PREFERENCE; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + initKeys(); + updatePreferencesSummary(); + } + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.download_settings); + } + + private void initKeys() { + DOWNLOAD_PATH_PREFERENCE = getString(R.string.download_path_key); + DOWNLOAD_PATH_AUDIO_PREFERENCE = getString(R.string.download_path_audio_key); + } + + private void updatePreferencesSummary() { + findPreference(DOWNLOAD_PATH_PREFERENCE).setSummary(defaultPreferences.getString(DOWNLOAD_PATH_PREFERENCE, getString(R.string.download_path_summary))); + findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE).setSummary(defaultPreferences.getString(DOWNLOAD_PATH_AUDIO_PREFERENCE, getString(R.string.download_path_audio_summary))); + } + + @Override + public boolean onPreferenceTreeClick(Preference preference) { + if (DEBUG) { + Log.d(TAG, "onPreferenceTreeClick() called with: preference = [" + preference + "]"); + } + + if (preference.getKey().equals(DOWNLOAD_PATH_PREFERENCE) || preference.getKey().equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) { + Intent i = new Intent(getActivity(), FilePickerActivity.class) + .putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false) + .putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true) + .putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR); + if (preference.getKey().equals(DOWNLOAD_PATH_PREFERENCE)) { + startActivityForResult(i, REQUEST_DOWNLOAD_PATH); + } else if (preference.getKey().equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) { + startActivityForResult(i, REQUEST_DOWNLOAD_AUDIO_PATH); + } + } + + return super.onPreferenceTreeClick(preference); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (DEBUG) { + Log.d(TAG, "onActivityResult() called with: requestCode = [" + requestCode + "], resultCode = [" + resultCode + "], data = [" + data + "]"); + } + + if ((requestCode == REQUEST_DOWNLOAD_PATH || requestCode == REQUEST_DOWNLOAD_AUDIO_PATH) && resultCode == Activity.RESULT_OK) { + String key = getString(requestCode == REQUEST_DOWNLOAD_PATH ? R.string.download_path_key : R.string.download_path_audio_key); + String path = data.getData().getPath(); + defaultPreferences.edit().putString(key, path).apply(); + updatePreferencesSummary(); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java new file mode 100644 index 000000000..e0836e06c --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java @@ -0,0 +1,12 @@ +package org.schabi.newpipe.settings; + +import android.os.Bundle; + +import org.schabi.newpipe.R; + +public class HistorySettingsFragment extends BasePreferenceFragment { + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.history_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 new file mode 100644 index 000000000..230f3b5ee --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java @@ -0,0 +1,12 @@ +package org.schabi.newpipe.settings; + +import android.os.Bundle; + +import org.schabi.newpipe.R; + +public class MainSettingsFragment extends BasePreferenceFragment { + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.main_settings); + } +} 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 105d65a34..109466c02 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java +++ b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java @@ -1,4 +1,4 @@ -/** +/* * Created by k3b on 07.01.2016. * * Copyright (C) Christian Schabesberger 2015 @@ -30,13 +30,11 @@ import org.schabi.newpipe.R; import java.io.File; -import us.shandian.giga.util.Utility; - /** * Helper for global settings */ -/** +/* * Copyright (C) Christian Schabesberger 2016 * NewPipeSettings.java is part of NewPipe. * @@ -60,7 +58,13 @@ public class NewPipeSettings { } public static void initSettings(Context context) { - PreferenceManager.setDefaultValues(context, R.xml.settings, false); + PreferenceManager.setDefaultValues(context, R.xml.appearance_settings, true); + PreferenceManager.setDefaultValues(context, R.xml.content_settings, true); + PreferenceManager.setDefaultValues(context, R.xml.download_settings, true); + 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); + getVideoDownloadFolder(context); getAudioDownloadFolder(context); } @@ -93,14 +97,13 @@ public class NewPipeSettings { final File folder = getFolder(defaultDirectoryName); SharedPreferences.Editor spEditor = prefs.edit(); - spEditor.putString(key - , new File(folder,"NewPipe").getAbsolutePath()); + spEditor.putString(key, new File(folder, "NewPipe").getAbsolutePath()); spEditor.apply(); return folder; } @NonNull private static File getFolder(String defaultDirectoryName) { - return new File(Environment.getExternalStorageDirectory(),defaultDirectoryName); + return new File(Environment.getExternalStorageDirectory(), defaultDirectoryName); } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java index 6a2364676..bfb19c23b 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java @@ -2,16 +2,20 @@ package org.schabi.newpipe.settings; import android.content.Context; import android.os.Bundle; +import android.support.v4.app.Fragment; import android.support.v7.app.ActionBar; import android.support.v7.app.AppCompatActivity; +import android.support.v7.preference.Preference; +import android.support.v7.preference.PreferenceFragmentCompat; import android.support.v7.widget.Toolbar; +import android.view.Menu; import android.view.MenuItem; import org.schabi.newpipe.R; import org.schabi.newpipe.util.ThemeHelper; -/** +/* * Created by Christian Schabesberger on 31.08.15. * * Copyright (C) Christian Schabesberger 2015 @@ -31,7 +35,7 @@ import org.schabi.newpipe.util.ThemeHelper; * along with NewPipe. If not, see . */ -public class SettingsActivity extends AppCompatActivity { +public class SettingsActivity extends AppCompatActivity implements BasePreferenceFragment.OnPreferenceStartFragmentCallback { public static void initSettings(Context context) { NewPipeSettings.initSettings(context); @@ -43,21 +47,25 @@ public class SettingsActivity extends AppCompatActivity { super.onCreate(savedInstanceBundle); setContentView(R.layout.settings_layout); - Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); + if (savedInstanceBundle == null) { + getSupportFragmentManager().beginTransaction() + .replace(R.id.fragment_holder, new MainSettingsFragment()) + .commit(); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); - actionBar.setTitle(R.string.settings); actionBar.setDisplayShowTitleEnabled(true); } - if (savedInstanceBundle == null) { - getFragmentManager().beginTransaction() - .replace(R.id.fragment_holder, new SettingsFragment()) - .commit(); - } + return super.onCreateOptionsMenu(menu); } @Override @@ -68,4 +76,15 @@ public class SettingsActivity extends AppCompatActivity { } return true; } + + @Override + public boolean onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference preference) { + Fragment fragment = Fragment.instantiate(this, preference.getFragment(), preference.getExtras()); + getSupportFragmentManager().beginTransaction() + .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out) + .replace(R.id.fragment_holder, fragment) + .addToBackStack(null) + .commit(); + return true; + } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsFragment.java deleted file mode 100644 index d93b30874..000000000 --- a/app/src/main/java/org/schabi/newpipe/settings/SettingsFragment.java +++ /dev/null @@ -1,142 +0,0 @@ -package org.schabi.newpipe.settings; - -import android.app.Activity; -import android.content.Intent; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.preference.Preference; -import android.preference.PreferenceFragment; -import android.preference.PreferenceManager; -import android.preference.PreferenceScreen; -import android.text.TextUtils; -import android.util.Log; - -import com.nononsenseapps.filepicker.FilePickerActivity; - -import org.schabi.newpipe.App; -import org.schabi.newpipe.MainActivity; -import org.schabi.newpipe.R; -import org.schabi.newpipe.util.Constants; - -import info.guardianproject.netcipher.proxy.OrbotHelper; - -public class SettingsFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener { - private static final int REQUEST_INSTALL_ORBOT = 0x1234; - private static final int REQUEST_DOWNLOAD_PATH = 0x1235; - private static final int REQUEST_DOWNLOAD_AUDIO_PATH = 0x1236; - - private String DOWNLOAD_PATH_PREFERENCE; - private String DOWNLOAD_PATH_AUDIO_PREFERENCE; - private String USE_TOR_KEY; - private String THEME; - - private String currentTheme; - private SharedPreferences defaultPreferences; - - private Activity activity; - - @Override - public void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - activity = getActivity(); - addPreferencesFromResource(R.xml.settings); - - defaultPreferences = PreferenceManager.getDefaultSharedPreferences(activity); - initKeys(); - updatePreferencesSummary(); - - currentTheme = defaultPreferences.getString(THEME, getString(R.string.default_theme_value)); - } - - @Override - public void onResume() { - super.onResume(); - defaultPreferences.registerOnSharedPreferenceChangeListener(this); - } - - @Override - public void onStop() { - super.onStop(); - defaultPreferences.unregisterOnSharedPreferenceChangeListener(this); - } - - private void initKeys() { - DOWNLOAD_PATH_PREFERENCE = getString(R.string.download_path_key); - DOWNLOAD_PATH_AUDIO_PREFERENCE = getString(R.string.download_path_audio_key); - THEME = getString(R.string.theme_key); - USE_TOR_KEY = getString(R.string.use_tor_key); - } - - @Override - public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { - if (MainActivity.DEBUG) Log.d("TAG", "onPreferenceTreeClick() called with: preferenceScreen = [" + preferenceScreen + "], preference = [" + preference + "]"); - if (preference.getKey().equals(DOWNLOAD_PATH_PREFERENCE) || preference.getKey().equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) { - Intent i = new Intent(activity, FilePickerActivity.class) - .putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false) - .putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true) - .putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR); - if (preference.getKey().equals(DOWNLOAD_PATH_PREFERENCE)) { - startActivityForResult(i, REQUEST_DOWNLOAD_PATH); - } else if (preference.getKey().equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) { - startActivityForResult(i, REQUEST_DOWNLOAD_AUDIO_PATH); - } - } - return super.onPreferenceTreeClick(preferenceScreen, preference); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (MainActivity.DEBUG) Log.d("TAG", "onActivityResult() called with: requestCode = [" + requestCode + "], resultCode = [" + resultCode + "], data = [" + data + "]"); - - if ((requestCode == REQUEST_DOWNLOAD_PATH || requestCode == REQUEST_DOWNLOAD_AUDIO_PATH) && resultCode == Activity.RESULT_OK) { - String key = getString(requestCode == REQUEST_DOWNLOAD_PATH ? R.string.download_path_key : R.string.download_path_audio_key); - String path = data.getData().getPath(); - defaultPreferences.edit().putString(key, path).apply(); - updatePreferencesSummary(); - } else if (requestCode == REQUEST_INSTALL_ORBOT) { - // try to start tor regardless of resultCode since clicking back after - // installing the app does not necessarily return RESULT_OK - App.configureTor(OrbotHelper.requestStartTor(activity)); - } - } - - /* - * Update ONLY the summary of some preferences that don't fire in the onSharedPreferenceChanged or CAN'T be update via xml (%s) - * - * For example, the download_path use the startActivityForResult, firing the onStop of this fragment, - * unregistering the listener (unregisterOnSharedPreferenceChangeListener) - */ - private void updatePreferencesSummary() { - findPreference(DOWNLOAD_PATH_PREFERENCE).setSummary(defaultPreferences.getString(DOWNLOAD_PATH_PREFERENCE, getString(R.string.download_path_summary))); - findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE).setSummary(defaultPreferences.getString(DOWNLOAD_PATH_AUDIO_PREFERENCE, getString(R.string.download_path_audio_summary))); - } - - @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - if (MainActivity.DEBUG) Log.d("TAG", "onSharedPreferenceChanged() called with: sharedPreferences = [" + sharedPreferences + "], key = [" + key + "]"); - String summary = null; - - if (key.equals(USE_TOR_KEY)) { - if (defaultPreferences.getBoolean(USE_TOR_KEY, false)) { - if (OrbotHelper.isOrbotInstalled(activity)) { - App.configureTor(true); - OrbotHelper.requestStartTor(activity); - } else { - Intent intent = OrbotHelper.getOrbotInstallIntent(activity); - startActivityForResult(intent, REQUEST_INSTALL_ORBOT); - } - } else App.configureTor(false); - return; - } else if (key.equals(THEME)) { - summary = sharedPreferences.getString(THEME, getString(R.string.default_theme_value)); - if (!summary.equals(currentTheme)) { // If it's not the current theme - getActivity().recreate(); - } - - defaultPreferences.edit().putBoolean(Constants.KEY_THEME_CHANGE, true).apply(); - } - - if (!TextUtils.isEmpty(summary)) findPreference(key).setSummary(summary); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/settings/VideoAudioSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/VideoAudioSettingsFragment.java new file mode 100644 index 000000000..9bbdd650d --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/VideoAudioSettingsFragment.java @@ -0,0 +1,12 @@ +package org.schabi.newpipe.settings; + +import android.os.Bundle; + +import org.schabi.newpipe.R; + +public class VideoAudioSettingsFragment extends BasePreferenceFragment { + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.video_audio_settings); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/util/AnimationUtils.java b/app/src/main/java/org/schabi/newpipe/util/AnimationUtils.java index c37eaa560..ac70bd05f 100644 --- a/app/src/main/java/org/schabi/newpipe/util/AnimationUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/AnimationUtils.java @@ -2,17 +2,24 @@ package org.schabi.newpipe.util; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; +import android.animation.ArgbEvaluator; +import android.animation.ValueAnimator; +import android.content.res.ColorStateList; +import android.support.annotation.ColorInt; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.animation.FastOutSlowInInterpolator; import android.util.Log; import android.view.View; +import android.widget.TextView; -import org.schabi.newpipe.player.BasePlayer; +import org.schabi.newpipe.MainActivity; public class AnimationUtils { private static final String TAG = "AnimationUtils"; - private static final boolean DEBUG = BasePlayer.DEBUG; + private static final boolean DEBUG = MainActivity.DEBUG; public enum Type { - ALPHA, SCALE_AND_ALPHA + ALPHA, SCALE_AND_ALPHA, LIGHT_SCALE_AND_ALPHA } public static void animateView(View view, boolean enterOrExit, long duration) { @@ -47,7 +54,16 @@ public class AnimationUtils { */ public static void animateView(final View view, Type animationType, boolean enterOrExit, long duration, long delay, Runnable execOnEnd) { if (DEBUG) { - Log.d(TAG, "animateView() called with: view = [" + view + "], animationType = [" + animationType + "], enterOrExit = [" + enterOrExit + "], duration = [" + duration + "], delay = [" + delay + "], execOnEnd = [" + execOnEnd + "]"); + String id; + try { + id = view.getResources().getResourceEntryName(view.getId()); + } catch (Exception e) { + id = view.getId() + ""; + } + + String msg = String.format("%8s → [%s:%s] [%s %s:%s] execOnEnd=%s", + enterOrExit, view.getClass().getSimpleName(), id, animationType, duration, delay, execOnEnd); + Log.d(TAG, "animateView()" + msg); } if (view.getVisibility() == View.VISIBLE && enterOrExit) { @@ -76,15 +92,132 @@ public class AnimationUtils { case SCALE_AND_ALPHA: animateScaleAndAlpha(view, enterOrExit, duration, delay, execOnEnd); break; + case LIGHT_SCALE_AND_ALPHA: + animateLightScaleAndAlpha(view, enterOrExit, duration, delay, execOnEnd); + break; + } + } + + /** + * Animate the background color of a view + */ + public static void animateBackgroundColor(final View view, long duration, @ColorInt final int colorStart, @ColorInt final int colorEnd) { + if (DEBUG) { + Log.d(TAG, "animateBackgroundColor() called with: view = [" + view + "], duration = [" + duration + "], colorStart = [" + colorStart + "], colorEnd = [" + colorEnd + "]"); + } + + final int[][] EMPTY = new int[][]{new int[0]}; + ValueAnimator viewPropertyAnimator = ValueAnimator.ofObject(new ArgbEvaluator(), colorStart, colorEnd); + viewPropertyAnimator.setInterpolator(new FastOutSlowInInterpolator()); + viewPropertyAnimator.setDuration(duration); + viewPropertyAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + ViewCompat.setBackgroundTintList(view, new ColorStateList(EMPTY, new int[]{(int) animation.getAnimatedValue()})); + } + }); + viewPropertyAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + ViewCompat.setBackgroundTintList(view, new ColorStateList(EMPTY, new int[]{colorEnd})); + } + + @Override + public void onAnimationCancel(Animator animation) { + onAnimationEnd(animation); + } + }); + viewPropertyAnimator.start(); + } + + /** + * Animate the text color of any view that extends {@link TextView} (Buttons, EditText...) + */ + public static void animateTextColor(final TextView view, long duration, @ColorInt final int colorStart, @ColorInt final int colorEnd) { + if (DEBUG) { + Log.d(TAG, "animateTextColor() called with: view = [" + view + "], duration = [" + duration + "], colorStart = [" + colorStart + "], colorEnd = [" + colorEnd + "]"); + } + + ValueAnimator viewPropertyAnimator = ValueAnimator.ofObject(new ArgbEvaluator(), colorStart, colorEnd); + viewPropertyAnimator.setInterpolator(new FastOutSlowInInterpolator()); + viewPropertyAnimator.setDuration(duration); + viewPropertyAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + view.setTextColor((int) animation.getAnimatedValue()); + } + }); + viewPropertyAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + view.setTextColor(colorEnd); + } + + @Override + public void onAnimationCancel(Animator animation) { + view.setTextColor(colorEnd); + } + }); + viewPropertyAnimator.start(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Internals + //////////////////////////////////////////////////////////////////////////*/ + + private static void animateAlpha(final View view, boolean enterOrExit, long duration, long delay, final Runnable execOnEnd) { + if (enterOrExit) { + view.animate().setInterpolator(new FastOutSlowInInterpolator()).alpha(1f) + .setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (execOnEnd != null) execOnEnd.run(); + } + }).start(); + } else { + view.animate().setInterpolator(new FastOutSlowInInterpolator()).alpha(0f) + .setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + view.setVisibility(View.GONE); + if (execOnEnd != null) execOnEnd.run(); + } + }).start(); } } private static void animateScaleAndAlpha(final View view, boolean enterOrExit, long duration, long delay, final Runnable execOnEnd) { if (enterOrExit) { - view.setAlpha(0f); view.setScaleX(.8f); view.setScaleY(.8f); - view.animate().alpha(1f).scaleX(1f).scaleY(1f).setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() { + view.animate().setInterpolator(new FastOutSlowInInterpolator()).alpha(1f).scaleX(1f).scaleY(1f) + .setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (execOnEnd != null) execOnEnd.run(); + } + }).start(); + } else { + view.setScaleX(1f); + view.setScaleY(1f); + view.animate().setInterpolator(new FastOutSlowInInterpolator()).alpha(0f).scaleX(.8f).scaleY(.8f) + .setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + view.setVisibility(View.GONE); + if (execOnEnd != null) execOnEnd.run(); + } + }).start(); + } + } + + private static void animateLightScaleAndAlpha(final View view, boolean enterOrExit, long duration, long delay, final Runnable execOnEnd) { + if (enterOrExit) { + view.setAlpha(.5f); + view.setScaleX(.95f); + view.setScaleY(.95f); + view.animate().setInterpolator(new FastOutSlowInInterpolator()).alpha(1f).scaleX(1f).scaleY(1f) + .setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (execOnEnd != null) execOnEnd.run(); @@ -94,27 +227,8 @@ public class AnimationUtils { view.setAlpha(1f); view.setScaleX(1f); view.setScaleY(1f); - view.animate().alpha(0f).scaleX(.8f).scaleY(.8f).setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - view.setVisibility(View.GONE); - if (execOnEnd != null) execOnEnd.run(); - } - }).start(); - } - } - - - private static void animateAlpha(final View view, boolean enterOrExit, long duration, long delay, final Runnable execOnEnd) { - if (enterOrExit) { - view.animate().alpha(1f).setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - if (execOnEnd != null) execOnEnd.run(); - } - }).start(); - } else { - view.animate().alpha(0f).setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() { + view.animate().setInterpolator(new FastOutSlowInInterpolator()).alpha(0f).scaleX(.95f).scaleY(.95f) + .setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { view.setVisibility(View.GONE); diff --git a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java new file mode 100644 index 000000000..8d5636804 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java @@ -0,0 +1,236 @@ +/* + * Copyright 2017 Mauricio Colli + * Extractors.java is part of NewPipe + * + * License: GPL-3.0+ + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.schabi.newpipe.util; + +import android.util.Log; + +import org.schabi.newpipe.MainActivity; +import org.schabi.newpipe.extractor.Info; +import org.schabi.newpipe.extractor.ListExtractor.NextItemsResult; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.channel.ChannelInfo; +import org.schabi.newpipe.extractor.playlist.PlaylistInfo; +import org.schabi.newpipe.extractor.search.SearchEngine; +import org.schabi.newpipe.extractor.search.SearchResult; +import org.schabi.newpipe.extractor.stream.StreamInfo; + +import java.io.InterruptedIOException; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Callable; + +import io.reactivex.Maybe; +import io.reactivex.MaybeSource; +import io.reactivex.Single; +import io.reactivex.annotations.NonNull; +import io.reactivex.functions.Consumer; +import io.reactivex.functions.Function; + +public final class ExtractorHelper { + private static final String TAG = ExtractorHelper.class.getSimpleName(); + private static final InfoCache cache = InfoCache.getInstance(); + + private ExtractorHelper() { + //no instance + } + + public static Single searchFor(final int serviceId, final String query, final int pageNumber, final String searchLanguage, final SearchEngine.Filter filter) { + return Single.fromCallable(new Callable() { + @Override + public SearchResult call() throws Exception { + return SearchResult.getSearchResult(NewPipe.getService(serviceId).getSearchEngine(), + query, pageNumber, searchLanguage, filter); + } + }); + } + + public static Single getMoreSearchItems(final int serviceId, final String query, final int nextPageNumber, final String searchLanguage, final SearchEngine.Filter filter) { + return searchFor(serviceId, query, nextPageNumber, searchLanguage, filter) + .map(new Function() { + @Override + public NextItemsResult apply(@NonNull SearchResult searchResult) throws Exception { + return new NextItemsResult(searchResult.resultList, nextPageNumber + "", searchResult.errors); + } + }); + } + + public static Single> suggestionsFor(final int serviceId, final String query, final String searchLanguage) { + return Single.fromCallable(new Callable>() { + @Override + public List call() throws Exception { + return NewPipe.getService(serviceId).getSuggestionExtractor().suggestionList(query, searchLanguage); + } + }); + } + + public static Single getStreamInfo(final int serviceId, final String url, boolean forceLoad) { + return checkCache(forceLoad, serviceId, url, Single.fromCallable(new Callable() { + @Override + public StreamInfo call() throws Exception { + return StreamInfo.getInfo(NewPipe.getService(serviceId), url); + } + })); + } + + public static Single getChannelInfo(final int serviceId, final String url, boolean forceLoad) { + return checkCache(forceLoad, serviceId, url, Single.fromCallable(new Callable() { + @Override + public ChannelInfo call() throws Exception { + return ChannelInfo.getInfo(NewPipe.getService(serviceId), url); + } + })); + } + + public static Single getMoreChannelItems(final int serviceId, final String nextStreamsUrl) { + return Single.fromCallable(new Callable() { + @Override + public NextItemsResult call() throws Exception { + return ChannelInfo.getMoreItems(NewPipe.getService(serviceId), nextStreamsUrl); + } + }); + } + + public static Single getPlaylistInfo(final int serviceId, final String url, boolean forceLoad) { + return checkCache(forceLoad, serviceId, url, Single.fromCallable(new Callable() { + @Override + public PlaylistInfo call() throws Exception { + return PlaylistInfo.getInfo(NewPipe.getService(serviceId), url); + } + })); + } + + public static Single getMorePlaylistItems(final int serviceId, final String nextStreamsUrl) { + return Single.fromCallable(new Callable() { + @Override + public NextItemsResult call() throws Exception { + return PlaylistInfo.getMoreItems(NewPipe.getService(serviceId), nextStreamsUrl); + } + }); + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + /** + * Check if we can load it from the cache (forceLoad parameter), if we can't, load from the network (Single loadFromNetwork) + * and put the results in the cache. + */ + private static Single checkCache(boolean forceLoad, int serviceId, String url, Single loadFromNetwork) { + loadFromNetwork = loadFromNetwork.doOnSuccess(new Consumer() { + @Override + public void accept(@NonNull I i) throws Exception { + cache.putInfo(i); + } + }); + + Single load; + if (forceLoad) { + cache.removeInfo(serviceId, url); + load = loadFromNetwork; + } else { + load = Maybe.concat(ExtractorHelper.loadFromCache(serviceId, url), loadFromNetwork.toMaybe()) + .firstElement() //Take the first valid + .toSingle(); + } + + return load; + } + + /** + * Default implementation uses the {@link InfoCache} to get cached results + */ + public static Maybe loadFromCache(final int serviceId, final String url) { + return Maybe.defer(new Callable>() { + @Override + public MaybeSource call() throws Exception { + //noinspection unchecked + I info = (I) cache.getFromKey(serviceId, url); + if (MainActivity.DEBUG) Log.d(TAG, "loadFromCache() called, info > " + info); + + // Only return info if it's not null (it is cached) + if (info != null) { + return Maybe.just(info); + } + + return Maybe.empty(); + } + }); + } + + /** + * Check if throwable have the cause that can be assignable from the causes to check. + * + * @see Class#isAssignableFrom(Class) + */ + public static boolean hasAssignableCauseThrowable(Throwable throwable, Class... causesToCheck) { + // Check if getCause is not the same as cause (the getCause is already the root), + // as it will cause a infinite loop if it is + Throwable cause, getCause = throwable; + + for (Class causesEl : causesToCheck) { + if (throwable.getClass().isAssignableFrom(causesEl)) { + return true; + } + } + + while ((cause = throwable.getCause()) != null && getCause != cause) { + getCause = cause; + for (Class causesEl : causesToCheck) { + if (cause.getClass().isAssignableFrom(causesEl)) { + return true; + } + } + } + return false; + } + + /** + * Check if throwable have the exact cause from one of the causes to check. + */ + public static boolean hasExactCauseThrowable(Throwable throwable, Class... causesToCheck) { + // Check if getCause is not the same as cause (the getCause is already the root), + // as it will cause a infinite loop if it is + Throwable cause, getCause = throwable; + + for (Class causesEl : causesToCheck) { + if (throwable.getClass().equals(causesEl)) { + return true; + } + } + + while ((cause = throwable.getCause()) != null && getCause != cause) { + getCause = cause; + for (Class causesEl : causesToCheck) { + if (cause.getClass().equals(causesEl)) { + return true; + } + } + } + return false; + } + + /** + * Check if throwable have Interrupted* exception as one of its causes. + */ + public static boolean isInterruptedCaused(Throwable throwable) { + return ExtractorHelper.hasExactCauseThrowable(throwable, InterruptedIOException.class, InterruptedException.class); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/util/InfoCache.java b/app/src/main/java/org/schabi/newpipe/util/InfoCache.java new file mode 100644 index 000000000..5b22af86a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/InfoCache.java @@ -0,0 +1,100 @@ +/* + * Copyright 2017 Mauricio Colli + * InfoCache.java is part of NewPipe + * + * License: GPL-3.0+ + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.schabi.newpipe.util; + +import android.support.annotation.NonNull; +import android.support.v4.util.LruCache; +import android.util.Log; + +import org.schabi.newpipe.MainActivity; +import org.schabi.newpipe.extractor.Info; + + +public final class InfoCache { + private static final boolean DEBUG = MainActivity.DEBUG; + private final String TAG = getClass().getSimpleName(); + + private static final InfoCache instance = new InfoCache(); + private static final int MAX_ITEMS_ON_CACHE = 60; + /** + * Trim the cache to this size + */ + private static final int TRIM_CACHE_TO = 30; + + // TODO: Replace to one with timeout (like the one from guava) + private static final LruCache lruCache = new LruCache<>(MAX_ITEMS_ON_CACHE); + + private InfoCache() { + //no instance + } + + public static InfoCache getInstance() { + return instance; + } + + public Info getFromKey(int serviceId, @NonNull String url) { + if (DEBUG) Log.d(TAG, "getFromKey() called with: serviceId = [" + serviceId + "], url = [" + url + "]"); + synchronized (lruCache) { + return lruCache.get(serviceId + url); + } + } + + public void putInfo(@NonNull Info info) { + if (DEBUG) Log.d(TAG, "putInfo() called with: info = [" + info + "]"); + synchronized (lruCache) { + lruCache.put(info.service_id + info.url, info); + } + } + + public void removeInfo(@NonNull Info info) { + if (DEBUG) Log.d(TAG, "removeInfo() called with: info = [" + info + "]"); + synchronized (lruCache) { + lruCache.remove(info.service_id + info.url); + } + } + + public void removeInfo(int serviceId, @NonNull String url) { + if (DEBUG) Log.d(TAG, "removeInfo() called with: serviceId = [" + serviceId + "], url = [" + url + "]"); + synchronized (lruCache) { + lruCache.remove(serviceId + url); + } + } + + public void clearCache() { + if (DEBUG) Log.d(TAG, "clearCache() called"); + synchronized (lruCache) { + lruCache.evictAll(); + } + } + + public void trimCache() { + if (DEBUG) Log.d(TAG, "trimCache() called"); + synchronized (lruCache) { + lruCache.trimToSize(TRIM_CACHE_TO); + } + } + + public long getSize() { + synchronized (lruCache) { + return lruCache.size(); + } + } + +} diff --git a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java new file mode 100644 index 000000000..3fda47438 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java @@ -0,0 +1,269 @@ +package org.schabi.newpipe.util; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.support.annotation.StringRes; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.MediaFormat; +import org.schabi.newpipe.extractor.stream.AudioStream; +import org.schabi.newpipe.extractor.stream.VideoStream; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; + +@SuppressWarnings("WeakerAccess") +public final class ListHelper { + + private static final List HIGH_RESOLUTION_LIST = Arrays.asList("1440p", "2160p", "1440p60", "2160p60"); + + /** + * Return the index of the default stream in the list, based on the parameters + * defaultResolution and defaultFormat + * + * @return index of the default resolution&format + */ + public static int getDefaultResolutionIndex(String defaultResolution, String bestResolutionKey, MediaFormat defaultFormat, List videoStreams) { + if (videoStreams == null || videoStreams.isEmpty()) return -1; + + sortStreamList(videoStreams, false); + if (defaultResolution.equals(bestResolutionKey)) { + return 0; + } + + int defaultStreamIndex = getDefaultStreamIndex(defaultResolution, defaultFormat, videoStreams); + if (defaultStreamIndex == -1 && defaultResolution.contains("p60")) { + defaultStreamIndex = getDefaultStreamIndex(defaultResolution.replace("p60", "p"), defaultFormat, videoStreams); + } + + // this is actually an error, + // but maybe there is really no stream fitting to the default value. + if (defaultStreamIndex == -1) return 0; + + return defaultStreamIndex; + } + + /** + * @see #getDefaultResolutionIndex(String, String, MediaFormat, List) + */ + public static int getDefaultResolutionIndex(Context context, List videoStreams) { + SharedPreferences defaultPreferences = PreferenceManager.getDefaultSharedPreferences(context); + if (defaultPreferences == null) return 0; + + String defaultResolution = defaultPreferences.getString(context.getString(R.string.default_resolution_key), context.getString(R.string.default_resolution_value)); + return getDefaultResolutionWithDefaultFormat(context, defaultResolution, videoStreams); + } + + /** + * @see #getDefaultResolutionIndex(String, String, MediaFormat, List) + */ + public static int getPopupDefaultResolutionIndex(Context context, List videoStreams) { + SharedPreferences defaultPreferences = PreferenceManager.getDefaultSharedPreferences(context); + if (defaultPreferences == null) return 0; + + String defaultResolution = defaultPreferences.getString(context.getString(R.string.default_popup_resolution_key), context.getString(R.string.default_popup_resolution_value)); + return getDefaultResolutionWithDefaultFormat(context, defaultResolution, videoStreams); + } + + public static int getDefaultAudioFormat(Context context, List audioStreams) { + MediaFormat defaultFormat = getDefaultFormat(context, R.string.default_audio_format_key, R.string.default_audio_format_value); + return getHighestQualityAudioIndex(defaultFormat, audioStreams); + } + + public static int getHighestQualityAudioIndex(List audioStreams) { + if (audioStreams == null || audioStreams.isEmpty()) return -1; + + int highestQualityIndex = 0; + if (audioStreams.size() > 1) for (int i = 1; i < audioStreams.size(); i++) { + AudioStream audioStream = audioStreams.get(i); + if (audioStream.average_bitrate >= audioStreams.get(highestQualityIndex).average_bitrate) highestQualityIndex = i; + } + return highestQualityIndex; + } + + /** + * Get the audio from the list with the highest bitrate + * + * @param audioStreams list the audio streams + * @return audio with highest average bitrate + */ + public static AudioStream getHighestQualityAudio(List audioStreams) { + if (audioStreams == null || audioStreams.isEmpty()) return null; + + return audioStreams.get(getHighestQualityAudioIndex(audioStreams)); + } + + /** + * Get the audio from the list with the highest bitrate + * + * @param audioStreams list the audio streams + * @return index of the audio with the highest average bitrate of the default format + */ + public static int getHighestQualityAudioIndex(MediaFormat defaultFormat, List audioStreams) { + if (audioStreams == null || audioStreams.isEmpty() || defaultFormat == null) return -1; + + int highestQualityIndex = -1; + for (int i = 0; i < audioStreams.size(); i++) { + AudioStream audioStream = audioStreams.get(i); + if (highestQualityIndex == -1 && audioStream.format == defaultFormat.id) highestQualityIndex = i; + + if (highestQualityIndex != -1 && audioStream.format == defaultFormat.id + && audioStream.average_bitrate > audioStreams.get(highestQualityIndex).average_bitrate) { + highestQualityIndex = i; + } + } + if (highestQualityIndex == -1) highestQualityIndex = getHighestQualityAudioIndex(audioStreams); + return highestQualityIndex; + } + + /** + * Join the two lists of video streams (video_only and normal videos), and sort them according with default format + * chosen by the user + * + * @param context context to search for the format to give preference + * @param videoStreams normal videos list + * @param videoOnlyStreams video only stream list + * @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest + * @return the sorted list + */ + public static List getSortedStreamVideosList(Context context, List videoStreams, List videoOnlyStreams, boolean ascendingOrder) { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + + boolean showHigherResolutions = preferences.getBoolean(context.getString(R.string.show_higher_resolutions_key), false); + MediaFormat defaultFormat = getDefaultFormat(context, R.string.default_video_format_key, R.string.default_video_format_value); + + return getSortedStreamVideosList(defaultFormat, showHigherResolutions, videoStreams, videoOnlyStreams, ascendingOrder); + } + + /** + * Join the two lists of video streams (video_only and normal videos), and sort them according with default format + * chosen by the user + * + * @param defaultFormat format to give preference + * @param showHigherResolutions show >1080p resolutions + * @param videoStreams normal videos list + * @param videoOnlyStreams video only stream list + * @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest @return the sorted list + * @return the sorted list + */ + public static List getSortedStreamVideosList(MediaFormat defaultFormat, boolean showHigherResolutions, List videoStreams, List videoOnlyStreams, boolean ascendingOrder) { + ArrayList retList = new ArrayList<>(); + HashMap hashMap = new HashMap<>(); + + if (videoOnlyStreams != null) { + for (VideoStream stream : videoOnlyStreams) { + if (!showHigherResolutions && HIGH_RESOLUTION_LIST.contains(stream.resolution)) continue; + retList.add(stream); + } + } + if (videoStreams != null) { + for (VideoStream stream : videoStreams) { + if (!showHigherResolutions && HIGH_RESOLUTION_LIST.contains(stream.resolution)) continue; + retList.add(stream); + } + } + + // Add all to the hashmap + for (VideoStream videoStream : retList) hashMap.put(videoStream.resolution, videoStream); + + // Override the values when the key == resolution, with the defaultFormat + for (VideoStream videoStream : retList) { + if (videoStream.format == defaultFormat.id) hashMap.put(videoStream.resolution, videoStream); + } + + retList.clear(); + retList.addAll(hashMap.values()); + sortStreamList(retList, ascendingOrder); + return retList; + } + + /** + * Sort the streams list depending on the parameter ascendingOrder; + *

+ * It works like that:
+ * - Take a string resolution, remove the letters, replace "0p60" (for 60fps videos) with "1" + * and sort by the greatest:
+ *

+     *      720p     ->  720
+     *      720p60   ->  721
+     *      360p     ->  360
+     *      1080p    ->  1080
+     *      1080p60  ->  1081
+     * 
+ * ascendingOrder ? 360 < 720 < 721 < 1080 < 1081 + * !ascendingOrder ? 1081 < 1080 < 721 < 720 < 360
+ * + * @param videoStreams list that the sorting will be applied + * @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest + */ + public static void sortStreamList(List videoStreams, final boolean ascendingOrder) { + Collections.sort(videoStreams, new Comparator() { + @Override + public int compare(VideoStream o1, VideoStream o2) { + int res1 = Integer.parseInt(o1.resolution.replace("0p60", "1").replaceAll("[^\\d.]", "")); + int res2 = Integer.parseInt(o2.resolution.replace("0p60", "1").replaceAll("[^\\d.]", "")); + + return ascendingOrder ? res1 - res2 : res2 - res1; + } + }); + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + private static int getDefaultStreamIndex(String defaultResolution, MediaFormat defaultFormat, List videoStreams) { + int defaultStreamIndex = -1; + for (int i = 0; i < videoStreams.size(); i++) { + VideoStream stream = videoStreams.get(i); + if (defaultStreamIndex == -1 && stream.resolution.equals(defaultResolution)) defaultStreamIndex = i; + + if (stream.format == defaultFormat.id && stream.resolution.equals(defaultResolution)) { + return i; + } + } + + return defaultStreamIndex; + } + + private static int getDefaultResolutionWithDefaultFormat(Context context, String defaultResolution, List videoStreams) { + MediaFormat defaultFormat = getDefaultFormat(context, R.string.default_video_format_key, R.string.default_video_format_value); + return getDefaultResolutionIndex(defaultResolution, context.getString(R.string.best_resolution_key), defaultFormat, videoStreams); + } + + private static MediaFormat getDefaultFormat(Context context, @StringRes int defaultFormatKey, @StringRes int defaultFormatValueKey) { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + + String defaultFormat = context.getString(defaultFormatValueKey); + String defaultFormatString = preferences.getString(context.getString(defaultFormatKey), defaultFormat); + + MediaFormat defaultMediaFormat = getMediaFormatFromKey(context, defaultFormatString); + if (defaultMediaFormat == null) { + preferences.edit().putString(context.getString(defaultFormatKey), defaultFormat).apply(); + defaultMediaFormat = getMediaFormatFromKey(context, defaultFormat); + } + + return defaultMediaFormat; + } + + private static MediaFormat getMediaFormatFromKey(Context context, String formatKey) { + MediaFormat format = null; + if (formatKey.equals(context.getString(R.string.video_webm_key))) { + format = MediaFormat.WEBM; + } else if (formatKey.equals(context.getString(R.string.video_mp4_key))) { + format = MediaFormat.MPEG_4; + } else if (formatKey.equals(context.getString(R.string.video_3gp_key))) { + format = MediaFormat.v3GPP; + } else if (formatKey.equals(context.getString(R.string.audio_webm_key))) { + format = MediaFormat.WEBMA; + } else if (formatKey.equals(context.getString(R.string.audio_m4a_key))) { + format = MediaFormat.M4A; + } + return format; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/util/Localization.java b/app/src/main/java/org/schabi/newpipe/util/Localization.java index f1eb50c6a..b6ec3cd3a 100644 --- a/app/src/main/java/org/schabi/newpipe/util/Localization.java +++ b/app/src/main/java/org/schabi/newpipe/util/Localization.java @@ -4,6 +4,8 @@ import android.content.Context; import android.content.SharedPreferences; import android.content.res.Resources; import android.preference.PreferenceManager; +import android.support.annotation.PluralsRes; +import android.support.annotation.StringRes; import org.schabi.newpipe.R; @@ -14,7 +16,7 @@ import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; -/** +/* * Created by chschtsch on 12/29/15. * * Copyright (C) Gregory Arkhipov 2015 @@ -42,38 +44,28 @@ public class Localization { public static Locale getPreferredLocale(Context context) { SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); - String languageCode = sp.getString(String.valueOf(R.string.search_language_key), - context.getString(R.string.default_language_value)); + String languageCode = sp.getString(context.getString(R.string.search_language_key), context.getString(R.string.default_language_value)); - if(languageCode.length() == 2) { - return new Locale(languageCode); - } - else if(languageCode.contains("_")) { - String country = languageCode - .substring(languageCode.indexOf("_"), languageCode.length()); - return new Locale(languageCode.substring(0, 2), country); + try { + if (languageCode.length() == 2) { + return new Locale(languageCode); + } else if (languageCode.contains("_")) { + String country = languageCode.substring(languageCode.indexOf("_"), languageCode.length()); + return new Locale(languageCode.substring(0, 2), country); + } + } catch (Exception ignored) { } + return Locale.getDefault(); } - public static String localizeViewCount(long viewCount, Context context) { - Locale locale = getPreferredLocale(context); - - Resources res = context.getResources(); - String viewsString = res.getString(R.string.view_count_text); - - NumberFormat nf = NumberFormat.getInstance(locale); - String formattedViewCount = nf.format(viewCount); - return String.format(viewsString, formattedViewCount); - } - - public static String localizeNumber(long number, Context context) { + public static String localizeNumber(Context context, long number) { Locale locale = getPreferredLocale(context); NumberFormat nf = NumberFormat.getInstance(locale); return nf.format(number); } - private static String formatDate(String date, Context context) { + private static String formatDate(Context context, String date) { Locale locale = getPreferredLocale(context); SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd"); Date datum = null; @@ -88,11 +80,75 @@ public class Localization { return df.format(datum); } - public static String localizeDate(String date, Context context) { + public static String localizeDate(Context context, String date) { Resources res = context.getResources(); String dateString = res.getString(R.string.upload_date_text); - String formattedDate = formatDate(date, context); + String formattedDate = formatDate(context, date); return String.format(dateString, formattedDate); } + + public static String localizeViewCount(Context context, long viewCount) { + return getQuantity(context, R.plurals.views, R.string.no_views, viewCount, localizeNumber(context, viewCount)); + } + + public static String localizeSubscribersCount(Context context, long subscriberCount) { + return getQuantity(context, R.plurals.subscribers, R.string.no_subscribers, subscriberCount, localizeNumber(context, subscriberCount)); + } + + public static String localizeStreamCount(Context context, long streamCount) { + return getQuantity(context, R.plurals.videos, R.string.no_videos, streamCount, localizeNumber(context, streamCount)); + } + + public static String shortCount(Context context, long count) { + if (count >= 1000000000) { + return Long.toString(count / 1000000000) + context.getString(R.string.short_billion); + } else if (count >= 1000000) { + return Long.toString(count / 1000000) + context.getString(R.string.short_million); + } else if (count >= 1000) { + return Long.toString(count / 1000) + context.getString(R.string.short_thousand); + } else { + return Long.toString(count); + } + } + + public static String shortViewCount(Context context, long viewCount) { + return getQuantity(context, R.plurals.views, R.string.no_views, viewCount, shortCount(context, viewCount)); + } + + public static String shortSubscriberCount(Context context, long subscriberCount) { + return getQuantity(context, R.plurals.subscribers, R.string.no_subscribers, subscriberCount, shortCount(context, subscriberCount)); + } + + private static String getQuantity(Context context, @PluralsRes int pluralId, @StringRes int zeroCaseStringId, long count, String formattedCount) { + if (count == 0) return context.getString(zeroCaseStringId); + + // As we use the already formatted count, is not the responsibility of this method handle long numbers + // (it probably will fall in the "other" category, or some language have some specific rule... then we have to change it) + int safeCount = count > Integer.MAX_VALUE ? Integer.MAX_VALUE : count < Integer.MIN_VALUE ? Integer.MIN_VALUE : (int) count; + return context.getResources().getQuantityString(pluralId, safeCount, formattedCount); + } + + public static String getDurationString(long duration) { + if (duration < 0) { + duration = 0; + } + String output; + long days = duration / (24 * 60 * 60L); /* greater than a day */ + duration %= (24 * 60 * 60L); + long hours = duration / (60 * 60L); /* greater than an hour */ + duration %= (60 * 60L); + long minutes = duration / 60L; + long seconds = duration % 60L; + + //handle days + if (days > 0) { + output = String.format(Locale.US, "%d:%02d:%02d:%02d", days, hours, minutes, seconds); + } else if (hours > 0) { + output = String.format(Locale.US, "%d:%02d:%02d", hours, minutes, seconds); + } else { + output = String.format(Locale.US, "%d:%02d", minutes, seconds); + } + return output; + } } diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index 6473741b4..0a529ab4e 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -4,30 +4,33 @@ import android.app.Activity; import android.content.Context; import android.content.Intent; import android.preference.PreferenceManager; -import android.support.annotation.CheckResult; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import com.nostra13.universalimageloader.core.ImageLoader; -import org.schabi.newpipe.about.AboutActivity; import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; +import org.schabi.newpipe.about.AboutActivity; import org.schabi.newpipe.download.DownloadActivity; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; -import org.schabi.newpipe.extractor.stream_info.AudioStream; -import org.schabi.newpipe.extractor.stream_info.StreamInfo; -import org.schabi.newpipe.fragments.FeedFragment; +import org.schabi.newpipe.extractor.stream.AudioStream; +import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.fragments.MainFragment; -import org.schabi.newpipe.fragments.channel.ChannelFragment; import org.schabi.newpipe.fragments.detail.VideoDetailFragment; -import org.schabi.newpipe.fragments.search.SearchFragment; +import org.schabi.newpipe.fragments.list.channel.ChannelFragment; +import org.schabi.newpipe.fragments.list.feed.FeedFragment; +import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment; +import org.schabi.newpipe.fragments.list.search.SearchFragment; +import org.schabi.newpipe.history.HistoryActivity; import org.schabi.newpipe.player.BackgroundPlayer; import org.schabi.newpipe.player.BasePlayer; import org.schabi.newpipe.player.VideoPlayer; import org.schabi.newpipe.settings.SettingsActivity; +import java.util.ArrayList; + @SuppressWarnings({"unused", "WeakerAccess"}) public class NavigationHelper { public static final String MAIN_FRAGMENT_TAG = "main_fragment_tag"; @@ -38,14 +41,14 @@ public class NavigationHelper { public static Intent getOpenVideoPlayerIntent(Context context, Class targetClazz, StreamInfo info, int selectedStreamIndex) { Intent mIntent = new Intent(context, targetClazz) - .putExtra(BasePlayer.VIDEO_TITLE, info.title) - .putExtra(BasePlayer.VIDEO_URL, info.webpage_url) + .putExtra(BasePlayer.VIDEO_TITLE, info.name) + .putExtra(BasePlayer.VIDEO_URL, info.url) .putExtra(BasePlayer.VIDEO_THUMBNAIL_URL, info.thumbnail_url) - .putExtra(BasePlayer.CHANNEL_NAME, info.uploader) + .putExtra(BasePlayer.CHANNEL_NAME, info.uploader_name) .putExtra(VideoPlayer.INDEX_SEL_VIDEO_STREAM, selectedStreamIndex) - .putExtra(VideoPlayer.VIDEO_STREAMS_LIST, Utils.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false)) - .putExtra(VideoPlayer.VIDEO_ONLY_AUDIO_STREAM, Utils.getHighestQualityAudio(info.audio_streams)); - if (info.start_position > 0) mIntent.putExtra(BasePlayer.START_POSITION, info.start_position * 1000); + .putExtra(VideoPlayer.VIDEO_STREAMS_LIST, new ArrayList<>(ListHelper.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false))) + .putExtra(VideoPlayer.VIDEO_ONLY_AUDIO_STREAM, ListHelper.getHighestQualityAudio(info.audio_streams)); + if (info.start_position > 0) mIntent.putExtra(BasePlayer.START_POSITION, info.start_position * 1000L); return mIntent; } @@ -54,27 +57,26 @@ public class NavigationHelper { .putExtra(BasePlayer.VIDEO_TITLE, instance.getVideoTitle()) .putExtra(BasePlayer.VIDEO_URL, instance.getVideoUrl()) .putExtra(BasePlayer.VIDEO_THUMBNAIL_URL, instance.getVideoThumbnailUrl()) - .putExtra(BasePlayer.CHANNEL_NAME, instance.getChannelName()) + .putExtra(BasePlayer.CHANNEL_NAME, instance.getUploaderName()) .putExtra(VideoPlayer.INDEX_SEL_VIDEO_STREAM, instance.getSelectedStreamIndex()) .putExtra(VideoPlayer.VIDEO_STREAMS_LIST, instance.getVideoStreamsList()) .putExtra(VideoPlayer.VIDEO_ONLY_AUDIO_STREAM, instance.getAudioStream()) - .putExtra(BasePlayer.START_POSITION, ((int) instance.getPlayer().getCurrentPosition())) + .putExtra(BasePlayer.START_POSITION, instance.getPlayer().getCurrentPosition()) .putExtra(BasePlayer.PLAYBACK_SPEED, instance.getPlaybackSpeed()); } public static Intent getOpenBackgroundPlayerIntent(Context context, StreamInfo info) { - return getOpenBackgroundPlayerIntent(context, info, info.audio_streams.get(Utils.getPreferredAudioFormat(context, info.audio_streams))); + return getOpenBackgroundPlayerIntent(context, info, info.audio_streams.get(ListHelper.getDefaultAudioFormat(context, info.audio_streams))); } public static Intent getOpenBackgroundPlayerIntent(Context context, StreamInfo info, AudioStream audioStream) { Intent mIntent = new Intent(context, BackgroundPlayer.class) - .putExtra(BasePlayer.VIDEO_TITLE, info.title) - .putExtra(BasePlayer.VIDEO_URL, info.webpage_url) + .putExtra(BasePlayer.VIDEO_TITLE, info.name) + .putExtra(BasePlayer.VIDEO_URL, info.url) .putExtra(BasePlayer.VIDEO_THUMBNAIL_URL, info.thumbnail_url) - .putExtra(BasePlayer.CHANNEL_NAME, info.uploader) - .putExtra(BasePlayer.CHANNEL_NAME, info.uploader) + .putExtra(BasePlayer.CHANNEL_NAME, info.uploader_name) .putExtra(BackgroundPlayer.AUDIO_STREAM, audioStream); - if (info.start_position > 0) mIntent.putExtra(BasePlayer.START_POSITION, info.start_position * 1000); + if (info.start_position > 0) mIntent.putExtra(BasePlayer.START_POSITION, info.start_position * 1000L); return mIntent; } @@ -90,9 +92,11 @@ public class NavigationHelper { } private static void openMainFragment(FragmentManager fragmentManager) { + InfoCache.getInstance().trimCache(); + fragmentManager.popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE); fragmentManager.beginTransaction() - .setCustomAnimations(R.anim.custom_fade_in, R.anim.custom_fade_out, R.anim.custom_fade_in, R.anim.custom_fade_out) + .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out) .replace(R.id.fragment_holder, new MainFragment()) .addToBackStack(MAIN_FRAGMENT_TAG) .commit(); @@ -100,7 +104,7 @@ public class NavigationHelper { public static void openSearchFragment(FragmentManager fragmentManager, int serviceId, String query) { fragmentManager.beginTransaction() - .setCustomAnimations(R.anim.custom_fade_in, R.anim.custom_fade_out, R.anim.custom_fade_in, R.anim.custom_fade_out) + .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out) .replace(R.id.fragment_holder, SearchFragment.getInstance(serviceId, query)) .addToBackStack(null) .commit(); @@ -125,7 +129,7 @@ public class NavigationHelper { instance.setAutoplay(autoPlay); fragmentManager.beginTransaction() - .setCustomAnimations(R.anim.custom_fade_in, R.anim.custom_fade_out, R.anim.custom_fade_in, R.anim.custom_fade_out) + .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out) .replace(R.id.fragment_holder, instance) .addToBackStack(null) .commit(); @@ -134,15 +138,24 @@ public class NavigationHelper { public static void openChannelFragment(FragmentManager fragmentManager, int serviceId, String url, String name) { if (name == null) name = ""; fragmentManager.beginTransaction() - .setCustomAnimations(R.anim.custom_fade_in, R.anim.custom_fade_out, R.anim.custom_fade_in, R.anim.custom_fade_out) + .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out) .replace(R.id.fragment_holder, ChannelFragment.getInstance(serviceId, url, name)) .addToBackStack(null) .commit(); } + public static void openPlaylistFragment(FragmentManager fragmentManager, int serviceId, String url, String name) { + if (name == null) name = ""; + fragmentManager.beginTransaction() + .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out) + .replace(R.id.fragment_holder, PlaylistFragment.getInstance(serviceId, url, name)) + .addToBackStack(null) + .commit(); + } + public static void openWhatsNewFragment(FragmentManager fragmentManager) { fragmentManager.beginTransaction() - .setCustomAnimations(R.anim.custom_fade_in, R.anim.custom_fade_out, R.anim.custom_fade_in, R.anim.custom_fade_out) + .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out) .replace(R.id.fragment_holder, new FeedFragment()) .addToBackStack(null) .commit(); @@ -182,48 +195,21 @@ public class NavigationHelper { public static void openMainActivity(Context context) { Intent mIntent = new Intent(context, MainActivity.class); + mIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); context.startActivity(mIntent); } - public static void openByLink(Context context, String url) throws Exception { - Intent intentByLink = getIntentByLink(context, url); - if (intentByLink == null) throw new NullPointerException("getIntentByLink(context = [" + context + "], url = [" + url + "]) returned null"); - intentByLink.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intentByLink.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); - context.startActivity(intentByLink); - } - - private static Intent getOpenIntent(Context context, String url, int serviceId, StreamingService.LinkType type) { - Intent mIntent = new Intent(context, MainActivity.class); - mIntent.putExtra(Constants.KEY_SERVICE_ID, serviceId); - mIntent.putExtra(Constants.KEY_URL, url); - mIntent.putExtra(Constants.KEY_LINK_TYPE, type); - return mIntent; - } - - private static Intent getIntentByLink(Context context, String url) throws Exception { - StreamingService service = NewPipe.getServiceByUrl(url); - if (service == null) throw new Exception("NewPipe.getServiceByUrl returned null for url > \"" + url + "\""); - int serviceId = service.getServiceId(); - switch (service.getLinkTypeByUrl(url)) { - case STREAM: - Intent sIntent = getOpenIntent(context, url, serviceId, StreamingService.LinkType.STREAM); - sIntent.putExtra(VideoDetailFragment.AUTO_PLAY, PreferenceManager.getDefaultSharedPreferences(context) - .getBoolean(context.getString(R.string.autoplay_through_intent_key), false)); - return sIntent; - case CHANNEL: - return getOpenIntent(context, url, serviceId, StreamingService.LinkType.CHANNEL); - case NONE: - throw new Exception("Url not known to service. service=" + serviceId + " url=" + url); - } - return null; - } - public static void openAbout(Context context) { Intent intent = new Intent(context, AboutActivity.class); context.startActivity(intent); } + public static void openHistory(Context context) { + Intent intent = new Intent(context, HistoryActivity.class); + context.startActivity(intent); + } + public static void openSettings(Context context) { Intent intent = new Intent(context, SettingsActivity.class); context.startActivity(intent); @@ -237,4 +223,62 @@ public class NavigationHelper { activity.startActivity(intent); return true; } + + /*////////////////////////////////////////////////////////////////////////// + // Link handling + //////////////////////////////////////////////////////////////////////////*/ + + public static void openByLink(Context context, String url) throws Exception { + Intent intentByLink = getIntentByLink(context, url); + if (intentByLink == null) + throw new NullPointerException("getIntentByLink(context = [" + context + "], url = [" + url + "]) returned null"); + intentByLink.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intentByLink.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + context.startActivity(intentByLink); + } + + private static Intent getOpenIntent(Context context, String url, int serviceId, StreamingService.LinkType type) { + Intent mIntent = new Intent(context, MainActivity.class); + mIntent.putExtra(Constants.KEY_SERVICE_ID, serviceId); + mIntent.putExtra(Constants.KEY_URL, url); + mIntent.putExtra(Constants.KEY_LINK_TYPE, type); + return mIntent; + } + + private static Intent getIntentByLink(Context context, String url) throws Exception { + StreamingService service = NewPipe.getServiceByUrl(url); + + int serviceId = service.getServiceId(); + StreamingService.LinkType linkType = service.getLinkTypeByUrl(url); + + if (linkType == StreamingService.LinkType.NONE) { + throw new Exception("Url not known to service. service=" + serviceId + " url=" + url); + } + + url = getCleanUrl(service, url, linkType); + Intent rIntent = getOpenIntent(context, url, serviceId, linkType); + + switch (linkType) { + case STREAM: + rIntent.putExtra(VideoDetailFragment.AUTO_PLAY, PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean(context.getString(R.string.autoplay_through_intent_key), false)); + break; + } + + return rIntent; + } + + private static String getCleanUrl(StreamingService service, String dirtyUrl, StreamingService.LinkType linkType) throws Exception { + switch (linkType) { + case STREAM: + return service.getStreamUrlIdHandler().cleanUrl(dirtyUrl); + case CHANNEL: + return service.getChannelUrlIdHandler().cleanUrl(dirtyUrl); + case PLAYLIST: + return service.getPlaylistUrlIdHandler().cleanUrl(dirtyUrl); + case NONE: + break; + } + return null; + } } diff --git a/app/src/main/java/org/schabi/newpipe/util/StateSaver.java b/app/src/main/java/org/schabi/newpipe/util/StateSaver.java new file mode 100644 index 000000000..bd268abf7 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/StateSaver.java @@ -0,0 +1,319 @@ +/* + * Copyright 2017 Mauricio Colli + * StateSaver.java is part of NewPipe + * + * License: GPL-3.0+ + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.schabi.newpipe.util; + + +import android.content.Context; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import android.util.Log; + +import org.schabi.newpipe.MainActivity; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.LinkedList; +import java.util.Queue; +import java.util.concurrent.ConcurrentHashMap; + +/** + * A way to save state to disk or in a in-memory map if it's just changing configurations (i.e. rotating the phone). + */ +public class StateSaver { + private static final ConcurrentHashMap> stateObjectsHolder = new ConcurrentHashMap<>(); + private static final String TAG = "StateSaver"; + private static final String CACHE_DIR_NAME = "state_cache"; + + public static final String KEY_SAVED_STATE = "key_saved_state"; + private static String cacheDirPath; + + private StateSaver() { + //no instance + } + + /** + * Initialize the StateSaver, usually you want to call this in the Application class + * + * @param context used to get the available cache dir + */ + public static void init(Context context) { + File externalCacheDir = context.getExternalCacheDir(); + if (externalCacheDir != null) cacheDirPath = externalCacheDir.getAbsolutePath(); + if (TextUtils.isEmpty(cacheDirPath)) cacheDirPath = context.getCacheDir().getAbsolutePath(); + } + + /** + * Used for describe how to save/read the objects. + *

+ * Queue was chosen by its FIFO property. + */ + public interface WriteRead { + /** + * Generate a changing suffix that will name the cache file, + * and be used to identify if it changed (thus reducing useless reading/saving). + * + * @return a unique value + */ + String generateSuffix(); + + /** + * Add to this queue objects that you want to save. + */ + void writeTo(Queue objectsToSave); + + /** + * Poll saved objects from the queue in the order they were written. + * + * @param savedObjects queue of objects returned by {@link #writeTo(Queue)} + */ + void readFrom(@NonNull Queue savedObjects) throws Exception; + } + + /** + * @see #tryToRestore(SavedState, WriteRead) + */ + public static SavedState tryToRestore(Bundle outState, WriteRead writeRead) { + if (outState == null || writeRead == null) return null; + + SavedState savedState = outState.getParcelable(KEY_SAVED_STATE); + if (savedState == null) return null; + + return tryToRestore(savedState, writeRead); + } + + /** + * Try to restore the state from memory and disk, using the {@link StateSaver.WriteRead#readFrom(Queue)} from the writeRead. + */ + private static SavedState tryToRestore(@NonNull SavedState savedState, @NonNull WriteRead writeRead) { + if (MainActivity.DEBUG) { + Log.d(TAG, "tryToRestore() called with: savedState = [" + savedState + "], writeRead = [" + writeRead + "]"); + } + + FileInputStream fileInputStream = null; + try { + Queue savedObjects = stateObjectsHolder.remove(savedState.prefixFileSaved); + if (savedObjects != null) { + writeRead.readFrom(savedObjects); + if (MainActivity.DEBUG) { + Log.d(TAG, "tryToSave: reading objects from holder > " + savedObjects + ", stateObjectsHolder > " + stateObjectsHolder); + } + return savedState; + } + + File file = new File(savedState.pathFileSaved); + if (!file.exists()) return null; + + fileInputStream = new FileInputStream(file); + ObjectInputStream inputStream = new ObjectInputStream(fileInputStream); + //noinspection unchecked + savedObjects = (Queue) inputStream.readObject(); + if (savedObjects != null) { + writeRead.readFrom(savedObjects); + } + + return savedState; + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (fileInputStream != null) { + try { + fileInputStream.close(); + } catch (IOException ignored) { + } + } + } + return null; + } + + /** + * @see #tryToSave(boolean, String, String, WriteRead) + */ + public static SavedState tryToSave(boolean isChangingConfig, @Nullable SavedState savedState, Bundle outState, WriteRead writeRead) { + String currentSavedPrefix = savedState == null || TextUtils.isEmpty(savedState.prefixFileSaved) + ? System.nanoTime() - writeRead.hashCode() + "" + : savedState.prefixFileSaved; + + savedState = tryToSave(isChangingConfig, currentSavedPrefix, writeRead.generateSuffix(), writeRead); + if (savedState != null) { + outState.putParcelable(StateSaver.KEY_SAVED_STATE, savedState); + return savedState; + } + + return null; + } + + /** + * If it's not changing configuration (i.e. rotating screen), try to write the state from {@link StateSaver.WriteRead#writeTo(Queue)} + * to the file with the name of prefixFileName + suffixFileName, in a cache folder got from the {@link #init(Context)}. + *

+ * It checks if the file already exists and if it does, just return the path, so a good way to save is: + *

  • A fixed prefix for the file + *
  • A changing suffix + */ + private static SavedState tryToSave(boolean isChangingConfig, final String prefixFileName, String suffixFileName, WriteRead writeRead) { + if (MainActivity.DEBUG) { + Log.d(TAG, "tryToSave() called with: isChangingConfig = [" + isChangingConfig + "], prefixFileName = [" + prefixFileName + "], suffixFileName = [" + suffixFileName + "], writeRead = [" + writeRead + "]"); + } + + Queue savedObjects = new LinkedList<>(); + writeRead.writeTo(savedObjects); + + if (isChangingConfig) { + if (savedObjects.size() > 0) { + stateObjectsHolder.put(prefixFileName, savedObjects); + return new SavedState(prefixFileName, ""); + } else return null; + } + + FileOutputStream fileOutputStream = null; + try { + File cacheDir = new File(cacheDirPath); + if (!cacheDir.exists()) throw new RuntimeException("Cache dir does not exist > " + cacheDirPath); + cacheDir = new File(cacheDir, CACHE_DIR_NAME); + if (!cacheDir.exists()) { + boolean mkdirResult = cacheDir.mkdir(); + if (!mkdirResult) return null; + } + + if (TextUtils.isEmpty(suffixFileName)) suffixFileName = ".cache"; + File file = new File(cacheDir, prefixFileName + suffixFileName); + if (file.exists() && file.length() > 0) { + // If the file already exists, just return it + return new SavedState(prefixFileName, file.getAbsolutePath()); + } else { + // Delete any file that contains the prefix + File[] files = cacheDir.listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return name.contains(prefixFileName); + } + }); + for (File file1 : files) file1.delete(); + } + + fileOutputStream = new FileOutputStream(file); + ObjectOutputStream outputStream = new ObjectOutputStream(fileOutputStream); + outputStream.writeObject(savedObjects); + + return new SavedState(prefixFileName, file.getAbsolutePath()); + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (fileOutputStream != null) { + try { + fileOutputStream.close(); + } catch (IOException ignored) { + } + } + } + return null; + } + + /** + * Delete the cache file contained in the savedState and remove any possible-existing value in the memory-cache. + */ + public static void onDestroy(SavedState savedState) { + if (MainActivity.DEBUG) Log.d(TAG, "onDestroy() called with: savedState = [" + savedState + "]"); + + if (savedState != null && !TextUtils.isEmpty(savedState.pathFileSaved)) { + stateObjectsHolder.remove(savedState.prefixFileSaved); + try { + //noinspection ResultOfMethodCallIgnored + new File(savedState.pathFileSaved).delete(); + } catch (Exception ignored) { + } + } + } + + /** + * Clear all the files in cache (in memory and disk). + */ + public static void clearStateFiles() { + if (MainActivity.DEBUG) Log.d(TAG, "clearStateFiles() called"); + + stateObjectsHolder.clear(); + File cacheDir = new File(cacheDirPath); + if (!cacheDir.exists()) return; + + cacheDir = new File(cacheDir, CACHE_DIR_NAME); + if (cacheDir.exists()) { + for (File file : cacheDir.listFiles()) file.delete(); + } + } + + /*////////////////////////////////////////////////////////////////////////// + // Inner + //////////////////////////////////////////////////////////////////////////*/ + + public static class SavedState implements Parcelable { + public String prefixFileSaved; + public String pathFileSaved; + + public SavedState(String prefixFileSaved, String pathFileSaved) { + this.prefixFileSaved = prefixFileSaved; + this.pathFileSaved = pathFileSaved; + } + + protected SavedState(Parcel in) { + prefixFileSaved = in.readString(); + pathFileSaved = in.readString(); + } + + @Override + public String toString() { + return prefixFileSaved + " > " + pathFileSaved; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(prefixFileSaved); + dest.writeString(pathFileSaved); + } + + @SuppressWarnings("unused") + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + +} diff --git a/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java b/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java index 4f3be4a0d..6fdf035f4 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java @@ -13,16 +13,17 @@ public class ThemeHelper { * @param context context that the theme will be applied */ public static void setTheme(Context context) { + String lightTheme = context.getResources().getString(R.string.light_theme_key); + String darkTheme = context.getResources().getString(R.string.dark_theme_key); + String blackTheme = context.getResources().getString(R.string.black_theme_key); - String themeKey = context.getString(R.string.theme_key); - String darkTheme = context.getResources().getString(R.string.dark_theme_title); - String blackTheme = context.getResources().getString(R.string.black_theme_title); + String selectedTheme = getSelectedTheme(context); - String sp = PreferenceManager.getDefaultSharedPreferences(context).getString(themeKey, context.getResources().getString(R.string.light_theme_title)); - - if (sp.equals(darkTheme)) context.setTheme(R.style.DarkTheme); - else if (sp.equals(blackTheme)) context.setTheme(R.style.BlackTheme); - else context.setTheme(R.style.AppTheme); + if (selectedTheme.equals(lightTheme)) context.setTheme(R.style.LightTheme); + else if (selectedTheme.equals(blackTheme)) context.setTheme(R.style.BlackTheme); + else if (selectedTheme.equals(darkTheme)) context.setTheme(R.style.DarkTheme); + // Fallback + else context.setTheme(R.style.DarkTheme); } /** @@ -31,12 +32,12 @@ public class ThemeHelper { * @param context context to get the preference */ public static boolean isLightThemeSelected(Context context) { + return getSelectedTheme(context).equals(context.getResources().getString(R.string.light_theme_key)); + } + + public static String getSelectedTheme(Context context) { String themeKey = context.getString(R.string.theme_key); - String darkTheme = context.getResources().getString(R.string.dark_theme_title); - String blackTheme = context.getResources().getString(R.string.black_theme_title); - - String sp = PreferenceManager.getDefaultSharedPreferences(context).getString(themeKey, context.getResources().getString(R.string.light_theme_title)); - - return !(sp.equals(darkTheme) || sp.equals(blackTheme)); + String defaultTheme = context.getResources().getString(R.string.default_theme_value); + return PreferenceManager.getDefaultSharedPreferences(context).getString(themeKey, defaultTheme); } } diff --git a/app/src/main/java/org/schabi/newpipe/util/Utils.java b/app/src/main/java/org/schabi/newpipe/util/Utils.java deleted file mode 100644 index 9581f379c..000000000 --- a/app/src/main/java/org/schabi/newpipe/util/Utils.java +++ /dev/null @@ -1,273 +0,0 @@ -package org.schabi.newpipe.util; - -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.MediaFormat; -import org.schabi.newpipe.extractor.stream_info.AudioStream; -import org.schabi.newpipe.extractor.stream_info.VideoStream; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; - -@SuppressWarnings("WeakerAccess") -public class Utils { - - private static final List HIGH_RESOLUTION_LIST = Arrays.asList("1440p", "2160p", "1440p60", "2160p60"); - - /** - * Return the index of the default stream in the list, based on the parameters - * defaultResolution and preferredFormat - * - * @param videoStreams the list that will be extracted the index - * - * @return index of the preferred resolution&format - */ - public static int getDefaultResolution(String defaultResolution, String preferredFormat, List videoStreams) { - - if (defaultResolution.equals("Best resolution")) { - return 0; - } - - // first try to find the one with the right resolution - int selectedFormat = 0; - for (int i = 0; i < videoStreams.size(); i++) { - VideoStream item = videoStreams.get(i); - if (defaultResolution.equals(item.resolution)) { - selectedFormat = i; - } - } - - // than try to find the one with the right resolution and format - for (int i = 0; i < videoStreams.size(); i++) { - VideoStream item = videoStreams.get(i); - if (defaultResolution.equals(item.resolution) - && preferredFormat.equals(MediaFormat.getNameById(item.format))) { - selectedFormat = i; - } - } - - if (selectedFormat == 0 && !videoStreams.get(selectedFormat).resolution.contains(defaultResolution.replace("p60", "p"))) { - // Maybe there's no 60 fps variant available, so fallback to the normal version - String replace = defaultResolution.replace("p60", "p"); - for (int i = 0; i < videoStreams.size(); i++) { - VideoStream item = videoStreams.get(i); - if (replace.equals(item.resolution)) selectedFormat = i; - } - - // than try to find the one with the right resolution and format - for (int i = 0; i < videoStreams.size(); i++) { - VideoStream item = videoStreams.get(i); - if (replace.equals(item.resolution) - && preferredFormat.equals(MediaFormat.getNameById(item.format))) { - selectedFormat = i; - } - } - - } - - // this is actually an error, - // but maybe there is really no stream fitting to the default value. - return selectedFormat; - } - - /** - * @see #getDefaultResolution(String, String, List) - */ - public static int getDefaultResolution(Context context, List videoStreams) { - SharedPreferences defaultPreferences = PreferenceManager.getDefaultSharedPreferences(context); - if (defaultPreferences == null) return 0; - - String defaultResolution = defaultPreferences - .getString(context.getString(R.string.default_resolution_key), context.getString(R.string.default_resolution_value)); - - String preferredFormat = defaultPreferences - .getString(context.getString(R.string.preferred_video_format_key), context.getString(R.string.preferred_video_format_default)); - - return getDefaultResolution(defaultResolution, preferredFormat, videoStreams); - } - - /** - * @see #getDefaultResolution(String, String, List) - */ - public static int getPopupDefaultResolution(Context context, List videoStreams) { - SharedPreferences defaultPreferences = PreferenceManager.getDefaultSharedPreferences(context); - if (defaultPreferences == null) return 0; - - String defaultResolution = defaultPreferences - .getString(context.getString(R.string.default_popup_resolution_key), context.getString(R.string.default_popup_resolution_value)); - - String preferredFormat = defaultPreferences - .getString(context.getString(R.string.preferred_video_format_key), context.getString(R.string.preferred_video_format_default)); - - return getDefaultResolution(defaultResolution, preferredFormat, videoStreams); - } - - /** - * Return the index of the default stream in the list, based on the - * preferred audio format chosen in the settings - * - * @param context context to get the preferred audio format - * @param audioStreams the list that will be extracted the index - * - * @return index of the preferred format - */ - public static int getPreferredAudioFormat(Context context, List audioStreams) { - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); - if (sharedPreferences == null) return 0; - - String preferredFormatString = sharedPreferences.getString(context.getString(R.string.default_audio_format_key), "webm"); - - int preferredFormat = MediaFormat.WEBMA.id; - switch (preferredFormatString) { - case "webm": - preferredFormat = MediaFormat.WEBMA.id; - break; - case "m4a": - preferredFormat = MediaFormat.M4A.id; - break; - default: - break; - } - - int highestQualityIndex = 0; - - // Try to find a audio stream with the preferred format - for (int i = 0; i < audioStreams.size(); i++) if (audioStreams.get(i).format == preferredFormat) highestQualityIndex = i; - - // Try to find a audio stream with the highest bitrate and preferred format - for (int i = 0; i < audioStreams.size(); i++) { - AudioStream audioStream = audioStreams.get(i); - if (audioStream.avgBitrate > audioStreams.get(highestQualityIndex).avgBitrate - && audioStream.format == preferredFormat) highestQualityIndex = i; - } - - return highestQualityIndex; - } - - /** - * Get the audio from the list with the highest bitrate - * - * @param audioStreams list the audio streams - * @return audio with highest average bitrate - */ - public static AudioStream getHighestQualityAudio(List audioStreams) { - int highestQualityIndex = 0; - - for (int i = 1; i < audioStreams.size(); i++) { - AudioStream audioStream = audioStreams.get(i); - if (audioStream.avgBitrate > audioStreams.get(highestQualityIndex).avgBitrate) highestQualityIndex = i; - } - - return audioStreams.get(highestQualityIndex); - } - - /** - * Join the two lists of video streams (video_only and normal videos), and sort them according with preferred format - * chosen by the user - * - * @param context context to search for the format to give preference - * @param videoStreams normal videos list - * @param videoOnlyStreams video only stream list - * @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest - * @return the sorted list - */ - public static ArrayList getSortedStreamVideosList(Context context, List videoStreams, List videoOnlyStreams, boolean ascendingOrder) { - boolean showHigherResolutions = PreferenceManager.getDefaultSharedPreferences(context).getBoolean(context.getString(R.string.show_higher_resolutions_key), false); - String preferredFormatString = PreferenceManager.getDefaultSharedPreferences(context).getString(context.getString(R.string.preferred_video_format_key), context.getString(R.string.preferred_video_format_default)); - MediaFormat preferredFormat = MediaFormat.WEBM; - switch (preferredFormatString) { - case "WebM": - preferredFormat = MediaFormat.WEBM; - break; - case "MPEG-4": - preferredFormat = MediaFormat.MPEG_4; - break; - case "3GPP": - preferredFormat = MediaFormat.v3GPP; - break; - default: - break; - } - return getSortedStreamVideosList(preferredFormat, showHigherResolutions, videoStreams, videoOnlyStreams, ascendingOrder); - } - - /** - * Join the two lists of video streams (video_only and normal videos), and sort them according with preferred format - * chosen by the user - * - * @param preferredFormat format to give preference - * @param showHigherResolutions show >1080p resolutions - * @param videoStreams normal videos list - * @param videoOnlyStreams video only stream list - * @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest @return the sorted list - * @return the sorted list - */ - public static ArrayList getSortedStreamVideosList(MediaFormat preferredFormat, boolean showHigherResolutions, List videoStreams, List videoOnlyStreams, boolean ascendingOrder) { - ArrayList retList = new ArrayList<>(); - HashMap hashMap = new HashMap<>(); - - if (videoOnlyStreams != null) { - for (VideoStream stream : videoOnlyStreams) { - if (!showHigherResolutions && HIGH_RESOLUTION_LIST.contains(stream.resolution)) continue; - retList.add(stream); - } - } - if (videoStreams != null) { - for (VideoStream stream : videoStreams) { - if (!showHigherResolutions && HIGH_RESOLUTION_LIST.contains(stream.resolution)) continue; - retList.add(stream); - } - } - - // Add all to the hashmap - for (VideoStream videoStream : retList) hashMap.put(videoStream.resolution, videoStream); - - // Override the values when the key == resolution, with the preferredFormat - for (VideoStream videoStream : retList) { - if (videoStream.format == preferredFormat.id) hashMap.put(videoStream.resolution, videoStream); - } - - retList.clear(); - retList.addAll(hashMap.values()); - sortStreamList(retList, ascendingOrder); - return retList; - } - - /** - * Sort the streams list depending on the parameter ascendingOrder; - *

    - * It works like that:
    - * - Take a string resolution, remove the letters, replace "0p60" (for 60fps videos) with "1" - * and sort by the greatest:
    - *

    -     *      720p     ->  720
    -     *      720p60   ->  721
    -     *      360p     ->  360
    -     *      1080p    ->  1080
    -     *      1080p60  ->  1081
    -     * 

    - * ascendingOrder ? 360 < 720 < 721 < 1080 < 1081 - * !ascendingOrder ? 1081 < 1080 < 721 < 720 < 360/pre>

    - *

    - * @param videoStreams list that the sorting will be applied - * @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest - */ - public static void sortStreamList(List videoStreams, final boolean ascendingOrder) { - Collections.sort(videoStreams, new Comparator() { - @Override - public int compare(VideoStream o1, VideoStream o2) { - int res1 = Integer.parseInt(o1.resolution.replace("0p60", "1").replaceAll("[^\\d.]", "")); - int res2 = Integer.parseInt(o2.resolution.replace("0p60", "1").replaceAll("[^\\d.]", "")); - - return ascendingOrder ? res1 - res2 : res2 - res1; - } - }); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/workers/AbstractWorker.java b/app/src/main/java/org/schabi/newpipe/workers/AbstractWorker.java deleted file mode 100644 index e67d5b119..000000000 --- a/app/src/main/java/org/schabi/newpipe/workers/AbstractWorker.java +++ /dev/null @@ -1,118 +0,0 @@ -package org.schabi.newpipe.workers; - -import android.content.Context; -import android.os.Handler; - -import org.schabi.newpipe.extractor.NewPipe; -import org.schabi.newpipe.extractor.StreamingService; -import org.schabi.newpipe.extractor.stream_info.StreamInfo; - -import java.io.InterruptedIOException; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * Common properties of Workers - * - * @author mauriciocolli - */ -@SuppressWarnings("WeakerAccess") -public abstract class AbstractWorker extends Thread { - - private final AtomicBoolean isRunning = new AtomicBoolean(false); - - private final int serviceId; - private Context context; - private Handler handler; - private StreamingService service; - - public AbstractWorker(Context context, int serviceId) { - this.context = context; - this.serviceId = serviceId; - this.handler = new Handler(context.getMainLooper()); - } - - @Override - public void run() { - try { - isRunning.set(true); - service = NewPipe.getService(serviceId); - doWork(serviceId); - } catch (Exception e) { - // Handle the exception only if thread is not interrupted - e.printStackTrace(); - if (!isInterrupted() && !(e instanceof InterruptedIOException) && !(e.getCause() instanceof InterruptedIOException)) { - handleException(e, serviceId); - } - } finally { - isRunning.set(false); - } - } - - /** - * Here is the place that the heavy work is realized - * - * @param serviceId serviceId that was passed when created this object - * - * @throws Exception these exceptions are handled by the {@link #handleException(Exception, int)} - */ - protected abstract void doWork(int serviceId) throws Exception; - - - /** - * Method that handle the exception thrown by the {@link #doWork(int)}. - * - * @param exception {@link Exception} that was thrown by {@link #doWork(int)} - */ - protected abstract void handleException(Exception exception, int serviceId); - - /** - * Return true if the extraction is not completed yet - * - * @return the value of the AtomicBoolean {@link #isRunning} - */ - public boolean isRunning() { - return isRunning.get(); - } - - /** - * Cancel this ExtractorWorker, calling {@link #onDestroy()} and interrupting this thread. - *

    - * Note: Any I/O that is active in the moment that this method is called will be canceled and a Exception will be thrown, because of the {@link #interrupt()}.
    - * This is useful when you don't want the resulting {@link StreamInfo} anymore, but don't want to waste bandwidth, otherwise it'd run till it receives the StreamInfo. - */ - public void cancel() { - onDestroy(); - this.interrupt(); - } - - /** - * Method that discards everything that doesn't need anymore.
    - * Subclasses can override this method to destroy their garbage. - */ - protected void onDestroy() { - this.isRunning.set(false); - this.context = null; - this.handler = null; - this.service = null; - } - - public Handler getHandler() { - return handler; - } - - public StreamingService getService() { - return service; - } - - public int getServiceId() { - return serviceId; - } - - public String getServiceName() { - return service == null ? "none" : service.getServiceInfo().name; - } - - public Context getContext() { - return context; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/workers/ChannelExtractorWorker.java b/app/src/main/java/org/schabi/newpipe/workers/ChannelExtractorWorker.java deleted file mode 100644 index dec2f3fcd..000000000 --- a/app/src/main/java/org/schabi/newpipe/workers/ChannelExtractorWorker.java +++ /dev/null @@ -1,116 +0,0 @@ -package org.schabi.newpipe.workers; - -import android.content.Context; - -import org.schabi.newpipe.MainActivity; -import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.channel.ChannelExtractor; -import org.schabi.newpipe.extractor.channel.ChannelInfo; -import org.schabi.newpipe.extractor.exceptions.ExtractionException; -import org.schabi.newpipe.extractor.exceptions.ParsingException; -import org.schabi.newpipe.report.ErrorActivity; - -import java.io.IOException; - -import static org.schabi.newpipe.report.UserAction.REQUESTED_CHANNEL; - -/** - * Extract {@link ChannelInfo} with {@link ChannelExtractor} from the given url of the given service - * - * @author mauriciocolli - */ -@SuppressWarnings("WeakerAccess") -public class ChannelExtractorWorker extends ExtractorWorker { - //private static final String TAG = "ChannelExtractorWorker"; - - private int pageNumber; - private boolean onlyVideos; - - private ChannelInfo channelInfo = null; - private OnChannelInfoReceive callback; - - /** - * Interface which will be called for result and errors - */ - public interface OnChannelInfoReceive { - void onReceive(ChannelInfo info, boolean onlyVideos); - void onError(int messageId); - /** - * Called when an unrecoverable error has occurred. - *

    This is a good place to finish the caller.

    - */ - void onUnrecoverableError(Exception exception); - } - - /** - * @param context context for error reporting purposes - * @param serviceId id of the request service - * @param channelUrl channelUrl of the service (e.g. https://www.youtube.com/channel/UC_aEa8K-EOJ3D6gOs7HcyNg) - * @param pageNumber which page to extract - * @param onlyVideos flag that will be send by {@link OnChannelInfoReceive#onReceive(ChannelInfo, boolean)} - * @param callback listener that will be called-back when events occur (check {@link ChannelExtractorWorker.OnChannelInfoReceive}) - */ - public ChannelExtractorWorker(Context context, int serviceId, String channelUrl, int pageNumber, boolean onlyVideos, OnChannelInfoReceive callback) { - super(context, channelUrl, serviceId); - this.pageNumber = pageNumber; - this.callback = callback; - this.onlyVideos = onlyVideos; - } - - @Override - protected void onDestroy() { - super.onDestroy(); - this.callback = null; - this.channelInfo = null; - } - - @Override - protected void doWork(int serviceId, String url) throws Exception { - ChannelExtractor extractor = getService().getChannelExtractorInstance(url, pageNumber); - channelInfo = ChannelInfo.getInfo(extractor); - - if (!channelInfo.errors.isEmpty()) handleErrorsDuringExtraction(channelInfo.errors, REQUESTED_CHANNEL); - - if (callback != null && channelInfo != null && !isInterrupted()) getHandler().post(new Runnable() { - @Override - public void run() { - if (isInterrupted() || callback == null) return; - - callback.onReceive(channelInfo, onlyVideos); - onDestroy(); - } - }); - } - - - @Override - protected void handleException(final Exception exception, int serviceId, String url) { - if (callback == null || getHandler() == null || isInterrupted()) return; - - if (exception instanceof IOException) { - getHandler().post(new Runnable() { - @Override - public void run() { - callback.onError(R.string.network_error); - } - }); - } else if (exception instanceof ParsingException || exception instanceof ExtractionException) { - ErrorActivity.reportError(getHandler(), getContext(), exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(REQUESTED_CHANNEL, getServiceName(), url, R.string.parsing_error)); - getHandler().post(new Runnable() { - @Override - public void run() { - callback.onUnrecoverableError(exception); - } - }); - } else { - ErrorActivity.reportError(getHandler(), getContext(), exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(REQUESTED_CHANNEL, getServiceName(), url, R.string.general_error)); - getHandler().post(new Runnable() { - @Override - public void run() { - callback.onUnrecoverableError(exception); - } - }); - } - } -} - diff --git a/app/src/main/java/org/schabi/newpipe/workers/ExtractorWorker.java b/app/src/main/java/org/schabi/newpipe/workers/ExtractorWorker.java deleted file mode 100644 index 40ce249a4..000000000 --- a/app/src/main/java/org/schabi/newpipe/workers/ExtractorWorker.java +++ /dev/null @@ -1,89 +0,0 @@ -package org.schabi.newpipe.workers; - -import android.app.Activity; -import android.content.Context; -import android.util.Log; -import android.view.View; - -import org.schabi.newpipe.report.ErrorActivity; -import org.schabi.newpipe.report.UserAction; - -import java.util.List; - -/** - * Common properties of ExtractorWorkers - * - * @author mauriciocolli - */ -@SuppressWarnings("WeakerAccess") -public abstract class ExtractorWorker extends AbstractWorker { - private final String url; - - public ExtractorWorker(Context context, String url, int serviceId) { - super(context, serviceId); - this.url = url; - if (url.length() >= 40) setName("Thread-" + url.substring(url.length() - 11, url.length())); - } - - @Override - protected void doWork(int serviceId) throws Exception { - doWork(serviceId, url); - } - - /** - * Here is the place that the heavy work is realized - * - * @param serviceId serviceId that was passed when created this object - * @param url url that was passed when created this object - * - * @throws Exception these exceptions are handled by the {@link #handleException(Exception, int, String)} - */ - protected abstract void doWork(int serviceId, String url) throws Exception; - - @Override - protected void handleException(Exception exception, int serviceId) { - handleException(exception, serviceId, url); - } - - /** - * Method that handle the exception thrown by the {@link #doWork(int, String)}. - * - * @param exception {@link Exception} that was thrown by {@link #doWork(int, String)} - */ - protected abstract void handleException(Exception exception, int serviceId, String url); - - /** - * Handle the errors during extraction and shows a Report button to the user.
    - * Subclasses maybe call this method. - * - * @param errorsList list of exceptions that happened during extraction - * @param errorUserAction what action was the user performing during the error. - * (One of the {@link ErrorActivity}.REQUEST_* error (message) ids) - */ - protected void handleErrorsDuringExtraction(List errorsList, UserAction errorUserAction){ - String errorString = ""; - switch (errorUserAction) { - case REQUESTED_STREAM: - errorString= errorUserAction.getMessage(); - break; - case REQUESTED_CHANNEL: - errorString= errorUserAction.getMessage(); - break; - } - - Log.e(errorString, "OCCURRED ERRORS DURING EXTRACTION:"); - for (Throwable e : errorsList) { - e.printStackTrace(); - Log.e(errorString, "------"); - } - - if (getContext() instanceof Activity) { - View rootView = getContext() instanceof Activity ? ((Activity) getContext()).findViewById(android.R.id.content) : null; - ErrorActivity.reportError(getHandler(), getContext(), errorsList, null, rootView, ErrorActivity.ErrorInfo.make(errorUserAction, getServiceName(), url, 0 /* no message for the user */)); - } - } - - public String getUrl() { - return url; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/workers/SearchWorker.java b/app/src/main/java/org/schabi/newpipe/workers/SearchWorker.java deleted file mode 100644 index dc62698ea..000000000 --- a/app/src/main/java/org/schabi/newpipe/workers/SearchWorker.java +++ /dev/null @@ -1,130 +0,0 @@ -package org.schabi.newpipe.workers; - -import android.app.Activity; -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; -import android.support.annotation.NonNull; -import android.view.View; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.exceptions.ExtractionException; -import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; -import org.schabi.newpipe.extractor.search.SearchEngine; -import org.schabi.newpipe.extractor.search.SearchResult; -import org.schabi.newpipe.report.ErrorActivity; -import org.schabi.newpipe.report.UserAction; - -import java.io.IOException; -import java.util.EnumSet; - -import static org.schabi.newpipe.report.UserAction.*; - -/** - * Return list of results based on a query - * - * @author mauriciocolli - */ -public class SearchWorker extends AbstractWorker { - - private EnumSet filter; - private String query; - private int page; - private OnSearchResult callback; - - /** - * Interface which will be called for result and errors - */ - public interface OnSearchResult { - void onSearchResult(SearchResult result); - void onNothingFound(String message); - void onSearchError(int messageId); - void onReCaptchaChallenge(); - } - - public SearchWorker(Context context, int serviceId, String query, int page, EnumSet filter, OnSearchResult callback) { - super(context, serviceId); - this.callback = callback; - this.query = query; - this.page = page; - this.filter = filter; - } - - public static SearchWorker startForQuery(Context context, int serviceId, @NonNull String query, int page, EnumSet filter, OnSearchResult callback) { - SearchWorker worker = new SearchWorker(context, serviceId, query, page, filter, callback); - worker.start(); - return worker; - } - - @Override - protected void onDestroy() { - super.onDestroy(); - this.callback = null; - } - - @Override - protected void doWork(int serviceId) throws Exception { - SearchEngine searchEngine = getService().getSearchEngineInstance(); - - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); - String searchLanguageKey = getContext().getString(R.string.search_language_key); - String searchLanguage = sharedPreferences.getString(searchLanguageKey, getContext().getString(R.string.default_language_value)); - - final SearchResult searchResult = SearchResult.getSearchResult(searchEngine, query, page, searchLanguage, filter); - if (callback != null && searchResult != null && !isInterrupted()) getHandler().post(new Runnable() { - @Override - public void run() { - if (isInterrupted() || callback == null) return; - - callback.onSearchResult(searchResult); - onDestroy(); - } - }); - } - - @Override - protected void handleException(final Exception exception, int serviceId) { - if (callback == null || getHandler() == null || isInterrupted()) return; - - if (exception instanceof ReCaptchaException) { - getHandler().post(new Runnable() { - @Override - public void run() { - callback.onReCaptchaChallenge(); - } - }); - } else if (exception instanceof IOException) { - getHandler().post(new Runnable() { - @Override - public void run() { - callback.onSearchError(R.string.network_error); - } - }); - } else if (exception instanceof SearchEngine.NothingFoundException) { - getHandler().post(new Runnable() { - @Override - public void run() { - callback.onNothingFound(exception.getMessage()); - } - }); - } else if (exception instanceof ExtractionException) { - View rootView = getContext() instanceof Activity ? ((Activity) getContext()).findViewById(android.R.id.content) : null; - ErrorActivity.reportError(getHandler(), getContext(), exception, null, rootView, ErrorActivity.ErrorInfo.make(SEARCHED, getServiceName(), query, R.string.parsing_error)); - getHandler().post(new Runnable() { - @Override - public void run() { - callback.onSearchError(R.string.parsing_error); - } - }); - } else { - View rootView = getContext() instanceof Activity ? ((Activity) getContext()).findViewById(android.R.id.content) : null; - ErrorActivity.reportError(getHandler(), getContext(), exception, null, rootView, ErrorActivity.ErrorInfo.make(SEARCHED, getServiceName(), query, R.string.general_error)); - getHandler().post(new Runnable() { - @Override - public void run() { - callback.onSearchError(R.string.general_error); - } - }); - } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/workers/StreamExtractorWorker.java b/app/src/main/java/org/schabi/newpipe/workers/StreamExtractorWorker.java deleted file mode 100644 index af8e156b0..000000000 --- a/app/src/main/java/org/schabi/newpipe/workers/StreamExtractorWorker.java +++ /dev/null @@ -1,168 +0,0 @@ -package org.schabi.newpipe.workers; - -import android.content.Context; - -import org.schabi.newpipe.MainActivity; -import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.exceptions.ParsingException; -import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; -import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor; -import org.schabi.newpipe.extractor.stream_info.StreamExtractor; -import org.schabi.newpipe.extractor.stream_info.StreamInfo; -import org.schabi.newpipe.report.ErrorActivity; -import org.schabi.newpipe.report.UserAction; - -import java.io.IOException; - -import static org.schabi.newpipe.report.UserAction.*; - -/** - * Extract {@link StreamInfo} with {@link StreamExtractor} from the given url of the given service - * - * @author mauriciocolli - */ -@SuppressWarnings("WeakerAccess") -public class StreamExtractorWorker extends ExtractorWorker { - //private static final String TAG = "StreamExtractorWorker"; - - private StreamInfo streamInfo = null; - private OnStreamInfoReceivedListener callback; - - /** - * Interface which will be called for result and errors - */ - public interface OnStreamInfoReceivedListener { - void onReceive(StreamInfo info); - void onError(int messageId); - void onReCaptchaException(); - void onBlockedByGemaError(); - void onContentErrorWithMessage(int messageId); - void onContentError(); - - /** - * Called when an unrecoverable error has occurred. - *

    This is a good place to finish the caller.

    - */ - void onUnrecoverableError(Exception exception); - } - - /** - * @param context context for error reporting purposes - * @param serviceId id of the request service - * @param videoUrl videoUrl of the service (e.g. https://www.youtube.com/watch?v=HyHNuVaZJ-k) - * @param callback listener that will be called-back when events occur (check {@link StreamExtractorWorker.OnStreamInfoReceivedListener}) - */ - public StreamExtractorWorker(Context context, int serviceId, String videoUrl, OnStreamInfoReceivedListener callback) { - super(context, videoUrl, serviceId); - this.callback = callback; - } - - @Override - protected void onDestroy() { - super.onDestroy(); - this.callback = null; - this.streamInfo = null; - } - - @Override - protected void doWork(int serviceId, String url) throws Exception { - StreamExtractor streamExtractor = getService().getExtractorInstance(url); - streamInfo = StreamInfo.getVideoInfo(streamExtractor); - - if (streamInfo != null && !streamInfo.errors.isEmpty()) handleErrorsDuringExtraction(streamInfo.errors, REQUESTED_STREAM); - - if (callback != null && getHandler() != null && streamInfo != null && !isInterrupted()) getHandler().post(new Runnable() { - @Override - public void run() { - if (isInterrupted() || callback == null) return; - - callback.onReceive(streamInfo); - onDestroy(); - } - }); - - } - - @Override - protected void handleException(final Exception exception, int serviceId, final String url) { - if (callback == null || getHandler() == null || isInterrupted()) return; - - if (exception instanceof ReCaptchaException) { - getHandler().post(new Runnable() { - @Override - public void run() { - callback.onReCaptchaException(); - } - }); - } else if (exception instanceof IOException) { - getHandler().post(new Runnable() { - @Override - public void run() { - callback.onError(R.string.network_error); - } - }); - } else if (exception instanceof YoutubeStreamExtractor.GemaException) { - getHandler().post(new Runnable() { - @Override - public void run() { - callback.onBlockedByGemaError(); - } - }); - } else if (exception instanceof YoutubeStreamExtractor.LiveStreamException) { - getHandler().post(new Runnable() { - @Override - public void run() { - callback.onContentErrorWithMessage(R.string.live_streams_not_supported); - } - }); - } else if (exception instanceof StreamExtractor.ContentNotAvailableException) { - getHandler().post(new Runnable() { - @Override - public void run() { - callback.onContentError(); - } - }); - } else if (exception instanceof YoutubeStreamExtractor.DecryptException) { - // custom service related exceptions - ErrorActivity.reportError(getHandler(), getContext(), exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(REQUESTED_STREAM, getServiceName(), url, R.string.youtube_signature_decryption_error)); - getHandler().post(new Runnable() { - @Override - public void run() { - callback.onUnrecoverableError(exception); - } - }); - } else if (exception instanceof StreamInfo.StreamExctractException) { - if (!streamInfo.errors.isEmpty()) { - // !!! if this case ever kicks in someone gets kicked out !!! - ErrorActivity.reportError(getHandler(), getContext(), exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(REQUESTED_STREAM, getServiceName(), url, R.string.could_not_get_stream)); - } else { - ErrorActivity.reportError(getHandler(), getContext(), streamInfo.errors, MainActivity.class, null, ErrorActivity.ErrorInfo.make(REQUESTED_STREAM, getServiceName(), url, R.string.could_not_get_stream)); - } - - getHandler().post(new Runnable() { - @Override - public void run() { - callback.onUnrecoverableError(exception); - } - }); - } else if (exception instanceof ParsingException) { - ErrorActivity.reportError(getHandler(), getContext(), exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(REQUESTED_STREAM, getServiceName(), url, R.string.parsing_error)); - getHandler().post(new Runnable() { - @Override - public void run() { - callback.onUnrecoverableError(exception); - } - }); - } else { - ErrorActivity.reportError(getHandler(), getContext(), exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(REQUESTED_STREAM, getServiceName(), url, R.string.general_error)); - getHandler().post(new Runnable() { - @Override - public void run() { - callback.onUnrecoverableError(exception); - } - }); - } - - } - -} diff --git a/app/src/main/java/org/schabi/newpipe/workers/SuggestionWorker.java b/app/src/main/java/org/schabi/newpipe/workers/SuggestionWorker.java deleted file mode 100644 index 66caad26c..000000000 --- a/app/src/main/java/org/schabi/newpipe/workers/SuggestionWorker.java +++ /dev/null @@ -1,111 +0,0 @@ -package org.schabi.newpipe.workers; - -import android.app.Activity; -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; -import android.support.annotation.NonNull; -import android.view.View; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.SuggestionExtractor; -import org.schabi.newpipe.extractor.exceptions.ExtractionException; -import org.schabi.newpipe.report.ErrorActivity; -import org.schabi.newpipe.report.UserAction; - -import java.io.IOException; -import java.util.List; - -import static org.schabi.newpipe.report.UserAction.*; - -/** - * Worker that get suggestions based on the query - * - * @author mauriciocolli - */ -public class SuggestionWorker extends AbstractWorker { - - private String query; - private OnSuggestionResult callback; - - /** - * Interface which will be called for result and errors - */ - public interface OnSuggestionResult { - void onSuggestionResult(@NonNull List suggestions); - void onSuggestionError(int messageId); - } - - public SuggestionWorker(Context context, int serviceId, String query, OnSuggestionResult callback) { - super(context, serviceId); - this.callback = callback; - this.query = query; - } - - public static SuggestionWorker startForQuery(Context context, int serviceId, @NonNull String query, OnSuggestionResult callback) { - SuggestionWorker worker = new SuggestionWorker(context, serviceId, query, callback); - worker.start(); - return worker; - } - - @Override - protected void onDestroy() { - super.onDestroy(); - this.callback = null; - this.query = null; - } - - @Override - protected void doWork(int serviceId) throws Exception { - SuggestionExtractor suggestionExtractor = getService().getSuggestionExtractorInstance(); - - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); - String searchLanguageKey = getContext().getString(R.string.search_language_key); - String searchLanguage = sharedPreferences.getString(searchLanguageKey, getContext().getString(R.string.default_language_value)); - - final List suggestions = suggestionExtractor.suggestionList(query, searchLanguage); - - if (callback != null && suggestions != null && !isInterrupted()) getHandler().post(new Runnable() { - @Override - public void run() { - if (isInterrupted() || callback == null) return; - - callback.onSuggestionResult(suggestions); - onDestroy(); - } - }); - } - - @Override - protected void handleException(final Exception exception, int serviceId) { - if (callback == null || getHandler() == null || isInterrupted()) return; - - if (exception instanceof ExtractionException) { - View rootView = getContext() instanceof Activity ? ((Activity) getContext()).findViewById(android.R.id.content) : null; - ErrorActivity.reportError(getHandler(), getContext(), exception, null, rootView, ErrorActivity.ErrorInfo.make(GET_SUGGESTIONS, getServiceName(), query, R.string.parsing_error)); - getHandler().post(new Runnable() { - @Override - public void run() { - callback.onSuggestionError(R.string.parsing_error); - } - }); - } else if (exception instanceof IOException) { - getHandler().post(new Runnable() { - @Override - public void run() { - callback.onSuggestionError(R.string.network_error); - } - }); - } else { - View rootView = getContext() instanceof Activity ? ((Activity) getContext()).findViewById(android.R.id.content) : null; - ErrorActivity.reportError(getHandler(), getContext(), exception, null, rootView, ErrorActivity.ErrorInfo.make(GET_SUGGESTIONS, getServiceName(), query, R.string.general_error)); - getHandler().post(new Runnable() { - @Override - public void run() { - callback.onSuggestionError(R.string.general_error); - } - }); - } - - } -} diff --git a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java index 5b394de11..de92bbdf8 100644 --- a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java +++ b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java @@ -305,12 +305,12 @@ public class MissionAdapter extends RecyclerView.Adapter - \ No newline at end of file diff --git a/app/src/main/res/anim/custom_fade_out.xml b/app/src/main/res/anim/custom_fade_out.xml deleted file mode 100644 index 03a6bc9bb..000000000 --- a/app/src/main/res/anim/custom_fade_out.xml +++ /dev/null @@ -1,6 +0,0 @@ - - \ No newline at end of file diff --git a/app/src/main/res/animator/custom_fade_in.xml b/app/src/main/res/animator/custom_fade_in.xml new file mode 100644 index 000000000..fa7f516c2 --- /dev/null +++ b/app/src/main/res/animator/custom_fade_in.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/animator/custom_fade_out.xml b/app/src/main/res/animator/custom_fade_out.xml new file mode 100644 index 000000000..db3662647 --- /dev/null +++ b/app/src/main/res/animator/custom_fade_out.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/ic_history_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_history_black_24dp.png new file mode 100644 index 000000000..9abddaa50 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_history_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_history_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_history_white_24dp.png new file mode 100644 index 000000000..485c826fd Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_history_white_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_language_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_language_black_24dp.png new file mode 100644 index 000000000..36125569b Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_language_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_language_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_language_white_24dp.png new file mode 100644 index 000000000..b7c8248fb Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_language_white_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_palette_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_palette_black_24dp.png new file mode 100644 index 000000000..8362e21a0 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_palette_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_palette_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_palette_white_24dp.png new file mode 100644 index 000000000..9470e79e1 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_palette_white_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_playlist_play_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_playlist_play_white_24dp.png new file mode 100644 index 000000000..34dc90f11 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_playlist_play_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_history_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_history_black_24dp.png new file mode 100644 index 000000000..9fade8bb5 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_history_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_history_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_history_white_24dp.png new file mode 100644 index 000000000..d67647c56 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_history_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_language_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_language_black_24dp.png new file mode 100644 index 000000000..62ef88c67 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_language_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_language_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_language_white_24dp.png new file mode 100644 index 000000000..0bc7dfd48 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_language_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_palette_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_palette_black_24dp.png new file mode 100644 index 000000000..f7491eb79 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_palette_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_palette_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_palette_white_24dp.png new file mode 100644 index 000000000..6ce59523d Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_palette_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_playlist_play_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_playlist_play_white_24dp.png new file mode 100644 index 000000000..1be3cdbd6 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_playlist_play_white_24dp.png differ diff --git a/app/src/main/res/drawable-nodpi/dummi_thumbnail_playlist.png b/app/src/main/res/drawable-nodpi/dummy_thumbnail_playlist.png similarity index 100% rename from app/src/main/res/drawable-nodpi/dummi_thumbnail_playlist.png rename to app/src/main/res/drawable-nodpi/dummy_thumbnail_playlist.png diff --git a/app/src/main/res/drawable-v21/splash_screen.xml b/app/src/main/res/drawable-v21/splash_screen.xml deleted file mode 100644 index 34a890727..000000000 --- a/app/src/main/res/drawable-v21/splash_screen.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable-xhdpi/ic_history_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_history_black_24dp.png new file mode 100644 index 000000000..f6096cab3 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_history_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_history_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_history_white_24dp.png new file mode 100644 index 000000000..3e73b49ee Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_history_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_language_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_language_black_24dp.png new file mode 100644 index 000000000..5ed4a9252 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_language_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_language_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_language_white_24dp.png new file mode 100644 index 000000000..eeaab46c0 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_language_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_palette_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_palette_black_24dp.png new file mode 100644 index 000000000..ce3a94c3c Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_palette_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_palette_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_palette_white_24dp.png new file mode 100644 index 000000000..4af10a4d0 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_palette_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_playlist_play_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_playlist_play_white_24dp.png new file mode 100644 index 000000000..e7d2159c5 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_playlist_play_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_history_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_history_black_24dp.png new file mode 100644 index 000000000..f837fda0e Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_history_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_history_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_history_white_24dp.png new file mode 100644 index 000000000..1358a129c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_history_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_language_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_language_black_24dp.png new file mode 100644 index 000000000..68608c70c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_language_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_language_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_language_white_24dp.png new file mode 100644 index 000000000..d4b55183c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_language_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_palette_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_palette_black_24dp.png new file mode 100644 index 000000000..d93fe2e0c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_palette_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_palette_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_palette_white_24dp.png new file mode 100644 index 000000000..119b1fd8c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_palette_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_playlist_play_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_playlist_play_white_24dp.png new file mode 100644 index 000000000..7f3b00168 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_playlist_play_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_history_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_history_black_24dp.png new file mode 100644 index 000000000..c7153092e Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_history_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_history_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_history_white_24dp.png new file mode 100644 index 000000000..5b99ef655 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_history_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_language_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_language_black_24dp.png new file mode 100644 index 000000000..48997bab8 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_language_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_language_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_language_white_24dp.png new file mode 100644 index 000000000..a576ac7a5 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_language_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_palette_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_palette_black_24dp.png new file mode 100644 index 000000000..79360b16d Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_palette_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_palette_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_palette_white_24dp.png new file mode 100644 index 000000000..dba40d4eb Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_palette_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_playlist_play_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_playlist_play_white_24dp.png new file mode 100644 index 000000000..c88d9c8e3 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_playlist_play_white_24dp.png differ diff --git a/app/src/main/res/drawable/player_controls_bg.xml b/app/src/main/res/drawable/player_controls_bg.xml index 9c9468112..7e1981347 100644 --- a/app/src/main/res/drawable/player_controls_bg.xml +++ b/app/src/main/res/drawable/player_controls_bg.xml @@ -1,8 +1,7 @@ + android:endColor="#00000000" + android:startColor="#8c000000"/> \ No newline at end of file diff --git a/app/src/main/res/drawable/player_top_controls_bg.xml b/app/src/main/res/drawable/player_top_controls_bg.xml index b7cdecc87..f1e8b98fc 100644 --- a/app/src/main/res/drawable/player_top_controls_bg.xml +++ b/app/src/main/res/drawable/player_top_controls_bg.xml @@ -1,8 +1,7 @@ + android:endColor="#00000000" + android:startColor="#8c000000"/> \ No newline at end of file diff --git a/app/src/main/res/drawable/splash_screen.xml b/app/src/main/res/drawable/splash_screen.xml deleted file mode 100644 index af9458579..000000000 --- a/app/src/main/res/drawable/splash_screen.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/toolbar_shadow.xml b/app/src/main/res/drawable/toolbar_shadow.xml deleted file mode 100644 index f924df533..000000000 --- a/app/src/main/res/drawable/toolbar_shadow.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/toolbar_shadow_dark.xml b/app/src/main/res/drawable/toolbar_shadow_dark.xml new file mode 100644 index 000000000..d5ebfc8fd --- /dev/null +++ b/app/src/main/res/drawable/toolbar_shadow_dark.xml @@ -0,0 +1,7 @@ + + + + diff --git a/app/src/main/res/drawable/toolbar_shadow_light.xml b/app/src/main/res/drawable/toolbar_shadow_light.xml new file mode 100644 index 000000000..7b800786c --- /dev/null +++ b/app/src/main/res/drawable/toolbar_shadow_light.xml @@ -0,0 +1,7 @@ + + + + diff --git a/app/src/main/res/drawable/white_circle.xml b/app/src/main/res/drawable/white_circle.xml deleted file mode 100644 index 312f9c37c..000000000 --- a/app/src/main/res/drawable/white_circle.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/layout-land/channel_header.xml b/app/src/main/res/layout-land/channel_header.xml deleted file mode 100644 index 1ccab7693..000000000 --- a/app/src/main/res/layout-land/channel_header.xml +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - - - - - - - - - - - -