SponsorBlock: Merge branch 'dev' into sponsorblock

# Conflicts:
#	app/src/main/res/values/strings.xml
This commit is contained in:
polymorphicshade 2020-11-22 18:23:20 -07:00
commit 86b324467d
246 changed files with 2587 additions and 1904 deletions

View file

@ -13,8 +13,8 @@ android {
resValue "string", "app_name", "NewPipe" resValue "string", "app_name", "NewPipe"
minSdkVersion 19 minSdkVersion 19
targetSdkVersion 29 targetSdkVersion 29
versionCode 957 versionCode 958
versionName "0.20.3" versionName "0.20.4"
multiDexEnabled true multiDexEnabled true
@ -89,20 +89,20 @@ android {
ext { ext {
icepickVersion = '3.2.0' icepickVersion = '3.2.0'
checkstyleVersion = '8.36.2' checkstyleVersion = '8.37'
stethoVersion = '1.5.1' stethoVersion = '1.5.1'
leakCanaryVersion = '2.2' leakCanaryVersion = '2.5'
exoPlayerVersion = '2.11.8' exoPlayerVersion = '2.11.8'
androidxLifecycleVersion = '2.2.0' androidxLifecycleVersion = '2.2.0'
androidxRoomVersion = '2.2.5' androidxRoomVersion = '2.3.0-alpha03'
groupieVersion = '2.8.0' groupieVersion = '2.8.1'
markwonVersion = '4.3.1' markwonVersion = '4.6.0'
googleAutoServiceVersion = '1.0-rc7' googleAutoServiceVersion = '1.0-rc7'
} }
configurations { configurations {
checkstyle checkstyle
// ktlint ktlint
} }
checkstyle { checkstyle {
@ -130,81 +130,85 @@ task runCheckstyle(type: Checkstyle) {
} }
} }
//task runKtlint(type: JavaExec) { def outputDir = "${project.buildDir}/reports/ktlint/"
// main = "com.pinterest.ktlint.Main" def inputFiles = project.fileTree(dir: "src", include: "**/*.kt")
// classpath = configurations.ktlint
// args "src/**/*.kt" task runKtlint(type: JavaExec) {
//} inputs.files(inputFiles)
// outputs.dir(outputDir)
//task formatKtlint(type: JavaExec) { main = "com.pinterest.ktlint.Main"
// main = "com.pinterest.ktlint.Main" classpath = configurations.ktlint
// classpath = configurations.ktlint args "src/**/*.kt"
// args "-F", "src/**/*.kt" }
//}
task formatKtlint(type: JavaExec) {
inputs.files(inputFiles)
outputs.dir(outputDir)
main = "com.pinterest.ktlint.Main"
classpath = configurations.ktlint
args "-F", "src/**/*.kt"
}
afterEvaluate { afterEvaluate {
preDebugBuild.dependsOn runCheckstyle //, runKtlint preDebugBuild.dependsOn formatKtlint, runCheckstyle, runKtlint
} }
dependencies { dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.10' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlin_version}"
implementation "frankiesardo:icepick:${icepickVersion}" implementation "frankiesardo:icepick:${icepickVersion}"
kapt "frankiesardo:icepick-processor:${icepickVersion}" kapt "frankiesardo:icepick-processor:${icepickVersion}"
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}" checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
// ktlint "com.pinterest:ktlint:0.35.0" ktlint "com.pinterest:ktlint:0.39.0"
debugImplementation "com.facebook.stetho:stetho:${stethoVersion}" debugImplementation "com.facebook.stetho:stetho:${stethoVersion}"
debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoVersion}" debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoVersion}"
debugImplementation "com.squareup.leakcanary:leakcanary-android:${leakCanaryVersion}" debugImplementation "com.squareup.leakcanary:leakcanary-android:${leakCanaryVersion}"
implementation "com.squareup.leakcanary:leakcanary-object-watcher-android:${leakCanaryVersion}" implementation "com.squareup.leakcanary:leakcanary-object-watcher-android:${leakCanaryVersion}"
implementation "com.squareup.leakcanary:plumber-android:${leakCanaryVersion}"
implementation "androidx.multidex:multidex:2.0.1" implementation "androidx.multidex:multidex:2.0.1"
testImplementation 'junit:junit:4.13.1'
testImplementation 'org.mockito:mockito-core:3.3.3'
androidTestImplementation "androidx.test.ext:junit:1.1.1"
androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}"
androidTestImplementation "androidx.test.espresso:espresso-core:3.2.0", {
exclude module: 'support-annotations'
}
// NewPipe dependencies // NewPipe dependencies
// You can use a local version by uncommenting a few lines in settings.gradle // You can use a local version by uncommenting a few lines in settings.gradle
implementation 'com.github.TeamNewPipe:NewPipeExtractor:6701b0fe718f6bdc385221341fa473e8aaab560e' implementation 'com.github.TeamNewPipe:NewPipeExtractor:650f0920fea535e08728d895d7b21f19c740817c'
implementation "com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751" implementation "com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751"
implementation "org.jsoup:jsoup:1.13.1" implementation "org.jsoup:jsoup:1.13.1"
//noinspection GradleDependency --> do not update okhttp to keep supporting Android 4.4 users
implementation "com.squareup.okhttp3:okhttp:3.12.12" implementation "com.squareup.okhttp3:okhttp:3.12.12"
implementation "com.google.android.exoplayer:exoplayer:${exoPlayerVersion}" implementation "com.google.android.exoplayer:exoplayer:${exoPlayerVersion}"
implementation "com.google.android.exoplayer:extension-mediasession:${exoPlayerVersion}" implementation "com.google.android.exoplayer:extension-mediasession:${exoPlayerVersion}"
implementation "com.google.android.material:material:1.1.0" implementation "com.google.android.material:material:1.2.1"
compileOnly "com.google.auto.service:auto-service-annotations:${googleAutoServiceVersion}" compileOnly "com.google.auto.service:auto-service-annotations:${googleAutoServiceVersion}"
kapt "com.google.auto.service:auto-service:${googleAutoServiceVersion}" kapt "com.google.auto.service:auto-service:${googleAutoServiceVersion}"
implementation "androidx.appcompat:appcompat:1.1.0" implementation "androidx.appcompat:appcompat:1.2.0"
implementation "androidx.preference:preference:1.1.1" implementation "androidx.preference:preference:1.1.1"
implementation "androidx.recyclerview:recyclerview:1.1.0" implementation "androidx.recyclerview:recyclerview:1.1.0"
implementation "androidx.cardview:cardview:1.0.0" implementation "androidx.cardview:cardview:1.0.0"
implementation "androidx.constraintlayout:constraintlayout:1.1.3" implementation "androidx.constraintlayout:constraintlayout:2.0.4"
implementation 'androidx.core:core-ktx:1.3.1' implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
implementation "androidx.lifecycle:lifecycle-livedata:${androidxLifecycleVersion}" implementation "androidx.lifecycle:lifecycle-livedata:${androidxLifecycleVersion}"
implementation "androidx.lifecycle:lifecycle-viewmodel:${androidxLifecycleVersion}" implementation "androidx.lifecycle:lifecycle-viewmodel:${androidxLifecycleVersion}"
implementation "androidx.room:room-runtime:${androidxRoomVersion}" implementation "androidx.room:room-runtime:${androidxRoomVersion}"
implementation "androidx.room:room-rxjava2:${androidxRoomVersion}" implementation "androidx.room:room-rxjava3:${androidxRoomVersion}"
kapt "androidx.room:room-compiler:${androidxRoomVersion}" kapt "androidx.room:room-compiler:${androidxRoomVersion}"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "com.xwray:groupie:${groupieVersion}" implementation "com.xwray:groupie:${groupieVersion}"
implementation "com.xwray:groupie-kotlin-android-extensions:${groupieVersion}" implementation "com.xwray:groupie-kotlin-android-extensions:${groupieVersion}"
@ -216,13 +220,22 @@ dependencies {
implementation "com.nononsenseapps:filepicker:4.2.1" implementation "com.nononsenseapps:filepicker:4.2.1"
implementation "ch.acra:acra-core:5.5.0" implementation "ch.acra:acra-core:5.7.0"
implementation "io.reactivex.rxjava2:rxjava:2.2.19" implementation "io.reactivex.rxjava3:rxjava:3.0.7"
implementation "io.reactivex.rxjava2:rxandroid:2.1.1" implementation "io.reactivex.rxjava3:rxandroid:3.0.0"
implementation "com.jakewharton.rxbinding2:rxbinding:2.2.0" implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
implementation "org.ocpsoft.prettytime:prettytime:4.0.6.Final" implementation "org.ocpsoft.prettytime:prettytime:4.0.6.Final"
testImplementation 'junit:junit:4.13.1'
testImplementation 'org.mockito:mockito-core:3.6.0'
androidTestImplementation "androidx.test.ext:junit:1.1.2"
androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}"
androidTestImplementation "androidx.test.espresso:espresso-core:3.3.0", {
exclude module: 'support-annotations'
}
} }
static String getGitWorkingBranch() { static String getGitWorkingBranch() {

View file

@ -31,49 +31,62 @@ class AppDatabaseTest {
} }
@get:Rule @get:Rule
val testHelper = MigrationTestHelper(InstrumentationRegistry.getInstrumentation(), val testHelper = MigrationTestHelper(
AppDatabase::class.java.canonicalName, FrameworkSQLiteOpenHelperFactory()) InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java.canonicalName, FrameworkSQLiteOpenHelperFactory()
)
@Test @Test
fun migrateDatabaseFrom2to3() { fun migrateDatabaseFrom2to3() {
val databaseInV2 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_2) val databaseInV2 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_2)
databaseInV2.run { databaseInV2.run {
insert("streams", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply { insert(
// put("uid", null) "streams", SQLiteDatabase.CONFLICT_FAIL,
put("service_id", DEFAULT_SERVICE_ID) ContentValues().apply {
put("url", DEFAULT_URL) // put("uid", null)
put("title", DEFAULT_TITLE) put("service_id", DEFAULT_SERVICE_ID)
put("stream_type", DEFAULT_TYPE.name) put("url", DEFAULT_URL)
put("duration", DEFAULT_DURATION) put("title", DEFAULT_TITLE)
put("uploader", DEFAULT_UPLOADER_NAME) put("stream_type", DEFAULT_TYPE.name)
put("thumbnail_url", DEFAULT_THUMBNAIL) put("duration", DEFAULT_DURATION)
}) put("uploader", DEFAULT_UPLOADER_NAME)
insert("streams", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply { put("thumbnail_url", DEFAULT_THUMBNAIL)
// put("uid", null) }
put("service_id", DEFAULT_SECOND_SERVICE_ID) )
put("url", DEFAULT_SECOND_URL) insert(
// put("title", null) "streams", SQLiteDatabase.CONFLICT_FAIL,
// put("stream_type", null) ContentValues().apply {
// put("duration", null) // put("uid", null)
// put("uploader", null) put("service_id", DEFAULT_SECOND_SERVICE_ID)
// put("thumbnail_url", null) put("url", DEFAULT_SECOND_URL)
}) // put("title", null)
insert("streams", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply { // put("stream_type", null)
// put("uid", null) // put("duration", null)
put("service_id", DEFAULT_SERVICE_ID) // put("uploader", null)
// put("url", null) // put("thumbnail_url", null)
// put("title", null) }
// put("stream_type", null) )
// put("duration", null) insert(
// put("uploader", null) "streams", SQLiteDatabase.CONFLICT_FAIL,
// put("thumbnail_url", null) ContentValues().apply {
}) // put("uid", null)
put("service_id", DEFAULT_SERVICE_ID)
// put("url", null)
// put("title", null)
// put("stream_type", null)
// put("duration", null)
// put("uploader", null)
// put("thumbnail_url", null)
}
)
close() close()
} }
testHelper.runMigrationsAndValidate(AppDatabase.DATABASE_NAME, Migrations.DB_VER_3, testHelper.runMigrationsAndValidate(
true, Migrations.MIGRATION_2_3) AppDatabase.DATABASE_NAME, Migrations.DB_VER_3,
true, Migrations.MIGRATION_2_3
)
val migratedDatabaseV3 = getMigratedDatabase() val migratedDatabaseV3 = getMigratedDatabase()
val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst() val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst()
@ -110,9 +123,11 @@ class AppDatabaseTest {
} }
private fun getMigratedDatabase(): AppDatabase { private fun getMigratedDatabase(): AppDatabase {
val database: AppDatabase = Room.databaseBuilder(ApplicationProvider.getApplicationContext(), val database: AppDatabase = Room.databaseBuilder(
AppDatabase::class.java, AppDatabase.DATABASE_NAME) ApplicationProvider.getApplicationContext(),
.build() AppDatabase::class.java, AppDatabase.DATABASE_NAME
)
.build()
testHelper.closeWhenFinished(database) testHelper.closeWhenFinished(database)
return database return database
} }

View file

@ -8,7 +8,6 @@ import androidx.test.filters.LargeTest;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.report.ErrorActivity.ErrorInfo;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@ -29,10 +28,10 @@ public class ErrorInfoTest {
parcel.setDataPosition(0); parcel.setDataPosition(0);
final ErrorInfo infoFromParcel = ErrorInfo.CREATOR.createFromParcel(parcel); final ErrorInfo infoFromParcel = ErrorInfo.CREATOR.createFromParcel(parcel);
assertEquals(UserAction.USER_REPORT, infoFromParcel.userAction); assertEquals(UserAction.USER_REPORT, infoFromParcel.getUserAction());
assertEquals("youtube", infoFromParcel.serviceName); assertEquals("youtube", infoFromParcel.getServiceName());
assertEquals("request", infoFromParcel.request); assertEquals("request", infoFromParcel.getRequest());
assertEquals(R.string.general_error, infoFromParcel.message); assertEquals(R.string.general_error, infoFromParcel.getMessage());
parcel.recycle(); parcel.recycle();
} }

View file

@ -15,14 +15,22 @@ class DebugApp : App() {
// Give each object 10 seconds to be GC'ed, before LeakCanary gets nosy on it // Give each object 10 seconds to be GC'ed, before LeakCanary gets nosy on it
AppWatcher.config = AppWatcher.config.copy(watchDurationMillis = 10000) AppWatcher.config = AppWatcher.config.copy(watchDurationMillis = 10000)
LeakCanary.config = LeakCanary.config.copy(dumpHeap = PreferenceManager LeakCanary.config = LeakCanary.config.copy(
.getDefaultSharedPreferences(this).getBoolean(getString( dumpHeap = PreferenceManager
R.string.allow_heap_dumping_key), false)) .getDefaultSharedPreferences(this).getBoolean(
getString(
R.string.allow_heap_dumping_key
),
false
)
)
} }
override fun getDownloader(): Downloader { override fun getDownloader(): Downloader {
val downloader = DownloaderImpl.init(OkHttpClient.Builder() val downloader = DownloaderImpl.init(
.addNetworkInterceptor(StethoInterceptor())) OkHttpClient.Builder()
.addNetworkInterceptor(StethoInterceptor())
)
setCookiesToDownloader(downloader) setCookiesToDownloader(downloader)
return downloader return downloader
} }
@ -36,7 +44,8 @@ class DebugApp : App() {
// Enable command line interface // Enable command line interface
initializerBuilder.enableDumpapp( initializerBuilder.enableDumpapp(
Stetho.defaultDumperPluginsProvider(applicationContext)) Stetho.defaultDumperPluginsProvider(applicationContext)
)
// Use the InitializerBuilder to generate an Initializer // Use the InitializerBuilder to generate an Initializer
val initializer = initializerBuilder.build() val initializer = initializerBuilder.build()
@ -47,6 +56,6 @@ class DebugApp : App() {
override fun isDisposedRxExceptionsReported(): Boolean { override fun isDisposedRxExceptionsReported(): Boolean {
return PreferenceManager.getDefaultSharedPreferences(this) return PreferenceManager.getDefaultSharedPreferences(this)
.getBoolean(getString(R.string.allow_disposed_exceptions_key), false) .getBoolean(getString(R.string.allow_disposed_exceptions_key), false)
} }
} }

View file

@ -305,8 +305,7 @@
<data android:host="peertube.cpy.re" /> <data android:host="peertube.cpy.re" />
<data android:host="peertube.mastodon.host" /> <data android:host="peertube.mastodon.host" />
<data android:host="peertube.fr" /> <data android:host="peertube.fr" />
<data android:host="peertube.live" /> <data android:host="tilvids.com" />
<data android:host="peertube.video" />
<data android:host="tube.privacytools.io" /> <data android:host="tube.privacytools.io" />
<data android:host="video.ploud.fr" /> <data android:host="video.ploud.fr" />
<data android:host="video.lqdn.fr" /> <data android:host="video.lqdn.fr" />

View file

@ -86,8 +86,8 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
private final int mBehavior; private final int mBehavior;
private FragmentTransaction mCurTransaction = null; private FragmentTransaction mCurTransaction = null;
private ArrayList<Fragment.SavedState> mSavedState = new ArrayList<Fragment.SavedState>(); private final ArrayList<Fragment.SavedState> mSavedState = new ArrayList<Fragment.SavedState>();
private ArrayList<Fragment> mFragments = new ArrayList<Fragment>(); private final ArrayList<Fragment> mFragments = new ArrayList<Fragment>();
private Fragment mCurrentPrimaryItem = null; private Fragment mCurrentPrimaryItem = null;
/** /**

View file

@ -8,6 +8,7 @@ import android.os.Build;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.multidex.MultiDexApplication; import androidx.multidex.MultiDexApplication;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
@ -22,6 +23,7 @@ import org.acra.config.CoreConfigurationBuilder;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.downloader.Downloader; import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.settings.SettingsActivity; import org.schabi.newpipe.settings.SettingsActivity;
import org.schabi.newpipe.util.ExceptionUtils; import org.schabi.newpipe.util.ExceptionUtils;
@ -36,13 +38,13 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import io.reactivex.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.exceptions.CompositeException; import io.reactivex.rxjava3.exceptions.CompositeException;
import io.reactivex.exceptions.MissingBackpressureException; import io.reactivex.rxjava3.exceptions.MissingBackpressureException;
import io.reactivex.exceptions.OnErrorNotImplementedException; import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException;
import io.reactivex.exceptions.UndeliverableException; import io.reactivex.rxjava3.exceptions.UndeliverableException;
import io.reactivex.functions.Consumer; import io.reactivex.rxjava3.functions.Consumer;
import io.reactivex.plugins.RxJavaPlugins; import io.reactivex.rxjava3.plugins.RxJavaPlugins;
/* /*
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org> * Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
@ -66,7 +68,7 @@ public class App extends MultiDexApplication {
protected static final String TAG = App.class.toString(); protected static final String TAG = App.class.toString();
private static App app; private static App app;
private Disposable disposable = null; @Nullable private Disposable disposable = null;
@NonNull @NonNull
public static App getApp() { public static App getApp() {
@ -226,7 +228,7 @@ public class App extends MultiDexApplication {
ace, ace,
null, null,
null, null,
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none", ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
"Could not initialize ACRA crash report", R.string.app_ui_crash)); "Could not initialize ACRA crash report", R.string.app_ui_crash));
} }
} }

View file

@ -12,6 +12,7 @@ import android.net.Uri;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat; import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
@ -21,12 +22,11 @@ import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonParser; import com.grack.nanojson.JsonParser;
import com.grack.nanojson.JsonParserException; import com.grack.nanojson.JsonParserException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.report.UserAction;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
@ -35,11 +35,10 @@ import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory; import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import io.reactivex.Observable; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Maybe;
import io.reactivex.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.disposables.Disposables; import io.reactivex.rxjava3.schedulers.Schedulers;
import io.reactivex.schedulers.Schedulers;
public final class CheckForNewAppVersion { public final class CheckForNewAppVersion {
private CheckForNewAppVersion() { } private CheckForNewAppVersion() { }
@ -57,48 +56,43 @@ public final class CheckForNewAppVersion {
* @param application The application * @param application The application
* @return String with the apk's SHA1 fingeprint in hexadecimal * @return String with the apk's SHA1 fingeprint in hexadecimal
*/ */
@NonNull
private static String getCertificateSHA1Fingerprint(@NonNull final Application application) { private static String getCertificateSHA1Fingerprint(@NonNull final Application application) {
final PackageManager pm = application.getPackageManager(); final PackageInfo packageInfo;
final String packageName = application.getPackageName();
final int flags = PackageManager.GET_SIGNATURES;
PackageInfo packageInfo = null;
try { try {
packageInfo = pm.getPackageInfo(packageName, flags); packageInfo = application.getPackageManager().getPackageInfo(
application.getPackageName(), PackageManager.GET_SIGNATURES);
} catch (final PackageManager.NameNotFoundException e) { } catch (final PackageManager.NameNotFoundException e) {
ErrorActivity.reportError(application, e, null, null, ErrorActivity.reportError(application, e, null, null,
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none", ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
"Could not find package info", R.string.app_ui_crash)); "Could not find package info", R.string.app_ui_crash));
return "";
} }
final Signature[] signatures = packageInfo.signatures; final X509Certificate c;
final byte[] cert = signatures[0].toByteArray();
final InputStream input = new ByteArrayInputStream(cert);
X509Certificate c = null;
try { try {
final Signature[] signatures = packageInfo.signatures;
final byte[] cert = signatures[0].toByteArray();
final InputStream input = new ByteArrayInputStream(cert);
final CertificateFactory cf = CertificateFactory.getInstance("X509"); final CertificateFactory cf = CertificateFactory.getInstance("X509");
c = (X509Certificate) cf.generateCertificate(input); c = (X509Certificate) cf.generateCertificate(input);
} catch (final CertificateException e) { } catch (final CertificateException e) {
ErrorActivity.reportError(application, e, null, null, ErrorActivity.reportError(application, e, null, null,
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none", ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
"Certificate error", R.string.app_ui_crash)); "Certificate error", R.string.app_ui_crash));
return "";
} }
String hexString = null;
try { try {
final MessageDigest md = MessageDigest.getInstance("SHA1"); final MessageDigest md = MessageDigest.getInstance("SHA1");
final byte[] publicKey = md.digest(c.getEncoded()); final byte[] publicKey = md.digest(c.getEncoded());
hexString = byte2HexFormatted(publicKey); return byte2HexFormatted(publicKey);
} catch (NoSuchAlgorithmException | CertificateEncodingException e) { } catch (NoSuchAlgorithmException | CertificateEncodingException e) {
ErrorActivity.reportError(application, e, null, null, ErrorActivity.reportError(application, e, null, null,
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none", ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
"Could not retrieve SHA1 key", R.string.app_ui_crash)); "Could not retrieve SHA1 key", R.string.app_ui_crash));
return "";
} }
return hexString;
} }
private static String byte2HexFormatted(final byte[] arr) { private static String byte2HexFormatted(final byte[] arr) {
@ -163,66 +157,66 @@ public final class CheckForNewAppVersion {
} }
private static boolean isConnected(@NonNull final App app) { private static boolean isConnected(@NonNull final App app) {
final ConnectivityManager cm = ContextCompat.getSystemService(app, final ConnectivityManager connectivityManager =
ConnectivityManager.class); ContextCompat.getSystemService(app, ConnectivityManager.class);
return cm.getActiveNetworkInfo() != null return connectivityManager != null && connectivityManager.getActiveNetworkInfo() != null
&& cm.getActiveNetworkInfo().isConnected(); && connectivityManager.getActiveNetworkInfo().isConnected();
} }
public static boolean isGithubApk(@NonNull final App app) { public static boolean isGithubApk(@NonNull final App app) {
return getCertificateSHA1Fingerprint(app).equals(GITHUB_APK_SHA1); return getCertificateSHA1Fingerprint(app).equals(GITHUB_APK_SHA1);
} }
@NonNull @Nullable
public static Disposable checkNewVersion(@NonNull final App app) { public static Disposable checkNewVersion(@NonNull final App app) {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app); final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
// Check if user has enabled/disabled update checking // Check if user has enabled/disabled update checking
// and if the current apk is a github one or not. // and if the current apk is a github one or not.
if (!prefs.getBoolean(app.getString(R.string.update_app_key), true) if (!prefs.getBoolean(app.getString(R.string.update_app_key), true) || !isGithubApk(app)) {
|| !isGithubApk(app)) { return null;
return Disposables.empty();
} }
return Observable.fromCallable(() -> { return Maybe
if (!isConnected(app)) { .fromCallable(() -> {
return null; if (!isConnected(app)) {
} return null;
}
// Make a network request to get latest NewPipe data. // Make a network request to get latest NewPipe data.
try { return DownloaderImpl.getInstance().get(NEWPIPE_API_URL).responseBody();
return DownloaderImpl.getInstance().get(NEWPIPE_API_URL).responseBody(); })
} catch (IOException | ReCaptchaException e) {
// connectivity problems, do not alarm user and fail silently
if (DEBUG) {
Log.w(TAG, Log.getStackTraceString(e));
}
}
return null;
})
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(response -> { .subscribe(
// Parse the json from the response. response -> {
if (response != null) { // Parse the json from the response.
try { try {
final JsonObject githubStableObject = JsonParser.object().from(response) final JsonObject githubStableObject = JsonParser.object()
.getObject("flavors").getObject("github").getObject("stable"); .from(response).getObject("flavors").getObject("github")
.getObject("stable");
final String versionName = githubStableObject.getString("version"); final String versionName = githubStableObject
final int versionCode = githubStableObject.getInt("version_code"); .getString("version");
final String apkLocationUrl = githubStableObject.getString("apk"); final int versionCode = githubStableObject
.getInt("version_code");
final String apkLocationUrl = githubStableObject
.getString("apk");
compareAppVersionAndShowNotification(app, versionName, apkLocationUrl, compareAppVersionAndShowNotification(app, versionName,
versionCode); apkLocationUrl, versionCode);
} catch (final JsonParserException e) { } catch (final JsonParserException e) {
// connectivity problems, do not alarm user and fail silently
if (DEBUG) {
Log.w(TAG, "Could not get NewPipe API: invalid json", e);
}
}
},
e -> {
// connectivity problems, do not alarm user and fail silently // connectivity problems, do not alarm user and fail silently
if (DEBUG) { if (DEBUG) {
Log.w(TAG, Log.getStackTraceString(e)); Log.w(TAG, "Could not get NewPipe API: network problem", e);
} }
} });
}
});
} }
} }

View file

@ -50,8 +50,8 @@ public final class DownloaderImpl extends Downloader {
public static final String YOUTUBE_DOMAIN = "youtube.com"; public static final String YOUTUBE_DOMAIN = "youtube.com";
private static DownloaderImpl instance; private static DownloaderImpl instance;
private Map<String, String> mCookies; private final Map<String, String> mCookies;
private OkHttpClient client; private final OkHttpClient client;
private DownloaderImpl(final OkHttpClient.Builder builder) { private DownloaderImpl(final OkHttpClient.Builder builder) {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) { if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {

View file

@ -139,8 +139,7 @@ public class MainActivity extends AppCompatActivity {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); setContentView(R.layout.activity_main);
if (getSupportFragmentManager() != null if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
&& getSupportFragmentManager().getBackStackEntryCount() == 0) {
initFragments(); initFragments();
} }

View file

@ -66,13 +66,13 @@ import java.util.List;
import icepick.Icepick; import icepick.Icepick;
import icepick.State; import icepick.State;
import io.reactivex.Observable; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.Single; import io.reactivex.rxjava3.core.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Single;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.functions.Consumer; import io.reactivex.rxjava3.functions.Consumer;
import io.reactivex.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.AUDIO; import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.AUDIO;
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.VIDEO; import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.VIDEO;
@ -519,7 +519,7 @@ public class RouterActivity extends AppCompatActivity {
disposables.add(ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, true) disposables.add(ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, true)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe((@NonNull StreamInfo result) -> { .subscribe(result -> {
final List<VideoStream> sortedVideoStreams = ListHelper final List<VideoStream> sortedVideoStreams = ListHelper
.getSortedStreamVideosList(this, result.getVideoStreams(), .getSortedStreamVideosList(this, result.getVideoStreams(),
result.getVideoOnlyStreams(), false); result.getVideoOnlyStreams(), false);
@ -534,9 +534,8 @@ public class RouterActivity extends AppCompatActivity {
downloadDialog.show(fm, "downloadDialog"); downloadDialog.show(fm, "downloadDialog");
fm.executePendingTransactions(); fm.executePendingTransactions();
downloadDialog.requireDialog().setOnDismissListener(dialog -> finish()); downloadDialog.requireDialog().setOnDismissListener(dialog -> finish());
}, (@NonNull Throwable throwable) -> { }, throwable ->
showUnsupportedUrlDialog(currentUrl); showUnsupportedUrlDialog(currentUrl)));
}));
} }
@Override @Override

View file

@ -31,7 +31,7 @@ public class AboutActivity extends AppCompatActivity {
/** /**
* List of all software components. * List of all software components.
*/ */
private static final SoftwareComponent[] SOFTWARE_COMPONENTS = new SoftwareComponent[]{ private static final SoftwareComponent[] SOFTWARE_COMPONENTS = {
new SoftwareComponent("Giga Get", "2014 - 2015", "Peter Cai", new SoftwareComponent("Giga Get", "2014 - 2015", "Peter Cai",
"https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL3), "https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL3),
new SoftwareComponent("NewPipe Extractor", "2017 - 2020", "Christian Schabesberger", new SoftwareComponent("NewPipe Extractor", "2017 - 2020", "Christian Schabesberger",

View file

@ -1,80 +0,0 @@
package org.schabi.newpipe.about;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import java.io.Serializable;
/**
* Class for storing information about a software license.
*/
public class License implements Parcelable, Serializable {
public static final Creator<License> CREATOR = new Creator<License>() {
@Override
public License createFromParcel(final Parcel source) {
return new License(source);
}
@Override
public License[] newArray(final int size) {
return new License[size];
}
};
private final String abbreviation;
private final String name;
private String filename;
public License(final String name, final String abbreviation, final String filename) {
if (name == null) {
throw new NullPointerException("name is null");
}
if (abbreviation == null) {
throw new NullPointerException("abbreviation is null");
}
if (filename == null) {
throw new NullPointerException("filename is null");
}
this.name = name;
this.filename = filename;
this.abbreviation = abbreviation;
}
protected License(final Parcel in) {
this.filename = in.readString();
this.abbreviation = in.readString();
this.name = in.readString();
}
public Uri getContentUri() {
return new Uri.Builder()
.scheme("file")
.path("/android_asset")
.appendPath(filename)
.build();
}
public String getAbbreviation() {
return abbreviation;
}
public String getFilename() {
return filename;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(final Parcel dest, final int flags) {
dest.writeString(this.filename);
dest.writeString(this.abbreviation);
dest.writeString(this.name);
}
public String getName() {
return name;
}
}

View file

@ -0,0 +1,19 @@
package org.schabi.newpipe.about
import android.net.Uri
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
import java.io.Serializable
/**
* Class for storing information about a software license.
*/
@Parcelize
class License(val name: String, val abbreviation: String, val filename: String) : Parcelable, Serializable {
val contentUri: Uri
get() = Uri.Builder()
.scheme("file")
.path("/android_asset")
.appendPath(filename)
.build()
}

View file

@ -20,7 +20,7 @@ import java.io.Serializable;
import java.util.Arrays; import java.util.Arrays;
import java.util.Comparator; import java.util.Comparator;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
/** /**
* Fragment containing the software licenses. * Fragment containing the software licenses.

View file

@ -16,11 +16,10 @@ import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import io.reactivex.Observable; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Observable;
import io.reactivex.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.disposables.Disposables; import io.reactivex.rxjava3.schedulers.Schedulers;
import io.reactivex.schedulers.Schedulers;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
@ -37,14 +36,12 @@ public final class LicenseFragmentHelper {
@NonNull final License license) { @NonNull final License license) {
final StringBuilder licenseContent = new StringBuilder(); final StringBuilder licenseContent = new StringBuilder();
final String webViewData; final String webViewData;
try { try (BufferedReader in = new BufferedReader(new InputStreamReader(
final BufferedReader in = new BufferedReader(new InputStreamReader( context.getAssets().open(license.getFilename()), StandardCharsets.UTF_8))) {
context.getAssets().open(license.getFilename()), StandardCharsets.UTF_8));
String str; String str;
while ((str = in.readLine()) != null) { while ((str = in.readLine()) != null) {
licenseContent.append(str); licenseContent.append(str);
} }
in.close();
// split the HTML file and insert the stylesheet into the HEAD of the file // split the HTML file and insert the stylesheet into the HEAD of the file
webViewData = licenseContent.toString().replace("</head>", webViewData = licenseContent.toString().replace("</head>",
@ -57,7 +54,7 @@ public final class LicenseFragmentHelper {
} }
/** /**
* @param context * @param context the Android context
* @return String which is a CSS stylesheet according to the context's theme * @return String which is a CSS stylesheet according to the context's theme
*/ */
private static String getLicenseStylesheet(@NonNull final Context context) { private static String getLicenseStylesheet(@NonNull final Context context) {
@ -88,7 +85,7 @@ public final class LicenseFragmentHelper {
static Disposable showLicense(@Nullable final Context context, @NonNull final License license) { static Disposable showLicense(@Nullable final Context context, @NonNull final License license) {
if (context == null) { if (context == null) {
return Disposables.empty(); return Disposable.empty();
} }
return Observable.fromCallable(() -> getFormattedLicense(context, license)) return Observable.fromCallable(() -> getFormattedLicense(context, license))

View file

@ -1,83 +0,0 @@
package org.schabi.newpipe.about;
import android.os.Parcel;
import android.os.Parcelable;
public class SoftwareComponent implements Parcelable {
public static final Creator<SoftwareComponent> CREATOR = new Creator<SoftwareComponent>() {
@Override
public SoftwareComponent createFromParcel(final Parcel source) {
return new SoftwareComponent(source);
}
@Override
public SoftwareComponent[] newArray(final int size) {
return new SoftwareComponent[size];
}
};
private final License license;
private final String name;
private final String years;
private final String copyrightOwner;
private final String link;
private final String version;
public SoftwareComponent(final String name, final String years, final String copyrightOwner,
final String link, final License license) {
this.name = name;
this.years = years;
this.copyrightOwner = copyrightOwner;
this.link = link;
this.license = license;
this.version = null;
}
protected SoftwareComponent(final Parcel in) {
this.name = in.readString();
this.license = in.readParcelable(License.class.getClassLoader());
this.copyrightOwner = in.readString();
this.link = in.readString();
this.years = in.readString();
this.version = in.readString();
}
public String getName() {
return name;
}
public String getYears() {
return years;
}
public String getCopyrightOwner() {
return copyrightOwner;
}
public String getLink() {
return link;
}
public String getVersion() {
return version;
}
public License getLicense() {
return license;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(final Parcel dest, final int flags) {
dest.writeString(name);
dest.writeParcelable(license, flags);
dest.writeString(copyrightOwner);
dest.writeString(link);
dest.writeString(years);
dest.writeString(version);
}
}

View file

@ -0,0 +1,16 @@
package org.schabi.newpipe.about
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
@Parcelize
class SoftwareComponent
@JvmOverloads
constructor(
val name: String,
val years: String,
val copyrightOwner: String,
val link: String,
val license: License,
val version: String? = null
) : Parcelable

View file

@ -9,7 +9,7 @@ import androidx.room.Update;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import io.reactivex.Flowable; import io.reactivex.rxjava3.core.Flowable;
@Dao @Dao
public interface BasicDAO<Entity> { public interface BasicDAO<Entity> {

View file

@ -6,19 +6,20 @@ import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import androidx.room.Transaction import androidx.room.Transaction
import androidx.room.Update import androidx.room.Update
import io.reactivex.Flowable import io.reactivex.rxjava3.core.Flowable
import java.time.OffsetDateTime
import org.schabi.newpipe.database.feed.model.FeedEntity import org.schabi.newpipe.database.feed.model.FeedEntity
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
import org.schabi.newpipe.database.stream.model.StreamEntity import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.subscription.SubscriptionEntity import org.schabi.newpipe.database.subscription.SubscriptionEntity
import java.time.OffsetDateTime
@Dao @Dao
abstract class FeedDAO { abstract class FeedDAO {
@Query("DELETE FROM feed") @Query("DELETE FROM feed")
abstract fun deleteAll(): Int abstract fun deleteAll(): Int
@Query(""" @Query(
"""
SELECT s.* FROM streams s SELECT s.* FROM streams s
INNER JOIN feed f INNER JOIN feed f
@ -27,10 +28,12 @@ abstract class FeedDAO {
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
LIMIT 500 LIMIT 500
""") """
)
abstract fun getAllStreams(): Flowable<List<StreamEntity>> abstract fun getAllStreams(): Flowable<List<StreamEntity>>
@Query(""" @Query(
"""
SELECT s.* FROM streams s SELECT s.* FROM streams s
INNER JOIN feed f INNER JOIN feed f
@ -46,10 +49,12 @@ abstract class FeedDAO {
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
LIMIT 500 LIMIT 500
""") """
)
abstract fun getAllStreamsFromGroup(groupId: Long): Flowable<List<StreamEntity>> abstract fun getAllStreamsFromGroup(groupId: Long): Flowable<List<StreamEntity>>
@Query(""" @Query(
"""
DELETE FROM feed WHERE DELETE FROM feed WHERE
feed.stream_id IN ( feed.stream_id IN (
@ -60,10 +65,12 @@ abstract class FeedDAO {
WHERE s.upload_date < :offsetDateTime WHERE s.upload_date < :offsetDateTime
) )
""") """
)
abstract fun unlinkStreamsOlderThan(offsetDateTime: OffsetDateTime) abstract fun unlinkStreamsOlderThan(offsetDateTime: OffsetDateTime)
@Query(""" @Query(
"""
DELETE FROM feed DELETE FROM feed
WHERE feed.subscription_id = :subscriptionId WHERE feed.subscription_id = :subscriptionId
@ -76,7 +83,8 @@ abstract class FeedDAO {
WHERE s.stream_type = "LIVE_STREAM" OR s.stream_type = "AUDIO_LIVE_STREAM" WHERE s.stream_type = "LIVE_STREAM" OR s.stream_type = "AUDIO_LIVE_STREAM"
) )
""") """
)
abstract fun unlinkOldLivestreams(subscriptionId: Long) abstract fun unlinkOldLivestreams(subscriptionId: Long)
@Insert(onConflict = OnConflictStrategy.IGNORE) @Insert(onConflict = OnConflictStrategy.IGNORE)
@ -100,12 +108,14 @@ abstract class FeedDAO {
} }
} }
@Query(""" @Query(
"""
SELECT MIN(lu.last_updated) FROM feed_last_updated lu SELECT MIN(lu.last_updated) FROM feed_last_updated lu
INNER JOIN feed_group_subscription_join fgs INNER JOIN feed_group_subscription_join fgs
ON fgs.subscription_id = lu.subscription_id AND fgs.group_id = :groupId ON fgs.subscription_id = lu.subscription_id AND fgs.group_id = :groupId
""") """
)
abstract fun oldestSubscriptionUpdate(groupId: Long): Flowable<List<OffsetDateTime>> abstract fun oldestSubscriptionUpdate(groupId: Long): Flowable<List<OffsetDateTime>>
@Query("SELECT MIN(last_updated) FROM feed_last_updated") @Query("SELECT MIN(last_updated) FROM feed_last_updated")
@ -114,7 +124,8 @@ abstract class FeedDAO {
@Query("SELECT COUNT(*) FROM feed_last_updated WHERE last_updated IS NULL") @Query("SELECT COUNT(*) FROM feed_last_updated WHERE last_updated IS NULL")
abstract fun notLoadedCount(): Flowable<Long> abstract fun notLoadedCount(): Flowable<Long>
@Query(""" @Query(
"""
SELECT COUNT(*) FROM subscriptions s SELECT COUNT(*) FROM subscriptions s
INNER JOIN feed_group_subscription_join fgs INNER JOIN feed_group_subscription_join fgs
@ -124,20 +135,24 @@ abstract class FeedDAO {
ON s.uid = lu.subscription_id ON s.uid = lu.subscription_id
WHERE lu.last_updated IS NULL WHERE lu.last_updated IS NULL
""") """
)
abstract fun notLoadedCountForGroup(groupId: Long): Flowable<Long> abstract fun notLoadedCountForGroup(groupId: Long): Flowable<Long>
@Query(""" @Query(
"""
SELECT s.* FROM subscriptions s SELECT s.* FROM subscriptions s
LEFT JOIN feed_last_updated lu LEFT JOIN feed_last_updated lu
ON s.uid = lu.subscription_id ON s.uid = lu.subscription_id
WHERE lu.last_updated IS NULL OR lu.last_updated < :outdatedThreshold WHERE lu.last_updated IS NULL OR lu.last_updated < :outdatedThreshold
""") """
)
abstract fun getAllOutdated(outdatedThreshold: OffsetDateTime): Flowable<List<SubscriptionEntity>> abstract fun getAllOutdated(outdatedThreshold: OffsetDateTime): Flowable<List<SubscriptionEntity>>
@Query(""" @Query(
"""
SELECT s.* FROM subscriptions s SELECT s.* FROM subscriptions s
INNER JOIN feed_group_subscription_join fgs INNER JOIN feed_group_subscription_join fgs
@ -147,6 +162,7 @@ abstract class FeedDAO {
ON s.uid = lu.subscription_id ON s.uid = lu.subscription_id
WHERE lu.last_updated IS NULL OR lu.last_updated < :outdatedThreshold WHERE lu.last_updated IS NULL OR lu.last_updated < :outdatedThreshold
""") """
)
abstract fun getAllOutdatedForGroup(groupId: Long, outdatedThreshold: OffsetDateTime): Flowable<List<SubscriptionEntity>> abstract fun getAllOutdatedForGroup(groupId: Long, outdatedThreshold: OffsetDateTime): Flowable<List<SubscriptionEntity>>
} }

View file

@ -6,8 +6,8 @@ import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import androidx.room.Transaction import androidx.room.Transaction
import androidx.room.Update import androidx.room.Update
import io.reactivex.Flowable import io.reactivex.rxjava3.core.Flowable
import io.reactivex.Maybe import io.reactivex.rxjava3.core.Maybe
import org.schabi.newpipe.database.feed.model.FeedGroupEntity import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity

View file

@ -10,21 +10,24 @@ import org.schabi.newpipe.database.feed.model.FeedEntity.Companion.SUBSCRIPTION_
import org.schabi.newpipe.database.stream.model.StreamEntity import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.subscription.SubscriptionEntity import org.schabi.newpipe.database.subscription.SubscriptionEntity
@Entity(tableName = FEED_TABLE, @Entity(
primaryKeys = [STREAM_ID, SUBSCRIPTION_ID], tableName = FEED_TABLE,
indices = [Index(SUBSCRIPTION_ID)], primaryKeys = [STREAM_ID, SUBSCRIPTION_ID],
foreignKeys = [ indices = [Index(SUBSCRIPTION_ID)],
ForeignKey( foreignKeys = [
entity = StreamEntity::class, ForeignKey(
parentColumns = [StreamEntity.STREAM_ID], entity = StreamEntity::class,
childColumns = [STREAM_ID], parentColumns = [StreamEntity.STREAM_ID],
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true), childColumns = [STREAM_ID],
ForeignKey( onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true
entity = SubscriptionEntity::class, ),
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID], ForeignKey(
childColumns = [SUBSCRIPTION_ID], entity = SubscriptionEntity::class,
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true) parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
] childColumns = [SUBSCRIPTION_ID],
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true
)
]
) )
data class FeedEntity( data class FeedEntity(
@ColumnInfo(name = STREAM_ID) @ColumnInfo(name = STREAM_ID)

View file

@ -9,8 +9,8 @@ import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.SORT_ORD
import org.schabi.newpipe.local.subscription.FeedGroupIcon import org.schabi.newpipe.local.subscription.FeedGroupIcon
@Entity( @Entity(
tableName = FEED_GROUP_TABLE, tableName = FEED_GROUP_TABLE,
indices = [Index(SORT_ORDER)] indices = [Index(SORT_ORDER)]
) )
data class FeedGroupEntity( data class FeedGroupEntity(
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)

View file

@ -11,22 +11,24 @@ import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity.Compan
import org.schabi.newpipe.database.subscription.SubscriptionEntity import org.schabi.newpipe.database.subscription.SubscriptionEntity
@Entity( @Entity(
tableName = FEED_GROUP_SUBSCRIPTION_TABLE, tableName = FEED_GROUP_SUBSCRIPTION_TABLE,
primaryKeys = [GROUP_ID, SUBSCRIPTION_ID], primaryKeys = [GROUP_ID, SUBSCRIPTION_ID],
indices = [Index(SUBSCRIPTION_ID)], indices = [Index(SUBSCRIPTION_ID)],
foreignKeys = [ foreignKeys = [
ForeignKey( ForeignKey(
entity = FeedGroupEntity::class, entity = FeedGroupEntity::class,
parentColumns = [FeedGroupEntity.ID], parentColumns = [FeedGroupEntity.ID],
childColumns = [GROUP_ID], childColumns = [GROUP_ID],
onDelete = CASCADE, onUpdate = CASCADE, deferred = true), onDelete = CASCADE, onUpdate = CASCADE, deferred = true
),
ForeignKey( ForeignKey(
entity = SubscriptionEntity::class, entity = SubscriptionEntity::class,
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID], parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
childColumns = [SUBSCRIPTION_ID], childColumns = [SUBSCRIPTION_ID],
onDelete = CASCADE, onUpdate = CASCADE, deferred = true) onDelete = CASCADE, onUpdate = CASCADE, deferred = true
] )
]
) )
data class FeedGroupSubscriptionEntity( data class FeedGroupSubscriptionEntity(
@ColumnInfo(name = GROUP_ID) @ColumnInfo(name = GROUP_ID)

View file

@ -4,20 +4,21 @@ import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey import androidx.room.ForeignKey
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import java.time.OffsetDateTime
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity.Companion.FEED_LAST_UPDATED_TABLE import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity.Companion.FEED_LAST_UPDATED_TABLE
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity.Companion.SUBSCRIPTION_ID import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity.Companion.SUBSCRIPTION_ID
import org.schabi.newpipe.database.subscription.SubscriptionEntity import org.schabi.newpipe.database.subscription.SubscriptionEntity
import java.time.OffsetDateTime
@Entity( @Entity(
tableName = FEED_LAST_UPDATED_TABLE, tableName = FEED_LAST_UPDATED_TABLE,
foreignKeys = [ foreignKeys = [
ForeignKey( ForeignKey(
entity = SubscriptionEntity::class, entity = SubscriptionEntity::class,
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID], parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
childColumns = [SUBSCRIPTION_ID], childColumns = [SUBSCRIPTION_ID],
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true) onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true
] )
]
) )
data class FeedLastUpdatedEntity( data class FeedLastUpdatedEntity(
@PrimaryKey @PrimaryKey

View file

@ -8,7 +8,7 @@ import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
import java.util.List; import java.util.List;
import io.reactivex.Flowable; import io.reactivex.rxjava3.core.Flowable;
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.CREATION_DATE; import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.CREATION_DATE;
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.ID; import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.ID;

View file

@ -10,7 +10,7 @@ import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
import java.util.List; import java.util.List;
import io.reactivex.Flowable; import io.reactivex.rxjava3.core.Flowable;
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID; import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID;
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE; import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE;
@ -20,6 +20,9 @@ import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_LA
import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_WATCH_COUNT; import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_WATCH_COUNT;
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID;
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_TIME;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
@Dao @Dao
public abstract class StreamHistoryDAO implements HistoryDAO<StreamHistoryEntity> { public abstract class StreamHistoryDAO implements HistoryDAO<StreamHistoryEntity> {
@ -73,6 +76,12 @@ public abstract class StreamHistoryDAO implements HistoryDAO<StreamHistoryEntity
+ " SUM(" + STREAM_REPEAT_COUNT + ") AS " + STREAM_WATCH_COUNT + " SUM(" + STREAM_REPEAT_COUNT + ") AS " + STREAM_WATCH_COUNT
+ " FROM " + STREAM_HISTORY_TABLE + " GROUP BY " + JOIN_STREAM_ID + ")" + " FROM " + STREAM_HISTORY_TABLE + " GROUP BY " + JOIN_STREAM_ID + ")"
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID) + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID
+ " LEFT JOIN "
+ "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", "
+ STREAM_PROGRESS_TIME
+ " FROM " + STREAM_STATE_TABLE + " )"
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS)
public abstract Flowable<List<StreamStatisticsEntry>> getStatistics(); public abstract Flowable<List<StreamStatisticsEntry>> getStatistics();
} }

View file

@ -2,8 +2,8 @@ package org.schabi.newpipe.database.history.model
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Embedded import androidx.room.Embedded
import java.time.OffsetDateTime
import org.schabi.newpipe.database.stream.model.StreamEntity import org.schabi.newpipe.database.stream.model.StreamEntity
import java.time.OffsetDateTime
data class StreamHistoryEntry( data class StreamHistoryEntry(
@Embedded @Embedded
@ -25,6 +25,6 @@ data class StreamHistoryEntry(
fun hasEqualValues(other: StreamHistoryEntry): Boolean { fun hasEqualValues(other: StreamHistoryEntry): Boolean {
return this.streamEntity.uid == other.streamEntity.uid && streamId == other.streamId && return this.streamEntity.uid == other.streamEntity.uid && streamId == other.streamId &&
accessDate.isEqual(other.accessDate) accessDate.isEqual(other.accessDate)
} }
} }

View file

@ -5,12 +5,16 @@ import androidx.room.Embedded
import org.schabi.newpipe.database.LocalItem import org.schabi.newpipe.database.LocalItem
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity
import org.schabi.newpipe.database.stream.model.StreamEntity import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.stream.model.StreamStateEntity
import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.extractor.stream.StreamInfoItem
class PlaylistStreamEntry( class PlaylistStreamEntry(
@Embedded @Embedded
val streamEntity: StreamEntity, val streamEntity: StreamEntity,
@ColumnInfo(name = StreamStateEntity.STREAM_PROGRESS_TIME, defaultValue = "0")
val progressTime: Long,
@ColumnInfo(name = PlaylistStreamEntity.JOIN_STREAM_ID) @ColumnInfo(name = PlaylistStreamEntity.JOIN_STREAM_ID)
val streamId: Long, val streamId: Long,

View file

@ -8,7 +8,7 @@ import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
import java.util.List; import java.util.List;
import io.reactivex.Flowable; import io.reactivex.rxjava3.core.Flowable;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID; import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE; import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;

View file

@ -9,7 +9,7 @@ import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import java.util.List; import java.util.List;
import io.reactivex.Flowable; import io.reactivex.rxjava3.core.Flowable;
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_ID; import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_ID;
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_SERVICE_ID; import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_SERVICE_ID;

View file

@ -11,7 +11,7 @@ import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
import java.util.List; import java.util.List;
import io.reactivex.Flowable; import io.reactivex.rxjava3.core.Flowable;
import static org.schabi.newpipe.database.playlist.PlaylistMetadataEntry.PLAYLIST_STREAM_COUNT; import static org.schabi.newpipe.database.playlist.PlaylistMetadataEntry.PLAYLIST_STREAM_COUNT;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID; import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
@ -24,6 +24,9 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JO
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE; import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE;
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID;
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_TIME;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
@Dao @Dao
public abstract class PlaylistStreamDAO implements BasicDAO<PlaylistStreamEntity> { public abstract class PlaylistStreamDAO implements BasicDAO<PlaylistStreamEntity> {
@ -58,6 +61,13 @@ public abstract class PlaylistStreamDAO implements BasicDAO<PlaylistStreamEntity
// then merge with the stream metadata // then merge with the stream metadata
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID
+ " LEFT JOIN "
+ "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", "
+ STREAM_PROGRESS_TIME
+ " FROM " + STREAM_STATE_TABLE + " )"
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS
+ " ORDER BY " + JOIN_INDEX + " ASC") + " ORDER BY " + JOIN_INDEX + " ASC")
public abstract Flowable<List<PlaylistStreamEntry>> getOrderedStreamsOf(long playlistId); public abstract Flowable<List<PlaylistStreamEntry>> getOrderedStreamsOf(long playlistId);

View file

@ -2,23 +2,27 @@ package org.schabi.newpipe.database.stream
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Embedded import androidx.room.Embedded
import java.time.OffsetDateTime
import org.schabi.newpipe.database.LocalItem import org.schabi.newpipe.database.LocalItem
import org.schabi.newpipe.database.history.model.StreamHistoryEntity import org.schabi.newpipe.database.history.model.StreamHistoryEntity
import org.schabi.newpipe.database.stream.model.StreamEntity import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_TIME
import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.extractor.stream.StreamInfoItem
import java.time.OffsetDateTime
class StreamStatisticsEntry( class StreamStatisticsEntry(
@Embedded @Embedded
val streamEntity: StreamEntity, val streamEntity: StreamEntity,
@ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID) @ColumnInfo(name = STREAM_PROGRESS_TIME, defaultValue = "0")
val progressTime: Long,
@ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID)
val streamId: Long, val streamId: Long,
@ColumnInfo(name = STREAM_LATEST_DATE) @ColumnInfo(name = STREAM_LATEST_DATE)
val latestAccessDate: OffsetDateTime, val latestAccessDate: OffsetDateTime,
@ColumnInfo(name = STREAM_WATCH_COUNT) @ColumnInfo(name = STREAM_WATCH_COUNT)
val watchCount: Long val watchCount: Long
) : LocalItem { ) : LocalItem {
fun toStreamInfoItem(): StreamInfoItem { fun toStreamInfoItem(): StreamInfoItem {

View file

@ -6,14 +6,14 @@ import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import androidx.room.Transaction import androidx.room.Transaction
import io.reactivex.Flowable import io.reactivex.rxjava3.core.Flowable
import java.time.OffsetDateTime
import org.schabi.newpipe.database.BasicDAO import org.schabi.newpipe.database.BasicDAO
import org.schabi.newpipe.database.stream.model.StreamEntity import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID
import org.schabi.newpipe.extractor.stream.StreamType import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM
import org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM import org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM
import java.time.OffsetDateTime
@Dao @Dao
abstract class StreamDAO : BasicDAO<StreamEntity> { abstract class StreamDAO : BasicDAO<StreamEntity> {
@ -35,10 +35,12 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
@Insert(onConflict = OnConflictStrategy.IGNORE) @Insert(onConflict = OnConflictStrategy.IGNORE)
internal abstract fun silentInsertAllInternal(streams: List<StreamEntity>): List<Long> internal abstract fun silentInsertAllInternal(streams: List<StreamEntity>): List<Long>
@Query(""" @Query(
"""
SELECT uid, stream_type, textual_upload_date, upload_date, is_upload_date_approximation, duration SELECT uid, stream_type, textual_upload_date, upload_date, is_upload_date_approximation, duration
FROM streams WHERE url = :url AND service_id = :serviceId FROM streams WHERE url = :url AND service_id = :serviceId
""") """
)
internal abstract fun getMinimalStreamForCompare(serviceId: Int, url: String): StreamCompareFeed? internal abstract fun getMinimalStreamForCompare(serviceId: Int, url: String): StreamCompareFeed?
@Transaction @Transaction
@ -79,7 +81,7 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
private fun compareAndUpdateStream(newerStream: StreamEntity) { private fun compareAndUpdateStream(newerStream: StreamEntity) {
val existentMinimalStream = getMinimalStreamForCompare(newerStream.serviceId, newerStream.url) val existentMinimalStream = getMinimalStreamForCompare(newerStream.serviceId, newerStream.url)
?: throw IllegalStateException("Stream cannot be null just after insertion.") ?: throw IllegalStateException("Stream cannot be null just after insertion.")
newerStream.uid = existentMinimalStream.uid newerStream.uid = existentMinimalStream.uid
val isNewerStreamLive = newerStream.streamType == AUDIO_LIVE_STREAM || newerStream.streamType == LIVE_STREAM val isNewerStreamLive = newerStream.streamType == AUDIO_LIVE_STREAM || newerStream.streamType == LIVE_STREAM
@ -88,7 +90,7 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
// Use the existent upload date if the newer stream does not have a better precision // Use the existent upload date if the newer stream does not have a better precision
// (i.e. is an approximation). This is done to prevent unnecessary changes. // (i.e. is an approximation). This is done to prevent unnecessary changes.
val hasBetterPrecision = val hasBetterPrecision =
newerStream.uploadDate != null && newerStream.isUploadDateApproximation != true newerStream.uploadDate != null && newerStream.isUploadDateApproximation != true
if (existentMinimalStream.uploadDate != null && !hasBetterPrecision) { if (existentMinimalStream.uploadDate != null && !hasBetterPrecision) {
newerStream.uploadDate = existentMinimalStream.uploadDate newerStream.uploadDate = existentMinimalStream.uploadDate
newerStream.textualUploadDate = existentMinimalStream.textualUploadDate newerStream.textualUploadDate = existentMinimalStream.textualUploadDate
@ -101,7 +103,8 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
} }
} }
@Query(""" @Query(
"""
DELETE FROM streams WHERE DELETE FROM streams WHERE
NOT EXISTS (SELECT 1 FROM stream_history sh NOT EXISTS (SELECT 1 FROM stream_history sh
@ -112,7 +115,8 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
AND NOT EXISTS (SELECT 1 FROM feed f AND NOT EXISTS (SELECT 1 FROM feed f
WHERE f.stream_id = streams.uid) WHERE f.stream_id = streams.uid)
""") """
)
abstract fun deleteOrphans(): Int abstract fun deleteOrphans(): Int
/** /**

View file

@ -11,7 +11,7 @@ import org.schabi.newpipe.database.stream.model.StreamStateEntity;
import java.util.List; import java.util.List;
import io.reactivex.Flowable; import io.reactivex.rxjava3.core.Flowable;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID; import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE; import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;

View file

@ -5,8 +5,6 @@ import androidx.room.Entity
import androidx.room.Ignore import androidx.room.Ignore
import androidx.room.Index import androidx.room.Index
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import java.io.Serializable
import java.time.OffsetDateTime
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_SERVICE_ID import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_SERVICE_ID
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_TABLE import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_TABLE
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_URL import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_URL
@ -15,11 +13,14 @@ import org.schabi.newpipe.extractor.stream.StreamInfo
import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.extractor.stream.StreamType import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.player.playqueue.PlayQueueItem import org.schabi.newpipe.player.playqueue.PlayQueueItem
import java.io.Serializable
import java.time.OffsetDateTime
@Entity(tableName = STREAM_TABLE, @Entity(
indices = [ tableName = STREAM_TABLE,
Index(value = [STREAM_SERVICE_ID, STREAM_URL], unique = true) indices = [
] Index(value = [STREAM_SERVICE_ID, STREAM_URL], unique = true)
]
) )
data class StreamEntity( data class StreamEntity(
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
@ -61,27 +62,27 @@ data class StreamEntity(
) : Serializable { ) : Serializable {
@Ignore @Ignore
constructor(item: StreamInfoItem) : this( constructor(item: StreamInfoItem) : this(
serviceId = item.serviceId, url = item.url, title = item.name, serviceId = item.serviceId, url = item.url, title = item.name,
streamType = item.streamType, duration = item.duration, uploader = item.uploaderName, streamType = item.streamType, duration = item.duration, uploader = item.uploaderName,
thumbnailUrl = item.thumbnailUrl, viewCount = item.viewCount, thumbnailUrl = item.thumbnailUrl, viewCount = item.viewCount,
textualUploadDate = item.textualUploadDate, uploadDate = item.uploadDate?.offsetDateTime(), textualUploadDate = item.textualUploadDate, uploadDate = item.uploadDate?.offsetDateTime(),
isUploadDateApproximation = item.uploadDate?.isApproximation isUploadDateApproximation = item.uploadDate?.isApproximation
) )
@Ignore @Ignore
constructor(info: StreamInfo) : this( constructor(info: StreamInfo) : this(
serviceId = info.serviceId, url = info.url, title = info.name, serviceId = info.serviceId, url = info.url, title = info.name,
streamType = info.streamType, duration = info.duration, uploader = info.uploaderName, streamType = info.streamType, duration = info.duration, uploader = info.uploaderName,
thumbnailUrl = info.thumbnailUrl, viewCount = info.viewCount, thumbnailUrl = info.thumbnailUrl, viewCount = info.viewCount,
textualUploadDate = info.textualUploadDate, uploadDate = info.uploadDate?.offsetDateTime(), textualUploadDate = info.textualUploadDate, uploadDate = info.uploadDate?.offsetDateTime(),
isUploadDateApproximation = info.uploadDate?.isApproximation isUploadDateApproximation = info.uploadDate?.isApproximation
) )
@Ignore @Ignore
constructor(item: PlayQueueItem) : this( constructor(item: PlayQueueItem) : this(
serviceId = item.serviceId, url = item.url, title = item.title, serviceId = item.serviceId, url = item.url, title = item.title,
streamType = item.streamType, duration = item.duration, uploader = item.uploader, streamType = item.streamType, duration = item.duration, uploader = item.uploader,
thumbnailUrl = item.thumbnailUrl thumbnailUrl = item.thumbnailUrl
) )
fun toStreamInfoItem(): StreamInfoItem { fun toStreamInfoItem(): StreamInfoItem {

View file

@ -22,6 +22,9 @@ import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_
public class StreamStateEntity { public class StreamStateEntity {
public static final String STREAM_STATE_TABLE = "stream_state"; public static final String STREAM_STATE_TABLE = "stream_state";
public static final String JOIN_STREAM_ID = "stream_id"; public static final String JOIN_STREAM_ID = "stream_id";
// This additional field is required for the SQL query because 'stream_id' is used
// for some other joins already
public static final String JOIN_STREAM_ID_ALIAS = "stream_id_alias";
public static final String STREAM_PROGRESS_TIME = "progress_time"; public static final String STREAM_PROGRESS_TIME = "progress_time";
/** /**

View file

@ -5,8 +5,8 @@ import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import androidx.room.Transaction import androidx.room.Transaction
import io.reactivex.Flowable import io.reactivex.rxjava3.core.Flowable
import io.reactivex.Maybe import io.reactivex.rxjava3.core.Maybe
import org.schabi.newpipe.database.BasicDAO import org.schabi.newpipe.database.BasicDAO
@Dao @Dao
@ -20,16 +20,19 @@ abstract class SubscriptionDAO : BasicDAO<SubscriptionEntity> {
@Query("SELECT * FROM subscriptions ORDER BY name COLLATE NOCASE ASC") @Query("SELECT * FROM subscriptions ORDER BY name COLLATE NOCASE ASC")
abstract override fun getAll(): Flowable<List<SubscriptionEntity>> abstract override fun getAll(): Flowable<List<SubscriptionEntity>>
@Query(""" @Query(
"""
SELECT * FROM subscriptions SELECT * FROM subscriptions
WHERE name LIKE '%' || :filter || '%' WHERE name LIKE '%' || :filter || '%'
ORDER BY name COLLATE NOCASE ASC ORDER BY name COLLATE NOCASE ASC
""") """
)
abstract fun getSubscriptionsFiltered(filter: String): Flowable<List<SubscriptionEntity>> abstract fun getSubscriptionsFiltered(filter: String): Flowable<List<SubscriptionEntity>>
@Query(""" @Query(
"""
SELECT * FROM subscriptions s SELECT * FROM subscriptions s
LEFT JOIN feed_group_subscription_join fgs LEFT JOIN feed_group_subscription_join fgs
@ -38,12 +41,14 @@ abstract class SubscriptionDAO : BasicDAO<SubscriptionEntity> {
WHERE (fgs.subscription_id IS NULL OR fgs.group_id = :currentGroupId) WHERE (fgs.subscription_id IS NULL OR fgs.group_id = :currentGroupId)
ORDER BY name COLLATE NOCASE ASC ORDER BY name COLLATE NOCASE ASC
""") """
)
abstract fun getSubscriptionsOnlyUngrouped( abstract fun getSubscriptionsOnlyUngrouped(
currentGroupId: Long currentGroupId: Long
): Flowable<List<SubscriptionEntity>> ): Flowable<List<SubscriptionEntity>>
@Query(""" @Query(
"""
SELECT * FROM subscriptions s SELECT * FROM subscriptions s
LEFT JOIN feed_group_subscription_join fgs LEFT JOIN feed_group_subscription_join fgs
@ -53,7 +58,8 @@ abstract class SubscriptionDAO : BasicDAO<SubscriptionEntity> {
AND s.name LIKE '%' || :filter || '%' AND s.name LIKE '%' || :filter || '%'
ORDER BY name COLLATE NOCASE ASC ORDER BY name COLLATE NOCASE ASC
""") """
)
abstract fun getSubscriptionsOnlyUngroupedFiltered( abstract fun getSubscriptionsOnlyUngroupedFiltered(
currentGroupId: Long, currentGroupId: Long,
filter: String filter: String

View file

@ -10,7 +10,6 @@ import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Environment; import android.os.Environment;
import android.os.IBinder; import android.os.IBinder;
import androidx.preference.PreferenceManager;
import android.util.Log; import android.util.Log;
import android.util.SparseArray; import android.util.SparseArray;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@ -34,6 +33,7 @@ import androidx.appcompat.view.menu.ActionMenuItemView;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
import androidx.documentfile.provider.DocumentFile; import androidx.documentfile.provider.DocumentFile;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import androidx.preference.PreferenceManager;
import com.nononsenseapps.filepicker.Utils; import com.nononsenseapps.filepicker.Utils;
@ -49,6 +49,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.SubtitlesStream; import org.schabi.newpipe.extractor.stream.SubtitlesStream;
import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.settings.NewPipeSettings; import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.util.FilePickerActivityHelper; import org.schabi.newpipe.util.FilePickerActivityHelper;
@ -69,7 +70,7 @@ import java.util.Locale;
import icepick.Icepick; import icepick.Icepick;
import icepick.State; import icepick.State;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
import us.shandian.giga.get.MissionRecoveryInfo; import us.shandian.giga.get.MissionRecoveryInfo;
import us.shandian.giga.io.StoredDirectoryHelper; import us.shandian.giga.io.StoredDirectoryHelper;
import us.shandian.giga.io.StoredFileHelper; import us.shandian.giga.io.StoredFileHelper;
@ -602,7 +603,7 @@ public class DownloadDialog extends DialogFragment
Collections.singletonList(e), Collections.singletonList(e),
null, null,
null, null,
ErrorActivity.ErrorInfo ErrorInfo
.make(UserAction.SOMETHING_ELSE, "-", "-", R.string.general_error) .make(UserAction.SOMETHING_ELSE, "-", "-", R.string.general_error)
); );
} }

View file

@ -13,7 +13,7 @@ import android.widget.Toast;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import com.jakewharton.rxbinding2.view.RxView; import com.jakewharton.rxbinding4.view.RxView;
import org.schabi.newpipe.BaseFragment; import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.MainActivity;
@ -23,6 +23,7 @@ import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException; import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.ExceptionUtils; import org.schabi.newpipe.util.ExceptionUtils;
import org.schabi.newpipe.util.InfoCache; import org.schabi.newpipe.util.InfoCache;
@ -33,7 +34,8 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import icepick.State; import icepick.State;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.Disposable;
import static org.schabi.newpipe.util.AnimationUtils.animateView; import static org.schabi.newpipe.util.AnimationUtils.animateView;
@ -47,6 +49,8 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
@Nullable @Nullable
private ProgressBar loadingProgressBar; private ProgressBar loadingProgressBar;
private Disposable errorDisposable;
protected View errorPanelRoot; protected View errorPanelRoot;
private Button errorButtonRetry; private Button errorButtonRetry;
private TextView errorTextView; private TextView errorTextView;
@ -63,6 +67,14 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
wasLoading.set(isLoading.get()); wasLoading.set(isLoading.get());
} }
@Override
public void onDestroy() {
super.onDestroy();
if (errorDisposable != null) {
errorDisposable.dispose();
}
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Init // Init
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -82,7 +94,7 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
@Override @Override
protected void initListeners() { protected void initListeners() {
super.initListeners(); super.initListeners();
RxView.clicks(errorButtonRetry) errorDisposable = RxView.clicks(errorButtonRetry)
.debounce(300, TimeUnit.MILLISECONDS) .debounce(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(o -> onRetryButtonClicked()); .subscribe(o -> onRetryButtonClicked());
@ -252,7 +264,7 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
} }
ErrorActivity.reportError(getContext(), exception, MainActivity.class, null, ErrorActivity.reportError(getContext(), exception, MainActivity.class, null,
ErrorActivity.ErrorInfo.make(userAction, serviceName == null ? "none" : serviceName, ErrorInfo.make(userAction, serviceName == null ? "none" : serviceName,
request == null ? "none" : request, errorId)); request == null ? "none" : request, errorId));
} }
@ -265,7 +277,7 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
/** /**
* Show a SnackBar and only call * Show a SnackBar and only call
* {@link ErrorActivity#reportError(Context, List, Class, View, ErrorActivity.ErrorInfo)} * {@link ErrorActivity#reportError(Context, List, Class, View, ErrorInfo)}
* IF we a find a valid view (otherwise the error screen appears). * IF we a find a valid view (otherwise the error screen appears).
* *
* @param exception List of the exceptions to show * @param exception List of the exceptions to show
@ -291,6 +303,6 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
} }
ErrorActivity.reportError(getContext(), exception, MainActivity.class, rootView, ErrorActivity.reportError(getContext(), exception, MainActivity.class, rootView,
ErrorActivity.ErrorInfo.make(userAction, serviceName, request, errorId)); ErrorInfo.make(userAction, serviceName, request, errorId));
} }
} }

View file

@ -27,6 +27,7 @@ import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.settings.tabs.Tab; import org.schabi.newpipe.settings.tabs.Tab;
import org.schabi.newpipe.settings.tabs.TabsManager; import org.schabi.newpipe.settings.tabs.TabsManager;
@ -43,7 +44,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
private SelectedTabsPagerAdapter pagerAdapter; private SelectedTabsPagerAdapter pagerAdapter;
private ScrollableTabLayout tabLayout; private ScrollableTabLayout tabLayout;
private List<Tab> tabsList = new ArrayList<>(); private final List<Tab> tabsList = new ArrayList<>();
private TabsManager tabsManager; private TabsManager tabsManager;
private boolean hasTabsChanged = false; private boolean hasTabsChanged = false;
@ -242,7 +243,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
} }
if (throwable != null) { if (throwable != null) {
ErrorActivity.reportError(context, throwable, null, null, ErrorActivity.ErrorInfo ErrorActivity.reportError(context, throwable, null, null, ErrorInfo
.make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash)); .make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash));
return new BlankFragment(); return new BlankFragment();
} }

View file

@ -14,12 +14,10 @@ public abstract class OnScrollBelowItemsListener extends RecyclerView.OnScrollLi
super.onScrolled(recyclerView, dx, dy); super.onScrolled(recyclerView, dx, dy);
if (dy > 0) { if (dy > 0) {
int pastVisibleItems = 0; int pastVisibleItems = 0;
final int visibleItemCount;
final int totalItemCount;
final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
visibleItemCount = layoutManager.getChildCount(); final int visibleItemCount = layoutManager.getChildCount();
totalItemCount = layoutManager.getItemCount(); final int totalItemCount = layoutManager.getItemCount();
// Already covers the GridLayoutManager case // Already covers the GridLayoutManager case
if (layoutManager instanceof LinearLayoutManager) { if (layoutManager instanceof LinearLayoutManager) {

View file

@ -15,11 +15,7 @@ import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.view.ViewTreeObserver;
import androidx.core.text.HtmlCompat;
import androidx.preference.PreferenceManager;
import android.provider.Settings; import android.provider.Settings;
import android.text.Spanned;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.util.Linkify; import android.text.util.Linkify;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
@ -28,6 +24,7 @@ import android.view.LayoutInflater;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowManager; import android.view.WindowManager;
import android.view.animation.DecelerateInterpolator; import android.view.animation.DecelerateInterpolator;
import android.widget.FrameLayout; import android.widget.FrameLayout;
@ -46,7 +43,9 @@ import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.core.text.HtmlCompat;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.preference.PreferenceManager;
import androidx.viewpager.widget.ViewPager; import androidx.viewpager.widget.ViewPager;
import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlaybackException;
@ -93,6 +92,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.DeviceUtils;
@ -116,11 +116,11 @@ import java.util.concurrent.TimeUnit;
import icepick.State; import icepick.State;
import io.noties.markwon.Markwon; import io.noties.markwon.Markwon;
import io.noties.markwon.linkify.LinkifyPlugin; import io.noties.markwon.linkify.LinkifyPlugin;
import io.reactivex.Single; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Single;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS; import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS;
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT; import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
@ -731,7 +731,7 @@ public final class VideoDetailFragment
} }
private View.OnTouchListener getOnControlsTouchListener() { private View.OnTouchListener getOnControlsTouchListener() {
return (View view, MotionEvent motionEvent) -> { return (view, motionEvent) -> {
if (!PreferenceManager.getDefaultSharedPreferences(activity) if (!PreferenceManager.getDefaultSharedPreferences(activity)
.getBoolean(getString(R.string.show_hold_to_append_key), true)) { .getBoolean(getString(R.string.show_hold_to_append_key), true)) {
return false; return false;
@ -948,7 +948,7 @@ public final class VideoDetailFragment
currentWorker = ExtractorHelper.getStreamInfo(serviceId, url, forceLoad) currentWorker = ExtractorHelper.getStreamInfo(serviceId, url, forceLoad)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe((@NonNull final StreamInfo result) -> { .subscribe(result -> {
isLoading.set(false); isLoading.set(false);
hideMainPlayer(); hideMainPlayer();
if (result.getAgeLimit() != NO_AGE_LIMIT && !prefs.getBoolean( if (result.getAgeLimit() != NO_AGE_LIMIT && !prefs.getBoolean(
@ -969,7 +969,7 @@ public final class VideoDetailFragment
openVideoPlayer(); openVideoPlayer();
} }
} }
}, (@NonNull final Throwable throwable) -> { }, throwable -> {
isLoading.set(false); isLoading.set(false);
onError(throwable); onError(throwable);
}); });
@ -1140,7 +1140,7 @@ public final class VideoDetailFragment
PlayQueue queue = playQueue; PlayQueue queue = playQueue;
// Size can be 0 because queue removes bad stream automatically when error occurs // Size can be 0 because queue removes bad stream automatically when error occurs
if (queue == null || queue.size() == 0) { if (queue == null || queue.isEmpty()) {
queue = new SinglePlayQueue(currentInfo); queue = new SinglePlayQueue(currentInfo);
} }
@ -1224,12 +1224,12 @@ public final class VideoDetailFragment
if (description.getType() == Description.HTML) { if (description.getType() == Description.HTML) {
disposables.add(Single.just(description.getContent()) disposables.add(Single.just(description.getContent())
.map((@NonNull final String descriptionText) -> .map(descriptionText ->
HtmlCompat.fromHtml(descriptionText, HtmlCompat.fromHtml(descriptionText,
HtmlCompat.FROM_HTML_MODE_LEGACY)) HtmlCompat.FROM_HTML_MODE_LEGACY))
.subscribeOn(Schedulers.computation()) .subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe((@NonNull final Spanned spanned) -> { .subscribe(spanned -> {
videoDescriptionView.setText(spanned); videoDescriptionView.setText(spanned);
videoDescriptionView.setVisibility(View.VISIBLE); videoDescriptionView.setVisibility(View.VISIBLE);
})); }));
@ -1346,19 +1346,24 @@ public final class VideoDetailFragment
broadcastReceiver = new BroadcastReceiver() { broadcastReceiver = new BroadcastReceiver() {
@Override @Override
public void onReceive(final Context context, final Intent intent) { public void onReceive(final Context context, final Intent intent) {
if (intent.getAction().equals(ACTION_SHOW_MAIN_PLAYER)) { switch (intent.getAction()) {
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); case ACTION_SHOW_MAIN_PLAYER:
} else if (intent.getAction().equals(ACTION_HIDE_MAIN_PLAYER)) { bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); break;
} else if (intent.getAction().equals(ACTION_PLAYER_STARTED)) { case ACTION_HIDE_MAIN_PLAYER:
// If the state is not hidden we don't need to show the mini player bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_HIDDEN) { break;
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); case ACTION_PLAYER_STARTED:
} // If the state is not hidden we don't need to show the mini player
// Rebound to the service if it was closed via notification or mini player if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_HIDDEN) {
if (!PlayerHolder.bound) { bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
PlayerHolder.startService(App.getApp(), false, VideoDetailFragment.this); }
} // Rebound to the service if it was closed via notification or mini player
if (!PlayerHolder.bound) {
PlayerHolder.startService(
App.getApp(), false, VideoDetailFragment.this);
}
break;
} }
} }
}; };
@ -1626,7 +1631,7 @@ public final class VideoDetailFragment
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog"); downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
} catch (final Exception e) { } catch (final Exception e) {
final ErrorActivity.ErrorInfo info = ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, final ErrorInfo info = ErrorInfo.make(UserAction.UI_ERROR,
ServiceList.all() ServiceList.all()
.get(currentInfo .get(currentInfo
.getServiceId()) .getServiceId())

View file

@ -48,7 +48,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
implements ListViewContract<I, N>, StateSaver.WriteRead, implements ListViewContract<I, N>, StateSaver.WriteRead,
SharedPreferences.OnSharedPreferenceChangeListener { SharedPreferences.OnSharedPreferenceChangeListener {
private static final int LIST_MODE_UPDATE_FLAG = 0x32; private static final int LIST_MODE_UPDATE_FLAG = 0x32;
protected StateSaver.SavedState savedState; protected org.schabi.newpipe.util.SavedState savedState;
private boolean useDefaultStateSaving = true; private boolean useDefaultStateSaving = true;
private int updateFlags = 0; private int updateFlags = 0;

View file

@ -16,10 +16,10 @@ import org.schabi.newpipe.views.NewPipeRecyclerView;
import java.util.Queue; import java.util.Queue;
import icepick.State; import icepick.State;
import io.reactivex.Single; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Single;
import io.reactivex.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
public abstract class BaseListInfoFragment<I extends ListInfo> public abstract class BaseListInfoFragment<I extends ListInfo>
extends BaseListFragment<I, ListExtractor.InfoItemsPage> { extends BaseListFragment<I, ListExtractor.InfoItemsPage> {
@ -204,7 +204,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
name = result.getName(); name = result.getName();
setTitle(name); setTitle(name);
if (infoListAdapter.getItemsList().size() == 0) { if (infoListAdapter.getItemsList().isEmpty()) {
if (result.getRelatedItems().size() > 0) { if (result.getRelatedItems().size() > 0) {
infoListAdapter.addInfoItemList(result.getRelatedItems()); infoListAdapter.addInfoItemList(result.getRelatedItems());
showListFooter(hasMoreItems()); showListFooter(hasMoreItems());

View file

@ -24,7 +24,7 @@ import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import com.jakewharton.rxbinding2.view.RxView; import com.jakewharton.rxbinding4.view.RxView;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.database.subscription.SubscriptionEntity; import org.schabi.newpipe.database.subscription.SubscriptionEntity;
@ -53,15 +53,15 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import io.reactivex.Observable; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.Single; import io.reactivex.rxjava3.core.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Single;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.functions.Action; import io.reactivex.rxjava3.functions.Action;
import io.reactivex.functions.Consumer; import io.reactivex.rxjava3.functions.Consumer;
import io.reactivex.functions.Function; import io.reactivex.rxjava3.functions.Function;
import io.reactivex.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
import static org.schabi.newpipe.util.AnimationUtils.animateBackgroundColor; import static org.schabi.newpipe.util.AnimationUtils.animateBackgroundColor;
import static org.schabi.newpipe.util.AnimationUtils.animateTextColor; import static org.schabi.newpipe.util.AnimationUtils.animateTextColor;

View file

@ -20,11 +20,11 @@ import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
import io.reactivex.Single; import io.reactivex.rxjava3.core.Single;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> { public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
private CompositeDisposable disposables = new CompositeDisposable(); private final CompositeDisposable disposables = new CompositeDisposable();
public static CommentsFragment getInstance(final int serviceId, final String url, public static CommentsFragment getInstance(final int serviceId, final String url,
final String name) { final String name) {

View file

@ -26,7 +26,7 @@ import org.schabi.newpipe.util.KioskTranslator;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import icepick.State; import icepick.State;
import io.reactivex.Single; import io.reactivex.rxjava3.core.Single;
import static org.schabi.newpipe.util.AnimationUtils.animateView; import static org.schabi.newpipe.util.AnimationUtils.animateView;

View file

@ -51,12 +51,11 @@ import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import io.reactivex.Flowable; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.Single; import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Single;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.disposables.Disposables;
import static org.schabi.newpipe.util.AnimationUtils.animateView; import static org.schabi.newpipe.util.AnimationUtils.animateView;
@ -460,7 +459,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
.doFinally(() -> playlistEntity = null) .doFinally(() -> playlistEntity = null)
.subscribe(ignored -> { /* Do nothing */ }, this::onError); .subscribe(ignored -> { /* Do nothing */ }, this::onError);
} else { } else {
action = Disposables.empty(); action = Disposable.empty();
} }
disposables.add(action); disposables.add(action);

View file

@ -49,6 +49,7 @@ import org.schabi.newpipe.fragments.BackPressable;
import org.schabi.newpipe.fragments.list.BaseListFragment; import org.schabi.newpipe.fragments.list.BaseListFragment;
import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.Constants;
@ -67,13 +68,13 @@ import java.util.Queue;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import icepick.State; import icepick.State;
import io.reactivex.Flowable; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.Observable; import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Observable;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
import io.reactivex.subjects.PublishSubject; import io.reactivex.rxjava3.subjects.PublishSubject;
import static androidx.recyclerview.widget.ItemTouchHelper.Callback.makeMovementFlags; import static androidx.recyclerview.widget.ItemTouchHelper.Callback.makeMovementFlags;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
@ -248,7 +249,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
} catch (final Exception e) { } catch (final Exception e) {
ErrorActivity.reportError(getActivity(), e, requireActivity().getClass(), ErrorActivity.reportError(getActivity(), e, requireActivity().getClass(),
requireActivity().findViewById(android.R.id.content), requireActivity().findViewById(android.R.id.content),
ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, ErrorInfo.make(UserAction.UI_ERROR,
"", "",
"", R.string.general_error)); "", R.string.general_error));
} }
@ -256,7 +257,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
if (!TextUtils.isEmpty(searchString)) { if (!TextUtils.isEmpty(searchString)) {
if (wasLoading.getAndSet(false)) { if (wasLoading.getAndSet(false)) {
search(searchString, contentFilter, sortFilter); search(searchString, contentFilter, sortFilter);
} else if (infoListAdapter.getItemsList().size() == 0) { } else if (infoListAdapter.getItemsList().isEmpty()) {
if (savedState == null) { if (savedState == null) {
search(searchString, contentFilter, sortFilter); search(searchString, contentFilter, sortFilter);
} else if (!isLoading.get() && !wasSearchFocused) { } else if (!isLoading.get() && !wasSearchFocused) {
@ -708,7 +709,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
final Observable<String> observable = suggestionPublisher final Observable<String> observable = suggestionPublisher
.debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS) .debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS)
.startWith(searchString != null .startWithItem(searchString != null
? searchString ? searchString
: "") : "")
.filter(ss -> isSuggestionsEnabled); .filter(ss -> isSuggestionsEnabled);
@ -977,7 +978,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
lastSearchedString = searchString; lastSearchedString = searchString;
nextPage = result.getNextPage(); nextPage = result.getNextPage();
if (infoListAdapter.getItemsList().size() == 0) { if (infoListAdapter.getItemsList().isEmpty()) {
if (!result.getRelatedItems().isEmpty()) { if (!result.getRelatedItems().isEmpty()) {
infoListAdapter.addInfoItemList(result.getRelatedItems()); infoListAdapter.addInfoItemList(result.getRelatedItems());
} else { } else {

View file

@ -3,7 +3,6 @@ package org.schabi.newpipe.fragments.list.videos;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Bundle; import android.os.Bundle;
import androidx.preference.PreferenceManager;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
@ -13,6 +12,7 @@ import android.widget.Switch;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.ListExtractor;
@ -25,13 +25,13 @@ import org.schabi.newpipe.util.RelatedStreamInfo;
import java.io.Serializable; import java.io.Serializable;
import io.reactivex.Single; import io.reactivex.rxjava3.core.Single;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInfo> public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInfo>
implements SharedPreferences.OnSharedPreferenceChangeListener { implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final String INFO_KEY = "related_info_key"; private static final String INFO_KEY = "related_info_key";
private CompositeDisposable disposables = new CompositeDisposable(); private final CompositeDisposable disposables = new CompositeDisposable();
private RelatedStreamInfo relatedStreamInfo; private RelatedStreamInfo relatedStreamInfo;
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////

View file

@ -371,7 +371,7 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
}; };
} }
public class HFHolder extends RecyclerView.ViewHolder { public static class HFHolder extends RecyclerView.ViewHolder {
public View view; public View view;
HFHolder(final View v) { HFHolder(final View v) {

View file

@ -144,7 +144,8 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
} }
if (item.getUploadDate() != null) { if (item.getUploadDate() != null) {
itemPublishedTime.setText(Localization.relativeTime(item.getUploadDate().date())); itemPublishedTime.setText(Localization.relativeTime(item.getUploadDate()
.offsetDateTime()));
} else { } else {
itemPublishedTime.setText(item.getTextualUploadDate()); itemPublishedTime.setText(item.getTextualUploadDate());
} }

View file

@ -95,7 +95,7 @@ public class StreamInfoItemHolder extends StreamMiniInfoItemHolder {
private String getFormattedRelativeUploadDate(final StreamInfoItem infoItem) { private String getFormattedRelativeUploadDate(final StreamInfoItem infoItem) {
if (infoItem.getUploadDate() != null) { if (infoItem.getUploadDate() != null) {
String formattedRelativeTime = Localization String formattedRelativeTime = Localization
.relativeTime(infoItem.getUploadDate().date()); .relativeTime(infoItem.getUploadDate().offsetDateTime());
if (DEBUG && PreferenceManager.getDefaultSharedPreferences(itemBuilder.getContext()) if (DEBUG && PreferenceManager.getDefaultSharedPreferences(itemBuilder.getContext())
.getBoolean(itemBuilder.getContext() .getBoolean(itemBuilder.getContext()

View file

@ -33,11 +33,11 @@ import org.schabi.newpipe.util.OnClickGesture;
import java.util.List; import java.util.List;
import icepick.State; import icepick.State;
import io.reactivex.Flowable; import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.Single; import io.reactivex.rxjava3.core.Single;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistLocalItem>, Void> { public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistLocalItem>, Void> {
@State @State

View file

@ -28,9 +28,9 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
public final class PlaylistAppendDialog extends PlaylistDialog { public final class PlaylistAppendDialog extends PlaylistDialog {
private static final String TAG = PlaylistAppendDialog.class.getCanonicalName(); private static final String TAG = PlaylistAppendDialog.class.getCanonicalName();
@ -38,7 +38,7 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
private RecyclerView playlistRecyclerView; private RecyclerView playlistRecyclerView;
private LocalItemListAdapter playlistAdapter; private LocalItemListAdapter playlistAdapter;
private CompositeDisposable playlistDisposables = new CompositeDisposable(); private final CompositeDisposable playlistDisposables = new CompositeDisposable();
public static Disposable onPlaylistFound( public static Disposable onPlaylistFound(
final Context context, final Runnable onSuccess, final Runnable onFailed final Context context, final Runnable onSuccess, final Runnable onFailed
@ -98,7 +98,7 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
final LocalPlaylistManager playlistManager = final LocalPlaylistManager playlistManager =
new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext())); new LocalPlaylistManager(NewPipeDatabase.getInstance(requireContext()));
playlistAdapter = new LocalItemListAdapter(getActivity()); playlistAdapter = new LocalItemListAdapter(getActivity());
playlistAdapter.setSelectedListener(new OnClickGesture<LocalItem>() { playlistAdapter.setSelectedListener(new OnClickGesture<LocalItem>() {
@ -113,7 +113,7 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
}); });
playlistRecyclerView = view.findViewById(R.id.playlist_list); playlistRecyclerView = view.findViewById(R.id.playlist_list);
playlistRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); playlistRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
playlistRecyclerView.setAdapter(playlistAdapter); playlistRecyclerView.setAdapter(playlistAdapter);
final View newPlaylistButton = view.findViewById(R.id.newPlaylist); final View newPlaylistButton = view.findViewById(R.id.newPlaylist);
@ -146,12 +146,12 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
public void openCreatePlaylistDialog() { public void openCreatePlaylistDialog() {
if (getStreams() == null || getFragmentManager() == null) { if (getStreams() == null || !isAdded()) {
return; return;
} }
PlaylistCreationDialog.newInstance(getStreams()).show(getFragmentManager(), TAG); PlaylistCreationDialog.newInstance(getStreams()).show(getParentFragmentManager(), TAG);
getDialog().dismiss(); requireDialog().dismiss();
} }
private void onPlaylistsReceived(@NonNull final List<PlaylistMetadataEntry> playlists) { private void onPlaylistsReceived(@NonNull final List<PlaylistMetadataEntry> playlists) {
@ -183,6 +183,6 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> successToast.show())); .subscribe(ignored -> successToast.show()));
getDialog().dismiss(); requireDialog().dismiss();
} }
} }

View file

@ -17,7 +17,7 @@ import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
import java.util.List; import java.util.List;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
public final class PlaylistCreationDialog extends PlaylistDialog { public final class PlaylistCreationDialog extends PlaylistDialog {
public static PlaylistCreationDialog newInstance(final List<StreamEntity> streams) { public static PlaylistCreationDialog newInstance(final List<StreamEntity> streams) {

View file

@ -17,7 +17,7 @@ import java.util.Queue;
public abstract class PlaylistDialog extends DialogFragment implements StateSaver.WriteRead { public abstract class PlaylistDialog extends DialogFragment implements StateSaver.WriteRead {
private List<StreamEntity> streamEntities; private List<StreamEntity> streamEntities;
private StateSaver.SavedState savedState; private org.schabi.newpipe.util.SavedState savedState;
protected void setInfo(final List<StreamEntity> entities) { protected void setInfo(final List<StreamEntity> entities) {
this.streamEntities = entities; this.streamEntities = entities;

View file

@ -2,14 +2,11 @@ package org.schabi.newpipe.local.feed
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import io.reactivex.Completable import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.Flowable import io.reactivex.rxjava3.core.Completable
import io.reactivex.Maybe import io.reactivex.rxjava3.core.Flowable
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Maybe
import io.reactivex.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import java.time.LocalDate
import java.time.OffsetDateTime
import java.time.ZoneOffset
import org.schabi.newpipe.MainActivity.DEBUG import org.schabi.newpipe.MainActivity.DEBUG
import org.schabi.newpipe.NewPipeDatabase import org.schabi.newpipe.NewPipeDatabase
import org.schabi.newpipe.database.feed.model.FeedEntity import org.schabi.newpipe.database.feed.model.FeedEntity
@ -19,6 +16,9 @@ import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.extractor.stream.StreamType import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.local.subscription.FeedGroupIcon import org.schabi.newpipe.local.subscription.FeedGroupIcon
import java.time.LocalDate
import java.time.OffsetDateTime
import java.time.ZoneOffset
class FeedDatabaseManager(context: Context) { class FeedDatabaseManager(context: Context) {
private val database = NewPipeDatabase.getInstance(context) private val database = NewPipeDatabase.getInstance(context)
@ -31,7 +31,7 @@ class FeedDatabaseManager(context: Context) {
* Only items that are newer than this will be saved. * Only items that are newer than this will be saved.
*/ */
val FEED_OLDEST_ALLOWED_DATE: OffsetDateTime = LocalDate.now().minusWeeks(13) val FEED_OLDEST_ALLOWED_DATE: OffsetDateTime = LocalDate.now().minusWeeks(13)
.atStartOfDay().atOffset(ZoneOffset.UTC) .atStartOfDay().atOffset(ZoneOffset.UTC)
} }
fun groups() = feedGroupTable.getAll() fun groups() = feedGroupTable.getAll()
@ -44,9 +44,9 @@ class FeedDatabaseManager(context: Context) {
else -> feedTable.getAllStreamsFromGroup(groupId) else -> feedTable.getAllStreamsFromGroup(groupId)
} }
return streams.map<List<StreamInfoItem>> { return streams.map {
val items = ArrayList<StreamInfoItem>(it.size) val items = ArrayList<StreamInfoItem>(it.size)
it.mapTo(items) { it.toStreamInfoItem() } it.mapTo(items) { stream -> stream.toStreamInfoItem() }
return@map items return@map items
} }
} }
@ -61,10 +61,10 @@ class FeedDatabaseManager(context: Context) {
} }
fun outdatedSubscriptionsForGroup(groupId: Long = FeedGroupEntity.GROUP_ALL_ID, outdatedThreshold: OffsetDateTime) = fun outdatedSubscriptionsForGroup(groupId: Long = FeedGroupEntity.GROUP_ALL_ID, outdatedThreshold: OffsetDateTime) =
feedTable.getAllOutdatedForGroup(groupId, outdatedThreshold) feedTable.getAllOutdatedForGroup(groupId, outdatedThreshold)
fun markAsOutdated(subscriptionId: Long) = feedTable fun markAsOutdated(subscriptionId: Long) = feedTable
.setLastUpdatedForSubscription(FeedLastUpdatedEntity(subscriptionId, null)) .setLastUpdatedForSubscription(FeedLastUpdatedEntity(subscriptionId, null))
fun upsertAll( fun upsertAll(
subscriptionId: Long, subscriptionId: Long,
@ -92,8 +92,12 @@ class FeedDatabaseManager(context: Context) {
feedTable.insertAll(feedEntities) feedTable.insertAll(feedEntities)
} }
feedTable.setLastUpdatedForSubscription(FeedLastUpdatedEntity(subscriptionId, feedTable.setLastUpdatedForSubscription(
OffsetDateTime.now(ZoneOffset.UTC))) FeedLastUpdatedEntity(
subscriptionId,
OffsetDateTime.now(ZoneOffset.UTC)
)
)
} }
fun removeOrphansOrOlderStreams(oldestAllowedDate: OffsetDateTime = FEED_OLDEST_ALLOWED_DATE) { fun removeOrphansOrOlderStreams(oldestAllowedDate: OffsetDateTime = FEED_OLDEST_ALLOWED_DATE) {
@ -113,38 +117,38 @@ class FeedDatabaseManager(context: Context) {
fun subscriptionIdsForGroup(groupId: Long): Flowable<List<Long>> { fun subscriptionIdsForGroup(groupId: Long): Flowable<List<Long>> {
return feedGroupTable.getSubscriptionIdsFor(groupId) return feedGroupTable.getSubscriptionIdsFor(groupId)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
} }
fun updateSubscriptionsForGroup(groupId: Long, subscriptionIds: List<Long>): Completable { fun updateSubscriptionsForGroup(groupId: Long, subscriptionIds: List<Long>): Completable {
return Completable.fromCallable { feedGroupTable.updateSubscriptionsForGroup(groupId, subscriptionIds) } return Completable.fromCallable { feedGroupTable.updateSubscriptionsForGroup(groupId, subscriptionIds) }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
} }
fun createGroup(name: String, icon: FeedGroupIcon): Maybe<Long> { fun createGroup(name: String, icon: FeedGroupIcon): Maybe<Long> {
return Maybe.fromCallable { feedGroupTable.insert(FeedGroupEntity(0, name, icon)) } return Maybe.fromCallable { feedGroupTable.insert(FeedGroupEntity(0, name, icon)) }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
} }
fun getGroup(groupId: Long): Maybe<FeedGroupEntity> { fun getGroup(groupId: Long): Maybe<FeedGroupEntity> {
return feedGroupTable.getGroup(groupId) return feedGroupTable.getGroup(groupId)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
} }
fun updateGroup(feedGroupEntity: FeedGroupEntity): Completable { fun updateGroup(feedGroupEntity: FeedGroupEntity): Completable {
return Completable.fromCallable { feedGroupTable.update(feedGroupEntity) } return Completable.fromCallable { feedGroupTable.update(feedGroupEntity) }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
} }
fun deleteGroup(groupId: Long): Completable { fun deleteGroup(groupId: Long): Completable {
return Completable.fromCallable { feedGroupTable.delete(groupId) } return Completable.fromCallable { feedGroupTable.delete(groupId) }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
} }
fun updateGroupsOrder(groupIdList: List<Long>): Completable { fun updateGroupsOrder(groupIdList: List<Long>): Completable {
@ -152,8 +156,8 @@ class FeedDatabaseManager(context: Context) {
val orderMap = groupIdList.associateBy({ it }, { index++ }) val orderMap = groupIdList.associateBy({ it }, { index++ })
return Completable.fromCallable { feedGroupTable.updateOrder(orderMap) } return Completable.fromCallable { feedGroupTable.updateOrder(orderMap) }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
} }
fun oldestSubscriptionUpdate(groupId: Long): Flowable<List<OffsetDateTime>> { fun oldestSubscriptionUpdate(groupId: Long): Flowable<List<OffsetDateTime>> {

View file

@ -29,13 +29,14 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.content.edit
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import icepick.State import icepick.State
import java.util.Calendar
import kotlinx.android.synthetic.main.error_retry.error_button_retry import kotlinx.android.synthetic.main.error_retry.error_button_retry
import kotlinx.android.synthetic.main.error_retry.error_message_view import kotlinx.android.synthetic.main.error_retry.error_message_view
import kotlinx.android.synthetic.main.fragment_feed.empty_state_view import kotlinx.android.synthetic.main.fragment_feed.empty_state_view
@ -53,9 +54,11 @@ import org.schabi.newpipe.local.feed.service.FeedLoadService
import org.schabi.newpipe.report.UserAction import org.schabi.newpipe.report.UserAction
import org.schabi.newpipe.util.AnimationUtils.animateView import org.schabi.newpipe.util.AnimationUtils.animateView
import org.schabi.newpipe.util.Localization import org.schabi.newpipe.util.Localization
import java.util.Calendar
class FeedFragment : BaseListFragment<FeedState, Unit>() { class FeedFragment : BaseListFragment<FeedState, Unit>() {
private lateinit var viewModel: FeedViewModel private lateinit var viewModel: FeedViewModel
private lateinit var swipeRefreshLayout: SwipeRefreshLayout
@State @State
@JvmField @JvmField
var listState: Parcelable? = null var listState: Parcelable? = null
@ -73,7 +76,7 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
groupId = arguments?.getLong(KEY_GROUP_ID, FeedGroupEntity.GROUP_ALL_ID) groupId = arguments?.getLong(KEY_GROUP_ID, FeedGroupEntity.GROUP_ALL_ID)
?: FeedGroupEntity.GROUP_ALL_ID ?: FeedGroupEntity.GROUP_ALL_ID
groupName = arguments?.getString(KEY_GROUP_NAME) ?: "" groupName = arguments?.getString(KEY_GROUP_NAME) ?: ""
} }
@ -83,7 +86,8 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
override fun onViewCreated(rootView: View, savedInstanceState: Bundle?) { override fun onViewCreated(rootView: View, savedInstanceState: Bundle?) {
super.onViewCreated(rootView, savedInstanceState) super.onViewCreated(rootView, savedInstanceState)
swipeRefreshLayout = requireView().findViewById(R.id.swiperefresh)
swipeRefreshLayout.setOnRefreshListener { reloadContent() }
viewModel = ViewModelProvider(this, FeedViewModel.Factory(requireContext(), groupId)).get(FeedViewModel::class.java) viewModel = ViewModelProvider(this, FeedViewModel.Factory(requireContext(), groupId)).get(FeedViewModel::class.java)
viewModel.stateLiveData.observe(viewLifecycleOwner, Observer { it?.let(::handleResult) }) viewModel.stateLiveData.observe(viewLifecycleOwner, Observer { it?.let(::handleResult) })
} }
@ -140,15 +144,15 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
} }
AlertDialog.Builder(requireContext()) AlertDialog.Builder(requireContext())
.setMessage(R.string.feed_use_dedicated_fetch_method_help_text) .setMessage(R.string.feed_use_dedicated_fetch_method_help_text)
.setNeutralButton(enableDisableButtonText) { _, _ -> .setNeutralButton(enableDisableButtonText) { _, _ ->
sharedPreferences.edit() sharedPreferences.edit {
.putBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), !usingDedicatedMethod) putBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), !usingDedicatedMethod)
.apply()
} }
.setPositiveButton(resources.getString(R.string.finish), null) }
.create() .setPositiveButton(resources.getString(R.string.finish), null)
.show() .create()
.show()
return true return true
} }
@ -189,6 +193,7 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
empty_state_view?.let { animateView(it, false, 0) } empty_state_view?.let { animateView(it, false, 0) }
animateView(error_panel, false, 0) animateView(error_panel, false, 0)
swipeRefreshLayout.isRefreshing = false
} }
override fun showEmptyState() { override fun showEmptyState() {
@ -229,7 +234,7 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
showLoading() showLoading()
val isIndeterminate = progressState.currentProgress == -1 && val isIndeterminate = progressState.currentProgress == -1 &&
progressState.maxProgress == -1 progressState.maxProgress == -1
if (!isIndeterminate) { if (!isIndeterminate) {
loading_progress_text.text = "${progressState.currentProgress}/${progressState.maxProgress}" loading_progress_text.text = "${progressState.currentProgress}/${progressState.maxProgress}"
@ -240,7 +245,7 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
} }
loading_progress_bar.isIndeterminate = isIndeterminate || loading_progress_bar.isIndeterminate = isIndeterminate ||
(progressState.maxProgress > 0 && progressState.currentProgress == 0) (progressState.maxProgress > 0 && progressState.currentProgress == 0)
loading_progress_bar.progress = progressState.currentProgress loading_progress_bar.progress = progressState.currentProgress
loading_progress_bar.max = progressState.maxProgress loading_progress_bar.max = progressState.maxProgress
@ -255,14 +260,17 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
oldestSubscriptionUpdate = loadedState.oldestUpdate oldestSubscriptionUpdate = loadedState.oldestUpdate
refresh_subtitle_text.isVisible = loadedState.notLoadedCount > 0 val loadedCount = loadedState.notLoadedCount > 0
if (loadedState.notLoadedCount > 0) { refresh_subtitle_text.isVisible = loadedCount
if (loadedCount) {
refresh_subtitle_text.text = getString(R.string.feed_subscription_not_loaded_count, loadedState.notLoadedCount) refresh_subtitle_text.text = getString(R.string.feed_subscription_not_loaded_count, loadedState.notLoadedCount)
} }
if (loadedState.itemsErrors.isNotEmpty()) { if (loadedState.itemsErrors.isNotEmpty()) {
showSnackBarError(loadedState.itemsErrors, UserAction.REQUESTED_FEED, showSnackBarError(
"none", "Loading feed", R.string.general_error) loadedState.itemsErrors, UserAction.REQUESTED_FEED,
"none", "Loading feed", R.string.general_error
)
} }
if (loadedState.items.isEmpty()) { if (loadedState.items.isEmpty()) {
@ -305,9 +313,11 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
override fun hasMoreItems() = false override fun hasMoreItems() = false
private fun triggerUpdate() { private fun triggerUpdate() {
getActivity()?.startService(Intent(requireContext(), FeedLoadService::class.java).apply { getActivity()?.startService(
putExtra(FeedLoadService.EXTRA_GROUP_ID, groupId) Intent(requireContext(), FeedLoadService::class.java).apply {
}) putExtra(FeedLoadService.EXTRA_GROUP_ID, groupId)
}
)
listState = null listState = null
} }

View file

@ -1,8 +1,8 @@
package org.schabi.newpipe.local.feed package org.schabi.newpipe.local.feed
import androidx.annotation.StringRes import androidx.annotation.StringRes
import java.util.Calendar
import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.extractor.stream.StreamInfoItem
import java.util.Calendar
sealed class FeedState { sealed class FeedState {
data class ProgressState( data class ProgressState(

View file

@ -5,12 +5,10 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import io.reactivex.Flowable import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Flowable
import io.reactivex.functions.Function4 import io.reactivex.rxjava3.functions.Function4
import io.reactivex.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import java.time.OffsetDateTime
import java.util.concurrent.TimeUnit
import org.schabi.newpipe.database.feed.model.FeedGroupEntity import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.ktx.toCalendar import org.schabi.newpipe.ktx.toCalendar
@ -20,6 +18,8 @@ import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.IdleEvent
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ProgressEvent import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ProgressEvent
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.SuccessResultEvent import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.SuccessResultEvent
import org.schabi.newpipe.util.DEFAULT_THROTTLE_TIMEOUT import org.schabi.newpipe.util.DEFAULT_THROTTLE_TIMEOUT
import java.time.OffsetDateTime
import java.util.concurrent.TimeUnit
class FeedViewModel(applicationContext: Context, val groupId: Long = FeedGroupEntity.GROUP_ALL_ID) : ViewModel() { class FeedViewModel(applicationContext: Context, val groupId: Long = FeedGroupEntity.GROUP_ALL_ID) : ViewModel() {
class Factory(val context: Context, val groupId: Long = FeedGroupEntity.GROUP_ALL_ID) : ViewModelProvider.Factory { class Factory(val context: Context, val groupId: Long = FeedGroupEntity.GROUP_ALL_ID) : ViewModelProvider.Factory {
@ -35,35 +35,34 @@ class FeedViewModel(applicationContext: Context, val groupId: Long = FeedGroupEn
val stateLiveData: LiveData<FeedState> = mutableStateLiveData val stateLiveData: LiveData<FeedState> = mutableStateLiveData
private var combineDisposable = Flowable private var combineDisposable = Flowable
.combineLatest( .combineLatest(
FeedEventManager.events(), FeedEventManager.events(),
feedDatabaseManager.asStreamItems(groupId), feedDatabaseManager.asStreamItems(groupId),
feedDatabaseManager.notLoadedCount(groupId), feedDatabaseManager.notLoadedCount(groupId),
feedDatabaseManager.oldestSubscriptionUpdate(groupId), feedDatabaseManager.oldestSubscriptionUpdate(groupId),
Function4 { t1: FeedEventManager.Event, t2: List<StreamInfoItem>, t3: Long, t4: List<OffsetDateTime> ->
return@Function4 CombineResultHolder(t1, t2, t3, t4.firstOrNull())
}
)
.throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { (event, listFromDB, notLoadedCount, oldestUpdate) ->
val oldestUpdateCalendar = oldestUpdate?.toCalendar()
Function4 { t1: FeedEventManager.Event, t2: List<StreamInfoItem>, t3: Long, t4: List<OffsetDateTime> -> mutableStateLiveData.postValue(
return@Function4 CombineResultHolder(t1, t2, t3, t4.firstOrNull()) when (event) {
}
)
.throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
val (event, listFromDB, notLoadedCount, oldestUpdate) = it
val oldestUpdateCalendar = oldestUpdate?.toCalendar()
mutableStateLiveData.postValue(when (event) {
is IdleEvent -> FeedState.LoadedState(listFromDB, oldestUpdateCalendar, notLoadedCount) is IdleEvent -> FeedState.LoadedState(listFromDB, oldestUpdateCalendar, notLoadedCount)
is ProgressEvent -> FeedState.ProgressState(event.currentProgress, event.maxProgress, event.progressMessage) is ProgressEvent -> FeedState.ProgressState(event.currentProgress, event.maxProgress, event.progressMessage)
is SuccessResultEvent -> FeedState.LoadedState(listFromDB, oldestUpdateCalendar, notLoadedCount, event.itemsErrors) is SuccessResultEvent -> FeedState.LoadedState(listFromDB, oldestUpdateCalendar, notLoadedCount, event.itemsErrors)
is ErrorResultEvent -> FeedState.ErrorState(event.error) is ErrorResultEvent -> FeedState.ErrorState(event.error)
})
if (event is ErrorResultEvent || event is SuccessResultEvent) {
FeedEventManager.reset()
} }
)
if (event is ErrorResultEvent || event is SuccessResultEvent) {
FeedEventManager.reset()
} }
}
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()

View file

@ -1,15 +1,15 @@
package org.schabi.newpipe.local.feed.service package org.schabi.newpipe.local.feed.service
import androidx.annotation.StringRes import androidx.annotation.StringRes
import io.reactivex.Flowable import io.reactivex.rxjava3.core.Flowable
import io.reactivex.processors.BehaviorProcessor import io.reactivex.rxjava3.processors.BehaviorProcessor
import java.util.concurrent.atomic.AtomicBoolean
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.IdleEvent import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.IdleEvent
import java.util.concurrent.atomic.AtomicBoolean
object FeedEventManager { object FeedEventManager {
private var processor: BehaviorProcessor<Event> = BehaviorProcessor.create() private var processor: BehaviorProcessor<Event> = BehaviorProcessor.create()
private var ignoreUpstream = AtomicBoolean() private var ignoreUpstream = AtomicBoolean()
private var eventsFlowable = processor.startWith(IdleEvent) private var eventsFlowable = processor.startWithItem(IdleEvent)
fun postEvent(event: Event) { fun postEvent(event: Event) {
processor.onNext(event) processor.onNext(event)

View file

@ -31,21 +31,15 @@ import android.util.Log
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import io.reactivex.Flowable import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.Notification import io.reactivex.rxjava3.core.Flowable
import io.reactivex.Single import io.reactivex.rxjava3.core.Notification
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Single
import io.reactivex.disposables.CompositeDisposable import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.functions.Consumer import io.reactivex.rxjava3.functions.Consumer
import io.reactivex.functions.Function import io.reactivex.rxjava3.functions.Function
import io.reactivex.processors.PublishProcessor import io.reactivex.rxjava3.processors.PublishProcessor
import io.reactivex.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import java.io.IOException
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import org.reactivestreams.Subscriber import org.reactivestreams.Subscriber
import org.reactivestreams.Subscription import org.reactivestreams.Subscription
import org.schabi.newpipe.MainActivity.DEBUG import org.schabi.newpipe.MainActivity.DEBUG
@ -56,13 +50,18 @@ import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.local.feed.FeedDatabaseManager import org.schabi.newpipe.local.feed.FeedDatabaseManager
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ErrorResultEvent import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ErrorResultEvent
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.IdleEvent
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ProgressEvent import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ProgressEvent
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.SuccessResultEvent import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.SuccessResultEvent
import org.schabi.newpipe.local.feed.service.FeedEventManager.postEvent import org.schabi.newpipe.local.feed.service.FeedEventManager.postEvent
import org.schabi.newpipe.local.subscription.SubscriptionManager import org.schabi.newpipe.local.subscription.SubscriptionManager
import org.schabi.newpipe.util.ExceptionUtils import org.schabi.newpipe.util.ExceptionUtils
import org.schabi.newpipe.util.ExtractorHelper import org.schabi.newpipe.util.ExtractorHelper
import java.io.IOException
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
class FeedLoadService : Service() { class FeedLoadService : Service() {
companion object { companion object {
@ -109,8 +108,11 @@ class FeedLoadService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "]," + Log.d(
" flags = [" + flags + "], startId = [" + startId + "]") TAG,
"onStartCommand() called with: intent = [" + intent + "]," +
" flags = [" + flags + "], startId = [" + startId + "]"
)
} }
if (intent == null || loadingSubscription != null) { if (intent == null || loadingSubscription != null) {
@ -123,10 +125,10 @@ class FeedLoadService : Service() {
val groupId = intent.getLongExtra(EXTRA_GROUP_ID, FeedGroupEntity.GROUP_ALL_ID) val groupId = intent.getLongExtra(EXTRA_GROUP_ID, FeedGroupEntity.GROUP_ALL_ID)
val useFeedExtractor = defaultSharedPreferences val useFeedExtractor = defaultSharedPreferences
.getBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), false) .getBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), false)
val thresholdOutdatedSecondsString = defaultSharedPreferences val thresholdOutdatedSecondsString = defaultSharedPreferences
.getString(getString(R.string.feed_update_threshold_key), getString(R.string.feed_update_threshold_default_value)) .getString(getString(R.string.feed_update_threshold_key), getString(R.string.feed_update_threshold_default_value))
val thresholdOutdatedSeconds = thresholdOutdatedSecondsString!!.toInt() val thresholdOutdatedSeconds = thresholdOutdatedSecondsString!!.toInt()
startLoading(groupId, useFeedExtractor, thresholdOutdatedSeconds) startLoading(groupId, useFeedExtractor, thresholdOutdatedSeconds)
@ -181,63 +183,63 @@ class FeedLoadService : Service() {
} }
subscriptions subscriptions
.limit(1) .take(1)
.doOnNext { .doOnNext {
currentProgress.set(0) currentProgress.set(0)
maxProgress.set(it.size) maxProgress.set(it.size)
}
.filter { it.isNotEmpty() }
.observeOn(AndroidSchedulers.mainThread())
.doOnNext {
startForeground(NOTIFICATION_ID, notificationBuilder.build())
updateNotificationProgress(null)
broadcastProgress()
}
.observeOn(Schedulers.io())
.flatMap { Flowable.fromIterable(it) }
.takeWhile { !cancelSignal.get() }
.parallel(PARALLEL_EXTRACTIONS, PARALLEL_EXTRACTIONS * 2)
.runOn(Schedulers.io(), PARALLEL_EXTRACTIONS * 2)
.filter { !cancelSignal.get() }
.map { subscriptionEntity ->
try {
val listInfo = if (useFeedExtractor) {
ExtractorHelper
.getFeedInfoFallbackToChannelInfo(subscriptionEntity.serviceId, subscriptionEntity.url)
.blockingGet()
} else {
ExtractorHelper
.getChannelInfo(subscriptionEntity.serviceId, subscriptionEntity.url, true)
.blockingGet()
} as ListInfo<StreamInfoItem>
return@map Notification.createOnNext(Pair(subscriptionEntity.uid, listInfo))
} catch (e: Throwable) {
val request = "${subscriptionEntity.serviceId}:${subscriptionEntity.url}"
val wrapper = RequestException(subscriptionEntity.uid, request, e)
return@map Notification.createOnError<Pair<Long, ListInfo<StreamInfoItem>>>(wrapper)
} }
.filter { it.isNotEmpty() } }
.sequential()
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.doOnNext { .doOnNext(errorHandlingConsumer)
startForeground(NOTIFICATION_ID, notificationBuilder.build())
updateNotificationProgress(null)
broadcastProgress()
}
.observeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread())
.flatMap { Flowable.fromIterable(it) } .doOnNext(notificationsConsumer)
.takeWhile { !cancelSignal.get() }
.parallel(PARALLEL_EXTRACTIONS, PARALLEL_EXTRACTIONS * 2) .observeOn(Schedulers.io())
.runOn(Schedulers.io(), PARALLEL_EXTRACTIONS * 2) .buffer(BUFFER_COUNT_BEFORE_INSERT)
.filter { !cancelSignal.get() } .doOnNext(databaseConsumer)
.map { subscriptionEntity -> .subscribeOn(Schedulers.io())
try { .observeOn(AndroidSchedulers.mainThread())
val listInfo = if (useFeedExtractor) { .subscribe(resultSubscriber)
ExtractorHelper
.getFeedInfoFallbackToChannelInfo(subscriptionEntity.serviceId, subscriptionEntity.url)
.blockingGet()
} else {
ExtractorHelper
.getChannelInfo(subscriptionEntity.serviceId, subscriptionEntity.url, true)
.blockingGet()
} as ListInfo<StreamInfoItem>
return@map Notification.createOnNext(Pair(subscriptionEntity.uid, listInfo))
} catch (e: Throwable) {
val request = "${subscriptionEntity.serviceId}:${subscriptionEntity.url}"
val wrapper = RequestException(subscriptionEntity.uid, request, e)
return@map Notification.createOnError<Pair<Long, ListInfo<StreamInfoItem>>>(wrapper)
}
}
.sequential()
.observeOn(AndroidSchedulers.mainThread())
.doOnNext(errorHandlingConsumer)
.observeOn(AndroidSchedulers.mainThread())
.doOnNext(notificationsConsumer)
.observeOn(Schedulers.io())
.buffer(BUFFER_COUNT_BEFORE_INSERT)
.doOnNext(databaseConsumer)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(resultSubscriber)
} }
private fun broadcastProgress() { private fun broadcastProgress() {
@ -262,7 +264,7 @@ class FeedLoadService : Service() {
override fun onComplete() { override fun onComplete() {
if (maxProgress.get() == 0) { if (maxProgress.get() == 0) {
postEvent(IdleEvent) postEvent(FeedEventManager.Event.IdleEvent)
stopService() stopService()
return return
@ -274,7 +276,8 @@ class FeedLoadService : Service() {
notificationUpdater.onNext(getString(R.string.feed_processing_message)) notificationUpdater.onNext(getString(R.string.feed_processing_message))
postEvent(ProgressEvent(R.string.feed_processing_message)) postEvent(ProgressEvent(R.string.feed_processing_message))
disposables.add(Single disposables.add(
Single
.fromCallable { .fromCallable {
feedResultsHolder.ready() feedResultsHolder.ready()
@ -293,7 +296,8 @@ class FeedLoadService : Service() {
return@subscribe return@subscribe
} }
stopService() stopService()
}) }
)
} }
} }
@ -364,16 +368,18 @@ class FeedLoadService : Service() {
private var maxProgress = AtomicInteger(-1) private var maxProgress = AtomicInteger(-1)
private fun createNotification(): NotificationCompat.Builder { private fun createNotification(): NotificationCompat.Builder {
val cancelActionIntent = PendingIntent.getBroadcast(this, val cancelActionIntent = PendingIntent.getBroadcast(
NOTIFICATION_ID, Intent(ACTION_CANCEL), 0) this,
NOTIFICATION_ID, Intent(ACTION_CANCEL), 0
)
return NotificationCompat.Builder(this, getString(R.string.notification_channel_id)) return NotificationCompat.Builder(this, getString(R.string.notification_channel_id))
.setOngoing(true) .setOngoing(true)
.setProgress(-1, -1, true) .setProgress(-1, -1, true)
.setSmallIcon(R.drawable.ic_newpipe_triangle_white) .setSmallIcon(R.drawable.ic_newpipe_triangle_white)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.addAction(0, getString(R.string.cancel), cancelActionIntent) .addAction(0, getString(R.string.cancel), cancelActionIntent)
.setContentTitle(getString(R.string.feed_notification_loading)) .setContentTitle(getString(R.string.feed_notification_loading))
} }
private fun setupNotification() { private fun setupNotification() {
@ -381,13 +387,15 @@ class FeedLoadService : Service() {
notificationBuilder = createNotification() notificationBuilder = createNotification()
val throttleAfterFirstEmission = Function { flow: Flowable<String> -> val throttleAfterFirstEmission = Function { flow: Flowable<String> ->
flow.limit(1).concatWith(flow.skip(1).throttleLatest(NOTIFICATION_SAMPLING_PERIOD.toLong(), TimeUnit.MILLISECONDS)) flow.take(1).concatWith(flow.skip(1).throttleLatest(NOTIFICATION_SAMPLING_PERIOD.toLong(), TimeUnit.MILLISECONDS))
} }
disposables.add(notificationUpdater disposables.add(
notificationUpdater
.publish(throttleAfterFirstEmission) .publish(throttleAfterFirstEmission)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(this::updateNotificationProgress)) .subscribe(this::updateNotificationProgress)
)
} }
private fun updateNotificationProgress(updateDescription: String?) { private fun updateNotificationProgress(updateDescription: String?) {

View file

@ -50,11 +50,11 @@ import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import io.reactivex.Completable; import io.reactivex.rxjava3.core.Completable;
import io.reactivex.Flowable; import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.Maybe; import io.reactivex.rxjava3.core.Maybe;
import io.reactivex.Single; import io.reactivex.rxjava3.core.Single;
import io.reactivex.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
public class HistoryRecordManager { public class HistoryRecordManager {
private final AppDatabase database; private final AppDatabase database;

View file

@ -34,6 +34,7 @@ import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.settings.SettingsActivity; import org.schabi.newpipe.settings.SettingsActivity;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
@ -48,9 +49,9 @@ import java.util.Comparator;
import java.util.List; import java.util.List;
import icepick.State; import icepick.State;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
public class StatisticsPlaylistFragment public class StatisticsPlaylistFragment
extends BaseLocalListFragment<List<StreamStatisticsEntry>, Void> { extends BaseLocalListFragment<List<StreamStatisticsEntry>, Void> {
@ -183,7 +184,7 @@ public class StatisticsPlaylistFragment
throwable -> ErrorActivity.reportError(getContext(), throwable -> ErrorActivity.reportError(getContext(),
throwable, throwable,
SettingsActivity.class, null, SettingsActivity.class, null,
ErrorActivity.ErrorInfo.make( ErrorInfo.make(
UserAction.DELETE_FROM_HISTORY, UserAction.DELETE_FROM_HISTORY,
"none", "none",
"Delete view history", "Delete view history",
@ -197,7 +198,7 @@ public class StatisticsPlaylistFragment
throwable -> ErrorActivity.reportError(getContext(), throwable -> ErrorActivity.reportError(getContext(),
throwable, throwable,
SettingsActivity.class, null, SettingsActivity.class, null,
ErrorActivity.ErrorInfo.make( ErrorInfo.make(
UserAction.DELETE_FROM_HISTORY, UserAction.DELETE_FROM_HISTORY,
"none", "none",
"Delete search history", "Delete search history",

View file

@ -11,7 +11,6 @@ import androidx.core.content.ContextCompat;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.local.LocalItemBuilder; import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.history.HistoryRecordManager;
@ -21,7 +20,6 @@ import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.views.AnimatedProgressBar; import org.schabi.newpipe.views.AnimatedProgressBar;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
public class LocalPlaylistStreamItemHolder extends LocalItemHolder { public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
@ -70,15 +68,11 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
R.color.duration_background_color)); R.color.duration_background_color));
itemDurationView.setVisibility(View.VISIBLE); itemDurationView.setVisibility(View.VISIBLE);
final StreamStateEntity state = historyRecordManager if (item.getProgressTime() > 0) {
.loadLocalStreamStateBatch(new ArrayList<LocalItem>() {{
add(localItem);
}}).blockingGet().get(0);
if (state != null) {
itemProgressView.setVisibility(View.VISIBLE); itemProgressView.setVisibility(View.VISIBLE);
itemProgressView.setMax((int) item.getStreamEntity().getDuration()); itemProgressView.setMax((int) item.getStreamEntity().getDuration());
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
.toSeconds(state.getProgressTime())); .toSeconds(item.getProgressTime()));
} else { } else {
itemProgressView.setVisibility(View.GONE); itemProgressView.setVisibility(View.GONE);
} }
@ -115,18 +109,14 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
} }
final PlaylistStreamEntry item = (PlaylistStreamEntry) localItem; final PlaylistStreamEntry item = (PlaylistStreamEntry) localItem;
final StreamStateEntity state = historyRecordManager if (item.getProgressTime() > 0 && item.getStreamEntity().getDuration() > 0) {
.loadLocalStreamStateBatch(new ArrayList<LocalItem>() {{
add(localItem);
}}).blockingGet().get(0);
if (state != null && item.getStreamEntity().getDuration() > 0) {
itemProgressView.setMax((int) item.getStreamEntity().getDuration()); itemProgressView.setMax((int) item.getStreamEntity().getDuration());
if (itemProgressView.getVisibility() == View.VISIBLE) { if (itemProgressView.getVisibility() == View.VISIBLE) {
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS
.toSeconds(state.getProgressTime())); .toSeconds(item.getProgressTime()));
} else { } else {
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
.toSeconds(state.getProgressTime())); .toSeconds(item.getProgressTime()));
AnimationUtils.animateView(itemProgressView, true, 500); AnimationUtils.animateView(itemProgressView, true, 500);
} }
} else if (itemProgressView.getVisibility() == View.VISIBLE) { } else if (itemProgressView.getVisibility() == View.VISIBLE) {

View file

@ -11,7 +11,6 @@ import androidx.core.content.ContextCompat;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.stream.StreamStatisticsEntry; import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.local.LocalItemBuilder; import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.history.HistoryRecordManager;
@ -21,7 +20,6 @@ import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.views.AnimatedProgressBar; import org.schabi.newpipe.views.AnimatedProgressBar;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/* /*
@ -98,15 +96,11 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
R.color.duration_background_color)); R.color.duration_background_color));
itemDurationView.setVisibility(View.VISIBLE); itemDurationView.setVisibility(View.VISIBLE);
final StreamStateEntity state = historyRecordManager if (item.getProgressTime() > 0) {
.loadLocalStreamStateBatch(new ArrayList<LocalItem>() {{
add(localItem);
}}).blockingGet().get(0);
if (state != null) {
itemProgressView.setVisibility(View.VISIBLE); itemProgressView.setVisibility(View.VISIBLE);
itemProgressView.setMax((int) item.getStreamEntity().getDuration()); itemProgressView.setMax((int) item.getStreamEntity().getDuration());
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
.toSeconds(state.getProgressTime())); .toSeconds(item.getProgressTime()));
} else { } else {
itemProgressView.setVisibility(View.GONE); itemProgressView.setVisibility(View.GONE);
} }
@ -146,18 +140,14 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
} }
final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem; final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem;
final StreamStateEntity state = historyRecordManager if (item.getProgressTime() > 0 && item.getStreamEntity().getDuration() > 0) {
.loadLocalStreamStateBatch(new ArrayList<LocalItem>() {{
add(localItem);
}}).blockingGet().get(0);
if (state != null && item.getStreamEntity().getDuration() > 0) {
itemProgressView.setMax((int) item.getStreamEntity().getDuration()); itemProgressView.setMax((int) item.getStreamEntity().getDuration());
if (itemProgressView.getVisibility() == View.VISIBLE) { if (itemProgressView.getVisibility() == View.VISIBLE) {
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS
.toSeconds(state.getProgressTime())); .toSeconds(item.getProgressTime()));
} else { } else {
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
.toSeconds(state.getProgressTime())); .toSeconds(item.getProgressTime()));
AnimationUtils.animateView(itemProgressView, true, 500); AnimationUtils.animateView(itemProgressView, true, 500);
} }
} else if (itemProgressView.getVisibility() == View.VISIBLE) { } else if (itemProgressView.getVisibility() == View.VISIBLE) {

View file

@ -55,13 +55,12 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import icepick.State; import icepick.State;
import io.reactivex.Flowable; import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.disposables.Disposables; import io.reactivex.rxjava3.schedulers.Schedulers;
import io.reactivex.schedulers.Schedulers; import io.reactivex.rxjava3.subjects.PublishSubject;
import io.reactivex.subjects.PublishSubject;
import static org.schabi.newpipe.util.AnimationUtils.animateView; import static org.schabi.newpipe.util.AnimationUtils.animateView;
@ -641,7 +640,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
private Disposable getDebouncedSaver() { private Disposable getDebouncedSaver() {
if (debouncedSaveSignal == null) { if (debouncedSaveSignal == null) {
return Disposables.empty(); return Disposable.empty();
} }
return debouncedSaveSignal return debouncedSaveSignal

View file

@ -15,11 +15,11 @@ import org.schabi.newpipe.database.stream.model.StreamEntity;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import io.reactivex.Completable; import io.reactivex.rxjava3.core.Completable;
import io.reactivex.Flowable; import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.Maybe; import io.reactivex.rxjava3.core.Maybe;
import io.reactivex.Single; import io.reactivex.rxjava3.core.Single;
import io.reactivex.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
public class LocalPlaylistManager { public class LocalPlaylistManager {
private final AppDatabase database; private final AppDatabase database;

View file

@ -7,9 +7,9 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import java.util.List; import java.util.List;
import io.reactivex.Flowable; import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.Single; import io.reactivex.rxjava3.core.Single;
import io.reactivex.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
public class RemotePlaylistManager { public class RemotePlaylistManager {

View file

@ -28,13 +28,7 @@ import com.xwray.groupie.Item
import com.xwray.groupie.Section import com.xwray.groupie.Section
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
import icepick.State import icepick.State
import io.reactivex.disposables.CompositeDisposable import io.reactivex.rxjava3.disposables.CompositeDisposable
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import kotlin.math.floor
import kotlin.math.max
import kotlinx.android.synthetic.main.dialog_title.view.itemAdditionalDetails import kotlinx.android.synthetic.main.dialog_title.view.itemAdditionalDetails
import kotlinx.android.synthetic.main.dialog_title.view.itemTitleView import kotlinx.android.synthetic.main.dialog_title.view.itemTitleView
import kotlinx.android.synthetic.main.fragment_subscription.items_list import kotlinx.android.synthetic.main.fragment_subscription.items_list
@ -68,6 +62,12 @@ import org.schabi.newpipe.util.NavigationHelper
import org.schabi.newpipe.util.OnClickGesture import org.schabi.newpipe.util.OnClickGesture
import org.schabi.newpipe.util.ShareUtils import org.schabi.newpipe.util.ShareUtils
import org.schabi.newpipe.util.ThemeHelper import org.schabi.newpipe.util.ThemeHelper
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import kotlin.math.floor
import kotlin.math.max
class SubscriptionFragment : BaseStateFragment<SubscriptionState>() { class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
private lateinit var viewModel: SubscriptionViewModel private lateinit var viewModel: SubscriptionViewModel
@ -208,14 +208,19 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
if (!exportFile.parentFile.canWrite() || !exportFile.parentFile.canRead()) { if (!exportFile.parentFile.canWrite() || !exportFile.parentFile.canRead()) {
Toast.makeText(activity, R.string.invalid_directory, Toast.LENGTH_SHORT).show() Toast.makeText(activity, R.string.invalid_directory, Toast.LENGTH_SHORT).show()
} else { } else {
activity.startService(Intent(activity, SubscriptionsExportService::class.java) activity.startService(
.putExtra(KEY_FILE_PATH, exportFile.absolutePath)) Intent(activity, SubscriptionsExportService::class.java)
.putExtra(KEY_FILE_PATH, exportFile.absolutePath)
)
} }
} else if (requestCode == REQUEST_IMPORT_CODE) { } else if (requestCode == REQUEST_IMPORT_CODE) {
val path = Utils.getFileForUri(data.data!!).absolutePath val path = Utils.getFileForUri(data.data!!).absolutePath
ImportConfirmationDialog.show(this, Intent(activity, SubscriptionsImportService::class.java) ImportConfirmationDialog.show(
this,
Intent(activity, SubscriptionsImportService::class.java)
.putExtra(KEY_MODE, PREVIOUS_EXPORT_MODE) .putExtra(KEY_MODE, PREVIOUS_EXPORT_MODE)
.putExtra(KEY_VALUE, path)) .putExtra(KEY_VALUE, path)
)
} }
} }
} }
@ -247,9 +252,9 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
feedGroupsCarousel = FeedGroupCarouselItem(requireContext(), carouselAdapter) feedGroupsCarousel = FeedGroupCarouselItem(requireContext(), carouselAdapter)
feedGroupsSortMenuItem = HeaderWithMenuItem( feedGroupsSortMenuItem = HeaderWithMenuItem(
getString(R.string.feed_groups_header_title), getString(R.string.feed_groups_header_title),
ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_sort), ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_sort),
menuItemOnClickListener = ::openReorderDialog menuItemOnClickListener = ::openReorderDialog
) )
add(Section(feedGroupsSortMenuItem, listOf(feedGroupsCarousel))) add(Section(feedGroupsSortMenuItem, listOf(feedGroupsCarousel)))
@ -260,10 +265,11 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
subscriptionsSection.setHideWhenEmpty(true) subscriptionsSection.setHideWhenEmpty(true)
importExportItem = FeedImportExportItem( importExportItem = FeedImportExportItem(
{ onImportPreviousSelected() }, { onImportPreviousSelected() },
{ onImportFromServiceSelected(it) }, { onImportFromServiceSelected(it) },
{ onExportSelected() }, { onExportSelected() },
importExportItemExpandedState ?: false) importExportItemExpandedState ?: false
)
groupAdapter.add(Section(importExportItem, listOf(subscriptionsSection))) groupAdapter.add(Section(importExportItem, listOf(subscriptionsSection)))
} }
@ -284,8 +290,8 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
private fun showLongTapDialog(selectedItem: ChannelInfoItem) { private fun showLongTapDialog(selectedItem: ChannelInfoItem) {
val commands = arrayOf( val commands = arrayOf(
getString(R.string.share), getString(R.string.share),
getString(R.string.unsubscribe) getString(R.string.unsubscribe)
) )
val actions = DialogInterface.OnClickListener { _, i -> val actions = DialogInterface.OnClickListener { _, i ->
@ -301,16 +307,18 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
bannerView.itemAdditionalDetails.visibility = View.GONE bannerView.itemAdditionalDetails.visibility = View.GONE
AlertDialog.Builder(requireContext()) AlertDialog.Builder(requireContext())
.setCustomTitle(bannerView) .setCustomTitle(bannerView)
.setItems(commands, actions) .setItems(commands, actions)
.create() .create()
.show() .show()
} }
private fun deleteChannel(selectedItem: ChannelInfoItem) { private fun deleteChannel(selectedItem: ChannelInfoItem) {
disposables.add(subscriptionManager.deleteSubscription(selectedItem.serviceId, selectedItem.url).subscribe { disposables.add(
Toast.makeText(requireContext(), getString(R.string.channel_unsubscribed), Toast.LENGTH_SHORT).show() subscriptionManager.deleteSubscription(selectedItem.serviceId, selectedItem.url).subscribe {
}) Toast.makeText(requireContext(), getString(R.string.channel_unsubscribed), Toast.LENGTH_SHORT).show()
}
)
} }
override fun doInitialLoadLogic() = Unit override fun doInitialLoadLogic() = Unit
@ -332,8 +340,10 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
} }
private val listenerChannelItem = object : OnClickGesture<ChannelInfoItem>() { private val listenerChannelItem = object : OnClickGesture<ChannelInfoItem>() {
override fun selected(selectedItem: ChannelInfoItem) = NavigationHelper.openChannelFragment(fm, override fun selected(selectedItem: ChannelInfoItem) = NavigationHelper.openChannelFragment(
selectedItem.serviceId, selectedItem.url, selectedItem.name) fm,
selectedItem.serviceId, selectedItem.url, selectedItem.name
)
override fun held(selectedItem: ChannelInfoItem) = showLongTapDialog(selectedItem) override fun held(selectedItem: ChannelInfoItem) = showLongTapDialog(selectedItem)
} }
@ -420,14 +430,16 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
private fun shouldUseGridLayout(): Boolean { private fun shouldUseGridLayout(): Boolean {
val listMode = PreferenceManager.getDefaultSharedPreferences(requireContext()) val listMode = PreferenceManager.getDefaultSharedPreferences(requireContext())
.getString(getString(R.string.list_view_mode_key), getString(R.string.list_view_mode_value)) .getString(getString(R.string.list_view_mode_key), getString(R.string.list_view_mode_value))
return when (listMode) { return when (listMode) {
getString(R.string.list_view_mode_auto_key) -> { getString(R.string.list_view_mode_auto_key) -> {
val configuration = resources.configuration val configuration = resources.configuration
(configuration.orientation == Configuration.ORIENTATION_LANDSCAPE && (
configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE)) configuration.orientation == Configuration.ORIENTATION_LANDSCAPE &&
configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE)
)
} }
getString(R.string.list_view_mode_grid_key) -> true getString(R.string.list_view_mode_grid_key) -> true
else -> false else -> false

View file

@ -1,10 +1,10 @@
package org.schabi.newpipe.local.subscription package org.schabi.newpipe.local.subscription
import android.content.Context import android.content.Context
import io.reactivex.Completable import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.Flowable import io.reactivex.rxjava3.core.Completable
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Flowable
import io.reactivex.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import org.schabi.newpipe.NewPipeDatabase import org.schabi.newpipe.NewPipeDatabase
import org.schabi.newpipe.database.feed.model.FeedGroupEntity import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.database.subscription.SubscriptionDAO import org.schabi.newpipe.database.subscription.SubscriptionDAO
@ -32,7 +32,8 @@ class SubscriptionManager(context: Context) {
filterQuery.isNotEmpty() -> { filterQuery.isNotEmpty() -> {
return if (showOnlyUngrouped) { return if (showOnlyUngrouped) {
subscriptionTable.getSubscriptionsOnlyUngroupedFiltered( subscriptionTable.getSubscriptionsOnlyUngroupedFiltered(
currentGroupId, filterQuery) currentGroupId, filterQuery
)
} else { } else {
subscriptionTable.getSubscriptionsFiltered(filterQuery) subscriptionTable.getSubscriptionsFiltered(filterQuery)
} }
@ -44,7 +45,8 @@ class SubscriptionManager(context: Context) {
fun upsertAll(infoList: List<ChannelInfo>): List<SubscriptionEntity> { fun upsertAll(infoList: List<ChannelInfo>): List<SubscriptionEntity> {
val listEntities = subscriptionTable.upsertAll( val listEntities = subscriptionTable.upsertAll(
infoList.map { SubscriptionEntity.from(it) }) infoList.map { SubscriptionEntity.from(it) }
)
database.runInTransaction { database.runInTransaction {
infoList.forEachIndexed { index, info -> infoList.forEachIndexed { index, info ->

View file

@ -5,12 +5,12 @@ import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.xwray.groupie.Group import com.xwray.groupie.Group
import io.reactivex.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import java.util.concurrent.TimeUnit
import org.schabi.newpipe.local.feed.FeedDatabaseManager import org.schabi.newpipe.local.feed.FeedDatabaseManager
import org.schabi.newpipe.local.subscription.item.ChannelItem import org.schabi.newpipe.local.subscription.item.ChannelItem
import org.schabi.newpipe.local.subscription.item.FeedGroupCardItem import org.schabi.newpipe.local.subscription.item.FeedGroupCardItem
import org.schabi.newpipe.util.DEFAULT_THROTTLE_TIMEOUT import org.schabi.newpipe.util.DEFAULT_THROTTLE_TIMEOUT
import java.util.concurrent.TimeUnit
class SubscriptionViewModel(application: Application) : AndroidViewModel(application) { class SubscriptionViewModel(application: Application) : AndroidViewModel(application) {
private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(application) private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(application)
@ -22,22 +22,22 @@ class SubscriptionViewModel(application: Application) : AndroidViewModel(applica
val feedGroupsLiveData: LiveData<List<Group>> = mutableFeedGroupsLiveData val feedGroupsLiveData: LiveData<List<Group>> = mutableFeedGroupsLiveData
private var feedGroupItemsDisposable = feedDatabaseManager.groups() private var feedGroupItemsDisposable = feedDatabaseManager.groups()
.throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS) .throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
.map { it.map(::FeedGroupCardItem) } .map { it.map(::FeedGroupCardItem) }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.subscribe( .subscribe(
{ mutableFeedGroupsLiveData.postValue(it) }, { mutableFeedGroupsLiveData.postValue(it) },
{ mutableStateLiveData.postValue(SubscriptionState.ErrorState(it)) } { mutableStateLiveData.postValue(SubscriptionState.ErrorState(it)) }
) )
private var stateItemsDisposable = subscriptionManager.subscriptions() private var stateItemsDisposable = subscriptionManager.subscriptions()
.throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS) .throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
.map { it.map { entity -> ChannelItem(entity.toChannelInfoItem(), entity.uid, ChannelItem.ItemVersion.MINI) } } .map { it.map { entity -> ChannelItem(entity.toChannelInfoItem(), entity.uid, ChannelItem.ItemVersion.MINI) } }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.subscribe( .subscribe(
{ mutableStateLiveData.postValue(SubscriptionState.LoadedState(it)) }, { mutableStateLiveData.postValue(SubscriptionState.LoadedState(it)) },
{ mutableStateLiveData.postValue(SubscriptionState.ErrorState(it)) } { mutableStateLiveData.postValue(SubscriptionState.ErrorState(it)) }
) )
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()

View file

@ -27,6 +27,7 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor; import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService; import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService;
import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.FilePickerActivityHelper; import org.schabi.newpipe.util.FilePickerActivityHelper;
@ -84,7 +85,7 @@ public class SubscriptionsImportFragment extends BaseFragment {
setupServiceVariables(); setupServiceVariables();
if (supportedSources.isEmpty() && currentServiceId != Constants.NO_SERVICE_ID) { if (supportedSources.isEmpty() && currentServiceId != Constants.NO_SERVICE_ID) {
ErrorActivity.reportError(activity, Collections.emptyList(), null, null, ErrorActivity.reportError(activity, Collections.emptyList(), null, null,
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, ErrorInfo.make(UserAction.SOMETHING_ELSE,
NewPipe.getNameOfService(currentServiceId), NewPipe.getNameOfService(currentServiceId),
"Service don't support importing", R.string.general_error)); "Service don't support importing", R.string.general_error));
activity.finish(); activity.finish();

View file

@ -3,7 +3,6 @@ package org.schabi.newpipe.local.subscription.dialog
import android.app.Dialog import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.text.TextUtils
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -25,8 +24,6 @@ import com.xwray.groupie.Section
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
import icepick.Icepick import icepick.Icepick
import icepick.State import icepick.State
import java.io.Serializable
import kotlin.collections.contains
import kotlinx.android.synthetic.main.dialog_feed_group_create.* import kotlinx.android.synthetic.main.dialog_feed_group_create.*
import kotlinx.android.synthetic.main.toolbar_search_layout.* import kotlinx.android.synthetic.main.toolbar_search_layout.*
import org.schabi.newpipe.R import org.schabi.newpipe.R
@ -44,6 +41,8 @@ import org.schabi.newpipe.local.subscription.item.PickerIconItem
import org.schabi.newpipe.local.subscription.item.PickerSubscriptionItem import org.schabi.newpipe.local.subscription.item.PickerSubscriptionItem
import org.schabi.newpipe.util.DeviceUtils import org.schabi.newpipe.util.DeviceUtils
import org.schabi.newpipe.util.ThemeHelper import org.schabi.newpipe.util.ThemeHelper
import java.io.Serializable
import kotlin.collections.contains
class FeedGroupDialog : DialogFragment(), BackPressable { class FeedGroupDialog : DialogFragment(), BackPressable {
private lateinit var viewModel: FeedGroupDialogViewModel private lateinit var viewModel: FeedGroupDialogViewModel
@ -117,21 +116,30 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this, viewModel = ViewModelProvider(
FeedGroupDialogViewModel.Factory(requireContext(), this,
groupId, subscriptionsCurrentSearchQuery, subscriptionsShowOnlyUngrouped) FeedGroupDialogViewModel.Factory(
requireContext(),
groupId, subscriptionsCurrentSearchQuery, subscriptionsShowOnlyUngrouped
)
).get(FeedGroupDialogViewModel::class.java) ).get(FeedGroupDialogViewModel::class.java)
viewModel.groupLiveData.observe(viewLifecycleOwner, Observer(::handleGroup)) viewModel.groupLiveData.observe(viewLifecycleOwner, Observer(::handleGroup))
viewModel.subscriptionsLiveData.observe(viewLifecycleOwner, Observer { viewModel.subscriptionsLiveData.observe(
setupSubscriptionPicker(it.first, it.second) viewLifecycleOwner,
}) Observer {
viewModel.dialogEventLiveData.observe(viewLifecycleOwner, Observer { setupSubscriptionPicker(it.first, it.second)
when (it) {
ProcessingEvent -> disableInput()
SuccessEvent -> dismiss()
} }
}) )
viewModel.dialogEventLiveData.observe(
viewLifecycleOwner,
Observer {
when (it) {
ProcessingEvent -> disableInput()
SuccessEvent -> dismiss()
}
}
)
subscriptionGroupAdapter = GroupAdapter<GroupieViewHolder>().apply { subscriptionGroupAdapter = GroupAdapter<GroupieViewHolder>().apply {
add(subscriptionMainSection) add(subscriptionMainSection)
@ -142,8 +150,10 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
// Disable animations, too distracting. // Disable animations, too distracting.
itemAnimator = null itemAnimator = null
adapter = subscriptionGroupAdapter adapter = subscriptionGroupAdapter
layoutManager = GridLayoutManager(requireContext(), subscriptionGroupAdapter.spanCount, layoutManager = GridLayoutManager(
RecyclerView.VERTICAL, false).apply { requireContext(), subscriptionGroupAdapter.spanCount,
RecyclerView.VERTICAL, false
).apply {
spanSizeLookup = subscriptionGroupAdapter.spanSizeLookup spanSizeLookup = subscriptionGroupAdapter.spanSizeLookup
} }
} }
@ -225,7 +235,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
} }
toolbar_search_clear.setOnClickListener { toolbar_search_clear.setOnClickListener {
if (TextUtils.isEmpty(toolbar_search_edit_text.text)) { if (toolbar_search_edit_text.text.isEmpty()) {
hideSearch() hideSearch()
return@setOnClickListener return@setOnClickListener
} }
@ -347,7 +357,8 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
val selectedCount = this.selectedSubscriptions.size val selectedCount = this.selectedSubscriptions.size
val selectedCountText = resources.getQuantityString( val selectedCountText = resources.getQuantityString(
R.plurals.feed_group_dialog_selection_count, R.plurals.feed_group_dialog_selection_count,
selectedCount, selectedCount) selectedCount, selectedCount
)
selected_subscription_count_view.text = selectedCountText selected_subscription_count_view.text = selectedCountText
subscriptions_header_info.text = selectedCountText subscriptions_header_info.text = selectedCountText
} }
@ -402,10 +413,12 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
separator.onlyVisibleIn(SubscriptionsPickerScreen, IconPickerScreen) separator.onlyVisibleIn(SubscriptionsPickerScreen, IconPickerScreen)
cancel_button.onlyVisibleIn(InitialScreen, DeleteScreen) cancel_button.onlyVisibleIn(InitialScreen, DeleteScreen)
confirm_button.setText(when { confirm_button.setText(
currentScreen == InitialScreen && groupId == NO_GROUP_SELECTED -> R.string.create when {
else -> android.R.string.ok currentScreen == InitialScreen && groupId == NO_GROUP_SELECTED -> R.string.create
}) else -> android.R.string.ok
}
)
delete_button.isGone = currentScreen != InitialScreen || groupId == NO_GROUP_SELECTED delete_button.isGone = currentScreen != InitialScreen || groupId == NO_GROUP_SELECTED
@ -455,8 +468,10 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
} }
private fun hideKeyboardSearch() { private fun hideKeyboardSearch() {
inputMethodManager.hideSoftInputFromWindow(toolbar_search_edit_text.windowToken, inputMethodManager.hideSoftInputFromWindow(
InputMethodManager.RESULT_UNCHANGED_SHOWN) toolbar_search_edit_text.windowToken,
InputMethodManager.RESULT_UNCHANGED_SHOWN
)
toolbar_search_edit_text.clearFocus() toolbar_search_edit_text.clearFocus()
} }
@ -467,8 +482,10 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
} }
private fun hideKeyboard() { private fun hideKeyboard() {
inputMethodManager.hideSoftInputFromWindow(group_name_input.windowToken, inputMethodManager.hideSoftInputFromWindow(
InputMethodManager.RESULT_UNCHANGED_SHOWN) group_name_input.windowToken,
InputMethodManager.RESULT_UNCHANGED_SHOWN
)
group_name_input.clearFocus() group_name_input.clearFocus()
} }

View file

@ -5,12 +5,12 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import io.reactivex.Completable import io.reactivex.rxjava3.core.Completable
import io.reactivex.Flowable import io.reactivex.rxjava3.core.Flowable
import io.reactivex.disposables.Disposable import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.functions.BiFunction import io.reactivex.rxjava3.functions.BiFunction
import io.reactivex.processors.BehaviorProcessor import io.reactivex.rxjava3.processors.BehaviorProcessor
import io.reactivex.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import org.schabi.newpipe.database.feed.model.FeedGroupEntity import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.local.feed.FeedDatabaseManager import org.schabi.newpipe.local.feed.FeedDatabaseManager
import org.schabi.newpipe.local.subscription.FeedGroupIcon import org.schabi.newpipe.local.subscription.FeedGroupIcon
@ -32,9 +32,9 @@ class FeedGroupDialogViewModel(
private var subscriptionsFlowable = Flowable private var subscriptionsFlowable = Flowable
.combineLatest( .combineLatest(
filterSubscriptions.startWith(initialQuery), filterSubscriptions.startWithItem(initialQuery),
toggleShowOnlyUngrouped.startWith(initialShowOnlyUngrouped), toggleShowOnlyUngrouped.startWithItem(initialShowOnlyUngrouped),
BiFunction { t1: String, t2: Boolean -> Filter(t1, t2) } BiFunction { t1: String, t2: Boolean -> Filter(t1, t2) }
) )
.distinctUntilChanged() .distinctUntilChanged()
.switchMap { (query, showOnlyUngrouped) -> .switchMap { (query, showOnlyUngrouped) ->
@ -55,8 +55,10 @@ class FeedGroupDialogViewModel(
.subscribe(mutableGroupLiveData::postValue) .subscribe(mutableGroupLiveData::postValue)
private var subscriptionsDisposable = Flowable private var subscriptionsDisposable = Flowable
.combineLatest(subscriptionsFlowable, feedDatabaseManager.subscriptionIdsForGroup(groupId), .combineLatest(
BiFunction { t1: List<PickerSubscriptionItem>, t2: List<Long> -> t1 to t2.toSet() }) subscriptionsFlowable, feedDatabaseManager.subscriptionIdsForGroup(groupId),
BiFunction { t1: List<PickerSubscriptionItem>, t2: List<Long> -> t1 to t2.toSet() }
)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.subscribe(mutableSubscriptionsLiveData::postValue) .subscribe(mutableSubscriptionsLiveData::postValue)
@ -68,15 +70,19 @@ class FeedGroupDialogViewModel(
} }
fun createGroup(name: String, selectedIcon: FeedGroupIcon, selectedSubscriptions: Set<Long>) { fun createGroup(name: String, selectedIcon: FeedGroupIcon, selectedSubscriptions: Set<Long>) {
doAction(feedDatabaseManager.createGroup(name, selectedIcon) doAction(
.flatMapCompletable { feedDatabaseManager.createGroup(name, selectedIcon)
feedDatabaseManager.updateSubscriptionsForGroup(it, selectedSubscriptions.toList()) .flatMapCompletable {
}) feedDatabaseManager.updateSubscriptionsForGroup(it, selectedSubscriptions.toList())
}
)
} }
fun updateGroup(name: String, selectedIcon: FeedGroupIcon, selectedSubscriptions: Set<Long>, sortOrder: Long) { fun updateGroup(name: String, selectedIcon: FeedGroupIcon, selectedSubscriptions: Set<Long>, sortOrder: Long) {
doAction(feedDatabaseManager.updateSubscriptionsForGroup(groupId, selectedSubscriptions.toList()) doAction(
.andThen(feedDatabaseManager.updateGroup(FeedGroupEntity(groupId, name, selectedIcon, sortOrder)))) feedDatabaseManager.updateSubscriptionsForGroup(groupId, selectedSubscriptions.toList())
.andThen(feedDatabaseManager.updateGroup(FeedGroupEntity(groupId, name, selectedIcon, sortOrder)))
)
} }
fun deleteGroup() { fun deleteGroup() {
@ -120,8 +126,10 @@ class FeedGroupDialogViewModel(
) : ViewModelProvider.Factory { ) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T { override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return FeedGroupDialogViewModel(context.applicationContext, return FeedGroupDialogViewModel(
groupId, initialQuery, initialShowOnlyUngrouped) as T context.applicationContext,
groupId, initialQuery, initialShowOnlyUngrouped
) as T
} }
} }
} }

View file

@ -16,7 +16,6 @@ import com.xwray.groupie.TouchCallback
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
import icepick.Icepick import icepick.Icepick
import icepick.State import icepick.State
import java.util.Collections
import kotlinx.android.synthetic.main.dialog_feed_group_reorder.confirm_button import kotlinx.android.synthetic.main.dialog_feed_group_reorder.confirm_button
import kotlinx.android.synthetic.main.dialog_feed_group_reorder.feed_groups_list import kotlinx.android.synthetic.main.dialog_feed_group_reorder.feed_groups_list
import org.schabi.newpipe.R import org.schabi.newpipe.R
@ -25,6 +24,7 @@ import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialogViewMo
import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialogViewModel.DialogEvent.SuccessEvent import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialogViewModel.DialogEvent.SuccessEvent
import org.schabi.newpipe.local.subscription.item.FeedGroupReorderItem import org.schabi.newpipe.local.subscription.item.FeedGroupReorderItem
import org.schabi.newpipe.util.ThemeHelper import org.schabi.newpipe.util.ThemeHelper
import java.util.Collections
class FeedGroupReorderDialog : DialogFragment() { class FeedGroupReorderDialog : DialogFragment() {
private lateinit var viewModel: FeedGroupReorderDialogViewModel private lateinit var viewModel: FeedGroupReorderDialogViewModel
@ -51,12 +51,15 @@ class FeedGroupReorderDialog : DialogFragment() {
viewModel = ViewModelProvider(this).get(FeedGroupReorderDialogViewModel::class.java) viewModel = ViewModelProvider(this).get(FeedGroupReorderDialogViewModel::class.java)
viewModel.groupsLiveData.observe(viewLifecycleOwner, Observer(::handleGroups)) viewModel.groupsLiveData.observe(viewLifecycleOwner, Observer(::handleGroups))
viewModel.dialogEventLiveData.observe(viewLifecycleOwner, Observer { viewModel.dialogEventLiveData.observe(
when (it) { viewLifecycleOwner,
ProcessingEvent -> disableInput() Observer {
SuccessEvent -> dismiss() when (it) {
ProcessingEvent -> disableInput()
SuccessEvent -> dismiss()
}
} }
}) )
feed_groups_list.layoutManager = LinearLayoutManager(requireContext()) feed_groups_list.layoutManager = LinearLayoutManager(requireContext())
feed_groups_list.adapter = groupAdapter feed_groups_list.adapter = groupAdapter

View file

@ -4,9 +4,9 @@ import android.app.Application
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import io.reactivex.Completable import io.reactivex.rxjava3.core.Completable
import io.reactivex.disposables.Disposable import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import org.schabi.newpipe.database.feed.model.FeedGroupEntity import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.local.feed.FeedDatabaseManager import org.schabi.newpipe.local.feed.FeedDatabaseManager
@ -21,9 +21,9 @@ class FeedGroupReorderDialogViewModel(application: Application) : AndroidViewMod
private var actionProcessingDisposable: Disposable? = null private var actionProcessingDisposable: Disposable? = null
private var groupsDisposable = feedDatabaseManager.groups() private var groupsDisposable = feedDatabaseManager.groups()
.limit(1) .take(1)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.subscribe(mutableGroupsLiveData::postValue) .subscribe(mutableGroupsLiveData::postValue)
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()
@ -40,8 +40,8 @@ class FeedGroupReorderDialogViewModel(application: Application) : AndroidViewMod
mutableDialogEventLiveData.value = DialogEvent.ProcessingEvent mutableDialogEventLiveData.value = DialogEvent.ProcessingEvent
actionProcessingDisposable = completable actionProcessingDisposable = completable
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.subscribe { mutableDialogEventLiveData.postValue(DialogEvent.SuccessEvent) } .subscribe { mutableDialogEventLiveData.postValue(DialogEvent.SuccessEvent) }
} }
} }

View file

@ -36,8 +36,10 @@ class ChannelItem(
viewHolder.itemAdditionalDetails.text = getDetailLine(viewHolder.root.context) viewHolder.itemAdditionalDetails.text = getDetailLine(viewHolder.root.context)
if (itemVersion == ItemVersion.NORMAL) viewHolder.itemChannelDescriptionView.text = infoItem.description if (itemVersion == ItemVersion.NORMAL) viewHolder.itemChannelDescriptionView.text = infoItem.description
ImageLoader.getInstance().displayImage(infoItem.thumbnailUrl, viewHolder.itemThumbnailView, ImageLoader.getInstance().displayImage(
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS) infoItem.thumbnailUrl, viewHolder.itemThumbnailView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS
)
gesturesListener?.run { gesturesListener?.run {
viewHolder.containerView.setOnClickListener { selected(infoItem) } viewHolder.containerView.setOnClickListener { selected(infoItem) }

View file

@ -20,7 +20,7 @@ data class FeedGroupReorderItem(
val dragCallback: ItemTouchHelper val dragCallback: ItemTouchHelper
) : Item() { ) : Item() {
constructor (feedGroupEntity: FeedGroupEntity, dragCallback: ItemTouchHelper) : constructor (feedGroupEntity: FeedGroupEntity, dragCallback: ItemTouchHelper) :
this(feedGroupEntity.uid, feedGroupEntity.name, feedGroupEntity.icon, dragCallback) this(feedGroupEntity.uid, feedGroupEntity.name, feedGroupEntity.icon, dragCallback)
override fun getId(): Long { override fun getId(): Long {
return when (groupId) { return when (groupId) {

View file

@ -49,8 +49,10 @@ class FeedImportExportItem(
expandIconListener?.let { viewHolder.import_export_options.removeListener(it) } expandIconListener?.let { viewHolder.import_export_options.removeListener(it) }
expandIconListener = CollapsibleView.StateListener { newState -> expandIconListener = CollapsibleView.StateListener { newState ->
AnimationUtils.animateRotation(viewHolder.import_export_expand_icon, AnimationUtils.animateRotation(
250, if (newState == CollapsibleView.COLLAPSED) 0 else 180) viewHolder.import_export_expand_icon,
250, if (newState == CollapsibleView.COLLAPSED) 0 else 180
)
} }
viewHolder.import_export_options.currentState = if (isExpanded) CollapsibleView.EXPANDED else CollapsibleView.COLLAPSED viewHolder.import_export_options.currentState = if (isExpanded) CollapsibleView.EXPANDED else CollapsibleView.COLLAPSED
@ -85,8 +87,10 @@ class FeedImportExportItem(
} }
private fun setupImportFromItems(listHolder: ViewGroup) { private fun setupImportFromItems(listHolder: ViewGroup) {
val previousBackupItem = addItemView(listHolder.context.getString(R.string.previous_export), val previousBackupItem = addItemView(
ThemeHelper.resolveResourceIdFromAttr(listHolder.context, R.attr.ic_backup), listHolder) listHolder.context.getString(R.string.previous_export),
ThemeHelper.resolveResourceIdFromAttr(listHolder.context, R.attr.ic_backup), listHolder
)
previousBackupItem.setOnClickListener { onImportPreviousSelected() } previousBackupItem.setOnClickListener { onImportPreviousSelected() }
val iconColor = if (ThemeHelper.isLightThemeSelected(listHolder.context)) Color.BLACK else Color.WHITE val iconColor = if (ThemeHelper.isLightThemeSelected(listHolder.context)) Color.BLACK else Color.WHITE
@ -112,8 +116,10 @@ class FeedImportExportItem(
} }
private fun setupExportToItems(listHolder: ViewGroup) { private fun setupExportToItems(listHolder: ViewGroup) {
val previousBackupItem = addItemView(listHolder.context.getString(R.string.file), val previousBackupItem = addItemView(
ThemeHelper.resolveResourceIdFromAttr(listHolder.context, R.attr.ic_save), listHolder) listHolder.context.getString(R.string.file),
ThemeHelper.resolveResourceIdFromAttr(listHolder.context, R.attr.ic_save), listHolder
)
previousBackupItem.setOnClickListener { onExportSelected() } previousBackupItem.setOnClickListener { onExportSelected() }
} }
} }

View file

@ -36,11 +36,11 @@ class HeaderWithMenuItem(
viewHolder.header_menu_item.setImageResource(itemIcon) viewHolder.header_menu_item.setImageResource(itemIcon)
val listener: OnClickListener? = val listener: OnClickListener? =
onClickListener?.let { OnClickListener { onClickListener.invoke() } } onClickListener?.let { OnClickListener { onClickListener.invoke() } }
viewHolder.root.setOnClickListener(listener) viewHolder.root.setOnClickListener(listener)
val menuItemListener: OnClickListener? = val menuItemListener: OnClickListener? =
menuItemOnClickListener?.let { OnClickListener { menuItemOnClickListener.invoke() } } menuItemOnClickListener?.let { OnClickListener { menuItemOnClickListener.invoke() } }
viewHolder.header_menu_item.setOnClickListener(menuItemListener) viewHolder.header_menu_item.setOnClickListener(menuItemListener)
updateMenuItemVisibility(viewHolder) updateMenuItemVisibility(viewHolder)
} }

View file

@ -22,8 +22,10 @@ data class PickerSubscriptionItem(
override fun getSpanSize(spanCount: Int, position: Int): Int = 1 override fun getSpanSize(spanCount: Int, position: Int): Int = 1
override fun bind(viewHolder: GroupieViewHolder, position: Int) { override fun bind(viewHolder: GroupieViewHolder, position: Int) {
ImageLoader.getInstance().displayImage(subscriptionEntity.avatarUrl, ImageLoader.getInstance().displayImage(
viewHolder.thumbnail_view, ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS) subscriptionEntity.avatarUrl,
viewHolder.thumbnail_view, ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS
)
viewHolder.title_view.text = subscriptionEntity.name viewHolder.title_view.text = subscriptionEntity.name
viewHolder.selected_highlight.isVisible = isSelected viewHolder.selected_highlight.isVisible = isSelected
@ -39,7 +41,9 @@ data class PickerSubscriptionItem(
fun updateSelected(containerView: View, isSelected: Boolean) { fun updateSelected(containerView: View, isSelected: Boolean) {
this.isSelected = isSelected this.isSelected = isSelected
animateView(containerView.selected_highlight, animateView(
AnimationUtils.Type.LIGHT_SCALE_AND_ALPHA, isSelected, 150) containerView.selected_highlight,
AnimationUtils.Type.LIGHT_SCALE_AND_ALPHA, isSelected, 150
)
} }
} }

View file

@ -37,6 +37,7 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor; import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
import org.schabi.newpipe.local.subscription.SubscriptionManager; import org.schabi.newpipe.local.subscription.SubscriptionManager;
import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.ExceptionUtils; import org.schabi.newpipe.util.ExceptionUtils;
@ -45,11 +46,11 @@ import java.util.Collections;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import io.reactivex.Flowable; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.functions.Function; import io.reactivex.rxjava3.functions.Function;
import io.reactivex.processors.PublishProcessor; import io.reactivex.rxjava3.processors.PublishProcessor;
public abstract class BaseImportExportService extends Service { public abstract class BaseImportExportService extends Service {
protected final String TAG = this.getClass().getSimpleName(); protected final String TAG = this.getClass().getSimpleName();
@ -119,7 +120,7 @@ public abstract class BaseImportExportService extends Service {
startForeground(getNotificationId(), notificationBuilder.build()); startForeground(getNotificationId(), notificationBuilder.build());
final Function<Flowable<String>, Publisher<String>> throttleAfterFirstEmission = flow -> final Function<Flowable<String>, Publisher<String>> throttleAfterFirstEmission = flow ->
flow.limit(1).concatWith(flow.skip(1) flow.take(1).concatWith(flow.skip(1)
.throttleLast(NOTIFICATION_SAMPLING_PERIOD, TimeUnit.MILLISECONDS)); .throttleLast(NOTIFICATION_SAMPLING_PERIOD, TimeUnit.MILLISECONDS));
disposables.add(notificationUpdater disposables.add(notificationUpdater
@ -153,7 +154,7 @@ public abstract class BaseImportExportService extends Service {
protected void stopAndReportError(@Nullable final Throwable error, final String request) { protected void stopAndReportError(@Nullable final Throwable error, final String request) {
stopService(); stopService();
final ErrorActivity.ErrorInfo errorInfo = ErrorActivity.ErrorInfo final ErrorInfo errorInfo = ErrorInfo
.make(UserAction.SUBSCRIPTION, "unknown", request, R.string.general_error); .make(UserAction.SUBSCRIPTION, "unknown", request, R.string.general_error);
ErrorActivity.reportError(this, error != null ? Collections.singletonList(error) ErrorActivity.reportError(this, error != null ? Collections.singletonList(error)
: Collections.emptyList(), null, null, errorInfo); : Collections.emptyList(), null, null, errorInfo);

View file

@ -37,9 +37,9 @@ import java.io.FileOutputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.functions.Function; import io.reactivex.rxjava3.functions.Function;
import io.reactivex.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
import static org.schabi.newpipe.MainActivity.DEBUG; import static org.schabi.newpipe.MainActivity.DEBUG;

View file

@ -46,12 +46,12 @@ import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import io.reactivex.Flowable; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.Notification; import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Notification;
import io.reactivex.functions.Consumer; import io.reactivex.rxjava3.functions.Consumer;
import io.reactivex.functions.Function; import io.reactivex.rxjava3.functions.Function;
import io.reactivex.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
import static org.schabi.newpipe.MainActivity.DEBUG; import static org.schabi.newpipe.MainActivity.DEBUG;

View file

@ -27,13 +27,13 @@ import android.content.SharedPreferences;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.media.AudioManager; import android.media.AudioManager;
import androidx.preference.PreferenceManager;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.preference.PreferenceManager;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.DefaultRenderersFactory;
@ -83,17 +83,16 @@ import org.schabi.newpipe.util.VideoSegment;
import java.io.IOException; import java.io.IOException;
import java.util.Set; import java.util.Set;
import io.reactivex.Observable; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Observable;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.disposables.SerialDisposable; import io.reactivex.rxjava3.disposables.SerialDisposable;
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_INTERNAL; import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_INTERNAL;
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_PERIOD_TRANSITION; import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_PERIOD_TRANSITION;
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK; import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK;
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT; import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT;
import static io.reactivex.android.schedulers.AndroidSchedulers.mainThread;
import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MILLISECONDS;
/** /**
@ -846,8 +845,9 @@ public abstract class BasePlayer implements
} }
private Disposable getProgressReactor() { private Disposable getProgressReactor() {
return Observable.interval(PROGRESS_LOOP_INTERVAL_MILLIS, MILLISECONDS, mainThread()) return Observable.interval(PROGRESS_LOOP_INTERVAL_MILLIS, MILLISECONDS,
.observeOn(mainThread()) AndroidSchedulers.mainThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> triggerProgressUpdate(), .subscribe(ignored -> triggerProgressUpdate(),
error -> Log.e(TAG, "Progress update failure: ", error)); error -> Log.e(TAG, "Progress update failure: ", error));
} }
@ -1512,7 +1512,7 @@ public abstract class BasePlayer implements
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
if (prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) { if (prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) {
final Disposable stateSaver = recordManager.saveStreamState(info, progress) final Disposable stateSaver = recordManager.saveStreamState(info, progress)
.observeOn(mainThread()) .observeOn(AndroidSchedulers.mainThread())
.doOnError((e) -> { .doOnError((e) -> {
if (DEBUG) { if (DEBUG) {
e.printStackTrace(); e.printStackTrace();
@ -1532,7 +1532,7 @@ public abstract class BasePlayer implements
if (prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) { if (prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) {
final Disposable stateSaver = queueItem.getStream() final Disposable stateSaver = queueItem.getStream()
.flatMapCompletable(info -> recordManager.saveStreamState(info, 0)) .flatMapCompletable(info -> recordManager.saveStreamState(info, 0))
.observeOn(mainThread()) .observeOn(AndroidSchedulers.mainThread())
.doOnError((e) -> { .doOnError((e) -> {
if (DEBUG) { if (DEBUG) {
e.printStackTrace(); e.printStackTrace();
@ -1738,8 +1738,7 @@ public abstract class BasePlayer implements
if (simpleExoPlayer == null) { if (simpleExoPlayer == null) {
return PlaybackParameters.DEFAULT; return PlaybackParameters.DEFAULT;
} }
final PlaybackParameters parameters = simpleExoPlayer.getPlaybackParameters(); return simpleExoPlayer.getPlaybackParameters();
return parameters == null ? PlaybackParameters.DEFAULT : parameters;
} }
/** /**

View file

@ -48,7 +48,7 @@ public final class NotificationUtil {
@Nullable private static NotificationUtil instance = null; @Nullable private static NotificationUtil instance = null;
@NotificationConstants.Action @NotificationConstants.Action
private int[] notificationSlots = NotificationConstants.SLOT_DEFAULTS.clone(); private final int[] notificationSlots = NotificationConstants.SLOT_DEFAULTS.clone();
private NotificationManagerCompat notificationManager; private NotificationManagerCompat notificationManager;
private NotificationCompat.Builder notificationBuilder; private NotificationCompat.Builder notificationBuilder;
@ -146,7 +146,11 @@ public final class NotificationUtil {
notificationBuilder.setContentText(player.getUploaderName()); notificationBuilder.setContentText(player.getUploaderName());
notificationBuilder.setTicker(player.getVideoTitle()); notificationBuilder.setTicker(player.getVideoTitle());
updateActions(notificationBuilder, player); updateActions(notificationBuilder, player);
setLargeIcon(notificationBuilder, player); final boolean showThumbnail = player.sharedPreferences.getBoolean(
player.context.getString(R.string.show_thumbnail_key), true);
if (showThumbnail) {
setLargeIcon(notificationBuilder, player);
}
} }

View file

@ -342,7 +342,7 @@ public class VideoPlayerImpl extends VideoPlayer
view.setFixedTextSize(TypedValue.COMPLEX_UNIT_PX, view.setFixedTextSize(TypedValue.COMPLEX_UNIT_PX,
(float) minimumLength / captionRatioInverse); (float) minimumLength / captionRatioInverse);
} }
view.setApplyEmbeddedStyles(captionStyle.equals(CaptionStyleCompat.DEFAULT)); view.setApplyEmbeddedStyles(captionStyle == CaptionStyleCompat.DEFAULT);
view.setStyle(captionStyle); view.setStyle(captionStyle);
} }
@ -715,9 +715,13 @@ public class VideoPlayerImpl extends VideoPlayer
super.onUpdateProgress(currentProgress, duration, bufferPercent); super.onUpdateProgress(currentProgress, duration, bufferPercent);
updateProgress(currentProgress, duration, bufferPercent); updateProgress(currentProgress, duration, bufferPercent);
final boolean showThumbnail =
sharedPreferences.getBoolean(
context.getString(R.string.show_thumbnail_key),
true);
// setMetadata only updates the metadata when any of the metadata keys are null // setMetadata only updates the metadata when any of the metadata keys are null
mediaSessionManager.setMetadata(getVideoTitle(), getUploaderName(), getThumbnail(), mediaSessionManager.setMetadata(getVideoTitle(), getUploaderName(),
duration); showThumbnail ? getThumbnail() : null, duration);
} }
@Override @Override

View file

@ -15,6 +15,7 @@ import org.schabi.newpipe.util.AnimationUtils
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.hypot import kotlin.math.hypot
import kotlin.math.max import kotlin.math.max
import kotlin.math.min
/** /**
* Base gesture handling for [VideoPlayerImpl] * Base gesture handling for [VideoPlayerImpl]
@ -117,22 +118,30 @@ abstract class BasePlayerGestureListener(
initSecPointerX = event.getX(1) initSecPointerX = event.getX(1)
initSecPointerY = event.getY(1) initSecPointerY = event.getY(1)
// record distance between fingers // record distance between fingers
initPointerDistance = Math.hypot(initFirstPointerX - initSecPointerX.toDouble(), initPointerDistance = hypot(
initFirstPointerY - initSecPointerY.toDouble()) initFirstPointerX - initSecPointerX.toDouble(),
initFirstPointerY - initSecPointerY.toDouble()
)
isResizing = true isResizing = true
} }
if (event.action == MotionEvent.ACTION_MOVE && !isMovingInPopup && isResizing) { if (event.action == MotionEvent.ACTION_MOVE && !isMovingInPopup && isResizing) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onTouch() ACTION_MOVE > v = [$v], e1.getRaw = [${event.rawX}" + Log.d(
", ${event.rawY}]") TAG,
"onTouch() ACTION_MOVE > v = [$v], e1.getRaw =" +
"[${event.rawX}, ${event.rawY}]"
)
} }
return handleMultiDrag(event) return handleMultiDrag(event)
} }
if (event.action == MotionEvent.ACTION_UP) { if (event.action == MotionEvent.ACTION_UP) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onTouch() ACTION_UP > v = [$v], e1.getRaw = [${event.rawX}" + Log.d(
", ${event.rawY}]") TAG,
"onTouch() ACTION_UP > v = [$v], e1.getRaw =" +
" [${event.rawX}, ${event.rawY}]"
)
} }
if (isMovingInPopup) { if (isMovingInPopup) {
isMovingInPopup = false isMovingInPopup = false
@ -162,18 +171,24 @@ abstract class BasePlayerGestureListener(
private fun handleMultiDrag(event: MotionEvent): Boolean { private fun handleMultiDrag(event: MotionEvent): Boolean {
if (initPointerDistance != -1.0 && event.pointerCount == 2) { if (initPointerDistance != -1.0 && event.pointerCount == 2) {
// get the movements of the fingers // get the movements of the fingers
val firstPointerMove = hypot(event.getX(0) - initFirstPointerX.toDouble(), val firstPointerMove = hypot(
event.getY(0) - initFirstPointerY.toDouble()) event.getX(0) - initFirstPointerX.toDouble(),
val secPointerMove = hypot(event.getX(1) - initSecPointerX.toDouble(), event.getY(0) - initFirstPointerY.toDouble()
event.getY(1) - initSecPointerY.toDouble()) )
val secPointerMove = hypot(
event.getX(1) - initSecPointerX.toDouble(),
event.getY(1) - initSecPointerY.toDouble()
)
// minimum threshold beyond which pinch gesture will work // minimum threshold beyond which pinch gesture will work
val minimumMove = ViewConfiguration.get(service).scaledTouchSlop val minimumMove = ViewConfiguration.get(service).scaledTouchSlop
if (max(firstPointerMove, secPointerMove) > minimumMove) { if (max(firstPointerMove, secPointerMove) > minimumMove) {
// calculate current distance between the pointers // calculate current distance between the pointers
val currentPointerDistance = hypot(event.getX(0) - event.getX(1).toDouble(), val currentPointerDistance = hypot(
event.getY(0) - event.getY(1).toDouble()) event.getX(0) - event.getX(1).toDouble(),
event.getY(0) - event.getY(1).toDouble()
)
val popupWidth = playerImpl.popupWidth.toDouble() val popupWidth = playerImpl.popupWidth.toDouble()
// change co-ordinates of popup so the center stays at the same position // change co-ordinates of popup so the center stays at the same position
@ -185,8 +200,9 @@ abstract class BasePlayerGestureListener(
playerImpl.updateScreenSize() playerImpl.updateScreenSize()
playerImpl.updatePopupSize( playerImpl.updatePopupSize(
Math.min(playerImpl.screenWidth.toDouble(), newWidth).toInt(), min(playerImpl.screenWidth.toDouble(), newWidth).toInt(),
-1) -1
)
return true return true
} }
} }
@ -315,22 +331,30 @@ abstract class BasePlayerGestureListener(
} }
val isTouchingStatusBar: Boolean = initialEvent.y < getStatusBarHeight(service) val isTouchingStatusBar: Boolean = initialEvent.y < getStatusBarHeight(service)
val isTouchingNavigationBar: Boolean = (initialEvent.y val isTouchingNavigationBar: Boolean =
> playerImpl.rootView.height - getNavigationBarHeight(service)) initialEvent.y > (playerImpl.rootView.height - getNavigationBarHeight(service))
if (isTouchingStatusBar || isTouchingNavigationBar) { if (isTouchingStatusBar || isTouchingNavigationBar) {
return false return false
} }
val insideThreshold = abs(movingEvent.y - initialEvent.y) <= MOVEMENT_THRESHOLD val insideThreshold = abs(movingEvent.y - initialEvent.y) <= MOVEMENT_THRESHOLD
if (!isMovingInMain && (insideThreshold || abs(distanceX) > abs(distanceY)) || if (
playerImpl.currentState == BasePlayer.STATE_COMPLETED) { !isMovingInMain && (insideThreshold || abs(distanceX) > abs(distanceY)) ||
playerImpl.currentState == BasePlayer.STATE_COMPLETED
) {
return false return false
} }
isMovingInMain = true isMovingInMain = true
onScroll(MainPlayer.PlayerType.VIDEO, getDisplayHalfPortion(initialEvent), onScroll(
initialEvent, movingEvent, distanceX, distanceY) MainPlayer.PlayerType.VIDEO,
getDisplayHalfPortion(initialEvent),
initialEvent,
movingEvent,
distanceX,
distanceY
)
return true return true
} }
@ -372,8 +396,14 @@ abstract class BasePlayerGestureListener(
playerImpl.popupLayoutParams.x = posX.toInt() playerImpl.popupLayoutParams.x = posX.toInt()
playerImpl.popupLayoutParams.y = posY.toInt() playerImpl.popupLayoutParams.y = posY.toInt()
onScroll(MainPlayer.PlayerType.POPUP, getDisplayHalfPortion(initialEvent), onScroll(
initialEvent, movingEvent, distanceX, distanceY) MainPlayer.PlayerType.POPUP,
getDisplayHalfPortion(initialEvent),
initialEvent,
movingEvent,
distanceX,
distanceY
)
playerImpl.windowManager playerImpl.windowManager
.updateViewLayout(playerImpl.rootView, playerImpl.popupLayoutParams) .updateViewLayout(playerImpl.rootView, playerImpl.popupLayoutParams)

View file

@ -36,15 +36,10 @@ public class PlayerGestureListener
private static final String TAG = ".PlayerGestureListener"; private static final String TAG = ".PlayerGestureListener";
private static final boolean DEBUG = BasePlayer.DEBUG; private static final boolean DEBUG = BasePlayer.DEBUG;
private final boolean isVolumeGestureEnabled;
private final boolean isBrightnessGestureEnabled;
private final int maxVolume; private final int maxVolume;
public PlayerGestureListener(final VideoPlayerImpl playerImpl, final MainPlayer service) { public PlayerGestureListener(final VideoPlayerImpl playerImpl, final MainPlayer service) {
super(playerImpl, service); super(playerImpl, service);
isVolumeGestureEnabled = PlayerHelper.isVolumeGestureEnabled(service);
isBrightnessGestureEnabled = PlayerHelper.isBrightnessGestureEnabled(service);
maxVolume = playerImpl.getAudioReactor().getMaxVolume(); maxVolume = playerImpl.getAudioReactor().getMaxVolume();
} }
@ -110,10 +105,20 @@ public class PlayerGestureListener
+ portion + "]"); + portion + "]");
} }
if (playerType == MainPlayer.PlayerType.VIDEO) { if (playerType == MainPlayer.PlayerType.VIDEO) {
if (portion == DisplayPortion.LEFT_HALF) { final boolean isBrightnessGestureEnabled =
onScrollMainBrightness(distanceX, distanceY); PlayerHelper.isBrightnessGestureEnabled(service);
final boolean isVolumeGestureEnabled = PlayerHelper.isVolumeGestureEnabled(service);
} else /* DisplayPortion.RIGHT_HALF */ { if (isBrightnessGestureEnabled && isVolumeGestureEnabled) {
if (portion == DisplayPortion.LEFT_HALF) {
onScrollMainBrightness(distanceX, distanceY);
} else /* DisplayPortion.RIGHT_HALF */ {
onScrollMainVolume(distanceX, distanceY);
}
} else if (isBrightnessGestureEnabled) {
onScrollMainBrightness(distanceX, distanceY);
} else if (isVolumeGestureEnabled) {
onScrollMainVolume(distanceX, distanceY); onScrollMainVolume(distanceX, distanceY);
} }
@ -132,75 +137,71 @@ public class PlayerGestureListener
} }
private void onScrollMainVolume(final float distanceX, final float distanceY) { private void onScrollMainVolume(final float distanceX, final float distanceY) {
if (isVolumeGestureEnabled) { playerImpl.getVolumeProgressBar().incrementProgressBy((int) distanceY);
playerImpl.getVolumeProgressBar().incrementProgressBy((int) distanceY); final float currentProgressPercent = (float) playerImpl
final float currentProgressPercent = (float) playerImpl .getVolumeProgressBar().getProgress() / playerImpl.getMaxGestureLength();
.getVolumeProgressBar().getProgress() / playerImpl.getMaxGestureLength(); final int currentVolume = (int) (maxVolume * currentProgressPercent);
final int currentVolume = (int) (maxVolume * currentProgressPercent); playerImpl.getAudioReactor().setVolume(currentVolume);
playerImpl.getAudioReactor().setVolume(currentVolume);
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onScroll().volumeControl, currentVolume = " + currentVolume); Log.d(TAG, "onScroll().volumeControl, currentVolume = " + currentVolume);
} }
playerImpl.getVolumeImageView().setImageDrawable( playerImpl.getVolumeImageView().setImageDrawable(
AppCompatResources.getDrawable(service, currentProgressPercent <= 0 AppCompatResources.getDrawable(service, currentProgressPercent <= 0
? R.drawable.ic_volume_off_white_24dp ? R.drawable.ic_volume_off_white_24dp
: currentProgressPercent < 0.25 ? R.drawable.ic_volume_mute_white_24dp : currentProgressPercent < 0.25 ? R.drawable.ic_volume_mute_white_24dp
: currentProgressPercent < 0.75 ? R.drawable.ic_volume_down_white_24dp : currentProgressPercent < 0.75 ? R.drawable.ic_volume_down_white_24dp
: R.drawable.ic_volume_up_white_24dp) : R.drawable.ic_volume_up_white_24dp)
); );
if (playerImpl.getVolumeRelativeLayout().getVisibility() != View.VISIBLE) { if (playerImpl.getVolumeRelativeLayout().getVisibility() != View.VISIBLE) {
animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA, true, 200); animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA, true, 200);
} }
if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) { if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) {
playerImpl.getBrightnessRelativeLayout().setVisibility(View.GONE); playerImpl.getBrightnessRelativeLayout().setVisibility(View.GONE);
}
} }
} }
private void onScrollMainBrightness(final float distanceX, final float distanceY) { private void onScrollMainBrightness(final float distanceX, final float distanceY) {
if (isBrightnessGestureEnabled) { final Activity parent = playerImpl.getParentActivity();
final Activity parent = playerImpl.getParentActivity(); if (parent == null) {
if (parent == null) { return;
return; }
}
final Window window = parent.getWindow(); final Window window = parent.getWindow();
final WindowManager.LayoutParams layoutParams = window.getAttributes(); final WindowManager.LayoutParams layoutParams = window.getAttributes();
final ProgressBar bar = playerImpl.getBrightnessProgressBar(); final ProgressBar bar = playerImpl.getBrightnessProgressBar();
final float oldBrightness = layoutParams.screenBrightness; final float oldBrightness = layoutParams.screenBrightness;
bar.setProgress((int) (bar.getMax() * Math.max(0, Math.min(1, oldBrightness)))); bar.setProgress((int) (bar.getMax() * Math.max(0, Math.min(1, oldBrightness))));
bar.incrementProgressBy((int) distanceY); bar.incrementProgressBy((int) distanceY);
final float currentProgressPercent = (float) bar.getProgress() / bar.getMax(); final float currentProgressPercent = (float) bar.getProgress() / bar.getMax();
layoutParams.screenBrightness = currentProgressPercent; layoutParams.screenBrightness = currentProgressPercent;
window.setAttributes(layoutParams); window.setAttributes(layoutParams);
// Save current brightness level // Save current brightness level
PlayerHelper.setScreenBrightness(parent, currentProgressPercent); PlayerHelper.setScreenBrightness(parent, currentProgressPercent);
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onScroll().brightnessControl, " Log.d(TAG, "onScroll().brightnessControl, "
+ "currentBrightness = " + currentProgressPercent); + "currentBrightness = " + currentProgressPercent);
} }
playerImpl.getBrightnessImageView().setImageDrawable( playerImpl.getBrightnessImageView().setImageDrawable(
AppCompatResources.getDrawable(service, AppCompatResources.getDrawable(service,
currentProgressPercent < 0.25 currentProgressPercent < 0.25
? R.drawable.ic_brightness_low_white_24dp ? R.drawable.ic_brightness_low_white_24dp
: currentProgressPercent < 0.75 : currentProgressPercent < 0.75
? R.drawable.ic_brightness_medium_white_24dp ? R.drawable.ic_brightness_medium_white_24dp
: R.drawable.ic_brightness_high_white_24dp) : R.drawable.ic_brightness_high_white_24dp)
); );
if (playerImpl.getBrightnessRelativeLayout().getVisibility() != View.VISIBLE) { if (playerImpl.getBrightnessRelativeLayout().getVisibility() != View.VISIBLE) {
animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, true, 200); animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, true, 200);
} }
if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) { if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) {
playerImpl.getVolumeRelativeLayout().setVisibility(View.GONE); playerImpl.getVolumeRelativeLayout().setVisibility(View.GONE);
}
} }
} }

View file

@ -30,14 +30,14 @@ import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import io.reactivex.Observable; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.Single; import io.reactivex.rxjava3.core.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Single;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.internal.subscriptions.EmptySubscription; import io.reactivex.rxjava3.internal.subscriptions.EmptySubscription;
import io.reactivex.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
import io.reactivex.subjects.PublishSubject; import io.reactivex.rxjava3.subjects.PublishSubject;
import static org.schabi.newpipe.player.mediasource.FailedMediaSource.MediaSourceResolutionException; import static org.schabi.newpipe.player.mediasource.FailedMediaSource.MediaSourceResolutionException;
import static org.schabi.newpipe.player.mediasource.FailedMediaSource.StreamInfoLoadException; import static org.schabi.newpipe.player.mediasource.FailedMediaSource.StreamInfoLoadException;
@ -123,7 +123,7 @@ public class MediaSourceManager {
@NonNull @NonNull
private ManagedMediaSourcePlaylist playlist; private ManagedMediaSourcePlaylist playlist;
private Handler removeMediaSourceHandler = new Handler(); private final Handler removeMediaSourceHandler = new Handler();
public MediaSourceManager(@NonNull final PlaybackListener listener, public MediaSourceManager(@NonNull final PlaybackListener listener,
@NonNull final PlayQueue playQueue) { @NonNull final PlayQueue playQueue) {

View file

@ -14,8 +14,8 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import io.reactivex.SingleObserver; import io.reactivex.rxjava3.core.SingleObserver;
import io.reactivex.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
abstract class AbstractInfoPlayQueue<T extends ListInfo, U extends InfoItem> extends PlayQueue { abstract class AbstractInfoPlayQueue<T extends ListInfo, U extends InfoItem> extends PlayQueue {
boolean isInitial; boolean isInitial;

View file

@ -9,8 +9,8 @@ import org.schabi.newpipe.util.ExtractorHelper;
import java.util.List; import java.util.List;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
public final class ChannelPlayQueue extends AbstractInfoPlayQueue<ChannelInfo, ChannelInfoItem> { public final class ChannelPlayQueue extends AbstractInfoPlayQueue<ChannelInfo, ChannelInfoItem> {
public ChannelPlayQueue(final ChannelInfoItem item) { public ChannelPlayQueue(final ChannelInfoItem item) {

View file

@ -21,10 +21,10 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import io.reactivex.BackpressureStrategy; import io.reactivex.rxjava3.core.BackpressureStrategy;
import io.reactivex.Flowable; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.subjects.BehaviorSubject; import io.reactivex.rxjava3.subjects.BehaviorSubject;
/** /**
* PlayQueue is responsible for keeping track of a list of streams and the index of * PlayQueue is responsible for keeping track of a list of streams and the index of
@ -80,7 +80,7 @@ public abstract class PlayQueue implements Serializable {
broadcastReceiver = eventBroadcast.toFlowable(BackpressureStrategy.BUFFER) broadcastReceiver = eventBroadcast.toFlowable(BackpressureStrategy.BUFFER)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.startWith(new InitEvent()); .startWithItem(new InitEvent());
} }
/** /**

View file

@ -20,8 +20,8 @@ import org.schabi.newpipe.util.FallbackViewHolder;
import java.util.List; import java.util.List;
import io.reactivex.Observer; import io.reactivex.rxjava3.core.Observer;
import io.reactivex.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
/** /**
* Created by Christian Schabesberger on 01.08.16. * Created by Christian Schabesberger on 01.08.16.
@ -215,7 +215,7 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
} }
} }
public class HFHolder extends RecyclerView.ViewHolder { public static class HFHolder extends RecyclerView.ViewHolder {
public View view; public View view;
public HFHolder(final View v) { public HFHolder(final View v) {

View file

@ -10,8 +10,8 @@ import org.schabi.newpipe.util.ExtractorHelper;
import java.io.Serializable; import java.io.Serializable;
import io.reactivex.Single; import io.reactivex.rxjava3.core.Single;
import io.reactivex.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
public class PlayQueueItem implements Serializable { public class PlayQueueItem implements Serializable {
public static final long RECOVERY_UNSET = Long.MIN_VALUE; public static final long RECOVERY_UNSET = Long.MIN_VALUE;

View file

@ -8,8 +8,8 @@ import org.schabi.newpipe.util.ExtractorHelper;
import java.util.List; import java.util.List;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
public final class PlaylistPlayQueue extends AbstractInfoPlayQueue<PlaylistInfo, PlaylistInfoItem> { public final class PlaylistPlayQueue extends AbstractInfoPlayQueue<PlaylistInfo, PlaylistInfoItem> {
public PlaylistPlayQueue(final PlaylistInfoItem item) { public PlaylistPlayQueue(final PlaylistInfoItem item) {

View file

@ -33,7 +33,7 @@ public class AcraReportSender implements ReportSender {
@Override @Override
public void send(@NonNull final Context context, @NonNull final CrashReportData report) { public void send(@NonNull final Context context, @NonNull final CrashReportData report) {
ErrorActivity.reportError(context, report, ErrorActivity.reportError(context, report,
ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, "none", ErrorInfo.make(UserAction.UI_ERROR, "none",
"App crash, UI failure", R.string.app_ui_crash)); "App crash, UI failure", R.string.app_ui_crash));
} }
} }

Some files were not shown because too many files have changed in this diff Show more