mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-10-31 15:23:00 +00:00 
			
		
		
		
	Update extractor and refactored NewPipe
This commit is contained in:
		| @@ -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' | ||||
| } | ||||
|   | ||||
| @@ -1,17 +1,17 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <manifest | ||||
|     package="org.schabi.newpipe" | ||||
|     xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:tools="http://schemas.android.com/tools"> | ||||
|  | ||||
|     <uses-permission android:name="android.permission.INTERNET"/> | ||||
|     <uses-permission android:name="android.permission.WAKE_LOCK"/> | ||||
|     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> | ||||
|     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> | ||||
|     <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/> | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     package="org.schabi.newpipe"> | ||||
|  | ||||
|     <application | ||||
|         tools:replace="android:name" | ||||
|         android:name=".DebugApp"/> | ||||
|         android:name=".DebugApp" | ||||
|         android:label="NewPipe Debug" | ||||
|         tools:replace="android:name, android:label"> | ||||
|         <activity | ||||
|             android:name=".MainActivity" | ||||
|             android:label="NewPipe Debug" | ||||
|             tools:replace="android:label"/> | ||||
|     </application> | ||||
|  | ||||
| </manifest> | ||||
| @@ -5,24 +5,6 @@ import android.support.multidex.MultiDex; | ||||
|  | ||||
| import com.facebook.stetho.Stetho; | ||||
|  | ||||
| /** | ||||
|  * Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org> | ||||
|  * 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 <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| public class DebugApp extends App { | ||||
|     private static final String TAG = DebugApp.class.toString(); | ||||
|  | ||||
|   | ||||
| @@ -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"> | ||||
|         <activity | ||||
|             android:name=".MainActivity" | ||||
| @@ -29,7 +29,7 @@ | ||||
|         </activity> | ||||
|  | ||||
|         <activity | ||||
|             android:name=".player.PlayVideoActivity" | ||||
|             android:name=".player.old.PlayVideoActivity" | ||||
|             android:configChanges="orientation|keyboardHidden|screenSize" | ||||
|             android:theme="@style/VideoPlayerTheme" | ||||
|             tools:ignore="UnusedAttribute"/> | ||||
| @@ -52,6 +52,15 @@ | ||||
|         <activity | ||||
|             android:name=".settings.SettingsActivity" | ||||
|             android:label="@string/settings"/> | ||||
|  | ||||
|         <activity | ||||
|             android:name=".about.AboutActivity" | ||||
|             android:label="@string/title_activity_about"/> | ||||
|  | ||||
|         <activity | ||||
|             android:name=".history.HistoryActivity" | ||||
|             android:label="@string/title_activity_history"/> | ||||
|  | ||||
|         <activity | ||||
|             android:name=".PanicResponderActivity" | ||||
|             android:launchMode="singleInstance" | ||||
| @@ -63,6 +72,7 @@ | ||||
|                 <category android:name="android.intent.category.DEFAULT"/> | ||||
|             </intent-filter> | ||||
|         </activity> | ||||
|  | ||||
|         <activity | ||||
|             android:name=".ExitActivity" | ||||
|             android:label="@string/general_error" | ||||
| @@ -73,8 +83,7 @@ | ||||
|         <activity | ||||
|             android:name=".download.DownloadActivity" | ||||
|             android:label="@string/app_name" | ||||
|             android:launchMode="singleTask" | ||||
|             android:theme="@style/AppTheme"/> | ||||
|             android:launchMode="singleTask"/> | ||||
|  | ||||
|         <service android:name="us.shandian.giga.service.DownloadManagerService"/> | ||||
|  | ||||
| @@ -83,6 +92,7 @@ | ||||
|             android:label="@string/app_name" | ||||
|             android:launchMode="singleTop" | ||||
|             android:theme="@style/FilePickerTheme"/> | ||||
|  | ||||
|         <activity | ||||
|             android:name=".ReCaptchaActivity" | ||||
|             android:label="@string/reCaptchaActivity"/> | ||||
| @@ -122,6 +132,8 @@ | ||||
|                 <!-- channel prefix --> | ||||
|                 <data android:pathPrefix="/channel/"/> | ||||
|                 <data android:pathPrefix="/user/"/> | ||||
|                 <!-- playlist prefix --> | ||||
|                 <data android:pathPrefix="/playlist"/> | ||||
|             </intent-filter> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.VIEW"/> | ||||
| @@ -155,12 +167,11 @@ | ||||
|                 <data android:mimeType="text/plain"/> | ||||
|             </intent-filter> | ||||
|         </activity> | ||||
|  | ||||
|         <activity | ||||
|             android:name=".RouterPopupActivity" | ||||
|             android:label="@string/popup_mode_share_menu_title" | ||||
|             android:taskAffinity="" | ||||
|             android:theme="@android:style/Theme.NoDisplay" | ||||
|             android:label="@string/popup_mode_share_menu_title"> | ||||
|             android:theme="@android:style/Theme.NoDisplay"> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.VIEW"/> | ||||
|                 <action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/> | ||||
| @@ -210,14 +221,5 @@ | ||||
|                 <data android:mimeType="text/plain"/> | ||||
|             </intent-filter> | ||||
|         </activity> | ||||
|         <activity | ||||
|             android:name=".about.AboutActivity" | ||||
|             android:label="@string/title_activity_about" | ||||
|             android:theme="@style/AppTheme" /> | ||||
|         <activity | ||||
|             android:name=".history.HistoryActivity" | ||||
|             android:label="@string/title_activity_history" | ||||
|             android:theme="@style/AppTheme" /> | ||||
|     </application> | ||||
|  | ||||
| </manifest> | ||||
| </manifest> | ||||
| @@ -1,6 +1,6 @@ | ||||
| package org.schabi.newpipe; | ||||
|  | ||||
| /** | ||||
| /* | ||||
|  * Created by Christian Schabesberger on 24.12.15. | ||||
|  * | ||||
|  * Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org> | ||||
|   | ||||
| @@ -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 <hans@eds.org> | ||||
|  * 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<? extends ReportSenderFactory>[] reportSenderFactoryClasses = new Class[]{AcraReportSenderFactory.class}; | ||||
|  | ||||
|     final Class<? extends ReportSenderFactory>[] 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<Throwable>() { | ||||
|             @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; | ||||
|   | ||||
							
								
								
									
										121
									
								
								app/src/main/java/org/schabi/newpipe/BaseFragment.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								app/src/main/java/org/schabi/newpipe/BaseFragment.java
									
									
									
									
									
										Normal file
									
								
							| @@ -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(); | ||||
| } | ||||
| @@ -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 <chris.schabesberger@mailbox.org> | ||||
| @@ -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<String, String> 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<String, String> 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<String, List<String>> 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); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -7,7 +7,7 @@ import android.content.Intent; | ||||
| import android.os.Build; | ||||
| import android.os.Bundle; | ||||
|  | ||||
| /** | ||||
| /* | ||||
|  * Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org> | ||||
|  * ExitActivity.java is part of NewPipe. | ||||
|  * | ||||
|   | ||||
| @@ -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 <chris.schabesberger@mailbox.org> | ||||
|  * 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 <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| 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) {} | ||||
| } | ||||
| @@ -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<HistoryEntry> 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<HistoryEntry> createHistoryEntryConsumer() { | ||||
|         return new Consumer<HistoryEntry>() { | ||||
|             @Override | ||||
|             public void accept(HistoryEntry historyEntry) throws Exception { | ||||
|                 //noinspection unchecked | ||||
|                 HistoryDAO<HistoryEntry> historyDAO = (HistoryDAO<HistoryEntry>) | ||||
|                         (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<HistoryEntry> 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<HistoryEntry> getHistoryEntryConsumer() { | ||||
|         return new Consumer<HistoryEntry>() { | ||||
|             @Override | ||||
|             public void accept(HistoryEntry historyEntry) throws Exception { | ||||
|                 //noinspection unchecked | ||||
|                 HistoryDAO<HistoryEntry> historyDAO = (HistoryDAO<HistoryEntry>) | ||||
|                         (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); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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 <hans@eds.org> | ||||
|  * PanicResponderActivity.java is part of NewPipe. | ||||
|  * | ||||
|   | ||||
| @@ -16,7 +16,7 @@ import android.webkit.WebSettings; | ||||
| import android.webkit.WebView; | ||||
| import android.webkit.WebViewClient; | ||||
|  | ||||
| /** | ||||
| /* | ||||
|  * Created by beneth <bmauduit@beneth.fr> on 06.12.16. | ||||
|  * | ||||
|  * Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org> | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -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(); | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|   | ||||
| @@ -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(); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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(), | ||||
|   | ||||
| @@ -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"; | ||||
|  | ||||
|   | ||||
| @@ -27,7 +27,7 @@ public interface BasicDAO<Entity> { | ||||
|     long upsert(final Entity entity); | ||||
|  | ||||
|     /* Searches */ | ||||
|     Flowable<List<Entity>> findAll(); | ||||
|     Flowable<List<Entity>> getAll(); | ||||
|  | ||||
|     Flowable<List<Entity>> listByService(int serviceId); | ||||
|  | ||||
|   | ||||
| @@ -29,7 +29,7 @@ public interface SearchHistoryDAO extends HistoryDAO<SearchHistoryEntry> { | ||||
|  | ||||
|     @Query("SELECT * FROM " + TABLE_NAME + ORDER_BY_CREATION_DATE) | ||||
|     @Override | ||||
|     Flowable<List<SearchHistoryEntry>> findAll(); | ||||
|     Flowable<List<SearchHistoryEntry>> getAll(); | ||||
|  | ||||
|     @Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE) | ||||
|     @Override | ||||
|   | ||||
| @@ -29,7 +29,7 @@ public interface WatchHistoryDAO extends HistoryDAO<WatchHistoryEntry> { | ||||
|  | ||||
|     @Query("SELECT * FROM " + TABLE_NAME + ORDER_BY_CREATION_DATE) | ||||
|     @Override | ||||
|     Flowable<List<WatchHistoryEntry>> findAll(); | ||||
|     Flowable<List<WatchHistoryEntry>> getAll(); | ||||
|  | ||||
|     @Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE) | ||||
|     @Override | ||||
|   | ||||
| @@ -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; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -17,7 +17,7 @@ import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCR | ||||
| public interface SubscriptionDAO extends BasicDAO<SubscriptionEntity> { | ||||
|     @Override | ||||
|     @Query("SELECT * FROM " + SUBSCRIPTION_TABLE) | ||||
|     Flowable<List<SubscriptionEntity>> findAll(); | ||||
|     Flowable<List<SubscriptionEntity>> getAll(); | ||||
|  | ||||
|     @Override | ||||
|     @Query("DELETE FROM " + SUBSCRIPTION_TABLE) | ||||
| @@ -30,5 +30,5 @@ public interface SubscriptionDAO extends BasicDAO<SubscriptionEntity> { | ||||
|     @Query("SELECT * FROM " + SUBSCRIPTION_TABLE + " WHERE " + | ||||
|             SUBSCRIPTION_URL + " LIKE :url AND " + | ||||
|             SUBSCRIPTION_SERVICE_ID + " = :serviceId") | ||||
|     Flowable<List<SubscriptionEntity>> findAll(int serviceId, String url); | ||||
|     Flowable<List<SubscriptionEntity>> getSubscription(int serviceId, String url); | ||||
| } | ||||
|   | ||||
| @@ -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; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -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.<Toolbar>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<String> 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; | ||||
|   | ||||
 Submodule app/src/main/java/org/schabi/newpipe/extractor deleted from ab530381cf
									
								
							| @@ -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(); | ||||
| } | ||||
| @@ -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(); | ||||
|     } | ||||
| } | ||||
| @@ -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<I> extends BaseFragment implements ViewContract<I> { | ||||
|  | ||||
|     @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<Object>() { | ||||
|                     @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<Throwable> 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<Throwable> 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)); | ||||
|     } | ||||
| } | ||||
| @@ -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() { | ||||
|  | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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<InfoItem> 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<List<SubscriptionEntity>> consumer = new Consumer<List<SubscriptionEntity>>() { | ||||
|             @Override | ||||
|             public void accept(@NonNull List<SubscriptionEntity> 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<Throwable> onError = new Consumer<Throwable>() { | ||||
|             @Override | ||||
|             public void accept(@NonNull Throwable exception) throws Exception { | ||||
|                 onRxError(exception, "Subscription Database Reactor"); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         if (subscriptionObserver != null) subscriptionObserver.dispose(); | ||||
|         subscriptionObserver = subscriptionService.getSubscription() | ||||
|                 .onErrorReturnItem(Collections.<SubscriptionEntity>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<SubscriptionEntity> getSubscriptionObserver() { | ||||
|         return new Subscriber<SubscriptionEntity>() { | ||||
|             @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<ChannelInfo> getChannelInfoObserver() { | ||||
|         return new MaybeObserver<ChannelInfo>() { | ||||
|             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<InfoItem> 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<Object> onNext = new Consumer<Object>() { | ||||
|             @Override | ||||
|             public void accept(Object o) throws Exception { | ||||
|                 requestFeed(FEED_LOAD_SIZE); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         final Consumer<Throwable> onError = new Consumer<Throwable>() { | ||||
|             @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(); | ||||
|     } | ||||
| } | ||||
| @@ -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: | ||||
|   | ||||
| @@ -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); | ||||
| } | ||||
| @@ -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<List<SubscriptionEntity>> getSubscriptionObserver() { | ||||
|         return new Observer<List<SubscriptionEntity>>() { | ||||
|             @Override | ||||
|             public void onSubscribe(Disposable d) { | ||||
|                 animateView(loadingProgressBar, true, 200); | ||||
|  | ||||
|                 disposables.add( d ); | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public void onNext(List<SubscriptionEntity> 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<InfoItem> getSubscriptionItems(List<SubscriptionEntity> subscriptions) { | ||||
|         List<InfoItem> 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<InfoItem>() { | ||||
|             @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(); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,10 @@ | ||||
| package org.schabi.newpipe.fragments; | ||||
|  | ||||
| public interface ViewContract<I> { | ||||
|     void showLoading(); | ||||
|     void hideLoading(); | ||||
|     void showEmptyState(); | ||||
|     void showError(String message, boolean showRetryButton); | ||||
|  | ||||
|     void handleResult(I result); | ||||
| } | ||||
| @@ -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<InfoItem> serializable = (ArrayList<InfoItem>) 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<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription) { | ||||
|         return new Function<Object, Object>() { | ||||
|             @Override | ||||
|             public Object apply(@NonNull Object o) throws Exception { | ||||
|                 subscriptionService.subscriptionTable().insert( subscription ); | ||||
|                 return o; | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     private Function<Object, Object> mapOnUnsubscribe(final SubscriptionEntity subscription) { | ||||
|         return new Function<Object, Object>() { | ||||
|             @Override | ||||
|             public Object apply(@NonNull Object o) throws Exception { | ||||
|                 subscriptionService.subscriptionTable().delete( subscription ); | ||||
|                 return o; | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     private Observer<List<SubscriptionEntity>> getSubscribeButtonMonitor(final int serviceId, | ||||
|                                                                          final String channelUrl, | ||||
|                                                                          final ChannelInfo info) { | ||||
|         return new Observer<List<SubscriptionEntity>>() { | ||||
|             @Override | ||||
|             public void onSubscribe(Disposable d) { | ||||
|                 disposables.add( d ); | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public void onNext(List<SubscriptionEntity> 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<Object, Object> action) { | ||||
|         final Consumer<Object> onNext = new Consumer<Object>() { | ||||
|             @Override | ||||
|             public void accept(@NonNull Object o) throws Exception { | ||||
|                 if (DEBUG) Log.d(TAG, "Changed subscription status to this channel!"); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         final Consumer<Throwable> onError = new Consumer<Throwable>() { | ||||
|             @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<Throwable> onError = new Consumer<Throwable>() { | ||||
|             @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(); | ||||
|     } | ||||
| } | ||||
| @@ -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. | ||||
|  * <p> | ||||
|  * Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org> | ||||
| @@ -68,7 +68,7 @@ class ActionBarHandler { | ||||
|     public void setupStreamList(final List<VideoStream> 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)); | ||||
|   | ||||
| @@ -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); | ||||
|  | ||||
|   | ||||
| @@ -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(); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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<String, StreamInfo> 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<String> 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(); | ||||
|     } | ||||
|  | ||||
| } | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -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<I, N> extends BaseStateFragment<I> implements ListViewContract<I, N>, 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<Object> objectsToSave) { | ||||
|         objectsToSave.add(infoListAdapter.getItemsList()); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     @SuppressWarnings("unchecked") | ||||
|     public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception { | ||||
|         infoListAdapter.getItemsList().clear(); | ||||
|         infoListAdapter.getItemsList().addAll((List<InfoItem>) 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<StreamInfoItem>() { | ||||
|             @Override | ||||
|             public void selected(StreamInfoItem selectedItem) { | ||||
|                 onItemSelected(selectedItem); | ||||
|                 NavigationHelper.openVideoDetailFragment(getFragmentManager(), selectedItem.service_id, selectedItem.url, selectedItem.name); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         infoListAdapter.setOnChannelSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener<ChannelInfoItem>() { | ||||
|             @Override | ||||
|             public void selected(ChannelInfoItem selectedItem) { | ||||
|                 onItemSelected(selectedItem); | ||||
|                 NavigationHelper.openChannelFragment(getFragmentManager(), selectedItem.service_id, selectedItem.url, selectedItem.name); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         infoListAdapter.setOnPlaylistSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener<PlaylistInfoItem>() { | ||||
|             @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); | ||||
|     } | ||||
| } | ||||
| @@ -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<I extends ListInfo> extends BaseListFragment<I, ListExtractor.NextItemsResult> { | ||||
|  | ||||
|     @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<Object> objectsToSave) { | ||||
|         super.writeTo(objectsToSave); | ||||
|         objectsToSave.add(currentInfo); | ||||
|         objectsToSave.add(currentNextItemsUrl); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     @SuppressWarnings("unchecked") | ||||
|     public void readFrom(@NonNull Queue<Object> 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.<br/> | ||||
|      * 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<I> 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<I>() { | ||||
|                     @Override | ||||
|                     public void accept(@NonNull I result) throws Exception { | ||||
|                         isLoading.set(false); | ||||
|                         currentInfo = result; | ||||
|                         currentNextItemsUrl = result.next_streams_url; | ||||
|                         handleResult(result); | ||||
|                     } | ||||
|                 }, new Consumer<Throwable>() { | ||||
|                     @Override | ||||
|                     public void accept(@NonNull Throwable throwable) throws Exception { | ||||
|                         onError(throwable); | ||||
|                     } | ||||
|                 }); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Implement the logic to load more items<br/> | ||||
|      * You can use the default implementations from {@link org.schabi.newpipe.util.ExtractorHelper} | ||||
|      */ | ||||
|     protected abstract Single<ListExtractor.NextItemsResult> loadMoreItemsLogic(); | ||||
|  | ||||
|     protected void loadMoreItems() { | ||||
|         isLoading.set(true); | ||||
|  | ||||
|         if (currentWorker != null) currentWorker.dispose(); | ||||
|         currentWorker = loadMoreItemsLogic() | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe(new Consumer<ListExtractor.NextItemsResult>() { | ||||
|                     @Override | ||||
|                     public void accept(@io.reactivex.annotations.NonNull ListExtractor.NextItemsResult nextItemsResult) throws Exception { | ||||
|                         isLoading.set(false); | ||||
|                         handleNextItems(nextItemsResult); | ||||
|                     } | ||||
|                 }, new Consumer<Throwable>() { | ||||
|                     @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 : ""; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,9 @@ | ||||
| package org.schabi.newpipe.fragments.list; | ||||
|  | ||||
| import org.schabi.newpipe.fragments.ViewContract; | ||||
|  | ||||
| public interface ListViewContract<I, N> extends ViewContract<I> { | ||||
|     void showListFooter(boolean show); | ||||
|  | ||||
|     void handleNextItems(N result); | ||||
| } | ||||
| @@ -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<ChannelInfo> { | ||||
|  | ||||
|     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<Throwable> onError = new Consumer<Throwable>() { | ||||
|             @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<List<SubscriptionEntity>> 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<List<SubscriptionEntity>>() { | ||||
|                     @Override | ||||
|                     public void accept(List<SubscriptionEntity> subscriptionEntities) throws Exception { | ||||
|                         updateSubscribeButton(!subscriptionEntities.isEmpty()); | ||||
|                     } | ||||
|                 }, onError)); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription) { | ||||
|         return new Function<Object, Object>() { | ||||
|             @Override | ||||
|             public Object apply(@NonNull Object o) throws Exception { | ||||
|                 subscriptionService.subscriptionTable().insert(subscription); | ||||
|                 return o; | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     private Function<Object, Object> mapOnUnsubscribe(final SubscriptionEntity subscription) { | ||||
|         return new Function<Object, Object>() { | ||||
|             @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<Throwable> onError = new Consumer<Throwable>() { | ||||
|             @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<Object, Object> action) { | ||||
|         final Consumer<Object> onNext = new Consumer<Object>() { | ||||
|             @Override | ||||
|             public void accept(@NonNull Object o) throws Exception { | ||||
|                 if (DEBUG) Log.d(TAG, "Changed subscription status to this channel!"); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         final Consumer<Throwable> onError = new Consumer<Throwable>() { | ||||
|             @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<List<SubscriptionEntity>> getSubscribeUpdateMonitor(final ChannelInfo info) { | ||||
|         return new Consumer<List<SubscriptionEntity>>() { | ||||
|             @Override | ||||
|             public void accept(List<SubscriptionEntity> 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<ListExtractor.NextItemsResult> loadMoreItemsLogic() { | ||||
|         return ExtractorHelper.getMoreChannelItems(serviceId, currentNextItemsUrl); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected Single<ChannelInfo> 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); | ||||
|     } | ||||
| } | ||||
| @@ -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<List<SubscriptionEntity>, 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<String> 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<Object> objectsToSave) { | ||||
|         super.writeTo(objectsToSave); | ||||
|         objectsToSave.add(allItemsLoaded); | ||||
|         objectsToSave.add(itemsLoaded); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     @SuppressWarnings("unchecked") | ||||
|     public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception { | ||||
|         super.readFrom(savedObjects); | ||||
|         allItemsLoaded = (AtomicBoolean) savedObjects.poll(); | ||||
|         itemsLoaded = (HashSet<String>) 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.<SubscriptionEntity>emptyList()) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe(new Consumer<List<SubscriptionEntity>>() { | ||||
|                     @Override | ||||
|                     public void accept(List<SubscriptionEntity> subscriptionEntities) throws Exception { | ||||
|                         handleResult(subscriptionEntities); | ||||
|                     } | ||||
|                 }, new Consumer<Throwable>() { | ||||
|                     @Override | ||||
|                     public void accept(Throwable throwable) throws Exception { | ||||
|                         onError(throwable); | ||||
|                     } | ||||
|                 }); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void handleResult(@android.support.annotation.NonNull List<SubscriptionEntity> 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. | ||||
|      * <p> | ||||
|      * On initialization, it automatically requests the amount of feed needed to display | ||||
|      * a minimum amount required (FEED_LOAD_SIZE). | ||||
|      * <p> | ||||
|      * Upon receiving a user pull, it creates a Single Observer to fetch the ChannelInfo | ||||
|      * containing the feed streams. | ||||
|      **/ | ||||
|     private Subscriber<SubscriptionEntity> getSubscriptionObserver() { | ||||
|         return new Subscriber<SubscriptionEntity>() { | ||||
|             @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<Throwable>() { | ||||
|                                 @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. | ||||
|      * <p> | ||||
|      * Currently, the feed uses the first into from the list of streams. | ||||
|      * <p> | ||||
|      * If chosen feed already displayed, then we request another feed from another | ||||
|      * subscription, until the subscription table runs out of new items. | ||||
|      * <p> | ||||
|      * 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. | ||||
|      * <p> | ||||
|      * 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<ChannelInfo> getChannelInfoObserver(final int serviceId, final String url) { | ||||
|         return new MaybeObserver<ChannelInfo>() { | ||||
|             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<InfoItem> 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; | ||||
|     } | ||||
| } | ||||
| @@ -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<PlaylistInfo> { | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // 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<ListExtractor.NextItemsResult> loadMoreItemsLogic() { | ||||
|         return ExtractorHelper.getMorePlaylistItems(serviceId, currentNextItemsUrl); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected Single<PlaylistInfo> 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); | ||||
|     } | ||||
| } | ||||
| @@ -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<SearchResult, ListExtractor.NextItemsResult> { | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // 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<String> 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<Object> objectsToSave) { | ||||
|         super.writeTo(objectsToSave); | ||||
|         objectsToSave.add(currentPage); | ||||
|         objectsToSave.add(currentNextPage); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void readFrom(@NonNull Queue<Object> 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<String>()); | ||||
|                 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<String> checkEnabledAndLength = new Predicate<String>() { | ||||
|             @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<String>()); | ||||
|                 } | ||||
|                 // 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<String, Observable<Notification<List<String>>>>() { | ||||
|                     @Override | ||||
|                     public Observable<Notification<List<String>>> 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<Notification<List<String>>>() { | ||||
|                     @Override | ||||
|                     public void accept(@io.reactivex.annotations.NonNull Notification<List<String>> 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<SearchResult>() { | ||||
|                     @Override | ||||
|                     public void accept(@NonNull SearchResult result) throws Exception { | ||||
|                         isLoading.set(false); | ||||
|                         handleResult(result); | ||||
|                     } | ||||
|                 }, new Consumer<Throwable>() { | ||||
|                     @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<ListExtractor.NextItemsResult>() { | ||||
|                     @Override | ||||
|                     public void accept(@NonNull ListExtractor.NextItemsResult result) throws Exception { | ||||
|                         isLoading.set(false); | ||||
|                         handleNextItems(result); | ||||
|                     } | ||||
|                 }, new Consumer<Throwable>() { | ||||
|                     @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<String> 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; | ||||
|     } | ||||
| } | ||||
| @@ -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 <chris.schabesberger@mailbox.org> | ||||
| @@ -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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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); | ||||
| } | ||||
| @@ -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<SearchEngine.Filter> 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<InfoItem> serializable = (ArrayList<InfoItem>) 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<String>()); | ||||
|                 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<SearchEngine.Filter> 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<String> 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); | ||||
|     } | ||||
| } | ||||
| @@ -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<List<SubscriptionEntity>> { | ||||
|     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<ChannelInfoItem>() { | ||||
|             @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<List<SubscriptionEntity>> getSubscriptionObserver() { | ||||
|         return new Observer<List<SubscriptionEntity>>() { | ||||
|             @Override | ||||
|             public void onSubscribe(Disposable d) { | ||||
|                 showLoading(); | ||||
|                 disposables.add(d); | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public void onNext(List<SubscriptionEntity> subscriptions) { | ||||
|                 handleResult(subscriptions); | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public void onError(Throwable exception) { | ||||
|                 SubscriptionFragment.this.onError(exception); | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public void onComplete() { | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void handleResult(@NonNull List<SubscriptionEntity> 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<InfoItem> getSubscriptionItems(List<SubscriptionEntity> subscriptions) { | ||||
|         List<InfoItem> items = new ArrayList<>(); | ||||
|         for (final SubscriptionEntity subscription : subscriptions) items.add(subscription.toChannelInfoItem()); | ||||
|  | ||||
|         Collections.sort(items, new Comparator<InfoItem>() { | ||||
|             @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; | ||||
|     } | ||||
| } | ||||
| @@ -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<List<SubscriptionEntity>> 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. | ||||
|      *  */ | ||||
|      * <p> | ||||
|      * This observer may be subscribed multiple times, where each subscriber obtains | ||||
|      * the latest synchronized changes available, effectively share the same data | ||||
|      * across all subscribers. | ||||
|      * <p> | ||||
|      * 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<List<SubscriptionEntity>> getSubscription() { | ||||
|         return subscription; | ||||
|     } | ||||
| 
 | ||||
|     public Maybe<ChannelInfo> 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<ChannelInfo> callable = new Callable<ChannelInfo>() { | ||||
|             @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<List<SubscriptionEntity>, CompletableSource> update = new Function<List<SubscriptionEntity>, CompletableSource>() { | ||||
|             @Override | ||||
|             public CompletableSource apply(@NonNull List<SubscriptionEntity> 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(); | ||||
|     } | ||||
| } | ||||
| @@ -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<Object, Observable<HistoryFragment>>() { | ||||
|                     @Override | ||||
|                     public Observable<HistoryFragment> 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<HistoryFragment>() { | ||||
|                 .subscribe(new Consumer<Object>() { | ||||
|                     @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<Fragment> 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) { | ||||
|   | ||||
| @@ -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<E extends HistoryEntry, VH extends Rec | ||||
|         notifyDataSetChanged(); | ||||
|     } | ||||
|  | ||||
|     public Collection<E> getItems() { | ||||
|         return mEntries; | ||||
|     } | ||||
|  | ||||
|     public void clear() { | ||||
|         mEntries.clear(); | ||||
|         notifyDataSetChanged(); | ||||
|   | ||||
| @@ -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<E extends HistoryEntry> extends Fragment | ||||
| public abstract class HistoryFragment<E extends HistoryEntry> extends BaseFragment | ||||
|         implements HistoryEntryAdapter.OnHistoryItemClickListener<E> { | ||||
|  | ||||
|     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<E> mHistoryDataSource; | ||||
|     private HistoryEntryAdapter<E, ? extends RecyclerView.ViewHolder> mHistoryAdapter; | ||||
|     private View mEmptyHistoryView; | ||||
|  | ||||
|     @State | ||||
|     Parcelable mRecyclerViewState; | ||||
|     private RecyclerView mRecyclerView; | ||||
|     private HistoryEntryAdapter<E, ? extends RecyclerView.ViewHolder> mHistoryAdapter; | ||||
|     private ItemTouchHelper.SimpleCallback mHistoryItemSwipeCallback; | ||||
|     private PublishSubject<E> mHistoryEntryDeleteSubject; | ||||
|     private int allowedSwipeToDeleteDirections = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT; | ||||
|  | ||||
|     private HistoryDAO<E> mHistoryDataSource; | ||||
|     private PublishSubject<Collection<E>> mHistoryEntryDeleteSubject; | ||||
|     private PublishSubject<Collection<E>> mHistoryEntryInsertSubject; | ||||
|  | ||||
|     @StringRes | ||||
|     abstract int getEnabledConfigKey(); | ||||
| @@ -64,19 +77,29 @@ public abstract class HistoryFragment<E extends HistoryEntry> extends Fragment | ||||
|         // Register history enabled listener | ||||
|         mSharedPreferences.registerOnSharedPreferenceChangeListener(mHistoryIsEnabledChangeListener); | ||||
|  | ||||
|         mHistoryDataSource = createHistoryDAO(getContext()); | ||||
|         mHistoryDataSource = createHistoryDAO(); | ||||
|  | ||||
|         mHistoryEntryDeleteSubject = PublishSubject.create(); | ||||
|         mHistoryEntryDeleteSubject | ||||
|                 .observeOn(Schedulers.io()) | ||||
|                 .subscribe(new Consumer<E>() { | ||||
|                 .subscribe(new Consumer<Collection<E>>() { | ||||
|                     @Override | ||||
|                     public void accept(E historyEntry) throws Exception { | ||||
|                         mHistoryDataSource.delete(historyEntry); | ||||
|                     public void accept(Collection<E> 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<Collection<E>>() { | ||||
|                     @Override | ||||
|                     public void accept(Collection<E> 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<E extends HistoryEntry> 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<E extends HistoryEntry> 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<E extends HistoryEntry> 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<E extends HistoryEntry> extends Fragment | ||||
|      */ | ||||
|     @MainThread | ||||
|     public void onHistoryCleared() { | ||||
|         final Parcelable stateBeforeClear = mRecyclerView.getLayoutManager().onSaveInstanceState(); | ||||
|         final Collection<E> 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<E extends HistoryEntry> 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<E extends HistoryEntry> extends Fragment | ||||
|     /** | ||||
|      * Creates a new history DAO | ||||
|      * | ||||
|      * @param context the fragments context | ||||
|      * @return the history DAO | ||||
|      */ | ||||
|     @NonNull | ||||
|     protected abstract HistoryDAO<E> createHistoryDAO(Context context); | ||||
|     protected abstract HistoryDAO<E> createHistoryDAO(); | ||||
|  | ||||
|     private class HistoryIsEnabledChangeListener implements SharedPreferences.OnSharedPreferenceChangeListener { | ||||
|         @Override | ||||
|   | ||||
| @@ -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); | ||||
| } | ||||
| @@ -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<SearchHistoryEntry> { | ||||
|  | ||||
|     @NonNull | ||||
|     @Override | ||||
|     protected HistoryDAO<SearchHistoryEntry> createHistoryDAO(Context context) { | ||||
|         return NewPipeDatabase.getInstance(context).searchHistoryDAO(); | ||||
|     protected HistoryDAO<SearchHistoryEntry> createHistoryDAO() { | ||||
|         return NewPipeDatabase.getInstance().searchHistoryDAO(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|   | ||||
| @@ -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<WatchHistoryEntry> { | ||||
|  | ||||
| @@ -50,8 +51,8 @@ public class WatchedHistoryFragment extends HistoryFragment<WatchHistoryEntry> { | ||||
|  | ||||
|     @NonNull | ||||
|     @Override | ||||
|     protected HistoryDAO<WatchHistoryEntry> createHistoryDAO(Context context) { | ||||
|         return NewPipeDatabase.getInstance(context).watchHistoryDAO(); | ||||
|     protected HistoryDAO<WatchHistoryEntry> createHistoryDAO() { | ||||
|         return NewPipeDatabase.getInstance().watchHistoryDAO(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -71,7 +72,7 @@ public class WatchedHistoryFragment extends HistoryFragment<WatchHistoryEntry> { | ||||
|         @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<WatchHistoryEntry> { | ||||
|             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); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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 <chris.schabesberger@mailbox.org> | ||||
|  * 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 <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| 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; | ||||
|     } | ||||
| } | ||||
| @@ -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. | ||||
|  * <p> | ||||
|  * Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org> | ||||
| @@ -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<T extends InfoItem> { | ||||
|         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<StreamInfoItem> onStreamSelectedListener; | ||||
|     private OnInfoItemSelectedListener<ChannelInfoItem> onChannelSelectedListener; | ||||
|     private OnInfoItemSelectedListener<PlaylistInfoItem> 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<StreamInfoItem> 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<StreamInfoItem> 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<ChannelInfoItem> 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<ChannelInfoItem> 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<PlaylistInfoItem> 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<PlaylistInfoItem> listener) { | ||||
|         this.onPlaylistSelectedListener = listener; | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -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 <chris.schabesberger@mailbox.org> | ||||
|  * 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 <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| public abstract class InfoItemHolder extends RecyclerView.ViewHolder { | ||||
|     public InfoItemHolder(View v) { | ||||
|         super(v); | ||||
|     } | ||||
|     public abstract InfoItem.InfoType infoType(); | ||||
| } | ||||
| @@ -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 <chris.schabesberger@mailbox.org> | ||||
| @@ -36,25 +42,32 @@ import java.util.List; | ||||
|  */ | ||||
|  | ||||
| public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { | ||||
|     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<InfoItem> 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<RecyclerView.ViewHolde | ||||
|         infoItemList = new ArrayList<>(); | ||||
|     } | ||||
|  | ||||
|     public void setOnStreamInfoItemSelectedListener | ||||
|             (InfoItemBuilder.OnInfoItemSelectedListener listener) { | ||||
|         infoItemBuilder.setOnStreamInfoItemSelectedListener(listener); | ||||
|     public void setOnStreamSelectedListener(OnInfoItemSelectedListener<StreamInfoItem> listener) { | ||||
|         infoItemBuilder.setOnStreamSelectedListener(listener); | ||||
|     } | ||||
|  | ||||
|     public void setOnChannelInfoItemSelectedListener | ||||
|             (InfoItemBuilder.OnInfoItemSelectedListener listener) { | ||||
|         infoItemBuilder.setOnChannelInfoItemSelectedListener(listener); | ||||
|     public void setOnChannelSelectedListener(OnInfoItemSelectedListener<ChannelInfoItem> listener) { | ||||
|         infoItemBuilder.setOnChannelSelectedListener(listener); | ||||
|     } | ||||
|  | ||||
|     public void setOnPlaylistSelectedListener(OnInfoItemSelectedListener<PlaylistInfoItem> listener) { | ||||
|         infoItemBuilder.setOnPlaylistSelectedListener(listener); | ||||
|     } | ||||
|  | ||||
|     public void useMiniItemVariants(boolean useMiniVariant) { | ||||
|         this.useMiniVariant = useMiniVariant; | ||||
|     } | ||||
|  | ||||
|     public void addInfoItemList(List<InfoItem> 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<RecyclerView.ViewHolde | ||||
|     } | ||||
|  | ||||
|     public void setHeader(View header) { | ||||
|         boolean changed = header != this.header; | ||||
|         this.header = header; | ||||
|         notifyDataSetChanged(); | ||||
|         if (changed) notifyDataSetChanged(); | ||||
|     } | ||||
|  | ||||
|     public void setFooter(View view) { | ||||
|         this.footer = view; | ||||
|         notifyDataSetChanged(); | ||||
|     } | ||||
|  | ||||
|     public void showFooter(boolean show) { | ||||
|         if (DEBUG) Log.d(TAG, "showFooter() called with: show = [" + show + "]"); | ||||
|         if (show == showFooter) return; | ||||
|  | ||||
|         showFooter = show; | ||||
|         if (show) notifyItemInserted(sizeConsideringHeaderOffset()); | ||||
|         else notifyItemRemoved(sizeConsideringHeaderOffset()); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     private int sizeConsideringHeaderOffset() { | ||||
|         int i = infoItemList.size() + (header != null ? 1 : 0); | ||||
|         if (DEBUG) Log.d(TAG, "sizeConsideringHeaderOffset() called → " + i); | ||||
|         return i; | ||||
|     } | ||||
|  | ||||
|     public ArrayList<InfoItem> getItemsList() { | ||||
| @@ -111,30 +179,35 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde | ||||
|     @Override | ||||
|     public int getItemCount() { | ||||
|         int count = infoItemList.size(); | ||||
|         if(header != null) count++; | ||||
|         if(footer != null && showFooter) count++; | ||||
|         if (header != null) count++; | ||||
|         if (footer != null && showFooter) count++; | ||||
|  | ||||
|         if (DEBUG) { | ||||
|             Log.d(TAG, "getItemCount() called, count = " + count + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter); | ||||
|         } | ||||
|         return count; | ||||
|     } | ||||
|  | ||||
|     // don't ask why we have to do that this way... it's android accept it -.- | ||||
|     @Override | ||||
|     public int getItemViewType(int position) { | ||||
|         if(header != null && position == 0) { | ||||
|             return 0; | ||||
|         } else if(header != null) { | ||||
|         if (DEBUG) Log.d(TAG, "getItemViewType() called with: position = [" + position + "]"); | ||||
|  | ||||
|         if (header != null && position == 0) { | ||||
|             return HEADER_TYPE; | ||||
|         } else if (header != null) { | ||||
|             position--; | ||||
|         } | ||||
|         if(footer != null && position == infoItemList.size() && showFooter) { | ||||
|             return 1; | ||||
|         if (footer != null && position == infoItemList.size() && showFooter) { | ||||
|             return FOOTER_TYPE; | ||||
|         } | ||||
|         InfoItem item = infoItemList.get(position); | ||||
|         switch(item.infoType()) { | ||||
|         switch (item.info_type) { | ||||
|             case STREAM: | ||||
|                 return 2; | ||||
|                 return useMiniVariant ? MINI_STREAM_HOLDER_TYPE : STREAM_HOLDER_TYPE; | ||||
|             case CHANNEL: | ||||
|                 return 3; | ||||
|                 return useMiniVariant ? MINI_CHANNEL_HOLDER_TYPE : CHANNEL_HOLDER_TYPE; | ||||
|             case PLAYLIST: | ||||
|                 return 4; | ||||
|                 return PLAYLIST_HOLDER_TYPE; | ||||
|             default: | ||||
|                 Log.e(TAG, "Trollolo"); | ||||
|                 return -1; | ||||
| @@ -143,20 +216,22 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde | ||||
|  | ||||
|     @Override | ||||
|     public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int type) { | ||||
|         switch(type) { | ||||
|             case 0: | ||||
|         if (DEBUG) Log.d(TAG, "onCreateViewHolder() called with: parent = [" + parent + "], type = [" + type + "]"); | ||||
|         switch (type) { | ||||
|             case HEADER_TYPE: | ||||
|                 return new HFHolder(header); | ||||
|             case 1: | ||||
|             case FOOTER_TYPE: | ||||
|                 return new HFHolder(footer); | ||||
|             case 2: | ||||
|                 return new StreamInfoItemHolder(LayoutInflater.from(parent.getContext()) | ||||
|                         .inflate(R.layout.stream_item, parent, false)); | ||||
|             case 3: | ||||
|                 return new ChannelInfoItemHolder(LayoutInflater.from(parent.getContext()) | ||||
|                         .inflate(R.layout.channel_item, parent, false)); | ||||
|             case 4: | ||||
|                 Log.e(TAG, "Playlist is not yet implemented"); | ||||
|                 return null; | ||||
|             case MINI_STREAM_HOLDER_TYPE: | ||||
|                 return new StreamMiniInfoItemHolder(infoItemBuilder, parent); | ||||
|             case STREAM_HOLDER_TYPE: | ||||
|                 return new StreamInfoItemHolder(infoItemBuilder, parent); | ||||
|             case MINI_CHANNEL_HOLDER_TYPE: | ||||
|                 return new ChannelMiniInfoItemHolder(infoItemBuilder, parent); | ||||
|             case CHANNEL_HOLDER_TYPE: | ||||
|                 return new ChannelInfoItemHolder(infoItemBuilder, parent); | ||||
|             case PLAYLIST_HOLDER_TYPE: | ||||
|                 return new PlaylistInfoItemHolder(infoItemBuilder, parent); | ||||
|             default: | ||||
|                 Log.e(TAG, "Trollolo"); | ||||
|                 return null; | ||||
| @@ -164,16 +239,16 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onBindViewHolder(RecyclerView.ViewHolder holder, int i) { | ||||
|         //god damn f*** ANDROID SH** | ||||
|         if(holder instanceof InfoItemHolder) { | ||||
|             if(header != null) { | ||||
|                 i--; | ||||
|             } | ||||
|             infoItemBuilder.buildByHolder((InfoItemHolder) holder, infoItemList.get(i)); | ||||
|         } else if(holder instanceof HFHolder && i == 0 && header != null) { | ||||
|     public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { | ||||
|         if (DEBUG) Log.d(TAG, "onBindViewHolder() called with: holder = [" + holder.getClass().getSimpleName() + "], position = [" + position + "]"); | ||||
|         if (holder instanceof InfoItemHolder) { | ||||
|             // If header isn't null, offset the items by -1 | ||||
|             if (header != null) position--; | ||||
|  | ||||
|             ((InfoItemHolder) holder).updateFromItem(infoItemList.get(position)); | ||||
|         } else if (holder instanceof HFHolder && position == 0 && header != null) { | ||||
|             ((HFHolder) holder).view = header; | ||||
|         } else if(holder instanceof HFHolder && i == infoItemList.size() && footer != null && showFooter) { | ||||
|         } else if (holder instanceof HFHolder && position == sizeConsideringHeaderOffset() && footer != null && showFooter) { | ||||
|             ((HFHolder) holder).view = footer; | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -1,53 +0,0 @@ | ||||
| package org.schabi.newpipe.info_list; | ||||
|  | ||||
| import android.view.View; | ||||
| import android.widget.ImageView; | ||||
| import android.widget.TextView; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.extractor.InfoItem; | ||||
|  | ||||
| /** | ||||
|  * Created by Christian Schabesberger on 01.08.16. | ||||
|  * <p> | ||||
|  * Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org> | ||||
|  * StreamInfoItemHolder.java is part of NewPipe. | ||||
|  * <p> | ||||
|  * 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. | ||||
|  * <p> | ||||
|  * 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. | ||||
|  * <p> | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with NewPipe.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| 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; | ||||
|     } | ||||
| } | ||||
| @@ -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 <chris.schabesberger@mailbox.org> | ||||
|  * 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 <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| 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; | ||||
|     } | ||||
| } | ||||
| @@ -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(); | ||||
| } | ||||
| @@ -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 <chris.schabesberger@mailbox.org> | ||||
|  * 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 <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| 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(); | ||||
| } | ||||
| @@ -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(); | ||||
| } | ||||
| @@ -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. | ||||
|  * <p> | ||||
|  * Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org> | ||||
|  * StreamInfoItemHolder.java is part of NewPipe. | ||||
|  * <p> | ||||
|  * 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. | ||||
|  * <p> | ||||
|  * 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. | ||||
|  * <p> | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with NewPipe.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| 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; | ||||
|     } | ||||
| } | ||||
| @@ -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(); | ||||
| } | ||||
| @@ -1,6 +1,24 @@ | ||||
| /* | ||||
|  * Copyright 2017 Mauricio Colli <mauriciocolli@outlook.com> | ||||
|  * 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 <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| 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)); | ||||
|   | ||||
| @@ -1,3 +1,22 @@ | ||||
| /* | ||||
|  * Copyright 2017 Mauricio Colli <mauriciocolli@outlook.com> | ||||
|  * 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 <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| 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() { | ||||
|   | ||||
| @@ -1,3 +1,22 @@ | ||||
| /* | ||||
|  * Copyright 2017 Mauricio Colli <mauriciocolli@outlook.com> | ||||
|  * 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 <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| 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 | ||||
|   | ||||
| @@ -1,3 +1,22 @@ | ||||
| /* | ||||
|  * Copyright 2017 Mauricio Colli <mauriciocolli@outlook.com> | ||||
|  * 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 <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| 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<StreamInfo>() { | ||||
|                         @Override | ||||
|                         public void accept(@NonNull StreamInfo info) throws Exception { | ||||
|                             fetcherRunnable.onReceive(info); | ||||
|                         } | ||||
|                     }, new Consumer<Throwable>() { | ||||
|                         @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(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -1,3 +1,22 @@ | ||||
| /* | ||||
|  * Copyright 2017 Mauricio Colli <mauriciocolli@outlook.com> | ||||
|  * 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 <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| 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); | ||||
|   | ||||
| @@ -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 <chris.schabesberger@mailbox.org> | ||||
|  * 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) { | ||||
| @@ -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 <chris.schabesberger@mailbox.org> | ||||
|   | ||||
| @@ -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 <chris.schabesberger@mailbox.org> | ||||
|   | ||||
| @@ -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 <chris.schabesberger@mailbox.org> | ||||
| @@ -95,37 +95,34 @@ public class ErrorActivity extends AppCompatActivity { | ||||
|  | ||||
|     public static void reportError(final Context context, final List<Throwable> 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<Throwable> 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<Throwable> 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<Throwable> 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<? extends Activity> getReturnActivity(Class<?> returnActivity) { | ||||
|         Class<? extends Activity> 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); | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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; | ||||
|         } | ||||
|     }; | ||||
| } | ||||
| @@ -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()); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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); | ||||
|     } | ||||
| } | ||||
| @@ -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(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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); | ||||
|     } | ||||
| } | ||||
| @@ -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); | ||||
|     } | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| /** | ||||
| /* | ||||
|  * Created by k3b on 07.01.2016. | ||||
|  * | ||||
|  * Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org> | ||||
| @@ -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 <chris.schabesberger@mailbox.org> | ||||
|  * 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); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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 <chris.schabesberger@mailbox.org> | ||||
| @@ -31,7 +35,7 @@ import org.schabi.newpipe.util.ThemeHelper; | ||||
|  * along with NewPipe.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| 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; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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); | ||||
|     } | ||||
| } | ||||
| @@ -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); | ||||
|     } | ||||
| } | ||||
| @@ -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); | ||||
|   | ||||
							
								
								
									
										236
									
								
								app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										236
									
								
								app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,236 @@ | ||||
| /* | ||||
|  * Copyright 2017 Mauricio Colli <mauriciocolli@outlook.com> | ||||
|  * 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 <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| 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<SearchResult> searchFor(final int serviceId, final String query, final int pageNumber, final String searchLanguage, final SearchEngine.Filter filter) { | ||||
|         return Single.fromCallable(new Callable<SearchResult>() { | ||||
|             @Override | ||||
|             public SearchResult call() throws Exception { | ||||
|                 return SearchResult.getSearchResult(NewPipe.getService(serviceId).getSearchEngine(), | ||||
|                         query, pageNumber, searchLanguage, filter); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     public static Single<NextItemsResult> 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<SearchResult, NextItemsResult>() { | ||||
|                     @Override | ||||
|                     public NextItemsResult apply(@NonNull SearchResult searchResult) throws Exception { | ||||
|                         return new NextItemsResult(searchResult.resultList, nextPageNumber + "", searchResult.errors); | ||||
|                     } | ||||
|                 }); | ||||
|     } | ||||
|  | ||||
|     public static Single<List<String>> suggestionsFor(final int serviceId, final String query, final String searchLanguage) { | ||||
|         return Single.fromCallable(new Callable<List<String>>() { | ||||
|             @Override | ||||
|             public List<String> call() throws Exception { | ||||
|                 return NewPipe.getService(serviceId).getSuggestionExtractor().suggestionList(query, searchLanguage); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     public static Single<StreamInfo> getStreamInfo(final int serviceId, final String url, boolean forceLoad) { | ||||
|         return checkCache(forceLoad, serviceId, url, Single.fromCallable(new Callable<StreamInfo>() { | ||||
|             @Override | ||||
|             public StreamInfo call() throws Exception { | ||||
|                 return StreamInfo.getInfo(NewPipe.getService(serviceId), url); | ||||
|             } | ||||
|         })); | ||||
|     } | ||||
|  | ||||
|     public static Single<ChannelInfo> getChannelInfo(final int serviceId, final String url, boolean forceLoad) { | ||||
|         return checkCache(forceLoad, serviceId, url, Single.fromCallable(new Callable<ChannelInfo>() { | ||||
|             @Override | ||||
|             public ChannelInfo call() throws Exception { | ||||
|                 return ChannelInfo.getInfo(NewPipe.getService(serviceId), url); | ||||
|             } | ||||
|         })); | ||||
|     } | ||||
|  | ||||
|     public static Single<NextItemsResult> getMoreChannelItems(final int serviceId, final String nextStreamsUrl) { | ||||
|         return Single.fromCallable(new Callable<NextItemsResult>() { | ||||
|             @Override | ||||
|             public NextItemsResult call() throws Exception { | ||||
|                 return ChannelInfo.getMoreItems(NewPipe.getService(serviceId), nextStreamsUrl); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     public static Single<PlaylistInfo> getPlaylistInfo(final int serviceId, final String url, boolean forceLoad) { | ||||
|         return checkCache(forceLoad, serviceId, url, Single.fromCallable(new Callable<PlaylistInfo>() { | ||||
|             @Override | ||||
|             public PlaylistInfo call() throws Exception { | ||||
|                 return PlaylistInfo.getInfo(NewPipe.getService(serviceId), url); | ||||
|             } | ||||
|         })); | ||||
|     } | ||||
|  | ||||
|     public static Single<NextItemsResult> getMorePlaylistItems(final int serviceId, final String nextStreamsUrl) { | ||||
|         return Single.fromCallable(new Callable<NextItemsResult>() { | ||||
|             @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 <I extends Info> Single<I> checkCache(boolean forceLoad, int serviceId, String url, Single<I> loadFromNetwork) { | ||||
|         loadFromNetwork = loadFromNetwork.doOnSuccess(new Consumer<I>() { | ||||
|             @Override | ||||
|             public void accept(@NonNull I i) throws Exception { | ||||
|                 cache.putInfo(i); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         Single<I> load; | ||||
|         if (forceLoad) { | ||||
|             cache.removeInfo(serviceId, url); | ||||
|             load = loadFromNetwork; | ||||
|         } else { | ||||
|             load = Maybe.concat(ExtractorHelper.<I>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 <I extends Info> Maybe<I> loadFromCache(final int serviceId, final String url) { | ||||
|         return Maybe.defer(new Callable<MaybeSource<? extends I>>() { | ||||
|             @Override | ||||
|             public MaybeSource<? extends I> 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); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										100
									
								
								app/src/main/java/org/schabi/newpipe/util/InfoCache.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								app/src/main/java/org/schabi/newpipe/util/InfoCache.java
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,100 @@ | ||||
| /* | ||||
|  * Copyright 2017 Mauricio Colli <mauriciocolli@outlook.com> | ||||
|  * 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 <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| 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<String, Info> 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(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
							
								
								
									
										269
									
								
								app/src/main/java/org/schabi/newpipe/util/ListHelper.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										269
									
								
								app/src/main/java/org/schabi/newpipe/util/ListHelper.java
									
									
									
									
									
										Normal file
									
								
							| @@ -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<String> 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<VideoStream> 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<VideoStream> 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<VideoStream> 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<AudioStream> 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<AudioStream> 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<AudioStream> 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<AudioStream> 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<VideoStream> getSortedStreamVideosList(Context context, List<VideoStream> videoStreams, List<VideoStream> 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<VideoStream> getSortedStreamVideosList(MediaFormat defaultFormat, boolean showHigherResolutions, List<VideoStream> videoStreams, List<VideoStream> videoOnlyStreams, boolean ascendingOrder) { | ||||
|         ArrayList<VideoStream> retList = new ArrayList<>(); | ||||
|         HashMap<String, VideoStream> 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; | ||||
|      * <p> | ||||
|      * It works like that:<br> | ||||
|      * - Take a string resolution, remove the letters, replace "0p60" (for 60fps videos) with "1" | ||||
|      * and sort by the greatest:<br> | ||||
|      * <blockquote><pre> | ||||
|      *      720p     ->  720 | ||||
|      *      720p60   ->  721 | ||||
|      *      360p     ->  360 | ||||
|      *      1080p    ->  1080 | ||||
|      *      1080p60  ->  1081 | ||||
|      * <br> | ||||
|      *  ascendingOrder  ? 360 < 720 < 721 < 1080 < 1081 | ||||
|      *  !ascendingOrder ? 1081 < 1080 < 721 < 720 < 360</pre></blockquote> | ||||
|      * | ||||
|      * @param videoStreams   list that the sorting will be applied | ||||
|      * @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest | ||||
|      */ | ||||
|     public static void sortStreamList(List<VideoStream> videoStreams, final boolean ascendingOrder) { | ||||
|         Collections.sort(videoStreams, new Comparator<VideoStream>() { | ||||
|             @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<VideoStream> 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<VideoStream> 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; | ||||
|     } | ||||
| } | ||||
| @@ -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; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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; | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										319
									
								
								app/src/main/java/org/schabi/newpipe/util/StateSaver.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										319
									
								
								app/src/main/java/org/schabi/newpipe/util/StateSaver.java
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,319 @@ | ||||
| /* | ||||
|  * Copyright 2017 Mauricio Colli <mauriciocolli@outlook.com> | ||||
|  * 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 <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| 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<String, Queue<Object>> 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. | ||||
|      * <p> | ||||
|      * 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<Object> 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<Object> 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<Object> 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<Object>) 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)}. | ||||
|      * <p> | ||||
|      * It checks if the file already exists and if it does, just return the path, so a good way to save is: | ||||
|      * <li> A fixed prefix for the file | ||||
|      * <li> 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<Object> 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<SavedState> CREATOR = new Parcelable.Creator<SavedState>() { | ||||
|             @Override | ||||
|             public SavedState createFromParcel(Parcel in) { | ||||
|                 return new SavedState(in); | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public SavedState[] newArray(int size) { | ||||
|                 return new SavedState[size]; | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|  | ||||
| } | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	 Mauricio Colli
					Mauricio Colli