Merge pull request #669 from mauriciocolli/refactor-newpipe

Update extractor and refactored NewPipe
This commit is contained in:
Mauricio Colli 2017-09-04 08:50:21 -03:00 committed by GitHub
commit 7885ae5d3f
254 changed files with 7836 additions and 7302 deletions

View File

@ -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'
}

View File

@ -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>

View File

@ -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();

View File

@ -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>

View File

@ -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>

View File

@ -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;

View 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();
}

View File

@ -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>
@ -36,15 +40,16 @@ 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);
}
}

View File

@ -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.
*

View File

@ -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) {}
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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.
*

View File

@ -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();

View File

@ -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();
}
/*//////////////////////////////////////////////////////////////////////////

View File

@ -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();
}
}

View File

@ -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);

View File

@ -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

View File

@ -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(),

View File

@ -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";

View File

@ -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);

View File

@ -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

View File

@ -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

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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();

View File

@ -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;

@ -1 +0,0 @@
Subproject commit ab530381cfb5cc3278f1c4f63f30b33ca3d54d5d

View File

@ -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();
}

View File

@ -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();
}
}

View File

@ -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));
}
}

View File

@ -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() {
}
}

View File

@ -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();
}
}

View File

@ -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:

View File

@ -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);
}

View File

@ -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();
}
}

View File

@ -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);
}

View File

@ -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();
}
}

View File

@ -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));

View File

@ -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);

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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 : "";
}
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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) {

View File

@ -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();

View File

@ -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

View File

@ -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);
}

View File

@ -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

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -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));

View File

@ -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() {

View File

@ -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

View File

@ -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();
}
}
}

View File

@ -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);

View File

@ -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) {

View File

@ -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>

View File

@ -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>

View File

@ -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);

View File

@ -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;

View File

@ -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;
}
};
}

View File

@ -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());
}
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);

View 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);
}
}

View 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();
}
}
}

View 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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View 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