1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2025-11-12 21:22:59 +00:00

Merged the latest changes

This commit is contained in:
Avently
2020-07-13 04:17:21 +03:00
1254 changed files with 39193 additions and 18652 deletions

View File

@@ -0,0 +1,119 @@
package org.schabi.newpipe.database
import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import androidx.room.Room
import androidx.room.testing.MigrationTestHelper
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.schabi.newpipe.extractor.stream.StreamType
@RunWith(AndroidJUnit4::class)
class AppDatabaseTest {
companion object {
private const val DEFAULT_SERVICE_ID = 0
private const val DEFAULT_URL = "https://www.youtube.com/watch?v=cDphUib5iG4"
private const val DEFAULT_TITLE = "Test Title"
private val DEFAULT_TYPE = StreamType.VIDEO_STREAM
private const val DEFAULT_DURATION = 480L
private const val DEFAULT_UPLOADER_NAME = "Uploader Test"
private const val DEFAULT_THUMBNAIL = "https://example.com/example.jpg"
private const val DEFAULT_SECOND_SERVICE_ID = 0
private const val DEFAULT_SECOND_URL = "https://www.youtube.com/watch?v=ncQU6iBn5Fc"
}
@get:Rule
val testHelper = MigrationTestHelper(InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java.canonicalName, FrameworkSQLiteOpenHelperFactory())
@Test
fun migrateDatabaseFrom2to3() {
val databaseInV2 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_2)
databaseInV2.run {
insert("streams", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply {
// put("uid", null)
put("service_id", DEFAULT_SERVICE_ID)
put("url", DEFAULT_URL)
put("title", DEFAULT_TITLE)
put("stream_type", DEFAULT_TYPE.name)
put("duration", DEFAULT_DURATION)
put("uploader", DEFAULT_UPLOADER_NAME)
put("thumbnail_url", DEFAULT_THUMBNAIL)
})
insert("streams", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply {
// put("uid", null)
put("service_id", DEFAULT_SECOND_SERVICE_ID)
put("url", DEFAULT_SECOND_URL)
// put("title", null)
// put("stream_type", null)
// put("duration", null)
// put("uploader", null)
// put("thumbnail_url", null)
})
insert("streams", SQLiteDatabase.CONFLICT_FAIL, 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()
}
testHelper.runMigrationsAndValidate(AppDatabase.DATABASE_NAME, Migrations.DB_VER_3,
true, Migrations.MIGRATION_2_3)
val migratedDatabaseV3 = getMigratedDatabase()
val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst()
// Only expect 2, the one with the null url will be ignored
assertEquals(2, listFromDB.size)
val streamFromMigratedDatabase = listFromDB[0]
assertEquals(DEFAULT_SERVICE_ID, streamFromMigratedDatabase.serviceId)
assertEquals(DEFAULT_URL, streamFromMigratedDatabase.url)
assertEquals(DEFAULT_TITLE, streamFromMigratedDatabase.title)
assertEquals(DEFAULT_TYPE, streamFromMigratedDatabase.streamType)
assertEquals(DEFAULT_DURATION, streamFromMigratedDatabase.duration)
assertEquals(DEFAULT_UPLOADER_NAME, streamFromMigratedDatabase.uploader)
assertEquals(DEFAULT_THUMBNAIL, streamFromMigratedDatabase.thumbnailUrl)
assertNull(streamFromMigratedDatabase.viewCount)
assertNull(streamFromMigratedDatabase.textualUploadDate)
assertNull(streamFromMigratedDatabase.uploadDate)
assertNull(streamFromMigratedDatabase.isUploadDateApproximation)
val secondStreamFromMigratedDatabase = listFromDB[1]
assertEquals(DEFAULT_SECOND_SERVICE_ID, secondStreamFromMigratedDatabase.serviceId)
assertEquals(DEFAULT_SECOND_URL, secondStreamFromMigratedDatabase.url)
assertEquals("", secondStreamFromMigratedDatabase.title)
// Should fallback to VIDEO_STREAM
assertEquals(StreamType.VIDEO_STREAM, secondStreamFromMigratedDatabase.streamType)
assertEquals(0, secondStreamFromMigratedDatabase.duration)
assertEquals("", secondStreamFromMigratedDatabase.uploader)
assertEquals("", secondStreamFromMigratedDatabase.thumbnailUrl)
assertNull(secondStreamFromMigratedDatabase.viewCount)
assertNull(secondStreamFromMigratedDatabase.textualUploadDate)
assertNull(secondStreamFromMigratedDatabase.uploadDate)
assertNull(secondStreamFromMigratedDatabase.isUploadDateApproximation)
}
private fun getMigratedDatabase(): AppDatabase {
val database: AppDatabase = Room.databaseBuilder(ApplicationProvider.getApplicationContext(),
AppDatabase::class.java, AppDatabase.DATABASE_NAME)
.build()
testHelper.closeWhenFinished(database)
return database
}
}

View File

@@ -1,9 +1,10 @@
package org.schabi.newpipe.report;
import android.os.Parcel;
import androidx.test.filters.LargeTest;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.schabi.newpipe.R;
@@ -12,15 +13,16 @@ import org.schabi.newpipe.report.ErrorActivity.ErrorInfo;
import static org.junit.Assert.assertEquals;
/**
* Instrumented tests for {@link ErrorInfo}
* Instrumented tests for {@link ErrorInfo}.
*/
@RunWith(AndroidJUnit4.class)
@LargeTest
public class ErrorInfoTest {
@Test
public void errorInfo_testParcelable() {
ErrorInfo info = ErrorInfo.make(UserAction.USER_REPORT, "youtube", "request", R.string.general_error);
public void errorInfoTestParcelable() {
ErrorInfo info = ErrorInfo.make(UserAction.USER_REPORT, "youtube", "request",
R.string.general_error);
// Obtain a Parcel object and write the parcelable object to it:
Parcel parcel = Parcel.obtain();
info.writeToParcel(parcel, 0);
@@ -34,4 +36,4 @@ public class ErrorInfoTest {
parcel.recycle();
}
}
}

View File

@@ -6,12 +6,5 @@
<application
android:name=".DebugApp"
android:label="NewPipe Debug"
tools:replace="android:name, android:label">
<activity
android:name=".MainActivity"
android:label="NewPipe Debug"
tools:replace="android:label"/>
</application>
tools:replace="android:name" />
</manifest>

View File

@@ -1,104 +0,0 @@
package org.schabi.newpipe;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import androidx.annotation.NonNull;
import androidx.multidex.MultiDex;
import com.facebook.stetho.Stetho;
import com.facebook.stetho.okhttp3.StethoInterceptor;
import com.squareup.leakcanary.AndroidHeapDumper;
import com.squareup.leakcanary.DefaultLeakDirectoryProvider;
import com.squareup.leakcanary.HeapDumper;
import com.squareup.leakcanary.LeakCanary;
import com.squareup.leakcanary.LeakDirectoryProvider;
import com.squareup.leakcanary.RefWatcher;
import org.schabi.newpipe.extractor.downloader.Downloader;
import java.io.File;
import java.util.concurrent.TimeUnit;
import okhttp3.OkHttpClient;
public class DebugApp extends App {
private static final String TAG = DebugApp.class.toString();
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}
@Override
public void onCreate() {
super.onCreate();
initStetho();
}
@Override
protected Downloader getDownloader() {
return DownloaderImpl.init(new OkHttpClient.Builder()
.addNetworkInterceptor(new StethoInterceptor()));
}
private void initStetho() {
// Create an InitializerBuilder
Stetho.InitializerBuilder initializerBuilder =
Stetho.newInitializerBuilder(this);
// Enable Chrome DevTools
initializerBuilder.enableWebKitInspector(
Stetho.defaultInspectorModulesProvider(this)
);
// Enable command line interface
initializerBuilder.enableDumpapp(
Stetho.defaultDumperPluginsProvider(getApplicationContext())
);
// Use the InitializerBuilder to generate an Initializer
Stetho.Initializer initializer = initializerBuilder.build();
// Initialize Stetho with the Initializer
Stetho.initialize(initializer);
}
@Override
protected boolean isDisposedRxExceptionsReported() {
return PreferenceManager.getDefaultSharedPreferences(this)
.getBoolean(getString(R.string.allow_disposed_exceptions_key), false);
}
@Override
protected RefWatcher installLeakCanary() {
return LeakCanary.refWatcher(this)
.heapDumper(new ToggleableHeapDumper(this))
// give each object 10 seconds to be gc'ed, before leak canary gets nosy on it
.watchDelay(10, TimeUnit.SECONDS)
.buildAndInstall();
}
public static class ToggleableHeapDumper implements HeapDumper {
private final HeapDumper dumper;
private final SharedPreferences preferences;
private final String dumpingAllowanceKey;
ToggleableHeapDumper(@NonNull final Context context) {
LeakDirectoryProvider leakDirectoryProvider = new DefaultLeakDirectoryProvider(context);
this.dumper = new AndroidHeapDumper(context, leakDirectoryProvider);
this.preferences = PreferenceManager.getDefaultSharedPreferences(context);
this.dumpingAllowanceKey = context.getString(R.string.allow_heap_dumping_key);
}
private boolean isDumpingAllowed() {
return preferences.getBoolean(dumpingAllowanceKey, false);
}
@Override
public File dumpHeap() {
return isDumpingAllowed() ? dumper.dumpHeap() : HeapDumper.RETRY_LATER;
}
}
}

View File

@@ -0,0 +1,59 @@
package org.schabi.newpipe
import android.content.Context
import androidx.multidex.MultiDex
import androidx.preference.PreferenceManager
import com.facebook.stetho.Stetho
import com.facebook.stetho.okhttp3.StethoInterceptor
import leakcanary.AppWatcher
import leakcanary.LeakCanary
import okhttp3.OkHttpClient
import org.schabi.newpipe.extractor.downloader.Downloader
class DebugApp : App() {
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base)
MultiDex.install(this)
}
override fun onCreate() {
super.onCreate()
initStetho()
// Give each object 10 seconds to be GC'ed, before LeakCanary gets nosy on it
AppWatcher.config = AppWatcher.config.copy(watchDurationMillis = 10000)
LeakCanary.config = LeakCanary.config.copy(dumpHeap = PreferenceManager
.getDefaultSharedPreferences(this).getBoolean(getString(
R.string.allow_heap_dumping_key), false))
}
override fun getDownloader(): Downloader {
val downloader = DownloaderImpl.init(OkHttpClient.Builder()
.addNetworkInterceptor(StethoInterceptor()))
setCookiesToDownloader(downloader)
return downloader
}
private fun initStetho() {
// Create an InitializerBuilder
val initializerBuilder = Stetho.newInitializerBuilder(this)
// Enable Chrome DevTools
initializerBuilder.enableWebKitInspector(Stetho.defaultInspectorModulesProvider(this))
// Enable command line interface
initializerBuilder.enableDumpapp(
Stetho.defaultDumperPluginsProvider(applicationContext))
// Use the InitializerBuilder to generate an Initializer
val initializer = initializerBuilder.build()
// Initialize Stetho with the Initializer
Stetho.initialize(initializer)
}
override fun isDisposedRxExceptionsReported(): Boolean {
return PreferenceManager.getDefaultSharedPreferences(this)
.getBoolean(getString(R.string.allow_disposed_exceptions_key), false)
}
}

View File

@@ -1,22 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="org.schabi.newpipe">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<application
android:name=".App"
android:allowBackup="true"
android:banner="@mipmap/newpipe_tv_banner"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:logo="@mipmap/ic_launcher"
android:requestLegacyExternalStorage="true"
android:theme="@style/OpeningTheme"
tools:ignore="AllowBackup">
<activity
@@ -24,12 +29,14 @@
android:label="@string/app_name"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
<receiver android:name="androidx.media.session.MediaButtonReceiver" >
<receiver android:name="androidx.media.session.MediaButtonReceiver">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
@@ -50,34 +57,36 @@
<activity
android:name=".player.BackgroundPlayerActivity"
android:launchMode="singleTask"
android:label="@string/title_activity_background_player"/>
android:label="@string/title_activity_background_player"
android:launchMode="singleTask" />
<activity
android:name=".player.PopupVideoPlayerActivity"
android:launchMode="singleTask"
android:label="@string/title_activity_popup_player"/>
android:label="@string/title_activity_popup_player"
android:launchMode="singleTask" />
<service
android:name=".player.PopupVideoPlayer"
android:exported="false"/>
android:exported="false" />
<activity
android:name=".player.MainVideoPlayer"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:label="@string/app_name"
android:launchMode="singleTask"/>
android:launchMode="singleTask"
android:theme="@style/VideoPlayerTheme" />
<activity
android:name=".settings.SettingsActivity"
android:label="@string/settings"/>
android:label="@string/settings" />
<activity
android:name=".about.AboutActivity"
android:label="@string/title_activity_about"/>
android:label="@string/title_activity_about" />
<service android:name=".local.subscription.services.SubscriptionsImportService"/>
<service android:name=".local.subscription.services.SubscriptionsExportService"/>
<service android:name=".local.subscription.services.SubscriptionsImportService" />
<service android:name=".local.subscription.services.SubscriptionsExportService" />
<service android:name=".local.feed.service.FeedLoadService" />
<activity
android:name=".PanicResponderActivity"
@@ -85,25 +94,25 @@
android:noHistory="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="info.guardianproject.panic.action.TRIGGER"/>
<action android:name="info.guardianproject.panic.action.TRIGGER" />
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name=".ExitActivity"
android:label="@string/general_error"
android:theme="@android:style/Theme.NoDisplay"/>
<activity android:name=".report.ErrorActivity"/>
android:theme="@android:style/Theme.NoDisplay" />
<activity android:name=".report.ErrorActivity" />
<!-- giga get related -->
<activity
android:name=".download.DownloadActivity"
android:label="@string/app_name"
android:launchMode="singleTask"/>
android:launchMode="singleTask" />
<service android:name="us.shandian.giga.service.DownloadManagerService"/>
<service android:name="us.shandian.giga.service.DownloadManagerService" />
<activity
android:name=".util.FilePickerActivityHelper"
@@ -117,7 +126,7 @@
<activity
android:name=".ReCaptchaActivity"
android:label="@string/reCaptchaActivity"/>
android:label="@string/recaptcha" />
<provider
android:name="androidx.core.content.FileProvider"
@@ -126,7 +135,7 @@
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/nnf_provider_paths"/>
android:resource="@xml/nnf_provider_paths" />
</provider>
<activity
@@ -138,152 +147,197 @@
<!-- Youtube filter -->
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http"/>
<data android:scheme="https"/>
<data android:host="youtube.com"/>
<data android:host="m.youtube.com"/>
<data android:host="www.youtube.com"/>
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="youtube.com" />
<data android:host="m.youtube.com" />
<data android:host="www.youtube.com" />
<data android:host="music.youtube.com" />
<!-- video prefix -->
<data android:pathPrefix="/v/"/>
<data android:pathPrefix="/embed/"/>
<data android:pathPrefix="/watch"/>
<data android:pathPrefix="/attribution_link"/>
<data android:pathPrefix="/v/" />
<data android:pathPrefix="/embed/" />
<data android:pathPrefix="/watch" />
<data android:pathPrefix="/attribution_link" />
<!-- channel prefix -->
<data android:pathPrefix="/channel/"/>
<data android:pathPrefix="/user/"/>
<data android:pathPrefix="/channel/" />
<data android:pathPrefix="/user/" />
<data android:pathPrefix="/c/" />
<!-- playlist prefix -->
<data android:pathPrefix="/playlist"/>
<data android:pathPrefix="/playlist" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http"/>
<data android:scheme="https"/>
<data android:host="youtu.be"/>
<data android:pathPrefix="/"/>
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="youtu.be" />
<data android:pathPrefix="/" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http"/>
<data android:scheme="https"/>
<data android:host="www.youtube-nocookie.com"/>
<data android:pathPrefix="/embed/"/>
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="www.youtube-nocookie.com" />
<data android:pathPrefix="/embed/" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="vnd.youtube"/>
<data android:scheme="vnd.youtube.launch"/>
<data android:scheme="vnd.youtube" />
<data android:scheme="vnd.youtube.launch" />
</intent-filter>
<!-- Hooktube filter -->
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http"/>
<data android:scheme="https"/>
<data android:host="hooktube.com"/>
<data android:host="*.hooktube.com"/>
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="hooktube.com" />
<data android:host="*.hooktube.com" />
<!-- video prefix -->
<data android:pathPrefix="/v/"/>
<data android:pathPrefix="/embed/"/>
<data android:pathPrefix="/watch"/>
<data android:pathPrefix="/v/" />
<data android:pathPrefix="/embed/" />
<data android:pathPrefix="/watch" />
<!-- channel prefix -->
<data android:pathPrefix="/channel/"/>
<data android:pathPrefix="/user/"/>
<data android:pathPrefix="/channel/" />
<data android:pathPrefix="/user/" />
</intent-filter>
<!-- Invidious filter -->
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http"/>
<data android:scheme="https"/>
<data android:host="invidio.us"/>
<data android:host="dev.invidio.us"/>
<data android:host="www.invidio.us"/>
<data android:host="invidious.snopyta.org"/>
<data android:host="de.invidious.snopyta.org"/>
<data android:host="fi.invidious.snopyta.org"/>
<data android:host="vid.wxzm.sx"/>
<data android:host="invidious.kabi.tk"/>
<data android:host="invidiou.sh"/>
<data android:host="www.invidiou.sh"/>
<data android:host="no.invidiou.sh"/>
<data android:host="invidious.enkirton.net"/>
<data android:host="tube.poal.co"/>
<data android:host="invidious.13ad.de"/>
<data android:host="yt.elukerio.org"/>
<!-- video prefix -->
<data android:pathPrefix="/embed/"/>
<data android:pathPrefix="/watch"/>
<!-- channel prefix -->
<data android:pathPrefix="/channel/"/>
<data android:pathPrefix="/user/"/>
<!-- playlist prefix -->
<data android:pathPrefix="/playlist"/>
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="invidio.us" />
<data android:host="dev.invidio.us" />
<data android:host="www.invidio.us" />
<data android:host="invidious.snopyta.org" />
<data android:host="fi.invidious.snopyta.org" />
<data android:host="yewtu.be" />
<data android:host="invidious.ggc-project.de" />
<data android:host="yt.maisputain.ovh" />
<data android:host="invidious.13ad.de" />
<data android:host="invidious.toot.koeln" />
<data android:host="invidious.fdn.fr" />
<data android:host="watch.nettohikari.com" />
<data android:host="invidious.snwmds.net" />
<data android:host="invidious.snwmds.org" />
<data android:host="invidious.snwmds.com" />
<data android:host="invidious.sunsetravens.com" />
<data android:host="invidious.gachirangers.com" />
<data android:pathPrefix="/" />
</intent-filter>
<!-- Soundcloud filter -->
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http"/>
<data android:scheme="https"/>
<data android:host="soundcloud.com"/>
<data android:host="m.soundcloud.com"/>
<data android:host="www.soundcloud.com"/>
<data android:pathPrefix="/"/>
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="soundcloud.com" />
<data android:host="m.soundcloud.com" />
<data android:host="www.soundcloud.com" />
<data android:pathPrefix="/" />
</intent-filter>
<!-- Share filter -->
<intent-filter>
<action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="text/plain"/>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
<!-- MediaCCC filter -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="media.ccc.de" />
<!-- video prefix -->
<data android:pathPrefix="/v/" />
<!-- channel prefix-->
<data android:pathPrefix="/c/" />
<data android:pathPrefix="/b/" />
</intent-filter>
<!-- PeerTube filter -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="framatube.org" />
<data android:host="media.assassinate-you.net" />
<data android:host="peertube.co.uk" />
<data android:host="peertube.cpy.re" />
<data android:host="peertube.mastodon.host" />
<data android:host="peertube.fr" />
<data android:host="peertube.live" />
<data android:host="peertube.video" />
<data android:host="tube.privacytools.io" />
<data android:host="video.ploud.fr" />
<data android:host="video.lqdn.fr" />
<data android:host="skeptikon.fr" />
<data android:pathPrefix="/videos/" /> <!-- it contains playlists -->
<data android:pathPrefix="/accounts/" />
<data android:pathPrefix="/video-channels/" />
</intent-filter>
</activity>
<service
android:name=".RouterActivity$FetcherService"
android:exported="false"/>
android:exported="false" />
</application>
</manifest>

View File

@@ -0,0 +1,329 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.fragment.app;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.Lifecycle;
import androidx.viewpager.widget.PagerAdapter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
// TODO: Replace this deprecated class with its ViewPager2 counterpart
/**
* This is a copy from {@link androidx.fragment.app.FragmentStatePagerAdapter}.
* <p>
* It includes a workaround to fix the menu visibility when the adapter is restored.
* </p>
* <p>
* When restoring the state of this adapter, all the fragments' menu visibility were set to false,
* effectively disabling the menu from the user until he switched pages or another event
* that triggered the menu to be visible again happened.
* </p>
* <p>
* <b>Check out the changes in:</b>
* </p>
* <ul>
* <li>{@link #saveState()}</li>
* <li>{@link #restoreState(Parcelable, ClassLoader)}</li>
* </ul>
*/
@SuppressWarnings("deprecation")
public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapter {
private static final String TAG = "FragmentStatePagerAdapt";
private static final boolean DEBUG = false;
@Retention(RetentionPolicy.SOURCE)
@IntDef({BEHAVIOR_SET_USER_VISIBLE_HINT, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT})
private @interface Behavior { }
/**
* Indicates that {@link Fragment#setUserVisibleHint(boolean)} will be called when the current
* fragment changes.
*
* @deprecated This behavior relies on the deprecated
* {@link Fragment#setUserVisibleHint(boolean)} API. Use
* {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT} to switch to its replacement,
* {@link FragmentTransaction#setMaxLifecycle}.
* @see #FragmentStatePagerAdapterMenuWorkaround(FragmentManager, int)
*/
@Deprecated
public static final int BEHAVIOR_SET_USER_VISIBLE_HINT = 0;
/**
* Indicates that only the current fragment will be in the {@link Lifecycle.State#RESUMED}
* state. All other Fragments are capped at {@link Lifecycle.State#STARTED}.
*
* @see #FragmentStatePagerAdapterMenuWorkaround(FragmentManager, int)
*/
public static final int BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT = 1;
private final FragmentManager mFragmentManager;
private final int mBehavior;
private FragmentTransaction mCurTransaction = null;
private ArrayList<Fragment.SavedState> mSavedState = new ArrayList<Fragment.SavedState>();
private ArrayList<Fragment> mFragments = new ArrayList<Fragment>();
private Fragment mCurrentPrimaryItem = null;
/**
* Constructor for {@link FragmentStatePagerAdapterMenuWorkaround}
* that sets the fragment manager for the adapter. This is the equivalent of calling
* {@link #FragmentStatePagerAdapterMenuWorkaround(FragmentManager, int)} and passing in
* {@link #BEHAVIOR_SET_USER_VISIBLE_HINT}.
*
* <p>Fragments will have {@link Fragment#setUserVisibleHint(boolean)} called whenever the
* current Fragment changes.</p>
*
* @param fm fragment manager that will interact with this adapter
* @deprecated use {@link #FragmentStatePagerAdapterMenuWorkaround(FragmentManager, int)} with
* {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT}
*/
@Deprecated
public FragmentStatePagerAdapterMenuWorkaround(@NonNull final FragmentManager fm) {
this(fm, BEHAVIOR_SET_USER_VISIBLE_HINT);
}
/**
* Constructor for {@link FragmentStatePagerAdapterMenuWorkaround}.
*
* If {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT} is passed in, then only the current
* Fragment is in the {@link Lifecycle.State#RESUMED} state, while all other fragments are
* capped at {@link Lifecycle.State#STARTED}. If {@link #BEHAVIOR_SET_USER_VISIBLE_HINT} is
* passed, all fragments are in the {@link Lifecycle.State#RESUMED} state and there will be
* callbacks to {@link Fragment#setUserVisibleHint(boolean)}.
*
* @param fm fragment manager that will interact with this adapter
* @param behavior determines if only current fragments are in a resumed state
*/
public FragmentStatePagerAdapterMenuWorkaround(@NonNull final FragmentManager fm,
@Behavior final int behavior) {
mFragmentManager = fm;
mBehavior = behavior;
}
/**
* @param position the position of the item you want
* @return the {@link Fragment} associated with a specified position
*/
@NonNull
public abstract Fragment getItem(int position);
@Override
public void startUpdate(@NonNull final ViewGroup container) {
if (container.getId() == View.NO_ID) {
throw new IllegalStateException("ViewPager with adapter " + this
+ " requires a view id");
}
}
@SuppressWarnings("deprecation")
@NonNull
@Override
public Object instantiateItem(@NonNull final ViewGroup container, final int position) {
// If we already have this item instantiated, there is nothing
// to do. This can happen when we are restoring the entire pager
// from its saved state, where the fragment manager has already
// taken care of restoring the fragments we previously had instantiated.
if (mFragments.size() > position) {
Fragment f = mFragments.get(position);
if (f != null) {
return f;
}
}
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
Fragment fragment = getItem(position);
if (DEBUG) {
Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
}
if (mSavedState.size() > position) {
Fragment.SavedState fss = mSavedState.get(position);
if (fss != null) {
fragment.setInitialSavedState(fss);
}
}
while (mFragments.size() <= position) {
mFragments.add(null);
}
fragment.setMenuVisibility(false);
if (mBehavior == BEHAVIOR_SET_USER_VISIBLE_HINT) {
fragment.setUserVisibleHint(false);
}
mFragments.set(position, fragment);
mCurTransaction.add(container.getId(), fragment);
if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED);
}
return fragment;
}
@Override
public void destroyItem(@NonNull final ViewGroup container, final int position,
@NonNull final Object object) {
Fragment fragment = (Fragment) object;
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
if (DEBUG) {
Log.v(TAG, "Removing item #" + position + ": f=" + object
+ " v=" + ((Fragment) object).getView());
}
while (mSavedState.size() <= position) {
mSavedState.add(null);
}
mSavedState.set(position, fragment.isAdded()
? mFragmentManager.saveFragmentInstanceState(fragment) : null);
mFragments.set(position, null);
mCurTransaction.remove(fragment);
if (fragment == mCurrentPrimaryItem) {
mCurrentPrimaryItem = null;
}
}
@Override
@SuppressWarnings({"ReferenceEquality", "deprecation"})
public void setPrimaryItem(@NonNull final ViewGroup container, final int position,
@NonNull final Object object) {
Fragment fragment = (Fragment) object;
if (fragment != mCurrentPrimaryItem) {
if (mCurrentPrimaryItem != null) {
mCurrentPrimaryItem.setMenuVisibility(false);
if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
mCurTransaction.setMaxLifecycle(mCurrentPrimaryItem, Lifecycle.State.STARTED);
} else {
mCurrentPrimaryItem.setUserVisibleHint(false);
}
}
fragment.setMenuVisibility(true);
if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED);
} else {
fragment.setUserVisibleHint(true);
}
mCurrentPrimaryItem = fragment;
}
}
@Override
public void finishUpdate(@NonNull final ViewGroup container) {
if (mCurTransaction != null) {
mCurTransaction.commitNowAllowingStateLoss();
mCurTransaction = null;
}
}
@Override
public boolean isViewFromObject(@NonNull final View view, @NonNull final Object object) {
return ((Fragment) object).getView() == view;
}
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
private final String selectedFragment = "selected_fragment";
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
@Override
@Nullable
public Parcelable saveState() {
Bundle state = null;
if (mSavedState.size() > 0) {
state = new Bundle();
Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()];
mSavedState.toArray(fss);
state.putParcelableArray("states", fss);
}
for (int i = 0; i < mFragments.size(); i++) {
Fragment f = mFragments.get(i);
if (f != null && f.isAdded()) {
if (state == null) {
state = new Bundle();
}
String key = "f" + i;
mFragmentManager.putFragment(state, key, f);
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// Check if it's the same fragment instance
if (f == mCurrentPrimaryItem) {
state.putString(selectedFragment, key);
}
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
}
}
return state;
}
@Override
public void restoreState(@Nullable final Parcelable state, @Nullable final ClassLoader loader) {
if (state != null) {
Bundle bundle = (Bundle) state;
bundle.setClassLoader(loader);
Parcelable[] fss = bundle.getParcelableArray("states");
mSavedState.clear();
mFragments.clear();
if (fss != null) {
for (int i = 0; i < fss.length; i++) {
mSavedState.add((Fragment.SavedState) fss[i]);
}
}
Iterable<String> keys = bundle.keySet();
for (String key: keys) {
if (key.startsWith("f")) {
int index = Integer.parseInt(key.substring(1));
Fragment f = mFragmentManager.getFragment(bundle, key);
if (f != null) {
while (mFragments.size() <= index) {
mFragments.add(null);
}
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
final boolean wasSelected = bundle.getString(selectedFragment, "")
.equals(key);
f.setMenuVisibility(wasSelected);
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
mFragments.set(index, f);
} else {
Log.w(TAG, "Bad fragment at key " + key);
}
}
}
}
}
}

View File

@@ -8,6 +8,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.OverScroller;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import org.jetbrains.annotations.NotNull;
@@ -15,10 +16,11 @@ import org.schabi.newpipe.R;
import java.lang.reflect.Field;
// check this https://stackoverflow.com/questions/56849221/recyclerview-fling-causes-laggy-while-appbarlayout-is-scrolling/57997489#57997489
// See https://stackoverflow.com/questions/56849221#57997489
public final class FlingBehavior extends AppBarLayout.Behavior {
private final Rect focusScrollRect = new Rect();
public FlingBehavior(Context context, AttributeSet attrs) {
public FlingBehavior(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
@@ -26,7 +28,40 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
private final Rect globalRect = new Rect();
@Override
public boolean onInterceptTouchEvent(CoordinatorLayout parent, AppBarLayout child, MotionEvent ev) {
public boolean onRequestChildRectangleOnScreen(
@NonNull final CoordinatorLayout coordinatorLayout, @NonNull final AppBarLayout child,
@NonNull final Rect rectangle, final boolean immediate) {
focusScrollRect.set(rectangle);
coordinatorLayout.offsetDescendantRectToMyCoords(child, focusScrollRect);
int height = coordinatorLayout.getHeight();
if (focusScrollRect.top <= 0 && focusScrollRect.bottom >= height) {
// the child is too big to fit inside ourselves completely, ignore request
return false;
}
int dy;
if (focusScrollRect.bottom > height) {
dy = focusScrollRect.top;
} else if (focusScrollRect.top < 0) {
// scrolling up
dy = -(height - focusScrollRect.bottom);
} else {
// nothing to do
return false;
}
int consumed = scroll(coordinatorLayout, child, dy, getMaxDragOffset(child), 0);
return consumed == dy;
}
public boolean onInterceptTouchEvent(final CoordinatorLayout parent, final AppBarLayout child,
final MotionEvent ev) {
final ViewGroup playQueue = child.findViewById(R.id.playQueuePanel);
if (playQueue != null) {
final boolean visible = playQueue.getGlobalVisibleRect(globalRect);
@@ -36,30 +71,36 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
}
}
allowScroll = true;
if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
// remove reference to old nested scrolling child
resetNestedScrollingChild();
// Stop fling when your finger touches the screen
stopAppBarLayoutFling();
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
// remove reference to old nested scrolling child
resetNestedScrollingChild();
// Stop fling when your finger touches the screen
stopAppBarLayoutFling();
break;
default:
break;
}
return super.onInterceptTouchEvent(parent, child, ev);
}
@Override
public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes, int type) {
public boolean onStartNestedScroll(@NotNull final CoordinatorLayout parent, @NotNull final AppBarLayout child,
@NotNull final View directTargetChild, final View target, final int nestedScrollAxes, final int type) {
return allowScroll && super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes, type);
}
@Override
public boolean onNestedFling(@NotNull CoordinatorLayout coordinatorLayout, @NotNull AppBarLayout child, @NotNull View target, float velocityX, float velocityY, boolean consumed) {
public boolean onNestedFling(@NotNull final CoordinatorLayout coordinatorLayout, @NotNull final AppBarLayout child,
@NotNull final View target, final float velocityX, final float velocityY, final boolean consumed) {
return allowScroll && super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
}
@Nullable
private OverScroller getScrollerField() {
try {
Class<?> headerBehaviorType = this.getClass().getSuperclass().getSuperclass().getSuperclass();
Class<?> headerBehaviorType = this.getClass()
.getSuperclass().getSuperclass().getSuperclass();
if (headerBehaviorType != null) {
Field field = headerBehaviorType.getDeclaredField("scroller");
field.setAccessible(true);
@@ -86,12 +127,14 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
return null;
}
private void resetNestedScrollingChild(){
private void resetNestedScrollingChild() {
Field field = getLastNestedScrollingChildRefField();
if(field != null){
if (field != null) {
try {
Object value = field.get(this);
if(value != null) field.set(this, null);
if (value != null) {
field.set(this, null);
}
} catch (IllegalAccessException e) {
// ?
}
@@ -100,7 +143,8 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
private void stopAppBarLayoutFling() {
OverScroller scroller = getScrollerField();
if (scroller != null) scroller.forceFinished(true);
if (scroller != null) {
scroller.forceFinished(true);
}
}
}
}

View File

@@ -23,17 +23,25 @@ package org.schabi.newpipe;
/**
* Singleton:
* Used to send data between certain Activity/Services within the same process.
* This can be considered as an ugly hack inside the Android universe. **/
* This can be considered as an ugly hack inside the Android universe.
**/
public class ActivityCommunicator {
private static ActivityCommunicator activityCommunicator;
private volatile Class returnActivity;
public static ActivityCommunicator getCommunicator() {
if(activityCommunicator == null) {
if (activityCommunicator == null) {
activityCommunicator = new ActivityCommunicator();
}
return activityCommunicator;
}
public volatile Class returnActivity;
public Class getReturnActivity() {
return returnActivity;
}
public void setReturnActivity(final Class returnActivity) {
this.returnActivity = returnActivity;
}
}

View File

@@ -5,21 +5,20 @@ import android.app.Application;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.preference.PreferenceManager;
import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
import com.squareup.leakcanary.LeakCanary;
import com.squareup.leakcanary.RefWatcher;
import org.acra.ACRA;
import org.acra.config.ACRAConfiguration;
import org.acra.config.ACRAConfigurationException;
import org.acra.config.ConfigurationBuilder;
import org.acra.config.CoreConfiguration;
import org.acra.config.CoreConfigurationBuilder;
import org.acra.sender.ReportSenderFactory;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.downloader.Downloader;
@@ -27,7 +26,7 @@ import org.schabi.newpipe.report.AcraReportSenderFactory;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.settings.SettingsActivity;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ExceptionUtils;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.StateSaver;
@@ -66,15 +65,17 @@ import io.reactivex.plugins.RxJavaPlugins;
public class App extends Application {
protected static final String TAG = App.class.toString();
private RefWatcher refWatcher;
private static App app;
@SuppressWarnings("unchecked")
private static final Class<? extends ReportSenderFactory>[]
reportSenderFactoryClasses = new Class[]{AcraReportSenderFactory.class};
REPORT_SENDER_FACTORY_CLASSES = new Class[]{AcraReportSenderFactory.class};
private static App app;
public static App getApp() {
return app;
}
@Override
protected void attachBaseContext(Context base) {
protected void attachBaseContext(final Context base) {
super.attachBaseContext(base);
initACRA();
@@ -84,13 +85,6 @@ public class App extends Application {
public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
refWatcher = installLeakCanary();
app = this;
// Initialize settings first because others inits can use its values
@@ -99,7 +93,7 @@ public class App extends Application {
NewPipe.init(getDownloader(),
Localization.getPreferredLocalization(this),
Localization.getPreferredContentCountry(this));
Localization.init();
Localization.init(getApplicationContext());
StateSaver.init(this);
initNotificationChannel();
@@ -116,31 +110,47 @@ public class App extends Application {
}
protected Downloader getDownloader() {
return DownloaderImpl.init(null);
DownloaderImpl downloader = DownloaderImpl.init(null);
setCookiesToDownloader(downloader);
return downloader;
}
protected void setCookiesToDownloader(final DownloaderImpl downloader) {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
getApplicationContext());
final String key = getApplicationContext().getString(R.string.recaptcha_cookies_key);
downloader.setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, prefs.getString(key, ""));
downloader.updateYoutubeRestrictedModeCookies(getApplicationContext());
}
private void configureRxJavaErrorHandler() {
// https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling
RxJavaPlugins.setErrorHandler(new Consumer<Throwable>() {
@Override
public void accept(@NonNull Throwable throwable) {
Log.e(TAG, "RxJavaPlugins.ErrorHandler called with -> : " +
"throwable = [" + throwable.getClass().getName() + "]");
public void accept(@NonNull final Throwable throwable) {
Log.e(TAG, "RxJavaPlugins.ErrorHandler called with -> : "
+ "throwable = [" + throwable.getClass().getName() + "]");
final Throwable actualThrowable;
if (throwable instanceof UndeliverableException) {
// As UndeliverableException is a wrapper, get the cause of it to get the "real" exception
throwable = throwable.getCause();
// As UndeliverableException is a wrapper,
// get the cause of it to get the "real" exception
actualThrowable = throwable.getCause();
} else {
actualThrowable = throwable;
}
final List<Throwable> errors;
if (throwable instanceof CompositeException) {
errors = ((CompositeException) throwable).getExceptions();
if (actualThrowable instanceof CompositeException) {
errors = ((CompositeException) actualThrowable).getExceptions();
} else {
errors = Collections.singletonList(throwable);
errors = Collections.singletonList(actualThrowable);
}
for (final Throwable error : errors) {
if (isThrowableIgnored(error)) return;
if (isThrowableIgnored(error)) {
return;
}
if (isThrowableCritical(error)) {
reportException(error);
return;
@@ -150,22 +160,24 @@ public class App extends Application {
// Out-of-lifecycle exceptions should only be reported if a debug user wishes so,
// When exception is not reported, log it
if (isDisposedRxExceptionsReported()) {
reportException(throwable);
reportException(actualThrowable);
} else {
Log.e(TAG, "RxJavaPlugin: Undeliverable Exception received: ", throwable);
Log.e(TAG, "RxJavaPlugin: Undeliverable Exception received: ", actualThrowable);
}
}
private boolean isThrowableIgnored(@NonNull final Throwable throwable) {
// Don't crash the application over a simple network problem
return ExtractorHelper.hasAssignableCauseThrowable(throwable,
IOException.class, SocketException.class, // network api cancellation
InterruptedException.class, InterruptedIOException.class); // blocking code disposed
return ExceptionUtils.hasAssignableCause(throwable,
// network api cancellation
IOException.class, SocketException.class,
// blocking code disposed
InterruptedException.class, InterruptedIOException.class);
}
private boolean isThrowableCritical(@NonNull final Throwable throwable) {
// Though these exceptions cannot be ignored
return ExtractorHelper.hasAssignableCauseThrowable(throwable,
return ExceptionUtils.hasAssignableCause(throwable,
NullPointerException.class, IllegalArgumentException.class, // bug in app
OnErrorNotImplementedException.class, MissingBackpressureException.class,
IllegalStateException.class); // bug in operator
@@ -190,8 +202,8 @@ public class App extends Application {
private void initACRA() {
try {
final ACRAConfiguration acraConfig = new ConfigurationBuilder(this)
.setReportSenderFactoryClasses(reportSenderFactoryClasses)
final CoreConfiguration acraConfig = new CoreConfigurationBuilder(this)
.setReportSenderFactoryClasses(REPORT_SENDER_FACTORY_CLASSES)
.setBuildConfigClass(BuildConfig.class)
.build();
ACRA.init(this, acraConfig);
@@ -202,7 +214,7 @@ public class App extends Application {
null,
null,
ErrorActivity.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));
}
}
@@ -230,11 +242,11 @@ public class App extends Application {
/**
* Set up notification channel for app update.
*
* @param importance
*/
@TargetApi(Build.VERSION_CODES.O)
private void setUpUpdateNotificationChannel(int importance) {
private void setUpUpdateNotificationChannel(final int importance) {
final String appUpdateId
= getString(R.string.app_update_notification_channel_id);
final CharSequence appUpdateName
@@ -251,21 +263,7 @@ public class App extends Application {
appUpdateNotificationManager.createNotificationChannel(appUpdateChannel);
}
@Nullable
public static RefWatcher getRefWatcher(Context context) {
final App application = (App) context.getApplicationContext();
return application.refWatcher;
}
protected RefWatcher installLeakCanary() {
return RefWatcher.DISABLED;
}
protected boolean isDisposedRxExceptionsReported() {
return false;
}
public static App getApp() {
return app;
}
}

View File

@@ -2,32 +2,31 @@ package org.schabi.newpipe;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.appcompat.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.squareup.leakcanary.RefWatcher;
import icepick.Icepick;
import icepick.State;
import leakcanary.AppWatcher;
public abstract class BaseFragment extends Fragment {
public static final ImageLoader IMAGE_LOADER = ImageLoader.getInstance();
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
protected final boolean DEBUG = MainActivity.DEBUG;
protected AppCompatActivity activity;
public static final ImageLoader imageLoader = ImageLoader.getInstance();
//These values are used for controlling framgents when they are part of the frontpage
//These values are used for controlling fragments when they are part of the frontpage
@State
protected boolean useAsFrontPage = false;
protected boolean mIsVisibleToUser = false;
private boolean mIsVisibleToUser = false;
public void useAsFrontPage(boolean value) {
public void useAsFrontPage(final boolean value) {
useAsFrontPage = value;
}
@@ -36,7 +35,7 @@ public abstract class BaseFragment extends Fragment {
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onAttach(Context context) {
public void onAttach(final Context context) {
super.onAttach(context);
activity = (AppCompatActivity) context;
}
@@ -48,43 +47,49 @@ public abstract class BaseFragment extends Fragment {
}
@Override
public void onCreate(Bundle savedInstanceState) {
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
public void onCreate(final Bundle savedInstanceState) {
if (DEBUG) {
Log.d(TAG, "onCreate() called with: "
+ "savedInstanceState = [" + savedInstanceState + "]");
}
super.onCreate(savedInstanceState);
Icepick.restoreInstanceState(this, savedInstanceState);
if (savedInstanceState != null) onRestoreInstanceState(savedInstanceState);
if (savedInstanceState != null) {
onRestoreInstanceState(savedInstanceState);
}
}
@Override
public void onViewCreated(View rootView, Bundle savedInstanceState) {
public void onViewCreated(final View rootView, final Bundle savedInstanceState) {
super.onViewCreated(rootView, savedInstanceState);
if (DEBUG) {
Log.d(TAG, "onViewCreated() called with: rootView = [" + rootView + "], savedInstanceState = [" + savedInstanceState + "]");
Log.d(TAG, "onViewCreated() called with: "
+ "rootView = [" + rootView + "], "
+ "savedInstanceState = [" + savedInstanceState + "]");
}
initViews(rootView, savedInstanceState);
initListeners();
}
@Override
public void onSaveInstanceState(Bundle outState) {
public void onSaveInstanceState(final Bundle outState) {
super.onSaveInstanceState(outState);
Icepick.saveInstanceState(this, outState);
}
protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
}
@Override
public void onDestroy() {
super.onDestroy();
RefWatcher refWatcher = App.getRefWatcher(getActivity());
if (refWatcher != null) refWatcher.watch(this);
AppWatcher.INSTANCE.getObjectWatcher().watch(this);
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
public void setUserVisibleHint(final boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
mIsVisibleToUser = isVisibleToUser;
}
@@ -93,7 +98,7 @@ public abstract class BaseFragment extends Fragment {
// Init
//////////////////////////////////////////////////////////////////////////*/
protected void initViews(View rootView, Bundle savedInstanceState) {
protected void initViews(final View rootView, final Bundle savedInstanceState) {
}
protected void initListeners() {
@@ -103,10 +108,12 @@ public abstract class BaseFragment extends Fragment {
// Utils
//////////////////////////////////////////////////////////////////////////*/
public void setTitle(String title) {
if (DEBUG) Log.d(TAG, "setTitle() called with: title = [" + title + "]");
if((!useAsFrontPage || mIsVisibleToUser)
&& (activity != null && activity.getSupportActionBar() != null)) {
public void setTitle(final String title) {
if (DEBUG) {
Log.d(TAG, "setTitle() called with: title = [" + title + "]");
}
if ((!useAsFrontPage || mIsVisibleToUser)
&& (activity != null && activity.getSupportActionBar() != null)) {
activity.getSupportActionBar().setDisplayShowTitleEnabled(true);
activity.getSupportActionBar().setTitle(title);
}

View File

@@ -12,12 +12,16 @@ import android.net.ConnectivityManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.preference.PreferenceManager;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonParser;
import com.grack.nanojson.JsonParserException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
@@ -30,11 +34,6 @@ import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.concurrent.TimeUnit;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
/**
* AsyncTask to check if there is a newer version of the NewPipe github apk available or not.
@@ -42,149 +41,44 @@ import okhttp3.Response;
* the notification, the user will be directed to the download link.
*/
public class CheckForNewAppVersionTask extends AsyncTask<Void, Void, String> {
private static final boolean DEBUG = MainActivity.DEBUG;
private static final String TAG = CheckForNewAppVersionTask.class.getSimpleName();
private static final Application app = App.getApp();
private static final String GITHUB_APK_SHA1 = "B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15";
private static final String newPipeApiUrl = "https://newpipe.schabi.org/api/data.json";
private static final int timeoutPeriod = 30;
private SharedPreferences mPrefs;
private OkHttpClient client;
@Override
protected void onPreExecute() {
mPrefs = PreferenceManager.getDefaultSharedPreferences(app);
// Check if user has enabled/ disabled update checking
// and if the current apk is a github one or not.
if (!mPrefs.getBoolean(app.getString(R.string.update_app_key), true)
|| !isGithubApk()) {
this.cancel(true);
}
}
@Override
protected String doInBackground(Void... voids) {
if(isCancelled() || !isConnected()) return null;
// Make a network request to get latest NewPipe data.
if (client == null) {
client = new OkHttpClient
.Builder()
.readTimeout(timeoutPeriod, TimeUnit.SECONDS)
.build();
}
Request request = new Request.Builder()
.url(newPipeApiUrl)
.build();
try {
Response response = client.newCall(request).execute();
return response.body().string();
} catch (IOException ex) {
// connectivity problems, do not alarm user and fail silently
if (DEBUG) Log.w(TAG, Log.getStackTraceString(ex));
}
return null;
}
@Override
protected void onPostExecute(String response) {
// Parse the json from the response.
if (response != null) {
try {
JSONObject mainObject = new JSONObject(response);
JSONObject flavoursObject = mainObject.getJSONObject("flavors");
JSONObject githubObject = flavoursObject.getJSONObject("github");
JSONObject githubStableObject = githubObject.getJSONObject("stable");
String versionName = githubStableObject.getString("version");
String versionCode = githubStableObject.getString("version_code");
String apkLocationUrl = githubStableObject.getString("apk");
compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode);
} catch (JSONException ex) {
// connectivity problems, do not alarm user and fail silently
if (DEBUG) Log.w(TAG, Log.getStackTraceString(ex));
}
}
}
private static final Application APP = App.getApp();
private static final String GITHUB_APK_SHA1
= "B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15";
private static final String NEWPIPE_API_URL = "https://newpipe.schabi.org/api/data.json";
/**
* Method to compare the current and latest available app version.
* If a newer version is available, we show the update notification.
* @param versionName
* @param apkLocationUrl
*/
private void compareAppVersionAndShowNotification(String versionName,
String apkLocationUrl,
String versionCode) {
int NOTIFICATION_ID = 2000;
if (BuildConfig.VERSION_CODE < Integer.valueOf(versionCode)) {
// A pending intent to open the apk location url in the browser.
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl));
PendingIntent pendingIntent
= PendingIntent.getActivity(app, 0, intent, 0);
NotificationCompat.Builder notificationBuilder = new NotificationCompat
.Builder(app, app.getString(R.string.app_update_notification_channel_id))
.setSmallIcon(R.drawable.ic_newpipe_update)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.setContentTitle(app.getString(R.string.app_update_notification_content_title))
.setContentText(app.getString(R.string.app_update_notification_content_text)
+ " " + versionName);
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(app);
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
}
}
/**
* Method to get the apk's SHA1 key.
* https://stackoverflow.com/questions/9293019/get-certificate-fingerprint-from-android-app#22506133
* Method to get the apk's SHA1 key. See https://stackoverflow.com/questions/9293019/#22506133.
*
* @return String with the apk's SHA1 fingeprint in hexadecimal
*/
private static String getCertificateSHA1Fingerprint() {
PackageManager pm = app.getPackageManager();
String packageName = app.getPackageName();
int flags = PackageManager.GET_SIGNATURES;
final PackageManager pm = APP.getPackageManager();
final String packageName = APP.getPackageName();
final int flags = PackageManager.GET_SIGNATURES;
PackageInfo packageInfo = null;
try {
packageInfo = pm.getPackageInfo(packageName, flags);
} catch (PackageManager.NameNotFoundException ex) {
ErrorActivity.reportError(app, ex, null, null,
} catch (PackageManager.NameNotFoundException e) {
ErrorActivity.reportError(APP, e, null, null,
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
"Could not find package info", R.string.app_ui_crash));
}
Signature[] signatures = packageInfo.signatures;
byte[] cert = signatures[0].toByteArray();
InputStream input = new ByteArrayInputStream(cert);
final Signature[] signatures = packageInfo.signatures;
final byte[] cert = signatures[0].toByteArray();
final InputStream input = new ByteArrayInputStream(cert);
CertificateFactory cf = null;
X509Certificate c = null;
try {
cf = CertificateFactory.getInstance("X509");
final CertificateFactory cf = CertificateFactory.getInstance("X509");
c = (X509Certificate) cf.generateCertificate(input);
} catch (CertificateException ex) {
ErrorActivity.reportError(app, ex, null, null,
} catch (CertificateException e) {
ErrorActivity.reportError(APP, e, null, null,
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
"Certificate error", R.string.app_ui_crash));
}
@@ -193,14 +87,10 @@ public class CheckForNewAppVersionTask extends AsyncTask<Void, Void, String> {
try {
MessageDigest md = MessageDigest.getInstance("SHA1");
byte[] publicKey = md.digest(c.getEncoded());
final byte[] publicKey = md.digest(c.getEncoded());
hexString = byte2HexFormatted(publicKey);
} catch (NoSuchAlgorithmException ex1) {
ErrorActivity.reportError(app, ex1, null, null,
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
"Could not retrieve SHA1 key", R.string.app_ui_crash));
} catch (CertificateEncodingException ex2) {
ErrorActivity.reportError(app, ex2, null, null,
} catch (NoSuchAlgorithmException | CertificateEncodingException e) {
ErrorActivity.reportError(APP, e, null, null,
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
"Could not retrieve SHA1 key", R.string.app_ui_crash));
}
@@ -208,31 +98,124 @@ public class CheckForNewAppVersionTask extends AsyncTask<Void, Void, String> {
return hexString;
}
private static String byte2HexFormatted(byte[] arr) {
StringBuilder str = new StringBuilder(arr.length * 2);
private static String byte2HexFormatted(final byte[] arr) {
final StringBuilder str = new StringBuilder(arr.length * 2);
for (int i = 0; i < arr.length; i++) {
String h = Integer.toHexString(arr[i]);
int l = h.length();
if (l == 1) h = "0" + h;
if (l > 2) h = h.substring(l - 2, l);
final int l = h.length();
if (l == 1) {
h = "0" + h;
}
if (l > 2) {
h = h.substring(l - 2, l);
}
str.append(h.toUpperCase());
if (i < (arr.length - 1)) str.append(':');
if (i < (arr.length - 1)) {
str.append(':');
}
}
return str.toString();
}
public static boolean isGithubApk() {
return getCertificateSHA1Fingerprint().equals(GITHUB_APK_SHA1);
}
@Override
protected void onPreExecute() {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(APP);
// Check if user has enabled/disabled update checking
// and if the current apk is a github one or not.
if (!prefs.getBoolean(APP.getString(R.string.update_app_key), true) || !isGithubApk()) {
this.cancel(true);
}
}
@Override
protected String doInBackground(final Void... voids) {
if (isCancelled() || !isConnected()) {
return null;
}
// Make a network request to get latest NewPipe data.
try {
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;
}
@Override
protected void onPostExecute(final String response) {
// Parse the json from the response.
if (response != null) {
try {
final JsonObject githubStableObject = JsonParser.object().from(response)
.getObject("flavors").getObject("github").getObject("stable");
final String versionName = githubStableObject.getString("version");
final int versionCode = githubStableObject.getInt("version_code");
final String apkLocationUrl = githubStableObject.getString("apk");
compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode);
} catch (JsonParserException e) {
// connectivity problems, do not alarm user and fail silently
if (DEBUG) {
Log.w(TAG, Log.getStackTraceString(e));
}
}
}
}
/**
* Method to compare the current and latest available app version.
* If a newer version is available, we show the update notification.
*
* @param versionName Name of new version
* @param apkLocationUrl Url with the new apk
* @param versionCode Code of new version
*/
private void compareAppVersionAndShowNotification(final String versionName,
final String apkLocationUrl,
final int versionCode) {
int notificationId = 2000;
if (BuildConfig.VERSION_CODE < versionCode) {
// A pending intent to open the apk location url in the browser.
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl));
final PendingIntent pendingIntent
= PendingIntent.getActivity(APP, 0, intent, 0);
final NotificationCompat.Builder notificationBuilder = new NotificationCompat
.Builder(APP, APP.getString(R.string.app_update_notification_channel_id))
.setSmallIcon(R.drawable.ic_newpipe_update)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.setContentTitle(APP.getString(R.string.app_update_notification_content_title))
.setContentText(APP.getString(R.string.app_update_notification_content_text)
+ " " + versionName);
final NotificationManagerCompat notificationManager
= NotificationManagerCompat.from(APP);
notificationManager.notify(notificationId, notificationBuilder.build());
}
}
private boolean isConnected() {
ConnectivityManager cm =
(ConnectivityManager) app.getSystemService(Context.CONNECTIVITY_SERVICE);
final ConnectivityManager cm =
(ConnectivityManager) APP.getSystemService(Context.CONNECTIVITY_SERVICE);
return cm.getActiveNetworkInfo() != null
&& cm.getActiveNetworkInfo().isConnected();
&& cm.getActiveNetworkInfo().isConnected();
}
}

View File

@@ -1,12 +1,18 @@
package org.schabi.newpipe;
import android.content.Context;
import android.os.Build;
import android.text.TextUtils;
import android.preference.PreferenceManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.downloader.Request;
import org.schabi.newpipe.extractor.downloader.Response;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.util.CookieUtils;
import org.schabi.newpipe.util.InfoCache;
import org.schabi.newpipe.util.TLSSocketFactoryCompat;
import java.io.IOException;
@@ -17,6 +23,7 @@ import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@@ -26,9 +33,6 @@ import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import okhttp3.CipherSuite;
import okhttp3.ConnectionSpec;
import okhttp3.OkHttpClient;
@@ -37,42 +41,139 @@ import okhttp3.ResponseBody;
import static org.schabi.newpipe.MainActivity.DEBUG;
public class DownloaderImpl extends Downloader {
public static final String USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:43.0) Gecko/20100101 Firefox/43.0";
public final class DownloaderImpl extends Downloader {
public static final String USER_AGENT
= "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:68.0) Gecko/20100101 Firefox/68.0";
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE_KEY
= "youtube_restricted_mode_key";
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE = "PREF=f2=8000000";
public static final String YOUTUBE_DOMAIN = "youtube.com";
private static DownloaderImpl instance;
private String mCookies;
private Map<String, String> mCookies;
private OkHttpClient client;
private DownloaderImpl(OkHttpClient.Builder builder) {
private DownloaderImpl(final OkHttpClient.Builder builder) {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
enableModernTLS(builder);
}
this.client = builder
.readTimeout(30, TimeUnit.SECONDS)
//.cache(new Cache(new File(context.getExternalCacheDir(), "okhttp"), 16 * 1024 * 1024))
// .cache(new Cache(new File(context.getExternalCacheDir(), "okhttp"),
// 16 * 1024 * 1024))
.build();
this.mCookies = new HashMap<>();
}
/**
* It's recommended to call exactly once in the entire lifetime of the application.
*
* @param builder if null, default builder will be used
* @return a new instance of {@link DownloaderImpl}
*/
public static DownloaderImpl init(@Nullable OkHttpClient.Builder builder) {
return instance = new DownloaderImpl(builder != null ? builder : new OkHttpClient.Builder());
public static DownloaderImpl init(@Nullable final OkHttpClient.Builder builder) {
instance = new DownloaderImpl(
builder != null ? builder : new OkHttpClient.Builder());
return instance;
}
public static DownloaderImpl getInstance() {
return instance;
}
public String getCookies() {
return mCookies;
/**
* Enable TLS 1.2 and 1.1 on Android Kitkat. This function is mostly taken
* from the documentation of OkHttpClient.Builder.sslSocketFactory(_,_).
* <p>
* If there is an error, the function will safely fall back to doing nothing
* and printing the error to the console.
* </p>
*
* @param builder The HTTPClient Builder on which TLS is enabled on (will be modified in-place)
*/
private static void enableModernTLS(final OkHttpClient.Builder builder) {
try {
// get the default TrustManager
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init((KeyStore) null);
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
throw new IllegalStateException("Unexpected default trust managers:"
+ Arrays.toString(trustManagers));
}
X509TrustManager trustManager = (X509TrustManager) trustManagers[0];
// insert our own TLSSocketFactory
SSLSocketFactory sslSocketFactory = TLSSocketFactoryCompat.getInstance();
builder.sslSocketFactory(sslSocketFactory, trustManager);
// This will try to enable all modern CipherSuites(+2 more)
// that are supported on the device.
// Necessary because some servers (e.g. Framatube.org)
// don't support the old cipher suites.
// https://github.com/square/okhttp/issues/4053#issuecomment-402579554
List<CipherSuite> cipherSuites = new ArrayList<>();
cipherSuites.addAll(ConnectionSpec.MODERN_TLS.cipherSuites());
cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA);
cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA);
ConnectionSpec legacyTLS = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.cipherSuites(cipherSuites.toArray(new CipherSuite[0]))
.build();
builder.connectionSpecs(Arrays.asList(legacyTLS, ConnectionSpec.CLEARTEXT));
} catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) {
if (DEBUG) {
e.printStackTrace();
}
}
}
public void setCookies(String cookies) {
mCookies = cookies;
public String getCookies(final String url) {
List<String> resultCookies = new ArrayList<>();
if (url.contains(YOUTUBE_DOMAIN)) {
String youtubeCookie = getCookie(YOUTUBE_RESTRICTED_MODE_COOKIE_KEY);
if (youtubeCookie != null) {
resultCookies.add(youtubeCookie);
}
}
// Recaptcha cookie is always added TODO: not sure if this is necessary
String recaptchaCookie = getCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY);
if (recaptchaCookie != null) {
resultCookies.add(recaptchaCookie);
}
return CookieUtils.concatCookies(resultCookies);
}
public String getCookie(final String key) {
return mCookies.get(key);
}
public void setCookie(final String key, final String cookie) {
mCookies.put(key, cookie);
}
public void removeCookie(final String key) {
mCookies.remove(key);
}
public void updateYoutubeRestrictedModeCookies(final Context context) {
String restrictedModeEnabledKey =
context.getString(R.string.youtube_restricted_mode_enabled);
boolean restrictedModeEnabled = PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(restrictedModeEnabledKey, false);
updateYoutubeRestrictedModeCookies(restrictedModeEnabled);
}
public void updateYoutubeRestrictedModeCookies(final boolean youtubeRestrictedModeEnabled) {
if (youtubeRestrictedModeEnabled) {
setCookie(YOUTUBE_RESTRICTED_MODE_COOKIE_KEY,
YOUTUBE_RESTRICTED_MODE_COOKIE);
} else {
removeCookie(YOUTUBE_RESTRICTED_MODE_COOKIE_KEY);
}
InfoCache.getInstance().clearCache();
}
/**
@@ -81,7 +182,7 @@ public class DownloaderImpl extends Downloader {
* @param url an url pointing to the content
* @return the size of the content, in bytes
*/
public long getContentLength(String url) throws IOException {
public long getContentLength(final String url) throws IOException {
try {
final Response response = head(url);
return Long.parseLong(response.getHeader("Content-Length"));
@@ -92,14 +193,15 @@ public class DownloaderImpl extends Downloader {
}
}
public InputStream stream(String siteUrl) throws IOException {
public InputStream stream(final String siteUrl) throws IOException {
try {
final okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder()
.method("GET", null).url(siteUrl)
.addHeader("User-Agent", USER_AGENT);
if (!TextUtils.isEmpty(mCookies)) {
requestBuilder.addHeader("Cookie", mCookies);
String cookies = getCookies(siteUrl);
if (!cookies.isEmpty()) {
requestBuilder.addHeader("Cookie", cookies);
}
final okhttp3.Request request = requestBuilder.build();
@@ -122,7 +224,8 @@ public class DownloaderImpl extends Downloader {
}
@Override
public Response execute(@NonNull Request request) throws IOException, ReCaptchaException {
public Response execute(@NonNull final Request request)
throws IOException, ReCaptchaException {
final String httpMethod = request.httpMethod();
final String url = request.url();
final Map<String, List<String>> headers = request.headers();
@@ -137,8 +240,9 @@ public class DownloaderImpl extends Downloader {
.method(httpMethod, requestBody).url(url)
.addHeader("User-Agent", USER_AGENT);
if (!TextUtils.isEmpty(mCookies)) {
requestBuilder.addHeader("Cookie", mCookies);
String cookies = getCookies(url);
if (!cookies.isEmpty()) {
requestBuilder.addHeader("Cookie", cookies);
}
for (Map.Entry<String, List<String>> pair : headers.entrySet()) {
@@ -171,49 +275,8 @@ public class DownloaderImpl extends Downloader {
responseBodyToReturn = body.string();
}
return new Response(response.code(), response.message(), response.headers().toMultimap(), responseBodyToReturn);
}
/**
* Enable TLS 1.2 and 1.1 on Android Kitkat. This function is mostly taken from the documentation of
* OkHttpClient.Builder.sslSocketFactory(_,_)
* <p>
* If there is an error, the function will safely fall back to doing nothing and printing the error to the console.
*
* @param builder The HTTPClient Builder on which TLS is enabled on (will be modified in-place)
*/
private static void enableModernTLS(OkHttpClient.Builder builder) {
try {
// get the default TrustManager
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init((KeyStore) null);
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
throw new IllegalStateException("Unexpected default trust managers:"
+ Arrays.toString(trustManagers));
}
X509TrustManager trustManager = (X509TrustManager) trustManagers[0];
// insert our own TLSSocketFactory
SSLSocketFactory sslSocketFactory = TLSSocketFactoryCompat.getInstance();
builder.sslSocketFactory(sslSocketFactory, trustManager);
// This will try to enable all modern CipherSuites(+2 more) that are supported on the device.
// Necessary because some servers (e.g. Framatube.org) don't support the old cipher suites.
// https://github.com/square/okhttp/issues/4053#issuecomment-402579554
List<CipherSuite> cipherSuites = new ArrayList<>();
cipherSuites.addAll(ConnectionSpec.MODERN_TLS.cipherSuites());
cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA);
cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA);
ConnectionSpec legacyTLS = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.cipherSuites(cipherSuites.toArray(new CipherSuite[0]))
.build();
builder.connectionSpecs(Arrays.asList(legacyTLS, ConnectionSpec.CLEARTEXT));
} catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) {
if (DEBUG) e.printStackTrace();
}
final String latestUrl = response.request().url().toString();
return new Response(response.code(), response.message(), response.headers().toMultimap(),
responseBodyToReturn, latestUrl);
}
}

View File

@@ -1,4 +1,3 @@
package org.schabi.newpipe;
import android.annotation.SuppressLint;
@@ -27,9 +26,20 @@ import android.os.Bundle;
public class ExitActivity extends Activity {
public static void exitAndRemoveFromRecentApps(final Activity activity) {
Intent intent = new Intent(activity, ExitActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
| Intent.FLAG_ACTIVITY_CLEAR_TASK
| Intent.FLAG_ACTIVITY_NO_ANIMATION);
activity.startActivity(intent);
}
@SuppressLint("NewApi")
@Override
protected void onCreate(Bundle savedInstanceState) {
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (Build.VERSION.SDK_INT >= 21) {
@@ -40,15 +50,4 @@ public class ExitActivity extends Activity {
System.exit(0);
}
public static void exitAndRemoveFromRecentApps(Activity activity) {
Intent intent = new Intent(activity, ExitActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
| Intent.FLAG_ACTIVITY_CLEAR_TASK
| Intent.FLAG_ACTIVITY_NO_ANIMATION);
activity.startActivity(intent);
}
}

View File

@@ -18,7 +18,7 @@ public class ImageDownloader extends BaseImageDownloader {
private final SharedPreferences preferences;
private final String downloadThumbnailKey;
public ImageDownloader(Context context) {
public ImageDownloader(final Context context) {
super(context);
this.resources = context.getResources();
this.preferences = PreferenceManager.getDefaultSharedPreferences(context);
@@ -31,7 +31,7 @@ public class ImageDownloader extends BaseImageDownloader {
@SuppressLint("ResourceType")
@Override
public InputStream getStream(String imageUri, Object extra) throws IOException {
public InputStream getStream(final String imageUri, final Object extra) throws IOException {
if (isDownloadingThumbnail()) {
return super.getStream(imageUri, extra);
} else {
@@ -39,7 +39,8 @@ public class ImageDownloader extends BaseImageDownloader {
}
}
protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException {
protected InputStream getStreamFromNetwork(final String imageUri, final Object extra)
throws IOException {
final DownloaderImpl downloader = (DownloaderImpl) NewPipe.getDownloader();
return downloader.stream(imageUri);
}

View File

@@ -53,31 +53,36 @@ import org.schabi.newpipe.fragments.MainFragment;
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
import org.schabi.newpipe.fragments.list.search.SearchFragment;
import org.schabi.newpipe.player.VideoPlayer;
import org.schabi.newpipe.player.event.OnKeyDownListener;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.util.*;
import org.schabi.newpipe.views.FocusOverlayView;
import java.util.ArrayList;
import java.util.List;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release");
private ActionBarDrawerToggle toggle = null;
private DrawerLayout drawer = null;
private NavigationView drawerItems = null;
private TextView headerServiceView = null;
private Button toggleServiceButton = null;
private ActionBarDrawerToggle toggle;
private DrawerLayout drawer;
private NavigationView drawerItems;
private ImageView headerServiceIcon;
private TextView headerServiceView;
private Button toggleServiceButton;
private boolean servicesShown = false;
private ImageView serviceArrow;
private static final int ITEM_ID_SUBSCRIPTIONS = - 1;
private static final int ITEM_ID_FEED = - 2;
private static final int ITEM_ID_BOOKMARKS = - 3;
private static final int ITEM_ID_DOWNLOADS = - 4;
private static final int ITEM_ID_HISTORY = - 5;
private static final int ITEM_ID_SUBSCRIPTIONS = -1;
private static final int ITEM_ID_FEED = -2;
private static final int ITEM_ID_BOOKMARKS = -3;
private static final int ITEM_ID_DOWNLOADS = -4;
private static final int ITEM_ID_HISTORY = -5;
private static final int ITEM_ID_SETTINGS = 0;
private static final int ITEM_ID_ABOUT = 1;
@@ -88,25 +93,24 @@ public class MainActivity extends AppCompatActivity {
//////////////////////////////////////////////////////////////////////////*/
@Override
protected void onCreate(Bundle savedInstanceState) {
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
protected void onCreate(final Bundle savedInstanceState) {
if (DEBUG) {
Log.d(TAG, "onCreate() called with: "
+ "savedInstanceState = [" + savedInstanceState + "]");
}
// enable TLS1.1/1.2 for kitkat devices, to fix download and play for mediaCCC sources
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
TLSSocketFactoryCompat.setAsDefault();
}
ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this));
assureCorrectAppLanguage(this);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Window w = getWindow();
w.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
}
if (getSupportFragmentManager() != null && getSupportFragmentManager().getBackStackEntryCount() == 0) {
if (getSupportFragmentManager() != null
&& getSupportFragmentManager().getBackStackEntryCount() == 0) {
initFragments();
}
@@ -116,6 +120,10 @@ public class MainActivity extends AppCompatActivity {
} catch (Exception e) {
ErrorActivity.reportUiError(this, e);
}
if (AndroidTvUtils.isTv(this)) {
FocusOverlayView.setupFocusObserver(this);
}
}
private void setupDrawer() throws Exception {
@@ -131,49 +139,52 @@ public class MainActivity extends AppCompatActivity {
for (final String ks : service.getKioskList().getAvailableKiosks()) {
drawerItems.getMenu()
.add(R.id.menu_tabs_group, kioskId, 0, KioskTranslator.getTranslatedKioskName(ks, this))
.setIcon(KioskTranslator.getKioskIcons(ks, this));
kioskId ++;
.add(R.id.menu_tabs_group, kioskId, 0, KioskTranslator
.getTranslatedKioskName(ks, this))
.setIcon(KioskTranslator.getKioskIcon(ks, this));
kioskId++;
}
drawerItems.getMenu()
.add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER, R.string.tab_subscriptions)
.add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER,
R.string.tab_subscriptions)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_channel));
drawerItems.getMenu()
.add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_whats_new)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.rss));
.add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_feed_title)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_rss));
drawerItems.getMenu()
.add(R.id.menu_tabs_group, ITEM_ID_BOOKMARKS, ORDER, R.string.tab_bookmarks)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_bookmark));
drawerItems.getMenu()
.add(R.id.menu_tabs_group, ITEM_ID_DOWNLOADS, ORDER, R.string.downloads)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.download));
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_file_download));
drawerItems.getMenu()
.add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.history));
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_history));
//Settings and About
drawerItems.getMenu()
.add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.settings));
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_settings));
drawerItems.getMenu()
.add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.info));
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_info_outline));
toggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.drawer_open, R.string.drawer_close);
toggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.drawer_open,
R.string.drawer_close);
toggle.syncState();
drawer.addDrawerListener(toggle);
drawer.addDrawerListener(new DrawerLayout.SimpleDrawerListener() {
private int lastService;
@Override
public void onDrawerOpened(View drawerView) {
public void onDrawerOpened(final View drawerView) {
lastService = ServiceHelper.getSelectedServiceId(MainActivity.this);
}
@Override
public void onDrawerClosed(View drawerView) {
if(servicesShown) {
public void onDrawerClosed(final View drawerView) {
if (servicesShown) {
toggleServices();
}
if (lastService != ServiceHelper.getSelectedServiceId(MainActivity.this)) {
@@ -186,7 +197,7 @@ public class MainActivity extends AppCompatActivity {
setupDrawerHeader();
}
private boolean drawerItemSelected(MenuItem item) {
private boolean drawerItemSelected(final MenuItem item) {
switch (item.getGroupId()) {
case R.id.menu_services_group:
changeService(item);
@@ -209,19 +220,21 @@ public class MainActivity extends AppCompatActivity {
return true;
}
private void changeService(MenuItem item) {
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(false);
private void changeService(final MenuItem item) {
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this))
.setChecked(false);
ServiceHelper.setSelectedServiceId(this, item.getItemId());
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true);
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this))
.setChecked(true);
}
private void tabSelected(MenuItem item) throws ExtractionException {
switch(item.getItemId()) {
private void tabSelected(final MenuItem item) throws ExtractionException {
switch (item.getItemId()) {
case ITEM_ID_SUBSCRIPTIONS:
NavigationHelper.openSubscriptionFragment(getSupportFragmentManager());
break;
case ITEM_ID_FEED:
NavigationHelper.openWhatsNewFragment(getSupportFragmentManager());
NavigationHelper.openFeedFragment(getSupportFragmentManager());
break;
case ITEM_ID_BOOKMARKS:
NavigationHelper.openBookmarksFragment(getSupportFragmentManager());
@@ -239,19 +252,20 @@ public class MainActivity extends AppCompatActivity {
int kioskId = 0;
for (final String ks : service.getKioskList().getAvailableKiosks()) {
if(kioskId == item.getItemId()) {
if (kioskId == item.getItemId()) {
serviceName = ks;
}
kioskId ++;
kioskId++;
}
NavigationHelper.openKioskFragment(getSupportFragmentManager(), currentServiceId, serviceName);
NavigationHelper.openKioskFragment(getSupportFragmentManager(), currentServiceId,
serviceName);
break;
}
}
private void optionsAboutSelected(MenuItem item) {
switch(item.getItemId()) {
private void optionsAboutSelected(final MenuItem item) {
switch (item.getItemId()) {
case ITEM_ID_SETTINGS:
NavigationHelper.openSettings(this);
break;
@@ -263,14 +277,27 @@ public class MainActivity extends AppCompatActivity {
private void setupDrawerHeader() {
NavigationView navigationView = findViewById(R.id.navigation);
View hView = navigationView.getHeaderView(0);
View hView = navigationView.getHeaderView(0);
serviceArrow = hView.findViewById(R.id.drawer_arrow);
headerServiceIcon = hView.findViewById(R.id.drawer_header_service_icon);
headerServiceView = hView.findViewById(R.id.drawer_header_service_view);
toggleServiceButton = hView.findViewById(R.id.drawer_header_action_button);
toggleServiceButton.setOnClickListener(view -> {
toggleServices();
});
toggleServiceButton.setOnClickListener(view -> toggleServices());
// If the current app name is bigger than the default "NewPipe" (7 chars),
// let the text view grow a little more as well.
if (getString(R.string.app_name).length() > "NewPipe".length()) {
final TextView headerTitle = hView.findViewById(R.id.drawer_header_newpipe_title);
final ViewGroup.LayoutParams layoutParams = headerTitle.getLayoutParams();
layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;
headerTitle.setLayoutParams(layoutParams);
headerTitle.setMaxLines(2);
headerTitle.setMinWidth(getResources()
.getDimensionPixelSize(R.dimen.drawer_header_newpipe_title_default_width));
headerTitle.setMaxWidth(getResources()
.getDimensionPixelSize(R.dimen.drawer_header_newpipe_title_max_width));
}
}
private void toggleServices() {
@@ -280,8 +307,7 @@ public class MainActivity extends AppCompatActivity {
drawerItems.getMenu().removeGroup(R.id.menu_tabs_group);
drawerItems.getMenu().removeGroup(R.id.menu_options_about_group);
if(servicesShown) {
if (servicesShown) {
showServices();
} else {
try {
@@ -293,57 +319,64 @@ public class MainActivity extends AppCompatActivity {
}
private void showServices() {
serviceArrow.setImageResource(R.drawable.ic_arrow_up_white);
serviceArrow.setImageResource(R.drawable.ic_arrow_drop_up_white_24dp);
for(StreamingService s : NewPipe.getServices()) {
final String title = s.getServiceInfo().getName() +
(ServiceHelper.isBeta(s) ? " (beta)" : "");
for (StreamingService s : NewPipe.getServices()) {
final String title = s.getServiceInfo().getName()
+ (ServiceHelper.isBeta(s) ? " (beta)" : "");
MenuItem menuItem = drawerItems.getMenu()
.add(R.id.menu_services_group, s.getServiceId(), ORDER, title)
.setIcon(ServiceHelper.getIcon(s.getServiceId()));
// peertube specifics
if(s.getServiceId() == 3){
if (s.getServiceId() == 3) {
enhancePeertubeMenu(s, menuItem);
}
}
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true);
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this))
.setChecked(true);
}
private void enhancePeertubeMenu(StreamingService s, MenuItem menuItem) {
private void enhancePeertubeMenu(final StreamingService s, final MenuItem menuItem) {
PeertubeInstance currentInstace = PeertubeHelper.getCurrentInstance();
menuItem.setTitle(currentInstace.getName() + (ServiceHelper.isBeta(s) ? " (beta)" : ""));
Spinner spinner = (Spinner) LayoutInflater.from(this).inflate(R.layout.instance_spinner_layout, null);
Spinner spinner = (Spinner) LayoutInflater.from(this)
.inflate(R.layout.instance_spinner_layout, null);
List<PeertubeInstance> instances = PeertubeHelper.getInstanceList(this);
List<String> items = new ArrayList<>();
int defaultSelect = 0;
for(PeertubeInstance instance: instances){
for (PeertubeInstance instance : instances) {
items.add(instance.getName());
if(instance.getUrl().equals(currentInstace.getUrl())){
defaultSelect = items.size()-1;
if (instance.getUrl().equals(currentInstace.getUrl())) {
defaultSelect = items.size() - 1;
}
}
ArrayAdapter<String> adapter = new ArrayAdapter<>(this, R.layout.instance_spinner_item, items);
ArrayAdapter<String> adapter = new ArrayAdapter<>(this,
R.layout.instance_spinner_item, items);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinner.setAdapter(adapter);
spinner.setSelection(defaultSelect, false);
spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
public void onItemSelected(final AdapterView<?> parent, final View view,
final int position, final long id) {
PeertubeInstance newInstance = instances.get(position);
if(newInstance.getUrl().equals(PeertubeHelper.getCurrentInstance().getUrl())) return;
if (newInstance.getUrl().equals(PeertubeHelper.getCurrentInstance().getUrl())) {
return;
}
PeertubeHelper.selectInstance(newInstance, getApplicationContext());
changeService(menuItem);
drawer.closeDrawers();
new Handler(Looper.getMainLooper()).postDelayed(() -> {
getSupportFragmentManager().popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
getSupportFragmentManager().popBackStack(null,
FragmentManager.POP_BACK_STACK_INCLUSIVE);
recreate();
}, 300);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
public void onNothingSelected(final AdapterView<?> parent) {
}
});
@@ -351,7 +384,7 @@ public class MainActivity extends AppCompatActivity {
}
private void showTabs() throws ExtractionException {
serviceArrow.setImageResource(R.drawable.ic_arrow_down_white);
serviceArrow.setImageResource(R.drawable.ic_arrow_drop_down_white_24dp);
//Tabs
int currentServiceId = ServiceHelper.getSelectedServiceId(this);
@@ -361,34 +394,35 @@ public class MainActivity extends AppCompatActivity {
for (final String ks : service.getKioskList().getAvailableKiosks()) {
drawerItems.getMenu()
.add(R.id.menu_tabs_group, kioskId, ORDER, KioskTranslator.getTranslatedKioskName(ks, this))
.setIcon(KioskTranslator.getKioskIcons(ks, this));
kioskId ++;
.add(R.id.menu_tabs_group, kioskId, ORDER,
KioskTranslator.getTranslatedKioskName(ks, this))
.setIcon(KioskTranslator.getKioskIcon(ks, this));
kioskId++;
}
drawerItems.getMenu()
.add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER, R.string.tab_subscriptions)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_channel));
drawerItems.getMenu()
.add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_whats_new)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.rss));
.add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_feed_title)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_rss));
drawerItems.getMenu()
.add(R.id.menu_tabs_group, ITEM_ID_BOOKMARKS, ORDER, R.string.tab_bookmarks)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_bookmark));
drawerItems.getMenu()
.add(R.id.menu_tabs_group, ITEM_ID_DOWNLOADS, ORDER, R.string.downloads)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.download));
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_file_download));
drawerItems.getMenu()
.add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.history));
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_history));
//Settings and About
drawerItems.getMenu()
.add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.settings));
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_settings));
drawerItems.getMenu()
.add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.info));
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_info_outline));
}
@Override
@@ -401,15 +435,21 @@ public class MainActivity extends AppCompatActivity {
@Override
protected void onResume() {
assureCorrectAppLanguage(this);
// Change the date format to match the selected language on resume
Localization.init(getApplicationContext());
super.onResume();
// close drawer on return, and don't show animation, so its looks like the drawer isn't open
// when the user returns to MainActivity
// Close drawer on return, and don't show animation,
// so it looks like the drawer isn't open when the user returns to MainActivity
drawer.closeDrawer(GravityCompat.START, false);
try {
String selectedServiceName = NewPipe.getService(
ServiceHelper.getSelectedServiceId(this)).getServiceInfo().getName();
final int selectedServiceId = ServiceHelper.getSelectedServiceId(this);
final String selectedServiceName = NewPipe.getService(selectedServiceId)
.getServiceInfo().getName();
headerServiceView.setText(selectedServiceName);
headerServiceIcon.setImageResource(ServiceHelper.getIcon(selectedServiceId));
headerServiceView.post(() -> headerServiceView.setSelected(true));
toggleServiceButton.setContentDescription(
getString(R.string.drawer_header_description) + selectedServiceName);
@@ -419,28 +459,42 @@ public class MainActivity extends AppCompatActivity {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
if (sharedPreferences.getBoolean(Constants.KEY_THEME_CHANGE, false)) {
if (DEBUG) Log.d(TAG, "Theme has changed, recreating activity...");
if (DEBUG) {
Log.d(TAG, "Theme has changed, recreating activity...");
}
sharedPreferences.edit().putBoolean(Constants.KEY_THEME_CHANGE, false).apply();
// https://stackoverflow.com/questions/10844112/runtimeexception-performing-pause-of-activity-that-is-not-resumed
// Briefly, let the activity resume properly posting the recreate call to end of the message queue
// https://stackoverflow.com/questions/10844112/
// Briefly, let the activity resume
// properly posting the recreate call to end of the message queue
new Handler(Looper.getMainLooper()).post(MainActivity.this::recreate);
}
if (sharedPreferences.getBoolean(Constants.KEY_MAIN_PAGE_CHANGE, false)) {
if (DEBUG) Log.d(TAG, "main page has changed, recreating main fragment...");
if (DEBUG) {
Log.d(TAG, "main page has changed, recreating main fragment...");
}
sharedPreferences.edit().putBoolean(Constants.KEY_MAIN_PAGE_CHANGE, false).apply();
NavigationHelper.openMainActivity(this);
}
final boolean isHistoryEnabled = sharedPreferences.getBoolean(
getString(R.string.enable_watch_history_key), true);
drawerItems.getMenu().findItem(ITEM_ID_HISTORY).setVisible(isHistoryEnabled);
}
@Override
protected void onNewIntent(Intent intent) {
if (DEBUG) Log.d(TAG, "onNewIntent() called with: intent = [" + intent + "]");
protected void onNewIntent(final Intent intent) {
if (DEBUG) {
Log.d(TAG, "onNewIntent() called with: intent = [" + intent + "]");
}
if (intent != null) {
// Return if launched from a launcher (e.g. Nova Launcher, Pixel Launcher ...)
// to not destroy the already created backstack
String action = intent.getAction();
if ((action != null && action.equals(Intent.ACTION_MAIN)) && intent.hasCategory(Intent.CATEGORY_LAUNCHER)) return;
if ((action != null && action.equals(Intent.ACTION_MAIN))
&& intent.hasCategory(Intent.CATEGORY_LAUNCHER)) {
return;
}
}
super.onNewIntent(intent);
@@ -448,17 +502,33 @@ public class MainActivity extends AppCompatActivity {
handleIntent(intent);
}
@Override
public boolean onKeyDown(final int keyCode, final KeyEvent event) {
final Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_player_holder);
if (fragment instanceof OnKeyDownListener && !bottomSheetHiddenOrCollapsed()) {
// Provide keyDown event to fragment which then sends this event to the main player service
return ((OnKeyDownListener) fragment).onKeyDown(keyCode) || super.onKeyDown(keyCode, event);
}
return super.onKeyDown(keyCode, event);
}
@Override
public void onBackPressed() {
if (DEBUG) Log.d(TAG, "onBackPressed() called");
if (DEBUG) {
Log.d(TAG, "onBackPressed() called");
}
final FrameLayout bottomSheetLayout = findViewById(R.id.fragment_player_holder);
final BottomSheetBehavior<FrameLayout> bottomSheetBehavior = BottomSheetBehavior.from(bottomSheetLayout);
if (AndroidTvUtils.isTv(this)) {
View drawerPanel = findViewById(R.id.navigation);
if (drawer.isDrawerOpen(drawerPanel)) {
drawer.closeDrawers();
return;
}
}
final int sheetState = bottomSheetBehavior.getState();
// In case bottomSheet is not visible on the screen or collapsed we can assume that the user interacts with a fragment
// inside fragment_holder so all back presses should be handled by it
if (sheetState == BottomSheetBehavior.STATE_HIDDEN || sheetState == BottomSheetBehavior.STATE_COLLAPSED) {
// In case bottomSheet is not visible on the screen or collapsed we can assume that the user
// interacts with a fragment inside fragment_holder so all back presses should be handled by it
if (bottomSheetHiddenOrCollapsed()) {
final Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
// If current fragment implements BackPressable (i.e. can/wanna handle back press) delegate the back press to it
if (fragment instanceof BackPressable) {
@@ -469,21 +539,27 @@ public class MainActivity extends AppCompatActivity {
final Fragment fragmentPlayer = getSupportFragmentManager().findFragmentById(R.id.fragment_player_holder);
// If current fragment implements BackPressable (i.e. can/wanna handle back press) delegate the back press to it
if (fragmentPlayer instanceof BackPressable) {
if (!((BackPressable) fragmentPlayer).onBackPressed())
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
if (!((BackPressable) fragmentPlayer).onBackPressed()) {
final FrameLayout bottomSheetLayout = findViewById(R.id.fragment_player_holder);
BottomSheetBehavior.from(bottomSheetLayout).setState(BottomSheetBehavior.STATE_COLLAPSED);
}
return;
}
}
if (getSupportFragmentManager().getBackStackEntryCount() == 1) {
finish();
} else super.onBackPressed();
} else {
super.onBackPressed();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
for (int i: grantResults){
if (i == PackageManager.PERMISSION_DENIED){
public void onRequestPermissionsResult(final int requestCode,
@NonNull final String[] permissions,
@NonNull final int[] grantResults) {
for (int i : grantResults) {
if (i == PackageManager.PERMISSION_DENIED) {
return;
}
}
@@ -537,16 +613,16 @@ public class MainActivity extends AppCompatActivity {
//////////////////////////////////////////////////////////////////////////*/
@Override
public boolean onCreateOptionsMenu(Menu menu) {
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "]");
public boolean onCreateOptionsMenu(final Menu menu) {
if (DEBUG) {
Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "]");
}
super.onCreateOptionsMenu(menu);
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
if (!(fragment instanceof SearchFragment)) {
findViewById(R.id.toolbar).findViewById(R.id.toolbar_search_container).setVisibility(View.GONE);
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.main_menu, menu);
findViewById(R.id.toolbar).findViewById(R.id.toolbar_search_container)
.setVisibility(View.GONE);
}
ActionBar actionBar = getSupportActionBar();
@@ -560,22 +636,16 @@ public class MainActivity extends AppCompatActivity {
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (DEBUG) Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]");
public boolean onOptionsItemSelected(final MenuItem item) {
if (DEBUG) {
Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]");
}
int id = item.getItemId();
switch (id) {
case android.R.id.home:
onHomeButtonPressed();
return true;
case R.id.action_show_downloads:
return NavigationHelper.openDownloads(this);
case R.id.action_history:
NavigationHelper.openStatisticFragment(getSupportFragmentManager());
return true;
case R.id.action_settings:
NavigationHelper.openSettings(this);
return true;
default:
return super.onOptionsItemSelected(item);
}
@@ -586,7 +656,9 @@ public class MainActivity extends AppCompatActivity {
//////////////////////////////////////////////////////////////////////////*/
private void initFragments() {
if (DEBUG) Log.d(TAG, "initFragments() called");
if (DEBUG) {
Log.d(TAG, "initFragments() called");
}
StateSaver.clearStateFiles();
if (getIntent() != null && getIntent().hasExtra(Constants.KEY_LINK_TYPE)) {
// When user watch a video inside popup and then tries to open the video in main player while the app is closed
@@ -595,7 +667,9 @@ public class MainActivity extends AppCompatActivity {
NavigationHelper.openMainFragment(getSupportFragmentManager());
handleIntent(getIntent());
} else NavigationHelper.gotoMainFragment(getSupportFragmentManager());
} else {
NavigationHelper.gotoMainFragment(getSupportFragmentManager());
}
}
/*//////////////////////////////////////////////////////////////////////////
@@ -603,12 +677,14 @@ public class MainActivity extends AppCompatActivity {
//////////////////////////////////////////////////////////////////////////*/
private void updateDrawerNavigation() {
if (getSupportActionBar() == null) return;
if (getSupportActionBar() == null) {
return;
}
final Toolbar toolbar = findViewById(R.id.toolbar);
final DrawerLayout drawer = findViewById(R.id.drawer_layout);
final Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
final Fragment fragment = getSupportFragmentManager()
.findFragmentById(R.id.fragment_holder);
if (fragment instanceof MainFragment) {
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
if (toggle != null) {
@@ -623,23 +699,18 @@ public class MainActivity extends AppCompatActivity {
}
}
private void updateDrawerHeaderString(String content) {
NavigationView navigationView = findViewById(R.id.navigation);
View hView = navigationView.getHeaderView(0);
Button action = hView.findViewById(R.id.drawer_header_action_button);
action.setContentDescription(content);
}
private void handleIntent(Intent intent) {
private void handleIntent(final Intent intent) {
try {
if (DEBUG) Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]");
if (DEBUG) {
Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]");
}
if (intent.hasExtra(Constants.KEY_LINK_TYPE)) {
String url = intent.getStringExtra(Constants.KEY_URL);
int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0);
String title = intent.getStringExtra(Constants.KEY_TITLE);
switch (((StreamingService.LinkType) intent.getSerializableExtra(Constants.KEY_LINK_TYPE))) {
switch (((StreamingService.LinkType) intent
.getSerializableExtra(Constants.KEY_LINK_TYPE))) {
case STREAM:
boolean autoPlay = intent.getBooleanExtra(VideoDetailFragment.AUTO_PLAY, false);
final String intentCacheKey = intent.getStringExtra(VideoPlayer.PLAY_QUEUE_KEY);
@@ -661,7 +732,9 @@ public class MainActivity extends AppCompatActivity {
}
} else if (intent.hasExtra(Constants.KEY_OPEN_SEARCH)) {
String searchString = intent.getStringExtra(Constants.KEY_SEARCH_STRING);
if (searchString == null) searchString = "";
if (searchString == null) {
searchString = "";
}
int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0);
NavigationHelper.openSearchFragment(
getSupportFragmentManager(),
@@ -675,4 +748,15 @@ public class MainActivity extends AppCompatActivity {
ErrorActivity.reportUiError(this, e);
}
}
/*
* Utils
* */
private boolean bottomSheetHiddenOrCollapsed() {
final FrameLayout bottomSheetLayout = findViewById(R.id.fragment_player_holder);
final BottomSheetBehavior<FrameLayout> bottomSheetBehavior = BottomSheetBehavior.from(bottomSheetLayout);
final int sheetState = bottomSheetBehavior.getState();
return sheetState == BottomSheetBehavior.STATE_HIDDEN || sheetState == BottomSheetBehavior.STATE_COLLAPSED;
}
}

View File

@@ -1,42 +1,54 @@
package org.schabi.newpipe;
import androidx.room.Room;
import android.content.Context;
import android.database.Cursor;
import androidx.annotation.NonNull;
import androidx.room.Room;
import org.schabi.newpipe.database.AppDatabase;
import static org.schabi.newpipe.database.AppDatabase.DATABASE_NAME;
import static org.schabi.newpipe.database.Migrations.MIGRATION_11_12;
import static org.schabi.newpipe.database.Migrations.MIGRATION_1_2;
import static org.schabi.newpipe.database.Migrations.MIGRATION_2_3;
public final class NewPipeDatabase {
private static volatile AppDatabase databaseInstance;
private NewPipeDatabase() {
//no instance
}
private static AppDatabase getDatabase(Context context) {
private static AppDatabase getDatabase(final Context context) {
return Room
.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
.addMigrations(MIGRATION_11_12)
.fallbackToDestructiveMigration()
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.build();
}
@NonNull
public static AppDatabase getInstance(@NonNull Context context) {
public static AppDatabase getInstance(@NonNull final Context context) {
AppDatabase result = databaseInstance;
if (result == null) {
synchronized (NewPipeDatabase.class) {
result = databaseInstance;
if (result == null) {
databaseInstance = (result = getDatabase(context));
databaseInstance = getDatabase(context);
result = databaseInstance;
}
}
}
return result;
}
public static void checkpoint() {
if (databaseInstance == null) {
throw new IllegalStateException("database is not initialized");
}
Cursor c = databaseInstance.query("pragma wal_checkpoint(full)", null);
if (c.moveToFirst() && c.getInt(0) == 1) {
throw new RuntimeException("Checkpoint was blocked from completing");
}
}
}

View File

@@ -1,4 +1,3 @@
package org.schabi.newpipe;
import android.annotation.SuppressLint;
@@ -26,17 +25,18 @@ import android.os.Bundle;
*/
public class PanicResponderActivity extends Activity {
public static final String PANIC_TRIGGER_ACTION = "info.guardianproject.panic.action.TRIGGER";
@SuppressLint("NewApi")
@Override
protected void onCreate(Bundle savedInstanceState) {
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
if (intent != null && PANIC_TRIGGER_ACTION.equals(intent.getAction())) {
// TODO explicitly clear the search results once they are restored when the app restarts
// or if the app reloads the current video after being killed, that should be cleared also
// TODO: Explicitly clear the search results
// once they are restored when the app restarts
// or if the app reloads the current video after being killed,
// that should be cleared also
ExitActivity.exitAndRemoveFromRecentApps(this);
}

View File

@@ -1,20 +1,32 @@
package org.schabi.newpipe;
import android.app.Activity;
import android.content.Intent;
import android.graphics.Bitmap;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import androidx.core.app.NavUtils;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.webkit.CookieManager;
import android.webkit.WebResourceRequest;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.NavUtils;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.util.ThemeHelper;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
/*
* Created by beneth <bmauduit@beneth.fr> on 06.12.16.
*
@@ -37,126 +49,200 @@ import android.webkit.WebViewClient;
public class ReCaptchaActivity extends AppCompatActivity {
public static final int RECAPTCHA_REQUEST = 10;
public static final String RECAPTCHA_URL_EXTRA = "recaptcha_url_extra";
public static final String TAG = ReCaptchaActivity.class.toString();
public static final String YT_URL = "https://www.youtube.com";
public static final String RECAPTCHA_COOKIES_KEY = "recaptcha_cookies";
private String url;
private WebView webView;
private String foundCookies = "";
@Override
protected void onCreate(Bundle savedInstanceState) {
protected void onCreate(final Bundle savedInstanceState) {
ThemeHelper.setTheme(this);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_recaptcha);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
url = getIntent().getStringExtra(RECAPTCHA_URL_EXTRA);
String url = getIntent().getStringExtra(RECAPTCHA_URL_EXTRA);
if (url == null || url.isEmpty()) {
url = YT_URL;
}
// Set return to Cancel by default
// set return to Cancel by default
setResult(RESULT_CANCELED);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setTitle(R.string.reCaptcha_title);
actionBar.setDisplayShowTitleEnabled(true);
}
webView = findViewById(R.id.reCaptchaWebView);
WebView myWebView = findViewById(R.id.reCaptchaWebView);
// Enable Javascript
WebSettings webSettings = myWebView.getSettings();
// enable Javascript
WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);
ReCaptchaWebViewClient webClient = new ReCaptchaWebViewClient(this);
myWebView.setWebViewClient(webClient);
webView.setWebViewClient(new WebViewClient() {
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public boolean shouldOverrideUrlLoading(final WebView view,
final WebResourceRequest request) {
String url = request.getUrl().toString();
if (MainActivity.DEBUG) {
Log.d(TAG, "shouldOverrideUrlLoading: request.url=" + url);
}
// Cleaning cache, history and cookies from webView
myWebView.clearCache(true);
myWebView.clearHistory();
handleCookiesFromUrl(url);
return false;
}
@Override
public boolean shouldOverrideUrlLoading(final WebView view, final String url) {
if (MainActivity.DEBUG) {
Log.d(TAG, "shouldOverrideUrlLoading: url=" + url);
}
handleCookiesFromUrl(url);
return false;
}
@Override
public void onPageFinished(final WebView view, final String url) {
super.onPageFinished(view, url);
handleCookiesFromUrl(url);
}
});
// cleaning cache, history and cookies from webView
webView.clearCache(true);
webView.clearHistory();
android.webkit.CookieManager cookieManager = CookieManager.getInstance();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
cookieManager.removeAllCookies(aBoolean -> {});
cookieManager.removeAllCookies(aBoolean -> {
});
} else {
cookieManager.removeAllCookie();
}
myWebView.loadUrl(url);
}
private class ReCaptchaWebViewClient extends WebViewClient {
private final Activity context;
private String mCookies;
ReCaptchaWebViewClient(Activity ctx) {
context = ctx;
}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
// TODO: Start Loader
super.onPageStarted(view, url, favicon);
}
@Override
public void onPageFinished(WebView view, String url) {
String cookies = CookieManager.getInstance().getCookie(url);
// TODO: Stop Loader
// find cookies : s_gl & goojf and Add cookies to Downloader
if (find_access_cookies(cookies)) {
// Give cookies to Downloader class
DownloaderImpl.getInstance().setCookies(mCookies);
// Closing activity and return to parent
setResult(RESULT_OK);
finish();
}
}
private boolean find_access_cookies(String cookies) {
boolean ret = false;
String c_s_gl = "";
String c_goojf = "";
String[] parts = cookies.split("; ");
for (String part : parts) {
if (part.trim().startsWith("s_gl")) {
c_s_gl = part.trim();
}
if (part.trim().startsWith("goojf")) {
c_goojf = part.trim();
}
}
if (c_s_gl.length() > 0 && c_goojf.length() > 0) {
ret = true;
//mCookies = c_s_gl + "; " + c_goojf;
// Youtube seems to also need the other cookies:
mCookies = cookies;
}
return ret;
}
webView.loadUrl(url);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
public boolean onCreateOptionsMenu(final Menu menu) {
getMenuInflater().inflate(R.menu.menu_recaptcha, menu);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(false);
actionBar.setTitle(R.string.title_activity_recaptcha);
actionBar.setSubtitle(R.string.subtitle_activity_recaptcha);
}
return true;
}
@Override
public void onBackPressed() {
saveCookiesAndFinish();
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
int id = item.getItemId();
switch (id) {
case android.R.id.home: {
Intent intent = new Intent(this, org.schabi.newpipe.MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
NavUtils.navigateUpTo(this, intent);
case R.id.menu_item_done:
saveCookiesAndFinish();
return true;
}
default:
return false;
}
}
private void saveCookiesAndFinish() {
handleCookiesFromUrl(webView.getUrl()); // try to get cookies of unclosed page
if (MainActivity.DEBUG) {
Log.d(TAG, "saveCookiesAndFinish: foundCookies=" + foundCookies);
}
if (!foundCookies.isEmpty()) {
// save cookies to preferences
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
getApplicationContext());
final String key = getApplicationContext().getString(R.string.recaptcha_cookies_key);
prefs.edit().putString(key, foundCookies).apply();
// give cookies to Downloader class
DownloaderImpl.getInstance().setCookie(RECAPTCHA_COOKIES_KEY, foundCookies);
setResult(RESULT_OK);
}
Intent intent = new Intent(this, org.schabi.newpipe.MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
NavUtils.navigateUpTo(this, intent);
}
private void handleCookiesFromUrl(@Nullable final String url) {
if (MainActivity.DEBUG) {
Log.d(TAG, "handleCookiesFromUrl: url=" + (url == null ? "null" : url));
}
if (url == null) {
return;
}
String cookies = CookieManager.getInstance().getCookie(url);
handleCookies(cookies);
// sometimes cookies are inside the url
int abuseStart = url.indexOf("google_abuse=");
if (abuseStart != -1) {
int abuseEnd = url.indexOf("+path");
try {
String abuseCookie = url.substring(abuseStart + 13, abuseEnd);
abuseCookie = URLDecoder.decode(abuseCookie, "UTF-8");
handleCookies(abuseCookie);
} catch (UnsupportedEncodingException | StringIndexOutOfBoundsException e) {
if (MainActivity.DEBUG) {
e.printStackTrace();
Log.d(TAG, "handleCookiesFromUrl: invalid google abuse starting at "
+ abuseStart + " and ending at " + abuseEnd + " for url " + url);
}
}
}
}
private void handleCookies(@Nullable final String cookies) {
if (MainActivity.DEBUG) {
Log.d(TAG, "handleCookies: cookies=" + (cookies == null ? "null" : cookies));
}
if (cookies == null) {
return;
}
addYoutubeCookies(cookies);
// add here methods to extract cookies for other services
}
private void addYoutubeCookies(@NonNull final String cookies) {
if (cookies.contains("s_gl=") || cookies.contains("goojf=")
|| cookies.contains("VISITOR_INFO1_LIVE=")
|| cookies.contains("GOOGLE_ABUSE_EXEMPTION=")) {
// youtube seems to also need the other cookies:
addCookie(cookies);
}
}
private void addCookie(final String cookie) {
if (foundCookies.contains(cookie)) {
return;
}
if (foundCookies.isEmpty() || foundCookies.endsWith("; ")) {
foundCookies += cookie;
} else if (foundCookies.endsWith(";")) {
foundCookies += " " + cookie;
} else {
foundCookies += "; " + cookie;
}
}
}

View File

@@ -9,12 +9,6 @@ import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.preference.PreferenceManager;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import android.text.TextUtils;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
@@ -26,6 +20,13 @@ import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.Toast;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.app.NotificationCompat;
import androidx.fragment.app.FragmentManager;
import org.schabi.newpipe.download.DownloadDialog;
@@ -44,12 +45,15 @@ import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.AndroidTvUtils;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.urlfinder.UrlFinder;
import org.schabi.newpipe.views.FocusOverlayView;
import java.io.Serializable;
import java.util.ArrayList;
@@ -73,29 +77,31 @@ import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCap
import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr;
/**
* Get the url from the intent and open it in the chosen preferred player
* Get the url from the intent and open it in the chosen preferred player.
*/
public class RouterActivity extends AppCompatActivity {
public static final String INTERNAL_ROUTE_KEY = "internalRoute";
/**
* Removes invisible separators (\p{Z}) and punctuation characters including
* brackets (\p{P}). See http://www.regular-expressions.info/unicode.html for
* more details.
*/
private static final String REGEX_REMOVE_FROM_URL = "[\\p{Z}\\p{P}]";
protected final CompositeDisposable disposables = new CompositeDisposable();
@State
protected int currentServiceId = -1;
private StreamingService currentService;
@State
protected LinkType currentLinkType;
@State
protected int selectedRadioPosition = -1;
protected int selectedPreviously = -1;
protected String currentUrl;
protected boolean internalRoute = false;
protected final CompositeDisposable disposables = new CompositeDisposable();
private StreamingService currentService;
private boolean selectionIsDownload = false;
public static final String internalRouteKey = "internalRoute";
@Override
protected void onCreate(Bundle savedInstanceState) {
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Icepick.restoreInstanceState(this, savedInstanceState);
@@ -108,14 +114,14 @@ public class RouterActivity extends AppCompatActivity {
}
}
internalRoute = getIntent().getBooleanExtra(internalRouteKey, false);
internalRoute = getIntent().getBooleanExtra(INTERNAL_ROUTE_KEY, false);
setTheme(ThemeHelper.isLightThemeSelected(this)
? R.style.RouterActivityThemeLight : R.style.RouterActivityThemeDark);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
protected void onSaveInstanceState(final Bundle outState) {
super.onSaveInstanceState(outState);
Icepick.saveInstanceState(this, outState);
}
@@ -134,7 +140,7 @@ public class RouterActivity extends AppCompatActivity {
disposables.clear();
}
private void handleUrl(String url) {
private void handleUrl(final String url) {
disposables.add(Observable
.fromCallable(() -> {
if (currentServiceId == -1) {
@@ -159,13 +165,14 @@ public class RouterActivity extends AppCompatActivity {
}, this::handleError));
}
private void handleError(Throwable error) {
private void handleError(final Throwable error) {
error.printStackTrace();
if (error instanceof ExtractionException) {
Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show();
} else {
ExtractorHelper.handleGeneralException(this, -1, null, error, UserAction.SOMETHING_ELSE, null);
ExtractorHelper.handleGeneralException(this, -1, null, error,
UserAction.SOMETHING_ELSE, null);
}
finish();
@@ -177,8 +184,11 @@ public class RouterActivity extends AppCompatActivity {
}
protected void onSuccess() {
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
final String selectedChoiceKey = preferences.getString(getString(R.string.preferred_open_action_key), getString(R.string.preferred_open_action_default));
final SharedPreferences preferences = PreferenceManager
.getDefaultSharedPreferences(this);
final String selectedChoiceKey = preferences
.getString(getString(R.string.preferred_open_action_key),
getString(R.string.preferred_open_action_default));
final String showInfoKey = getString(R.string.show_info_key);
final String videoPlayerKey = getString(R.string.video_player_key);
@@ -188,7 +198,8 @@ public class RouterActivity extends AppCompatActivity {
final String alwaysAskKey = getString(R.string.always_ask_open_action_key);
if (selectedChoiceKey.equals(alwaysAskKey)) {
final List<AdapterChoiceItem> choices = getChoicesForService(currentService, currentLinkType);
final List<AdapterChoiceItem> choices
= getChoicesForService(currentService, currentLinkType);
switch (choices.size()) {
case 1:
@@ -206,20 +217,26 @@ public class RouterActivity extends AppCompatActivity {
} else if (selectedChoiceKey.equals(downloadKey)) {
handleChoice(downloadKey);
} else {
final boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false);
final boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false);
final boolean isVideoPlayerSelected = selectedChoiceKey.equals(videoPlayerKey) || selectedChoiceKey.equals(popupPlayerKey);
final boolean isExtVideoEnabled = preferences.getBoolean(
getString(R.string.use_external_video_player_key), false);
final boolean isExtAudioEnabled = preferences.getBoolean(
getString(R.string.use_external_audio_player_key), false);
final boolean isVideoPlayerSelected = selectedChoiceKey.equals(videoPlayerKey)
|| selectedChoiceKey.equals(popupPlayerKey);
final boolean isAudioPlayerSelected = selectedChoiceKey.equals(backgroundPlayerKey);
if (currentLinkType != LinkType.STREAM) {
if (isExtAudioEnabled && isAudioPlayerSelected || isExtVideoEnabled && isVideoPlayerSelected) {
Toast.makeText(this, R.string.external_player_unsupported_link_type, Toast.LENGTH_LONG).show();
if (isExtAudioEnabled && isAudioPlayerSelected
|| isExtVideoEnabled && isVideoPlayerSelected) {
Toast.makeText(this, R.string.external_player_unsupported_link_type,
Toast.LENGTH_LONG).show();
handleChoice(showInfoKey);
return;
}
}
final List<StreamingService.ServiceInfo.MediaCapability> capabilities = currentService.getServiceInfo().getMediaCapabilities();
final List<StreamingService.ServiceInfo.MediaCapability> capabilities
= currentService.getServiceInfo().getMediaCapabilities();
boolean serviceSupportsChoice = false;
if (isVideoPlayerSelected) {
@@ -241,7 +258,8 @@ public class RouterActivity extends AppCompatActivity {
final Context themeWrapperContext = getThemeWrapperContext();
final LayoutInflater inflater = LayoutInflater.from(themeWrapperContext);
final LinearLayout rootLayout = (LinearLayout) inflater.inflate(R.layout.preferred_player_dialog_view, null, false);
final LinearLayout rootLayout = (LinearLayout) inflater.inflate(
R.layout.preferred_player_dialog_view, null, false);
final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list);
final DialogInterface.OnClickListener dialogButtonsClickListener = (dialog, which) -> {
@@ -252,7 +270,9 @@ public class RouterActivity extends AppCompatActivity {
handleChoice(choice.key);
if (which == DialogInterface.BUTTON_POSITIVE) {
preferences.edit().putString(getString(R.string.preferred_open_action_key), choice.key).apply();
preferences.edit()
.putString(getString(R.string.preferred_open_action_key), choice.key)
.apply();
}
};
@@ -263,7 +283,9 @@ public class RouterActivity extends AppCompatActivity {
.setNegativeButton(R.string.just_once, dialogButtonsClickListener)
.setPositiveButton(R.string.always, dialogButtonsClickListener)
.setOnDismissListener((dialog) -> {
if (!selectionIsDownload) finish();
if (!selectionIsDownload) {
finish();
}
})
.create();
@@ -272,10 +294,13 @@ public class RouterActivity extends AppCompatActivity {
setDialogButtonsState(alertDialog, radioGroup.getCheckedRadioButtonId() != -1);
});
radioGroup.setOnCheckedChangeListener((group, checkedId) -> setDialogButtonsState(alertDialog, true));
radioGroup.setOnCheckedChangeListener((group, checkedId) ->
setDialogButtonsState(alertDialog, true));
final View.OnClickListener radioButtonsClickListener = v -> {
final int indexOfChild = radioGroup.indexOfChild(v);
if (indexOfChild == -1) return;
if (indexOfChild == -1) {
return;
}
selectedPreviously = selectedRadioPosition;
selectedRadioPosition = indexOfChild;
@@ -287,18 +312,23 @@ public class RouterActivity extends AppCompatActivity {
int id = 12345;
for (AdapterChoiceItem item : choices) {
final RadioButton radioButton = (RadioButton) inflater.inflate(R.layout.list_radio_icon_item, null);
final RadioButton radioButton
= (RadioButton) inflater.inflate(R.layout.list_radio_icon_item, null);
radioButton.setText(item.description);
radioButton.setCompoundDrawablesWithIntrinsicBounds(item.icon, 0, 0, 0);
radioButton.setCompoundDrawablesWithIntrinsicBounds(
AppCompatResources.getDrawable(getApplicationContext(), item.icon),
null, null, null);
radioButton.setChecked(false);
radioButton.setId(id++);
radioButton.setLayoutParams(new RadioGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
radioButton.setLayoutParams(new RadioGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
radioButton.setOnClickListener(radioButtonsClickListener);
radioGroup.addView(radioButton);
}
if (selectedRadioPosition == -1) {
final String lastSelectedPlayer = preferences.getString(getString(R.string.preferred_open_action_last_selected_key), null);
final String lastSelectedPlayer = preferences.getString(
getString(R.string.preferred_open_action_last_selected_key), null);
if (!TextUtils.isEmpty(lastSelectedPlayer)) {
for (int i = 0; i < choices.size(); i++) {
AdapterChoiceItem c = choices.get(i);
@@ -317,48 +347,64 @@ public class RouterActivity extends AppCompatActivity {
selectedPreviously = selectedRadioPosition;
alertDialog.show();
if (AndroidTvUtils.isTv(this)) {
FocusOverlayView.setupFocusObserver(alertDialog);
}
}
private List<AdapterChoiceItem> getChoicesForService(StreamingService service, LinkType linkType) {
private List<AdapterChoiceItem> getChoicesForService(final StreamingService service,
final LinkType linkType) {
final Context context = getThemeWrapperContext();
final List<AdapterChoiceItem> returnList = new ArrayList<>();
final List<StreamingService.ServiceInfo.MediaCapability> capabilities = service.getServiceInfo().getMediaCapabilities();
final List<StreamingService.ServiceInfo.MediaCapability> capabilities
= service.getServiceInfo().getMediaCapabilities();
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false);
boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false);
final SharedPreferences preferences = PreferenceManager
.getDefaultSharedPreferences(this);
boolean isExtVideoEnabled = preferences.getBoolean(
getString(R.string.use_external_video_player_key), false);
boolean isExtAudioEnabled = preferences.getBoolean(
getString(R.string.use_external_audio_player_key), false);
returnList.add(new AdapterChoiceItem(getString(R.string.show_info_key), getString(R.string.show_info),
resolveResourceIdFromAttr(context, R.attr.info)));
returnList.add(new AdapterChoiceItem(getString(R.string.show_info_key),
getString(R.string.show_info),
resolveResourceIdFromAttr(context, R.attr.ic_info_outline)));
if (capabilities.contains(VIDEO) && !(isExtVideoEnabled && linkType != LinkType.STREAM)) {
returnList.add(new AdapterChoiceItem(getString(R.string.video_player_key), getString(R.string.video_player),
resolveResourceIdFromAttr(context, R.attr.play)));
returnList.add(new AdapterChoiceItem(getString(R.string.popup_player_key), getString(R.string.popup_player),
resolveResourceIdFromAttr(context, R.attr.popup)));
returnList.add(new AdapterChoiceItem(getString(R.string.video_player_key),
getString(R.string.video_player),
resolveResourceIdFromAttr(context, R.attr.ic_play_arrow)));
returnList.add(new AdapterChoiceItem(getString(R.string.popup_player_key),
getString(R.string.popup_player),
resolveResourceIdFromAttr(context, R.attr.ic_popup)));
}
if (capabilities.contains(AUDIO) && !(isExtAudioEnabled && linkType != LinkType.STREAM)) {
returnList.add(new AdapterChoiceItem(getString(R.string.background_player_key), getString(R.string.background_player),
resolveResourceIdFromAttr(context, R.attr.audio)));
returnList.add(new AdapterChoiceItem(getString(R.string.background_player_key),
getString(R.string.background_player),
resolveResourceIdFromAttr(context, R.attr.ic_headset)));
}
returnList.add(new AdapterChoiceItem(getString(R.string.download_key), getString(R.string.download),
resolveResourceIdFromAttr(context, R.attr.download)));
returnList.add(new AdapterChoiceItem(getString(R.string.download_key),
getString(R.string.download),
resolveResourceIdFromAttr(context, R.attr.ic_file_download)));
return returnList;
}
private Context getThemeWrapperContext() {
return new ContextThemeWrapper(this,
ThemeHelper.isLightThemeSelected(this) ? R.style.LightTheme : R.style.DarkTheme);
return new ContextThemeWrapper(this, ThemeHelper.isLightThemeSelected(this)
? R.style.LightTheme : R.style.DarkTheme);
}
private void setDialogButtonsState(AlertDialog dialog, boolean state) {
private void setDialogButtonsState(final AlertDialog dialog, final boolean state) {
final Button negativeButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE);
final Button positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
if (negativeButton == null || positiveButton == null) return;
if (negativeButton == null || positiveButton == null) {
return;
}
negativeButton.setEnabled(state);
positiveButton.setEnabled(state);
@@ -374,21 +420,25 @@ public class RouterActivity extends AppCompatActivity {
}
private void handleChoice(final String selectedChoiceKey) {
final List<String> validChoicesList = Arrays.asList(getResources().getStringArray(R.array.preferred_open_action_values_list));
final List<String> validChoicesList = Arrays.asList(getResources()
.getStringArray(R.array.preferred_open_action_values_list));
if (validChoicesList.contains(selectedChoiceKey)) {
PreferenceManager.getDefaultSharedPreferences(this).edit()
.putString(getString(R.string.preferred_open_action_last_selected_key), selectedChoiceKey)
.putString(getString(
R.string.preferred_open_action_last_selected_key), selectedChoiceKey)
.apply();
}
if (selectedChoiceKey.equals(getString(R.string.popup_player_key)) && !PermissionHelper.isPopupEnabled(this)) {
if (selectedChoiceKey.equals(getString(R.string.popup_player_key))
&& !PermissionHelper.isPopupEnabled(this)) {
PermissionHelper.showPopupEnablementToast(this);
finish();
return;
}
if (selectedChoiceKey.equals(getString(R.string.download_key))) {
if (PermissionHelper.checkStoragePermissions(this, PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
if (PermissionHelper.checkStoragePermissions(this,
PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
selectionIsDownload = true;
openDownloadDialog();
}
@@ -416,7 +466,8 @@ public class RouterActivity extends AppCompatActivity {
}
final Intent intent = new Intent(this, FetcherService.class);
final Choice choice = new Choice(currentService.getServiceId(), currentLinkType, currentUrl, selectedChoiceKey);
final Choice choice = new Choice(currentService.getServiceId(), currentLinkType,
currentUrl, selectedChoiceKey);
intent.putExtra(FetcherService.KEY_CHOICE, choice);
startService(intent);
@@ -429,12 +480,11 @@ public class RouterActivity extends AppCompatActivity {
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe((@NonNull StreamInfo result) -> {
List<VideoStream> sortedVideoStreams = ListHelper.getSortedStreamVideosList(this,
result.getVideoStreams(),
result.getVideoOnlyStreams(),
false);
int selectedVideoStreamIndex = ListHelper.getDefaultResolutionIndex(this,
sortedVideoStreams);
List<VideoStream> sortedVideoStreams = ListHelper
.getSortedStreamVideosList(this, result.getVideoStreams(),
result.getVideoOnlyStreams(), false);
int selectedVideoStreamIndex = ListHelper
.getDefaultResolutionIndex(this, sortedVideoStreams);
FragmentManager fm = getSupportFragmentManager();
DownloadDialog downloadDialog = DownloadDialog.newInstance(result);
@@ -452,7 +502,9 @@ public class RouterActivity extends AppCompatActivity {
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
public void onRequestPermissionsResult(final int requestCode,
@NonNull final String[] permissions,
@NonNull final int[] grantResults) {
for (int i : grantResults) {
if (i == PackageManager.PERMISSION_DENIED) {
finish();
@@ -464,196 +516,10 @@ public class RouterActivity extends AppCompatActivity {
}
}
private static class AdapterChoiceItem {
final String description, key;
@DrawableRes
final int icon;
AdapterChoiceItem(String key, String description, int icon) {
this.description = description;
this.key = key;
this.icon = icon;
}
}
private static class Choice implements Serializable {
final int serviceId;
final String url, playerChoice;
final LinkType linkType;
Choice(int serviceId, LinkType linkType, String url, String playerChoice) {
this.serviceId = serviceId;
this.linkType = linkType;
this.url = url;
this.playerChoice = playerChoice;
}
@Override
public String toString() {
return serviceId + ":" + url + " > " + linkType + " ::: " + playerChoice;
}
}
/*//////////////////////////////////////////////////////////////////////////
// Service Fetcher
//////////////////////////////////////////////////////////////////////////*/
public static class FetcherService extends IntentService {
private static final int ID = 456;
public static final String KEY_CHOICE = "key_choice";
private Disposable fetcher;
public FetcherService() {
super(FetcherService.class.getSimpleName());
}
@Override
public void onCreate() {
super.onCreate();
startForeground(ID, createNotification().build());
}
@Override
protected void onHandleIntent(@Nullable Intent intent) {
if (intent == null) return;
final Serializable serializable = intent.getSerializableExtra(KEY_CHOICE);
if (!(serializable instanceof Choice)) return;
Choice playerChoice = (Choice) serializable;
handleChoice(playerChoice);
}
public void handleChoice(Choice choice) {
Single<? extends Info> single = null;
UserAction userAction = UserAction.SOMETHING_ELSE;
switch (choice.linkType) {
case STREAM:
single = ExtractorHelper.getStreamInfo(choice.serviceId, choice.url, false);
userAction = UserAction.REQUESTED_STREAM;
break;
case CHANNEL:
single = ExtractorHelper.getChannelInfo(choice.serviceId, choice.url, false);
userAction = UserAction.REQUESTED_CHANNEL;
break;
case PLAYLIST:
single = ExtractorHelper.getPlaylistInfo(choice.serviceId, choice.url, false);
userAction = UserAction.REQUESTED_PLAYLIST;
break;
}
if (single != null) {
final UserAction finalUserAction = userAction;
final Consumer<Info> resultHandler = getResultHandler(choice);
fetcher = single
.observeOn(AndroidSchedulers.mainThread())
.subscribe(info -> {
resultHandler.accept(info);
if (fetcher != null) fetcher.dispose();
}, throwable -> ExtractorHelper.handleGeneralException(this,
choice.serviceId, choice.url, throwable, finalUserAction, ", opened with " + choice.playerChoice));
}
}
public Consumer<Info> getResultHandler(Choice choice) {
return info -> {
final String videoPlayerKey = getString(R.string.video_player_key);
final String backgroundPlayerKey = getString(R.string.background_player_key);
final String popupPlayerKey = getString(R.string.popup_player_key);
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false);
boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false);
;
PlayQueue playQueue;
String playerChoice = choice.playerChoice;
if (info instanceof StreamInfo) {
if (playerChoice.equals(backgroundPlayerKey) && isExtAudioEnabled) {
NavigationHelper.playOnExternalAudioPlayer(this, (StreamInfo) info);
} else if (playerChoice.equals(videoPlayerKey) && isExtVideoEnabled) {
NavigationHelper.playOnExternalVideoPlayer(this, (StreamInfo) info);
} else {
playQueue = new SinglePlayQueue((StreamInfo) info);
if (playerChoice.equals(videoPlayerKey)) {
openMainPlayer(playQueue, choice);
} else if (playerChoice.equals(backgroundPlayerKey)) {
NavigationHelper.enqueueOnBackgroundPlayer(this, playQueue, true);
} else if (playerChoice.equals(popupPlayerKey)) {
NavigationHelper.enqueueOnPopupPlayer(this, playQueue, true);
}
}
}
if (info instanceof ChannelInfo || info instanceof PlaylistInfo) {
playQueue = info instanceof ChannelInfo ? new ChannelPlayQueue((ChannelInfo) info) : new PlaylistPlayQueue((PlaylistInfo) info);
if (playerChoice.equals(videoPlayerKey)) {
openMainPlayer(playQueue, choice);
} else if (playerChoice.equals(backgroundPlayerKey)) {
NavigationHelper.playOnBackgroundPlayer(this, playQueue, true);
} else if (playerChoice.equals(popupPlayerKey)) {
NavigationHelper.playOnPopupPlayer(this, playQueue, true);
}
}
};
}
private void openMainPlayer(PlayQueue playQueue, Choice choice) {
NavigationHelper.playOnMainPlayer(this, playQueue, choice.linkType,
choice.url, "", true, true);
}
@Override
public void onDestroy() {
super.onDestroy();
stopForeground(true);
if (fetcher != null) fetcher.dispose();
}
private NotificationCompat.Builder createNotification() {
return new NotificationCompat.Builder(this, getString(R.string.notification_channel_id))
.setOngoing(true)
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContentTitle(getString(R.string.preferred_player_fetcher_notification_title))
.setContentText(getString(R.string.preferred_player_fetcher_notification_message));
}
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
/**
* Removes invisible separators (\p{Z}) and punctuation characters including
* brackets (\p{P}). See http://www.regular-expressions.info/unicode.html for
* more details.
*/
private final static String REGEX_REMOVE_FROM_URL = "[\\p{Z}\\p{P}]";
private String getUrl(Intent intent) {
// first gather data and find service
String videoUrl = null;
if (intent.getData() != null) {
// this means the video was called though another app
videoUrl = intent.getData().toString();
} else if (intent.getStringExtra(Intent.EXTRA_TEXT) != null) {
//this means that vidoe was called through share menu
String extraText = intent.getStringExtra(Intent.EXTRA_TEXT);
final String[] uris = getUris(extraText);
videoUrl = uris.length > 0 ? uris[0] : null;
}
return videoUrl;
}
private String removeHeadingGibberish(final String input) {
int start = 0;
for (int i = input.indexOf("://") - 1; i >= 0; i--) {
@@ -662,9 +528,13 @@ public class RouterActivity extends AppCompatActivity {
break;
}
}
return input.substring(start, input.length());
return input.substring(start);
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
private String trim(final String input) {
if (input == null || input.length() < 1) {
return input;
@@ -674,7 +544,7 @@ public class RouterActivity extends AppCompatActivity {
output = output.substring(1);
}
while (output.length() > 0
&& output.substring(output.length() - 1, output.length()).matches(REGEX_REMOVE_FROM_URL)) {
&& output.substring(output.length() - 1).matches(REGEX_REMOVE_FROM_URL)) {
output = output.substring(0, output.length() - 1);
}
return output;
@@ -705,4 +575,200 @@ public class RouterActivity extends AppCompatActivity {
}
return result.toArray(new String[result.size()]);
}
private static class AdapterChoiceItem {
final String description;
final String key;
@DrawableRes
final int icon;
AdapterChoiceItem(final String key, final String description, final int icon) {
this.description = description;
this.key = key;
this.icon = icon;
}
}
private static class Choice implements Serializable {
final int serviceId;
final String url;
final String playerChoice;
final LinkType linkType;
Choice(final int serviceId, final LinkType linkType,
final String url, final String playerChoice) {
this.serviceId = serviceId;
this.linkType = linkType;
this.url = url;
this.playerChoice = playerChoice;
}
@Override
public String toString() {
return serviceId + ":" + url + " > " + linkType + " ::: " + playerChoice;
}
}
public static class FetcherService extends IntentService {
public static final String KEY_CHOICE = "key_choice";
private static final int ID = 456;
private Disposable fetcher;
public FetcherService() {
super(FetcherService.class.getSimpleName());
}
@Override
public void onCreate() {
super.onCreate();
startForeground(ID, createNotification().build());
}
@Override
protected void onHandleIntent(@Nullable final Intent intent) {
if (intent == null) {
return;
}
final Serializable serializable = intent.getSerializableExtra(KEY_CHOICE);
if (!(serializable instanceof Choice)) {
return;
}
Choice playerChoice = (Choice) serializable;
handleChoice(playerChoice);
}
public void handleChoice(final Choice choice) {
Single<? extends Info> single = null;
UserAction userAction = UserAction.SOMETHING_ELSE;
switch (choice.linkType) {
case STREAM:
single = ExtractorHelper.getStreamInfo(choice.serviceId, choice.url, false);
userAction = UserAction.REQUESTED_STREAM;
break;
case CHANNEL:
single = ExtractorHelper.getChannelInfo(choice.serviceId, choice.url, false);
userAction = UserAction.REQUESTED_CHANNEL;
break;
case PLAYLIST:
single = ExtractorHelper.getPlaylistInfo(choice.serviceId, choice.url, false);
userAction = UserAction.REQUESTED_PLAYLIST;
break;
}
if (single != null) {
final UserAction finalUserAction = userAction;
final Consumer<Info> resultHandler = getResultHandler(choice);
fetcher = single
.observeOn(AndroidSchedulers.mainThread())
.subscribe(info -> {
resultHandler.accept(info);
if (fetcher != null) {
fetcher.dispose();
}
}, throwable -> ExtractorHelper.handleGeneralException(this,
choice.serviceId, choice.url, throwable, finalUserAction,
", opened with " + choice.playerChoice));
}
}
public Consumer<Info> getResultHandler(final Choice choice) {
return info -> {
final String videoPlayerKey = getString(R.string.video_player_key);
final String backgroundPlayerKey = getString(R.string.background_player_key);
final String popupPlayerKey = getString(R.string.popup_player_key);
final SharedPreferences preferences = PreferenceManager
.getDefaultSharedPreferences(this);
boolean isExtVideoEnabled = preferences.getBoolean(
getString(R.string.use_external_video_player_key), false);
boolean isExtAudioEnabled = preferences.getBoolean(
getString(R.string.use_external_audio_player_key), false);
PlayQueue playQueue;
String playerChoice = choice.playerChoice;
if (info instanceof StreamInfo) {
if (playerChoice.equals(backgroundPlayerKey) && isExtAudioEnabled) {
NavigationHelper.playOnExternalAudioPlayer(this, (StreamInfo) info);
} else if (playerChoice.equals(videoPlayerKey) && isExtVideoEnabled) {
NavigationHelper.playOnExternalVideoPlayer(this, (StreamInfo) info);
} else {
playQueue = new SinglePlayQueue((StreamInfo) info);
if (playerChoice.equals(videoPlayerKey)) {
openMainPlayer(playQueue, choice);
} else if (playerChoice.equals(backgroundPlayerKey)) {
NavigationHelper.enqueueOnBackgroundPlayer(this, playQueue, true);
} else if (playerChoice.equals(popupPlayerKey)) {
NavigationHelper.enqueueOnPopupPlayer(this, playQueue, true);
}
}
}
if (info instanceof ChannelInfo || info instanceof PlaylistInfo) {
playQueue = info instanceof ChannelInfo
? new ChannelPlayQueue((ChannelInfo) info)
: new PlaylistPlayQueue((PlaylistInfo) info);
if (playerChoice.equals(videoPlayerKey)) {
openMainPlayer(playQueue, choice);
} else if (playerChoice.equals(backgroundPlayerKey)) {
NavigationHelper.playOnBackgroundPlayer(this, playQueue, true);
} else if (playerChoice.equals(popupPlayerKey)) {
NavigationHelper.playOnPopupPlayer(this, playQueue, true);
}
}
};
}
private void openMainPlayer(PlayQueue playQueue, Choice choice) {
NavigationHelper.playOnMainPlayer(this, playQueue, choice.linkType,
choice.url, "", true, true);
}
@Override
public void onDestroy() {
super.onDestroy();
stopForeground(true);
if (fetcher != null) {
fetcher.dispose();
}
}
private NotificationCompat.Builder createNotification() {
return new NotificationCompat.Builder(this, getString(R.string.notification_channel_id))
.setOngoing(true)
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContentTitle(
getString(R.string.preferred_player_fetcher_notification_title))
.setContentText(
getString(R.string.preferred_player_fetcher_notification_message));
}
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
@Nullable
private String getUrl(final Intent intent) {
String foundUrl = null;
if (intent.getData() != null) {
// Called from another app
foundUrl = intent.getData().toString();
} else if (intent.getStringExtra(Intent.EXTRA_TEXT) != null) {
// Called from the share menu
final String extraText = intent.getStringExtra(Intent.EXTRA_TEXT);
foundUrl = UrlFinder.firstUrlFromInput(extraText);
}
return foundUrl;
}
}

View File

@@ -1,48 +1,67 @@
package org.schabi.newpipe.about;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import com.google.android.material.tabs.TabLayout;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.fragment.app.FragmentStatePagerAdapter;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.google.android.material.tabs.TabLayout;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ThemeHelper;
public class AboutActivity extends AppCompatActivity {
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import static org.schabi.newpipe.util.ShareUtils.openUrlInBrowser;
public class AboutActivity extends AppCompatActivity {
/**
* List of all software components
* List of all software components.
*/
private static final SoftwareComponent[] SOFTWARE_COMPONENTS = new SoftwareComponent[]{
new SoftwareComponent("Giga Get", "2014", "Peter Cai", "https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL2),
new SoftwareComponent("NewPipe Extractor", "2017", "Christian Schabesberger", "https://github.com/TeamNewPipe/NewPipeExtractor", StandardLicenses.GPL3),
new SoftwareComponent("Jsoup", "2017", "Jonathan Hedley", "https://github.com/jhy/jsoup", StandardLicenses.MIT),
new SoftwareComponent("Rhino", "2015", "Mozilla", "https://www.mozilla.org/rhino/", StandardLicenses.MPL2),
new SoftwareComponent("ACRA", "2013", "Kevin Gaudin", "http://www.acra.ch", StandardLicenses.APACHE2),
new SoftwareComponent("Universal Image Loader", "2011 - 2015", "Sergey Tarasevich", "https://github.com/nostra13/Android-Universal-Image-Loader", StandardLicenses.APACHE2),
new SoftwareComponent("CircleImageView", "2014 - 2017", "Henning Dodenhof", "https://github.com/hdodenhof/CircleImageView", StandardLicenses.APACHE2),
new SoftwareComponent("NoNonsense-FilePicker", "2016", "Jonas Kalderstam", "https://github.com/spacecowboy/NoNonsense-FilePicker", StandardLicenses.MPL2),
new SoftwareComponent("ExoPlayer", "2014-2017", "Google Inc", "https://github.com/google/ExoPlayer", StandardLicenses.APACHE2),
new SoftwareComponent("RxAndroid", "2015", "The RxAndroid authors", "https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2),
new SoftwareComponent("RxJava", "2016-present", "RxJava Contributors", "https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2),
new SoftwareComponent("RxBinding", "2015", "Jake Wharton", "https://github.com/JakeWharton/RxBinding", StandardLicenses.APACHE2)
new SoftwareComponent("Giga Get", "2014 - 2015", "Peter Cai",
"https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL2),
new SoftwareComponent("NewPipe Extractor", "2017 - 2020", "Christian Schabesberger",
"https://github.com/TeamNewPipe/NewPipeExtractor", StandardLicenses.GPL3),
new SoftwareComponent("Jsoup", "2017", "Jonathan Hedley",
"https://github.com/jhy/jsoup", StandardLicenses.MIT),
new SoftwareComponent("Rhino", "2015", "Mozilla",
"https://www.mozilla.org/rhino/", StandardLicenses.MPL2),
new SoftwareComponent("ACRA", "2013", "Kevin Gaudin",
"http://www.acra.ch", StandardLicenses.APACHE2),
new SoftwareComponent("Universal Image Loader", "2011 - 2015", "Sergey Tarasevich",
"https://github.com/nostra13/Android-Universal-Image-Loader",
StandardLicenses.APACHE2),
new SoftwareComponent("CircleImageView", "2014 - 2020", "Henning Dodenhof",
"https://github.com/hdodenhof/CircleImageView", StandardLicenses.APACHE2),
new SoftwareComponent("NoNonsense-FilePicker", "2016", "Jonas Kalderstam",
"https://github.com/spacecowboy/NoNonsense-FilePicker", StandardLicenses.MPL2),
new SoftwareComponent("ExoPlayer", "2014 - 2020", "Google Inc",
"https://github.com/google/ExoPlayer", StandardLicenses.APACHE2),
new SoftwareComponent("RxAndroid", "2015 - 2018", "The RxAndroid authors",
"https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2),
new SoftwareComponent("RxJava", "2016 - 2020", "RxJava Contributors",
"https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2),
new SoftwareComponent("RxBinding", "2015 - 2018", "Jake Wharton",
"https://github.com/JakeWharton/RxBinding", StandardLicenses.APACHE2),
new SoftwareComponent("PrettyTime", "2012 - 2020", "Lincoln Baxter, III",
"https://github.com/ocpsoft/prettytime", StandardLicenses.APACHE2),
new SoftwareComponent("Markwon", "2017 - 2020", "Noties",
"https://github.com/noties/Markwon", StandardLicenses.APACHE2),
new SoftwareComponent("Groupie", "2016", "Lisa Wray",
"https://github.com/lisawray/groupie", StandardLicenses.MIT)
};
/**
@@ -61,9 +80,11 @@ public class AboutActivity extends AppCompatActivity {
private ViewPager mViewPager;
@Override
protected void onCreate(Bundle savedInstanceState) {
protected void onCreate(final Bundle savedInstanceState) {
assureCorrectAppLanguage(this);
super.onCreate(savedInstanceState);
ThemeHelper.setTheme(this);
this.setTitle(getString(R.string.title_activity_about));
setContentView(R.layout.activity_about);
@@ -82,28 +103,14 @@ public class AboutActivity extends AppCompatActivity {
tabLayout.setupWithViewPager(mViewPager);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_about, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
public boolean onOptionsItemSelected(final MenuItem item) {
int id = item.getItemId();
switch (id) {
case android.R.id.home:
finish();
return true;
case R.id.action_settings:
NavigationHelper.openSettings(this);
return true;
case R.id.action_show_downloads:
return NavigationHelper.openDownloads(this);
}
return super.onOptionsItemSelected(item);
@@ -113,21 +120,20 @@ public class AboutActivity extends AppCompatActivity {
* A placeholder fragment containing a simple view.
*/
public static class AboutFragment extends Fragment {
public AboutFragment() {
}
public AboutFragment() { }
/**
* Returns a new instance of this fragment for the given section
* number.
* Created a new instance of this fragment for the given section number.
*
* @return New instance of {@link AboutFragment}
*/
public static AboutFragment newInstance() {
return new AboutFragment();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
final Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_about, container, false);
Context context = this.getContext();
@@ -135,40 +141,37 @@ public class AboutActivity extends AppCompatActivity {
version.setText(BuildConfig.VERSION_NAME);
View githubLink = rootView.findViewById(R.id.github_link);
githubLink.setOnClickListener(nv -> openWebsite(context.getString(R.string.github_url), context));
githubLink.setOnClickListener(nv ->
openUrlInBrowser(context, context.getString(R.string.github_url)));
View donationLink = rootView.findViewById(R.id.donation_link);
donationLink.setOnClickListener(v -> openWebsite(context.getString(R.string.donation_url), context));
donationLink.setOnClickListener(v ->
openUrlInBrowser(context, context.getString(R.string.donation_url)));
View websiteLink = rootView.findViewById(R.id.website_link);
websiteLink.setOnClickListener(nv -> openWebsite(context.getString(R.string.website_url), context));
websiteLink.setOnClickListener(nv ->
openUrlInBrowser(context, context.getString(R.string.website_url)));
View privacyPolicyLink = rootView.findViewById(R.id.privacy_policy_link);
privacyPolicyLink.setOnClickListener(v -> openWebsite(context.getString(R.string.privacy_policy_url), context));
privacyPolicyLink.setOnClickListener(v ->
openUrlInBrowser(context, context.getString(R.string.privacy_policy_url)));
return rootView;
}
private void openWebsite(String url, Context context) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
context.startActivity(intent);
}
}
/**
* A {@link FragmentPagerAdapter} that returns a fragment corresponding to
* one of the sections/tabs/pages.
*/
public class SectionsPagerAdapter extends FragmentPagerAdapter {
public SectionsPagerAdapter(FragmentManager fm) {
public SectionsPagerAdapter(final FragmentManager fm) {
super(fm);
}
@Override
public Fragment getItem(int position) {
public Fragment getItem(final int position) {
switch (position) {
case 0:
return AboutFragment.newInstance();
@@ -185,7 +188,7 @@ public class AboutActivity extends AppCompatActivity {
}
@Override
public CharSequence getPageTitle(int position) {
public CharSequence getPageTitle(final int position) {
switch (position) {
case 0:
return getString(R.string.tab_about);

View File

@@ -5,18 +5,17 @@ import android.os.Parcel;
import android.os.Parcelable;
/**
* A software license
* Class for storing information about a software license.
*/
public class License implements Parcelable {
public static final Creator<License> CREATOR = new Creator<License>() {
@Override
public License createFromParcel(Parcel source) {
public License createFromParcel(final Parcel source) {
return new License(source);
}
@Override
public License[] newArray(int size) {
public License[] newArray(final int size) {
return new License[size];
}
};
@@ -24,16 +23,22 @@ public class License implements Parcelable {
private final String name;
private String filename;
public License(String name, String abbreviation, 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");
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(Parcel in) {
protected License(final Parcel in) {
this.filename = in.readString();
this.abbreviation = in.readString();
this.name = in.readString();
@@ -50,7 +55,7 @@ public class License implements Parcelable {
public String getAbbreviation() {
return abbreviation;
}
public String getFilename() {
return filename;
}
@@ -61,7 +66,7 @@ public class License implements Parcelable {
}
@Override
public void writeToParcel(Parcel dest, int flags) {
public void writeToParcel(final Parcel dest, final int flags) {
dest.writeString(this.filename);
dest.writeString(this.abbreviation);
dest.writeString(this.name);

View File

@@ -2,29 +2,33 @@ package org.schabi.newpipe.about;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import android.view.*;
import android.widget.TextView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.ShareUtils;
import java.util.Arrays;
import java.util.Comparator;
/**
* Fragment containing the software licenses
* Fragment containing the software licenses.
*/
public class LicenseFragment extends Fragment {
private static final String ARG_COMPONENTS = "components";
private SoftwareComponent[] softwareComponents;
private SoftwareComponent mComponentForContextMenu;
private SoftwareComponent componentForContextMenu;
public static LicenseFragment newInstance(SoftwareComponent[] softwareComponents) {
if(softwareComponents == null) {
public static LicenseFragment newInstance(final SoftwareComponent[] softwareComponents) {
if (softwareComponents == null) {
throw new NullPointerException("softwareComponents is null");
}
LicenseFragment fragment = new LicenseFragment();
@@ -35,57 +39,50 @@ public class LicenseFragment extends Fragment {
}
/**
* Shows a popup containing the license
* Shows a popup containing the license.
*
* @param context the context to use
* @param license the license to show
*/
public static void showLicense(Context context, License license) {
private static void showLicense(final Context context, final License license) {
new LicenseFragmentHelper((Activity) context).execute(license);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
softwareComponents = (SoftwareComponent[]) getArguments().getParcelableArray(ARG_COMPONENTS);
softwareComponents = (SoftwareComponent[]) getArguments()
.getParcelableArray(ARG_COMPONENTS);
// Sort components by name
Arrays.sort(softwareComponents, new Comparator<SoftwareComponent>() {
@Override
public int compare(SoftwareComponent o1, SoftwareComponent o2) {
return o1.getName().compareTo(o2.getName());
}
});
Arrays.sort(softwareComponents, (o1, o2) -> o1.getName().compareTo(o2.getName()));
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_licenses, container, false);
ViewGroup softwareComponentsView = rootView.findViewById(R.id.software_components);
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
final View rootView = inflater.inflate(R.layout.fragment_licenses, container, false);
final ViewGroup softwareComponentsView = rootView.findViewById(R.id.software_components);
View licenseLink = rootView.findViewById(R.id.app_read_license);
licenseLink.setOnClickListener(new OnReadFullLicenseClickListener());
final View licenseLink = rootView.findViewById(R.id.app_read_license);
licenseLink.setOnClickListener(v ->
showLicense(getActivity(), StandardLicenses.GPL3));
for (final SoftwareComponent component : softwareComponents) {
View componentView = inflater.inflate(R.layout.item_software_component, container, false);
TextView softwareName = componentView.findViewById(R.id.name);
TextView copyright = componentView.findViewById(R.id.copyright);
final View componentView = inflater
.inflate(R.layout.item_software_component, container, false);
final TextView softwareName = componentView.findViewById(R.id.name);
final TextView copyright = componentView.findViewById(R.id.copyright);
softwareName.setText(component.getName());
copyright.setText(getContext().getString(R.string.copyright,
copyright.setText(getString(R.string.copyright,
component.getYears(),
component.getCopyrightOwner(),
component.getLicense().getAbbreviation()));
componentView.setTag(component);
componentView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Context context = v.getContext();
if (context != null) {
showLicense(context, component.getLicense());
}
}
});
componentView.setOnClickListener(v ->
showLicense(getActivity(), component.getLicense()));
softwareComponentsView.addView(componentView);
registerForContextMenu(componentView);
}
@@ -93,41 +90,30 @@ public class LicenseFragment extends Fragment {
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
MenuInflater inflater = getActivity().getMenuInflater();
SoftwareComponent component = (SoftwareComponent) v.getTag();
public void onCreateContextMenu(final ContextMenu menu, final View v,
final ContextMenu.ContextMenuInfo menuInfo) {
final MenuInflater inflater = getActivity().getMenuInflater();
final SoftwareComponent component = (SoftwareComponent) v.getTag();
menu.setHeaderTitle(component.getName());
inflater.inflate(R.menu.software_component, menu);
super.onCreateContextMenu(menu, v, menuInfo);
mComponentForContextMenu = (SoftwareComponent) v.getTag();
componentForContextMenu = (SoftwareComponent) v.getTag();
}
@Override
public boolean onContextItemSelected(MenuItem item) {
public boolean onContextItemSelected(final MenuItem item) {
// item.getMenuInfo() is null so we use the tag of the view
final SoftwareComponent component = mComponentForContextMenu;
final SoftwareComponent component = componentForContextMenu;
if (component == null) {
return false;
}
switch (item.getItemId()) {
case R.id.action_website:
openWebsite(component.getLink());
ShareUtils.openUrlInBrowser(getActivity(), component.getLink());
return true;
case R.id.action_show_license:
showLicense(getContext(), component.getLicense());
showLicense(getActivity(), component.getLicense());
}
return false;
}
private void openWebsite(String componentLink) {
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(componentLink));
startActivity(browserIntent);
}
private static class OnReadFullLicenseClickListener implements View.OnClickListener {
@Override
public void onClick(View v) {
LicenseFragment.showLicense(v.getContext(), StandardLicenses.GPL3);
}
}
}

View File

@@ -2,30 +2,95 @@ package org.schabi.newpipe.about;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.os.AsyncTask;
import android.util.Base64;
import android.webkit.WebView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import android.webkit.WebView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.ThemeHelper;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.ref.WeakReference;
import java.nio.charset.StandardCharsets;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
public class LicenseFragmentHelper extends AsyncTask<Object, Void, Integer> {
final WeakReference<Activity> weakReference;
private final WeakReference<Activity> weakReference;
private License license;
public LicenseFragmentHelper(@Nullable Activity activity) {
public LicenseFragmentHelper(@Nullable final Activity activity) {
weakReference = new WeakReference<>(activity);
}
/**
* @param context the context to use
* @param license the license
* @return String which contains a HTML formatted license page
* styled according to the context's theme
*/
private static String getFormattedLicense(@NonNull final Context context,
@NonNull final License license) {
final StringBuilder licenseContent = new StringBuilder();
final String webViewData;
try {
final BufferedReader in = new BufferedReader(new InputStreamReader(
context.getAssets().open(license.getFilename()), StandardCharsets.UTF_8));
String str;
while ((str = in.readLine()) != null) {
licenseContent.append(str);
}
in.close();
// split the HTML file and insert the stylesheet into the HEAD of the file
webViewData = licenseContent.toString().replace("</head>",
"<style>" + getLicenseStylesheet(context) + "</style></head>");
} catch (IOException e) {
throw new IllegalArgumentException(
"Could not get license file: " + license.getFilename(), e);
}
return webViewData;
}
/**
* @param context
* @return String which is a CSS stylesheet according to the context's theme
*/
private static String getLicenseStylesheet(final Context context) {
final boolean isLightTheme = ThemeHelper.isLightThemeSelected(context);
return "body{padding:12px 15px;margin:0;"
+ "background:#" + getHexRGBColor(context, isLightTheme
? R.color.light_license_background_color
: R.color.dark_license_background_color) + ";"
+ "color:#" + getHexRGBColor(context, isLightTheme
? R.color.light_license_text_color
: R.color.dark_license_text_color) + "}"
+ "a[href]{color:#" + getHexRGBColor(context, isLightTheme
? R.color.light_youtube_primary_color
: R.color.dark_youtube_primary_color) + "}"
+ "pre{white-space:pre-wrap}";
}
/**
* Cast R.color to a hexadecimal color value.
*
* @param context the context to use
* @param color the color number from R.color
* @return a six characters long String with hexadecimal RGB values
*/
private static String getHexRGBColor(final Context context, final int color) {
return context.getResources().getString(color).substring(3);
}
@Nullable
private Activity getActivity() {
Activity activity = weakReference.get();
final Activity activity = weakReference.get();
if (activity != null && activity.isFinishing()) {
return null;
@@ -35,99 +100,29 @@ public class LicenseFragmentHelper extends AsyncTask<Object, Void, Integer> {
}
@Override
protected Integer doInBackground(Object... objects) {
protected Integer doInBackground(final Object... objects) {
license = (License) objects[0];
return 1;
}
@Override
protected void onPostExecute(Integer result) {
Activity activity = getActivity();
protected void onPostExecute(final Integer result) {
final Activity activity = getActivity();
if (activity == null) {
return;
}
String webViewData = getFormattedLicense(activity, license);
AlertDialog.Builder alert = new AlertDialog.Builder(activity);
final String webViewData = Base64.encodeToString(getFormattedLicense(activity, license)
.getBytes(StandardCharsets.UTF_8), Base64.NO_PADDING);
final WebView webView = new WebView(activity);
webView.loadData(webViewData, "text/html; charset=UTF-8", "base64");
final AlertDialog.Builder alert = new AlertDialog.Builder(activity);
alert.setTitle(license.getName());
WebView wv = new WebView(activity);
wv.loadData(webViewData, "text/html; charset=UTF-8", null);
alert.setView(wv);
alert.setNegativeButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
alert.setView(webView);
assureCorrectAppLanguage(activity);
alert.setNegativeButton(activity.getString(R.string.finish),
(dialog, which) -> dialog.dismiss());
alert.show();
}
/**
* @param context the context to use
* @param license the license
* @return String which contains a HTML formatted license page styled according to the context's theme
*/
public static String getFormattedLicense(Context context, License license) {
if(context == null) {
throw new NullPointerException("context is null");
}
if(license == null) {
throw new NullPointerException("license is null");
}
StringBuilder licenseContent = new StringBuilder();
String webViewData;
try {
BufferedReader in = new BufferedReader(new InputStreamReader(context.getAssets().open(license.getFilename()), "UTF-8"));
String str;
while ((str = in.readLine()) != null) {
licenseContent.append(str);
}
in.close();
// split the HTML file and insert the stylesheet into the HEAD of the file
String[] insert = licenseContent.toString().split("</head>");
webViewData = insert[0] + "<style type=\"text/css\">"
+ getLicenseStylesheet(context) + "</style></head>"
+ insert[1];
} catch (Exception e) {
throw new NullPointerException("could not get license file:" + getLicenseStylesheet(context));
}
return webViewData;
}
/**
*
* @param context
* @return String which is a CSS stylesheet according to the context's theme
*/
public static String getLicenseStylesheet(Context context) {
boolean isLightTheme = ThemeHelper.isLightThemeSelected(context);
return "body{padding:12px 15px;margin:0;background:#"
+ getHexRGBColor(context, isLightTheme
? R.color.light_license_background_color
: R.color.dark_license_background_color)
+ ";color:#"
+ getHexRGBColor(context, isLightTheme
? R.color.light_license_text_color
: R.color.dark_license_text_color) + ";}"
+ "a[href]{color:#"
+ getHexRGBColor(context, isLightTheme
? R.color.light_youtube_primary_color
: R.color.dark_youtube_primary_color) + ";}"
+ "pre{white-space: pre-wrap;}";
}
/**
* Cast R.color to a hexadecimal color value
* @param context the context to use
* @param color the color number from R.color
* @return a six characters long String with hexadecimal RGB values
*/
public static String getHexRGBColor(Context context, int color) {
return context.getResources().getString(color).substring(3);
}
}

View File

@@ -4,19 +4,44 @@ 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(Parcel source) {
public SoftwareComponent createFromParcel(final Parcel source) {
return new SoftwareComponent(source);
}
@Override
public SoftwareComponent[] newArray(int size) {
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;
}
@@ -37,31 +62,6 @@ public class SoftwareComponent implements Parcelable {
return version;
}
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(String name, String years, String copyrightOwner, String link, License license) {
this.name = name;
this.years = years;
this.copyrightOwner = copyrightOwner;
this.link = link;
this.license = license;
this.version = null;
}
protected SoftwareComponent(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 License getLicense() {
return license;
}
@@ -72,7 +72,7 @@ public class SoftwareComponent implements Parcelable {
}
@Override
public void writeToParcel(Parcel dest, int flags) {
public void writeToParcel(final Parcel dest, final int flags) {
dest.writeString(name);
dest.writeParcelable(license, flags);
dest.writeString(copyrightOwner);

View File

@@ -1,12 +1,19 @@
package org.schabi.newpipe.about;
/**
* Standard software licenses
* Class containing information about standard software licenses.
*/
public final class StandardLicenses {
public static final License GPL2 = new License("GNU General Public License, Version 2.0", "GPLv2", "gpl_2.html");
public static final License GPL3 = new License("GNU General Public License, Version 3.0", "GPLv3", "gpl_3.html");
public static final License APACHE2 = new License("Apache License, Version 2.0", "ALv2", "apache2.html");
public static final License MPL2 = new License("Mozilla Public License, Version 2.0", "MPL 2.0", "mpl2.html");
public static final License MIT = new License("MIT License", "MIT", "mit.html");
public static final License GPL2
= new License("GNU General Public License, Version 2.0", "GPLv2", "gpl_2.html");
public static final License GPL3
= new License("GNU General Public License, Version 3.0", "GPLv3", "gpl_3.html");
public static final License APACHE2
= new License("Apache License, Version 2.0", "ALv2", "apache2.html");
public static final License MPL2
= new License("Mozilla Public License, Version 2.0", "MPL 2.0", "mpl2.html");
public static final License MIT
= new License("MIT License", "MIT", "mit.html");
private StandardLicenses() { }
}

View File

@@ -4,6 +4,12 @@ import androidx.room.Database;
import androidx.room.RoomDatabase;
import androidx.room.TypeConverters;
import org.schabi.newpipe.database.feed.dao.FeedDAO;
import org.schabi.newpipe.database.feed.dao.FeedGroupDAO;
import org.schabi.newpipe.database.feed.model.FeedEntity;
import org.schabi.newpipe.database.feed.model.FeedGroupEntity;
import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity;
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity;
import org.schabi.newpipe.database.history.dao.SearchHistoryDAO;
import org.schabi.newpipe.database.history.dao.StreamHistoryDAO;
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
@@ -21,24 +27,22 @@ import org.schabi.newpipe.database.stream.model.StreamStateEntity;
import org.schabi.newpipe.database.subscription.SubscriptionDAO;
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import static org.schabi.newpipe.database.Migrations.DB_VER_12_0;
import static org.schabi.newpipe.database.Migrations.DB_VER_3;
@TypeConverters({Converters.class})
@Database(
entities = {
SubscriptionEntity.class, SearchHistoryEntry.class,
StreamEntity.class, StreamHistoryEntity.class, StreamStateEntity.class,
PlaylistEntity.class, PlaylistStreamEntity.class, PlaylistRemoteEntity.class
PlaylistEntity.class, PlaylistStreamEntity.class, PlaylistRemoteEntity.class,
FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class,
FeedLastUpdatedEntity.class
},
version = DB_VER_12_0,
exportSchema = false
version = DB_VER_3
)
public abstract class AppDatabase extends RoomDatabase {
public static final String DATABASE_NAME = "newpipe.db";
public abstract SubscriptionDAO subscriptionDAO();
public abstract SearchHistoryDAO searchHistoryDAO();
public abstract StreamDAO streamDAO();
@@ -52,4 +56,10 @@ public abstract class AppDatabase extends RoomDatabase {
public abstract PlaylistStreamDAO playlistStreamDAO();
public abstract PlaylistRemoteDAO playlistRemoteDAO();
public abstract FeedDAO feedDAO();
public abstract FeedGroupDAO feedGroupDAO();
public abstract SubscriptionDAO subscriptionDAO();
}

View File

@@ -15,13 +15,13 @@ import io.reactivex.Flowable;
public interface BasicDAO<Entity> {
/* Inserts */
@Insert(onConflict = OnConflictStrategy.FAIL)
long insert(final Entity entity);
long insert(Entity entity);
@Insert(onConflict = OnConflictStrategy.FAIL)
List<Long> insertAll(final Entity... entities);
List<Long> insertAll(Entity... entities);
@Insert(onConflict = OnConflictStrategy.FAIL)
List<Long> insertAll(final Collection<Entity> entities);
List<Long> insertAll(Collection<Entity> entities);
/* Searches */
Flowable<List<Entity>> getAll();
@@ -30,17 +30,17 @@ public interface BasicDAO<Entity> {
/* Deletes */
@Delete
void delete(final Entity entity);
void delete(Entity entity);
@Delete
int delete(final Collection<Entity> entities);
int delete(Collection<Entity> entities);
int deleteAll();
/* Updates */
@Update
int update(final Entity entity);
int update(Entity entity);
@Update
void update(final Collection<Entity> entities);
void update(Collection<Entity> entities);
}

View File

@@ -3,38 +3,58 @@ package org.schabi.newpipe.database;
import androidx.room.TypeConverter;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.local.subscription.FeedGroupIcon;
import java.util.Date;
public class Converters {
public final class Converters {
private Converters() { }
/**
* Convert a long value to a date
* Convert a long value to a date.
*
* @param value the long value
* @return the date
*/
@TypeConverter
public static Date fromTimestamp(Long value) {
public static Date fromTimestamp(final Long value) {
return value == null ? null : new Date(value);
}
/**
* Convert a date to a long value
* Convert a date to a long value.
*
* @param date the date
* @return the long value
*/
@TypeConverter
public static Long dateToTimestamp(Date date) {
public static Long dateToTimestamp(final Date date) {
return date == null ? null : date.getTime();
}
@TypeConverter
public static StreamType streamTypeOf(String value) {
public static StreamType streamTypeOf(final String value) {
return StreamType.valueOf(value);
}
@TypeConverter
public static String stringOf(StreamType streamType) {
public static String stringOf(final StreamType streamType) {
return streamType.name();
}
@TypeConverter
public static Integer integerOf(final FeedGroupIcon feedGroupIcon) {
return feedGroupIcon.getId();
}
@TypeConverter
public static FeedGroupIcon feedGroupIconOf(final Integer id) {
for (FeedGroupIcon icon : FeedGroupIcon.values()) {
if (icon.getId() == id) {
return icon;
}
}
throw new IllegalArgumentException("There's no feed group icon with the id \"" + id + "\"");
}
}

View File

@@ -1,6 +1,8 @@
package org.schabi.newpipe.database;
public interface LocalItem {
LocalItemType getLocalItemType();
enum LocalItemType {
PLAYLIST_LOCAL_ITEM,
PLAYLIST_REMOTE_ITEM,
@@ -8,6 +10,4 @@ public interface LocalItem {
PLAYLIST_STREAM_ITEM,
STATISTIC_STREAM_ITEM,
}
LocalItemType getLocalItemType();
}

View File

@@ -1,74 +1,164 @@
package org.schabi.newpipe.database;
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.room.migration.Migration;
import androidx.annotation.NonNull;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.room.migration.Migration;
import androidx.sqlite.db.SupportSQLiteDatabase;
import org.schabi.newpipe.BuildConfig;
public class Migrations {
public final class Migrations {
public static final int DB_VER_1 = 1;
public static final int DB_VER_2 = 2;
public static final int DB_VER_3 = 3;
public static final int DB_VER_11_0 = 1;
public static final int DB_VER_12_0 = 2;
public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release");
private static final String TAG = Migrations.class.getName();
public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release");
public static final Migration MIGRATION_11_12 = new Migration(DB_VER_11_0, DB_VER_12_0) {
public static final Migration MIGRATION_1_2 = new Migration(DB_VER_1, DB_VER_2) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
if(DEBUG) {
public void migrate(@NonNull final SupportSQLiteDatabase database) {
if (DEBUG) {
Log.d(TAG, "Start migrating database");
}
/*
* Unfortunately these queries must be hardcoded due to the possibility of
* schema and names changing at a later date, thus invalidating the older migration
* scripts if they are not hardcoded.
* */
* Unfortunately these queries must be hardcoded due to the possibility of
* schema and names changing at a later date, thus invalidating the older migration
* scripts if they are not hardcoded.
* */
// Not much we can do about this, since room doesn't create tables before migration.
// It's either this or blasting the entire database anew.
database.execSQL("CREATE INDEX `index_search_history_search` ON `search_history` (`search`)");
database.execSQL("CREATE TABLE IF NOT EXISTS `streams` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, `stream_type` TEXT, `duration` INTEGER, `uploader` TEXT, `thumbnail_url` TEXT)");
database.execSQL("CREATE UNIQUE INDEX `index_streams_service_id_url` ON `streams` (`service_id`, `url`)");
database.execSQL("CREATE TABLE IF NOT EXISTS `stream_history` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, `repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )");
database.execSQL("CREATE INDEX `index_stream_history_stream_id` ON `stream_history` (`stream_id`)");
database.execSQL("CREATE TABLE IF NOT EXISTS `stream_state` (`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )");
database.execSQL("CREATE TABLE IF NOT EXISTS `playlists` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `thumbnail_url` TEXT)");
database.execSQL("CREATE INDEX `index_search_history_search` "
+ "ON `search_history` (`search`)");
database.execSQL("CREATE TABLE IF NOT EXISTS `streams` "
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
+ "`service_id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, "
+ "`stream_type` TEXT, `duration` INTEGER, `uploader` TEXT, "
+ "`thumbnail_url` TEXT)");
database.execSQL("CREATE UNIQUE INDEX `index_streams_service_id_url` "
+ "ON `streams` (`service_id`, `url`)");
database.execSQL("CREATE TABLE IF NOT EXISTS `stream_history` "
+ "(`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, "
+ "`repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), "
+ "FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) "
+ "ON UPDATE CASCADE ON DELETE CASCADE )");
database.execSQL("CREATE INDEX `index_stream_history_stream_id` "
+ "ON `stream_history` (`stream_id`)");
database.execSQL("CREATE TABLE IF NOT EXISTS `stream_state` "
+ "(`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, "
+ "PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) "
+ "REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )");
database.execSQL("CREATE TABLE IF NOT EXISTS `playlists` "
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
+ "`name` TEXT, `thumbnail_url` TEXT)");
database.execSQL("CREATE INDEX `index_playlists_name` ON `playlists` (`name`)");
database.execSQL("CREATE TABLE IF NOT EXISTS `playlist_stream_join` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)");
database.execSQL("CREATE UNIQUE INDEX `index_playlist_stream_join_playlist_id_join_index` ON `playlist_stream_join` (`playlist_id`, `join_index`)");
database.execSQL("CREATE INDEX `index_playlist_stream_join_stream_id` ON `playlist_stream_join` (`stream_id`)");
database.execSQL("CREATE TABLE IF NOT EXISTS `remote_playlists` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)");
database.execSQL("CREATE INDEX `index_remote_playlists_name` ON `remote_playlists` (`name`)");
database.execSQL("CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` ON `remote_playlists` (`service_id`, `url`)");
database.execSQL("CREATE TABLE IF NOT EXISTS `playlist_stream_join` "
+ "(`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, "
+ "`join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), "
+ "FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) "
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, "
+ "FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) "
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)");
database.execSQL("CREATE UNIQUE INDEX "
+ "`index_playlist_stream_join_playlist_id_join_index` "
+ "ON `playlist_stream_join` (`playlist_id`, `join_index`)");
database.execSQL("CREATE INDEX `index_playlist_stream_join_stream_id` "
+ "ON `playlist_stream_join` (`stream_id`)");
database.execSQL("CREATE TABLE IF NOT EXISTS `remote_playlists` "
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
+ "`service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, "
+ "`thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)");
database.execSQL("CREATE INDEX `index_remote_playlists_name` "
+ "ON `remote_playlists` (`name`)");
database.execSQL("CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` "
+ "ON `remote_playlists` (`service_id`, `url`)");
// Populate streams table with existing entries in watch history
// Latest data first, thus ignoring older entries with the same indices
database.execSQL("INSERT OR IGNORE INTO streams (service_id, url, title, " +
"stream_type, duration, uploader, thumbnail_url) " +
database.execSQL("INSERT OR IGNORE INTO streams (service_id, url, title, "
+ "stream_type, duration, uploader, thumbnail_url) "
"SELECT service_id, url, title, 'VIDEO_STREAM', duration, " +
"uploader, thumbnail_url " +
+ "SELECT service_id, url, title, 'VIDEO_STREAM', duration, "
+ "uploader, thumbnail_url "
"FROM watch_history " +
"ORDER BY creation_date DESC");
+ "FROM watch_history "
+ "ORDER BY creation_date DESC");
// Once the streams have PKs, join them with the normalized history table
// and populate it with the remaining data from watch history
database.execSQL("INSERT INTO stream_history (stream_id, access_date, repeat_count)" +
"SELECT uid, creation_date, 1 " +
"FROM watch_history INNER JOIN streams " +
"ON watch_history.service_id == streams.service_id " +
"AND watch_history.url == streams.url " +
"ORDER BY creation_date DESC");
database.execSQL("INSERT INTO stream_history (stream_id, access_date, repeat_count)"
+ "SELECT uid, creation_date, 1 "
+ "FROM watch_history INNER JOIN streams "
+ "ON watch_history.service_id == streams.service_id "
+ "AND watch_history.url == streams.url "
+ "ORDER BY creation_date DESC");
database.execSQL("DROP TABLE IF EXISTS watch_history");
if(DEBUG) {
if (DEBUG) {
Log.d(TAG, "Stop migrating database");
}
}
};
public static final Migration MIGRATION_2_3 = new Migration(DB_VER_2, DB_VER_3) {
@Override
public void migrate(@NonNull final SupportSQLiteDatabase database) {
// Add NOT NULLs and new fields
database.execSQL("CREATE TABLE IF NOT EXISTS streams_new "
+ "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
+ "service_id INTEGER NOT NULL, url TEXT NOT NULL, title TEXT NOT NULL, "
+ "stream_type TEXT NOT NULL, duration INTEGER NOT NULL, "
+ "uploader TEXT NOT NULL, thumbnail_url TEXT, view_count INTEGER, "
+ "textual_upload_date TEXT, upload_date INTEGER, "
+ "is_upload_date_approximation INTEGER)");
database.execSQL("INSERT INTO streams_new (uid, service_id, url, title, stream_type, "
+ "duration, uploader, thumbnail_url, view_count, textual_upload_date, "
+ "upload_date, is_upload_date_approximation) "
+ "SELECT uid, service_id, url, ifnull(title, ''), "
+ "ifnull(stream_type, 'VIDEO_STREAM'), ifnull(duration, 0), "
+ "ifnull(uploader, ''), ifnull(thumbnail_url, ''), NULL, NULL, NULL, NULL "
+ "FROM streams WHERE url IS NOT NULL");
database.execSQL("DROP TABLE streams");
database.execSQL("ALTER TABLE streams_new RENAME TO streams");
database.execSQL("CREATE UNIQUE INDEX index_streams_service_id_url "
+ "ON streams (service_id, url)");
// Tables for feed feature
database.execSQL("CREATE TABLE IF NOT EXISTS feed "
+ "(stream_id INTEGER NOT NULL, subscription_id INTEGER NOT NULL, "
+ "PRIMARY KEY(stream_id, subscription_id), "
+ "FOREIGN KEY(stream_id) REFERENCES streams(uid) "
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, "
+ "FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) "
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)");
database.execSQL("CREATE INDEX index_feed_subscription_id ON feed (subscription_id)");
database.execSQL("CREATE TABLE IF NOT EXISTS feed_group "
+ "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name TEXT NOT NULL, "
+ "icon_id INTEGER NOT NULL, sort_order INTEGER NOT NULL)");
database.execSQL("CREATE INDEX index_feed_group_sort_order ON feed_group (sort_order)");
database.execSQL("CREATE TABLE IF NOT EXISTS feed_group_subscription_join "
+ "(group_id INTEGER NOT NULL, subscription_id INTEGER NOT NULL, "
+ "PRIMARY KEY(group_id, subscription_id), "
+ "FOREIGN KEY(group_id) REFERENCES feed_group(uid) "
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, "
+ "FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) "
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)");
database.execSQL("CREATE INDEX index_feed_group_subscription_join_subscription_id "
+ "ON feed_group_subscription_join (subscription_id)");
database.execSQL("CREATE TABLE IF NOT EXISTS feed_last_updated "
+ "(subscription_id INTEGER NOT NULL, last_updated INTEGER, "
+ "PRIMARY KEY(subscription_id), "
+ "FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) "
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)");
}
};
private Migrations() { }
}

View File

@@ -0,0 +1,152 @@
package org.schabi.newpipe.database.feed.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import io.reactivex.Flowable
import java.util.Date
import org.schabi.newpipe.database.feed.model.FeedEntity
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.subscription.SubscriptionEntity
@Dao
abstract class FeedDAO {
@Query("DELETE FROM feed")
abstract fun deleteAll(): Int
@Query("""
SELECT s.* FROM streams s
INNER JOIN feed f
ON s.uid = f.stream_id
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
LIMIT 500
""")
abstract fun getAllStreams(): Flowable<List<StreamEntity>>
@Query("""
SELECT s.* FROM streams s
INNER JOIN feed f
ON s.uid = f.stream_id
INNER JOIN feed_group_subscription_join fgs
ON fgs.subscription_id = f.subscription_id
INNER JOIN feed_group fg
ON fg.uid = fgs.group_id
WHERE fgs.group_id = :groupId
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
LIMIT 500
""")
abstract fun getAllStreamsFromGroup(groupId: Long): Flowable<List<StreamEntity>>
@Query("""
DELETE FROM feed WHERE
feed.stream_id IN (
SELECT s.uid FROM streams s
INNER JOIN feed f
ON s.uid = f.stream_id
WHERE s.upload_date < :date
)
""")
abstract fun unlinkStreamsOlderThan(date: Date)
@Query("""
DELETE FROM feed
WHERE feed.subscription_id = :subscriptionId
AND feed.stream_id IN (
SELECT s.uid FROM streams s
INNER JOIN feed f
ON s.uid = f.stream_id
WHERE s.stream_type = "LIVE_STREAM" OR s.stream_type = "AUDIO_LIVE_STREAM"
)
""")
abstract fun unlinkOldLivestreams(subscriptionId: Long)
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract fun insert(feedEntity: FeedEntity)
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract fun insertAll(entities: List<FeedEntity>): List<Long>
@Insert(onConflict = OnConflictStrategy.IGNORE)
internal abstract fun insertLastUpdated(lastUpdatedEntity: FeedLastUpdatedEntity): Long
@Update(onConflict = OnConflictStrategy.IGNORE)
internal abstract fun updateLastUpdated(lastUpdatedEntity: FeedLastUpdatedEntity)
@Transaction
open fun setLastUpdatedForSubscription(lastUpdatedEntity: FeedLastUpdatedEntity) {
val id = insertLastUpdated(lastUpdatedEntity)
if (id == -1L) {
updateLastUpdated(lastUpdatedEntity)
}
}
@Query("""
SELECT MIN(lu.last_updated) FROM feed_last_updated lu
INNER JOIN feed_group_subscription_join fgs
ON fgs.subscription_id = lu.subscription_id AND fgs.group_id = :groupId
""")
abstract fun oldestSubscriptionUpdate(groupId: Long): Flowable<List<Date>>
@Query("SELECT MIN(last_updated) FROM feed_last_updated")
abstract fun oldestSubscriptionUpdateFromAll(): Flowable<List<Date>>
@Query("SELECT COUNT(*) FROM feed_last_updated WHERE last_updated IS NULL")
abstract fun notLoadedCount(): Flowable<Long>
@Query("""
SELECT COUNT(*) FROM subscriptions s
INNER JOIN feed_group_subscription_join fgs
ON s.uid = fgs.subscription_id AND fgs.group_id = :groupId
LEFT JOIN feed_last_updated lu
ON s.uid = lu.subscription_id
WHERE lu.last_updated IS NULL
""")
abstract fun notLoadedCountForGroup(groupId: Long): Flowable<Long>
@Query("""
SELECT s.* FROM subscriptions s
LEFT JOIN feed_last_updated lu
ON s.uid = lu.subscription_id
WHERE lu.last_updated IS NULL OR lu.last_updated < :outdatedThreshold
""")
abstract fun getAllOutdated(outdatedThreshold: Date): Flowable<List<SubscriptionEntity>>
@Query("""
SELECT s.* FROM subscriptions s
INNER JOIN feed_group_subscription_join fgs
ON s.uid = fgs.subscription_id AND fgs.group_id = :groupId
LEFT JOIN feed_last_updated lu
ON s.uid = lu.subscription_id
WHERE lu.last_updated IS NULL OR lu.last_updated < :outdatedThreshold
""")
abstract fun getAllOutdatedForGroup(groupId: Long, outdatedThreshold: Date): Flowable<List<SubscriptionEntity>>
}

View File

@@ -0,0 +1,67 @@
package org.schabi.newpipe.database.feed.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import io.reactivex.Flowable
import io.reactivex.Maybe
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity
@Dao
abstract class FeedGroupDAO {
@Query("SELECT * FROM feed_group ORDER BY sort_order ASC")
abstract fun getAll(): Flowable<List<FeedGroupEntity>>
@Query("SELECT * FROM feed_group WHERE uid = :groupId")
abstract fun getGroup(groupId: Long): Maybe<FeedGroupEntity>
@Transaction
open fun insert(feedGroupEntity: FeedGroupEntity): Long {
val nextSortOrder = nextSortOrder()
feedGroupEntity.sortOrder = nextSortOrder
return insertInternal(feedGroupEntity)
}
@Update(onConflict = OnConflictStrategy.IGNORE)
abstract fun update(feedGroupEntity: FeedGroupEntity): Int
@Query("DELETE FROM feed_group")
abstract fun deleteAll(): Int
@Query("DELETE FROM feed_group WHERE uid = :groupId")
abstract fun delete(groupId: Long): Int
@Query("SELECT subscription_id FROM feed_group_subscription_join WHERE group_id = :groupId")
abstract fun getSubscriptionIdsFor(groupId: Long): Flowable<List<Long>>
@Query("DELETE FROM feed_group_subscription_join WHERE group_id = :groupId")
abstract fun deleteSubscriptionsFromGroup(groupId: Long): Int
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract fun insertSubscriptionsToGroup(entities: List<FeedGroupSubscriptionEntity>): List<Long>
@Transaction
open fun updateSubscriptionsForGroup(groupId: Long, subscriptionIds: List<Long>) {
deleteSubscriptionsFromGroup(groupId)
insertSubscriptionsToGroup(subscriptionIds.map { FeedGroupSubscriptionEntity(groupId, it) })
}
@Transaction
open fun updateOrder(orderMap: Map<Long, Long>) {
orderMap.forEach { (groupId, sortOrder) -> updateOrder(groupId, sortOrder) }
}
@Query("UPDATE feed_group SET sort_order = :sortOrder WHERE uid = :groupId")
abstract fun updateOrder(groupId: Long, sortOrder: Long): Int
@Query("SELECT IFNULL(MAX(sort_order) + 1, 0) FROM feed_group")
protected abstract fun nextSortOrder(): Long
@Insert(onConflict = OnConflictStrategy.ABORT)
protected abstract fun insertInternal(feedGroupEntity: FeedGroupEntity): Long
}

View File

@@ -0,0 +1,43 @@
package org.schabi.newpipe.database.feed.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import org.schabi.newpipe.database.feed.model.FeedEntity.Companion.FEED_TABLE
import org.schabi.newpipe.database.feed.model.FeedEntity.Companion.STREAM_ID
import org.schabi.newpipe.database.feed.model.FeedEntity.Companion.SUBSCRIPTION_ID
import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.subscription.SubscriptionEntity
@Entity(tableName = FEED_TABLE,
primaryKeys = [STREAM_ID, SUBSCRIPTION_ID],
indices = [Index(SUBSCRIPTION_ID)],
foreignKeys = [
ForeignKey(
entity = StreamEntity::class,
parentColumns = [StreamEntity.STREAM_ID],
childColumns = [STREAM_ID],
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true),
ForeignKey(
entity = SubscriptionEntity::class,
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
childColumns = [SUBSCRIPTION_ID],
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true)
]
)
data class FeedEntity(
@ColumnInfo(name = STREAM_ID)
var streamId: Long,
@ColumnInfo(name = SUBSCRIPTION_ID)
var subscriptionId: Long
) {
companion object {
const val FEED_TABLE = "feed"
const val STREAM_ID = "stream_id"
const val SUBSCRIPTION_ID = "subscription_id"
}
}

View File

@@ -0,0 +1,39 @@
package org.schabi.newpipe.database.feed.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.FEED_GROUP_TABLE
import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.SORT_ORDER
import org.schabi.newpipe.local.subscription.FeedGroupIcon
@Entity(
tableName = FEED_GROUP_TABLE,
indices = [Index(SORT_ORDER)]
)
data class FeedGroupEntity(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = ID)
val uid: Long,
@ColumnInfo(name = NAME)
var name: String,
@ColumnInfo(name = ICON)
var icon: FeedGroupIcon,
@ColumnInfo(name = SORT_ORDER)
var sortOrder: Long = -1
) {
companion object {
const val FEED_GROUP_TABLE = "feed_group"
const val ID = "uid"
const val NAME = "name"
const val ICON = "icon_id"
const val SORT_ORDER = "sort_order"
const val GROUP_ALL_ID = -1L
}
}

View File

@@ -0,0 +1,45 @@
package org.schabi.newpipe.database.feed.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.ForeignKey.CASCADE
import androidx.room.Index
import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity.Companion.FEED_GROUP_SUBSCRIPTION_TABLE
import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity.Companion.GROUP_ID
import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity.Companion.SUBSCRIPTION_ID
import org.schabi.newpipe.database.subscription.SubscriptionEntity
@Entity(
tableName = FEED_GROUP_SUBSCRIPTION_TABLE,
primaryKeys = [GROUP_ID, SUBSCRIPTION_ID],
indices = [Index(SUBSCRIPTION_ID)],
foreignKeys = [
ForeignKey(
entity = FeedGroupEntity::class,
parentColumns = [FeedGroupEntity.ID],
childColumns = [GROUP_ID],
onDelete = CASCADE, onUpdate = CASCADE, deferred = true),
ForeignKey(
entity = SubscriptionEntity::class,
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
childColumns = [SUBSCRIPTION_ID],
onDelete = CASCADE, onUpdate = CASCADE, deferred = true)
]
)
data class FeedGroupSubscriptionEntity(
@ColumnInfo(name = GROUP_ID)
var feedGroupId: Long,
@ColumnInfo(name = SUBSCRIPTION_ID)
var subscriptionId: Long
) {
companion object {
const val FEED_GROUP_SUBSCRIPTION_TABLE = "feed_group_subscription_join"
const val GROUP_ID = "group_id"
const val SUBSCRIPTION_ID = "subscription_id"
}
}

View File

@@ -0,0 +1,37 @@
package org.schabi.newpipe.database.feed.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import java.util.Date
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.subscription.SubscriptionEntity
@Entity(
tableName = FEED_LAST_UPDATED_TABLE,
foreignKeys = [
ForeignKey(
entity = SubscriptionEntity::class,
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
childColumns = [SUBSCRIPTION_ID],
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true)
]
)
data class FeedLastUpdatedEntity(
@PrimaryKey
@ColumnInfo(name = SUBSCRIPTION_ID)
var subscriptionId: Long,
@ColumnInfo(name = LAST_UPDATED)
var lastUpdated: Date? = null
) {
companion object {
const val FEED_LAST_UPDATED_TABLE = "feed_last_updated"
const val SUBSCRIPTION_ID = "subscription_id"
const val LAST_UPDATED = "last_updated"
}
}

View File

@@ -1,8 +1,8 @@
package org.schabi.newpipe.database.history.dao;
import androidx.annotation.Nullable;
import androidx.room.Dao;
import androidx.room.Query;
import androidx.annotation.Nullable;
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
@@ -18,11 +18,10 @@ import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.TABLE
@Dao
public interface SearchHistoryDAO extends HistoryDAO<SearchHistoryEntry> {
String ORDER_BY_CREATION_DATE = " ORDER BY " + CREATION_DATE + " DESC";
@Query("SELECT * FROM " + TABLE_NAME +
" WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")")
@Query("SELECT * FROM " + TABLE_NAME
+ " WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")")
@Nullable
SearchHistoryEntry getLatestEntry();
@@ -37,13 +36,16 @@ public interface SearchHistoryDAO extends HistoryDAO<SearchHistoryEntry> {
@Override
Flowable<List<SearchHistoryEntry>> getAll();
@Query("SELECT * FROM " + TABLE_NAME + " GROUP BY " + SEARCH + ORDER_BY_CREATION_DATE + " LIMIT :limit")
@Query("SELECT * FROM " + TABLE_NAME + " GROUP BY " + SEARCH + ORDER_BY_CREATION_DATE
+ " LIMIT :limit")
Flowable<List<SearchHistoryEntry>> getUniqueEntries(int limit);
@Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE)
@Query("SELECT * FROM " + TABLE_NAME
+ " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE)
@Override
Flowable<List<SearchHistoryEntry>> listByService(int serviceId);
@Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SEARCH + " LIKE :query || '%' GROUP BY " + SEARCH + " LIMIT :limit")
@Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SEARCH + " LIKE :query || '%'"
+ " GROUP BY " + SEARCH + " LIMIT :limit")
Flowable<List<SearchHistoryEntry>> getSimilarEntries(String query, int limit);
}

View File

@@ -1,32 +1,31 @@
package org.schabi.newpipe.database.history.dao;
import androidx.annotation.Nullable;
import androidx.room.Dao;
import androidx.room.Query;
import androidx.annotation.Nullable;
import org.schabi.newpipe.database.history.model.StreamHistoryEntity;
import org.schabi.newpipe.database.history.model.StreamHistoryEntry;
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
import org.schabi.newpipe.database.history.model.StreamHistoryEntity;
import java.util.List;
import io.reactivex.Flowable;
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_HISTORY_TABLE;
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_REPEAT_COUNT;
import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_LATEST_DATE;
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_TABLE;
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_HISTORY_TABLE;
@Dao
public abstract class StreamHistoryDAO implements HistoryDAO<StreamHistoryEntity> {
@Query("SELECT * FROM " + STREAM_HISTORY_TABLE +
" WHERE " + STREAM_ACCESS_DATE + " = " +
"(SELECT MAX(" + STREAM_ACCESS_DATE + ") FROM " + STREAM_HISTORY_TABLE + ")")
@Query("SELECT * FROM " + STREAM_HISTORY_TABLE
+ " WHERE " + STREAM_ACCESS_DATE + " = "
+ "(SELECT MAX(" + STREAM_ACCESS_DATE + ") FROM " + STREAM_HISTORY_TABLE + ")")
@Override
@Nullable
public abstract StreamHistoryEntity getLatestEntry();
@@ -40,33 +39,40 @@ public abstract class StreamHistoryDAO implements HistoryDAO<StreamHistoryEntity
public abstract int deleteAll();
@Override
public Flowable<List<StreamHistoryEntity>> listByService(int serviceId) {
public Flowable<List<StreamHistoryEntity>> listByService(final int serviceId) {
throw new UnsupportedOperationException();
}
@Query("SELECT * FROM " + STREAM_TABLE +
" INNER JOIN " + STREAM_HISTORY_TABLE +
" ON " + STREAM_ID + " = " + JOIN_STREAM_ID +
" ORDER BY " + STREAM_ACCESS_DATE + " DESC")
@Query("SELECT * FROM " + STREAM_TABLE
+ " INNER JOIN " + STREAM_HISTORY_TABLE
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID
+ " ORDER BY " + STREAM_ACCESS_DATE + " DESC")
public abstract Flowable<List<StreamHistoryEntry>> getHistory();
@Query("SELECT * FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID +
" = :streamId ORDER BY " + STREAM_ACCESS_DATE + " DESC LIMIT 1")
@Query("SELECT * FROM " + STREAM_TABLE
+ " INNER JOIN " + STREAM_HISTORY_TABLE
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID
+ " ORDER BY " + STREAM_ID + " ASC")
public abstract Flowable<List<StreamHistoryEntry>> getHistorySortedById();
@Query("SELECT * FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID
+ " = :streamId ORDER BY " + STREAM_ACCESS_DATE + " DESC LIMIT 1")
@Nullable
public abstract StreamHistoryEntity getLatestEntry(final long streamId);
public abstract StreamHistoryEntity getLatestEntry(long streamId);
@Query("DELETE FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
public abstract int deleteStreamHistory(final long streamId);
public abstract int deleteStreamHistory(long streamId);
@Query("SELECT * FROM " + STREAM_TABLE +
@Query("SELECT * FROM " + STREAM_TABLE
// Select the latest entry and watch count for each stream id on history table
" INNER JOIN " +
"(SELECT " + JOIN_STREAM_ID + ", " +
" MAX(" + STREAM_ACCESS_DATE + ") AS " + STREAM_LATEST_DATE + ", " +
" SUM(" + STREAM_REPEAT_COUNT + ") AS " + STREAM_WATCH_COUNT +
" FROM " + STREAM_HISTORY_TABLE + " GROUP BY " + JOIN_STREAM_ID + ")" +
+ " INNER JOIN "
+ "(SELECT " + JOIN_STREAM_ID + ", "
+ " MAX(" + STREAM_ACCESS_DATE + ") AS " + STREAM_LATEST_DATE + ", "
+ " SUM(" + STREAM_REPEAT_COUNT + ") AS " + STREAM_WATCH_COUNT
+ " FROM " + STREAM_HISTORY_TABLE + " GROUP BY " + JOIN_STREAM_ID + ")"
" ON " + STREAM_ID + " = " + JOIN_STREAM_ID)
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID)
public abstract Flowable<List<StreamStatisticsEntry>> getStatistics();
}

View File

@@ -13,7 +13,6 @@ import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SEARC
@Entity(tableName = SearchHistoryEntry.TABLE_NAME,
indices = {@Index(value = SEARCH)})
public class SearchHistoryEntry {
public static final String ID = "id";
public static final String TABLE_NAME = "search_history";
public static final String SERVICE_ID = "service_id";
@@ -33,7 +32,7 @@ public class SearchHistoryEntry {
@ColumnInfo(name = SEARCH)
private String search;
public SearchHistoryEntry(Date creationDate, int serviceId, String search) {
public SearchHistoryEntry(final Date creationDate, final int serviceId, final String search) {
this.serviceId = serviceId;
this.creationDate = creationDate;
this.search = search;
@@ -43,7 +42,7 @@ public class SearchHistoryEntry {
return id;
}
public void setId(long id) {
public void setId(final long id) {
this.id = id;
}
@@ -51,7 +50,7 @@ public class SearchHistoryEntry {
return creationDate;
}
public void setCreationDate(Date creationDate) {
public void setCreationDate(final Date creationDate) {
this.creationDate = creationDate;
}
@@ -59,7 +58,7 @@ public class SearchHistoryEntry {
return serviceId;
}
public void setServiceId(int serviceId) {
public void setServiceId(final int serviceId) {
this.serviceId = serviceId;
}
@@ -67,13 +66,13 @@ public class SearchHistoryEntry {
return search;
}
public void setSearch(String search) {
public void setSearch(final String search) {
this.search = search;
}
@Ignore
public boolean hasEqualValues(SearchHistoryEntry otherEntry) {
return getServiceId() == otherEntry.getServiceId() &&
getSearch().equals(otherEntry.getSearch());
public boolean hasEqualValues(final SearchHistoryEntry otherEntry) {
return getServiceId() == otherEntry.getServiceId()
&& getSearch().equals(otherEntry.getSearch());
}
}

View File

@@ -1,20 +1,20 @@
package org.schabi.newpipe.database.history.model;
import androidx.annotation.NonNull;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.ForeignKey;
import androidx.room.Ignore;
import androidx.room.Index;
import androidx.annotation.NonNull;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import java.util.Date;
import static androidx.room.ForeignKey.CASCADE;
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE;
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_HISTORY_TABLE;
@Entity(tableName = STREAM_HISTORY_TABLE,
primaryKeys = {JOIN_STREAM_ID, STREAM_ACCESS_DATE},
@@ -27,10 +27,10 @@ import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STRE
onDelete = CASCADE, onUpdate = CASCADE)
})
public class StreamHistoryEntity {
final public static String STREAM_HISTORY_TABLE = "stream_history";
final public static String JOIN_STREAM_ID = "stream_id";
final public static String STREAM_ACCESS_DATE = "access_date";
final public static String STREAM_REPEAT_COUNT = "repeat_count";
public static final String STREAM_HISTORY_TABLE = "stream_history";
public static final String JOIN_STREAM_ID = "stream_id";
public static final String STREAM_ACCESS_DATE = "access_date";
public static final String STREAM_REPEAT_COUNT = "repeat_count";
@ColumnInfo(name = JOIN_STREAM_ID)
private long streamUid;
@@ -42,14 +42,15 @@ public class StreamHistoryEntity {
@ColumnInfo(name = STREAM_REPEAT_COUNT)
private long repeatCount;
public StreamHistoryEntity(long streamUid, @NonNull Date accessDate, long repeatCount) {
public StreamHistoryEntity(final long streamUid, @NonNull final Date accessDate,
final long repeatCount) {
this.streamUid = streamUid;
this.accessDate = accessDate;
this.repeatCount = repeatCount;
}
@Ignore
public StreamHistoryEntity(long streamUid, @NonNull Date accessDate) {
public StreamHistoryEntity(final long streamUid, @NonNull final Date accessDate) {
this(streamUid, accessDate, 1);
}
@@ -57,7 +58,7 @@ public class StreamHistoryEntity {
return streamUid;
}
public void setStreamUid(long streamUid) {
public void setStreamUid(final long streamUid) {
this.streamUid = streamUid;
}
@@ -65,7 +66,7 @@ public class StreamHistoryEntity {
return accessDate;
}
public void setAccessDate(@NonNull Date accessDate) {
public void setAccessDate(@NonNull final Date accessDate) {
this.accessDate = accessDate;
}
@@ -73,7 +74,7 @@ public class StreamHistoryEntity {
return repeatCount;
}
public void setRepeatCount(long repeatCount) {
public void setRepeatCount(final long repeatCount) {
this.repeatCount = repeatCount;
}
}

View File

@@ -1,59 +0,0 @@
package org.schabi.newpipe.database.history.model;
import androidx.room.ColumnInfo;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.extractor.stream.StreamType;
import java.util.Date;
public class StreamHistoryEntry {
@ColumnInfo(name = StreamEntity.STREAM_ID)
final public long uid;
@ColumnInfo(name = StreamEntity.STREAM_SERVICE_ID)
final public int serviceId;
@ColumnInfo(name = StreamEntity.STREAM_URL)
final public String url;
@ColumnInfo(name = StreamEntity.STREAM_TITLE)
final public String title;
@ColumnInfo(name = StreamEntity.STREAM_TYPE)
final public StreamType streamType;
@ColumnInfo(name = StreamEntity.STREAM_DURATION)
final public long duration;
@ColumnInfo(name = StreamEntity.STREAM_UPLOADER)
final public String uploader;
@ColumnInfo(name = StreamEntity.STREAM_THUMBNAIL_URL)
final public String thumbnailUrl;
@ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID)
final public long streamId;
@ColumnInfo(name = StreamHistoryEntity.STREAM_ACCESS_DATE)
final public Date accessDate;
@ColumnInfo(name = StreamHistoryEntity.STREAM_REPEAT_COUNT)
final public long repeatCount;
public StreamHistoryEntry(long uid, int serviceId, String url, String title,
StreamType streamType, long duration, String uploader,
String thumbnailUrl, long streamId, Date accessDate,
long repeatCount) {
this.uid = uid;
this.serviceId = serviceId;
this.url = url;
this.title = title;
this.streamType = streamType;
this.duration = duration;
this.uploader = uploader;
this.thumbnailUrl = thumbnailUrl;
this.streamId = streamId;
this.accessDate = accessDate;
this.repeatCount = repeatCount;
}
public StreamHistoryEntity toStreamHistoryEntity() {
return new StreamHistoryEntity(streamId, accessDate, repeatCount);
}
public boolean hasEqualValues(StreamHistoryEntry other) {
return this.uid == other.uid && streamId == other.streamId &&
accessDate.compareTo(other.accessDate) == 0;
}
}

View File

@@ -0,0 +1,30 @@
package org.schabi.newpipe.database.history.model
import androidx.room.ColumnInfo
import androidx.room.Embedded
import java.util.Date
import org.schabi.newpipe.database.stream.model.StreamEntity
data class StreamHistoryEntry(
@Embedded
val streamEntity: StreamEntity,
@ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID)
val streamId: Long,
@ColumnInfo(name = StreamHistoryEntity.STREAM_ACCESS_DATE)
val accessDate: Date,
@ColumnInfo(name = StreamHistoryEntity.STREAM_REPEAT_COUNT)
val repeatCount: Long
) {
fun toStreamHistoryEntity(): StreamHistoryEntity {
return StreamHistoryEntity(streamId, accessDate, repeatCount)
}
fun hasEqualValues(other: StreamHistoryEntry): Boolean {
return this.streamEntity.uid == other.streamEntity.uid && streamId == other.streamId &&
accessDate.compareTo(other.accessDate) == 0
}
}

View File

@@ -1,7 +1,33 @@
package org.schabi.newpipe.database.playlist;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public interface PlaylistLocalItem extends LocalItem {
String getOrderingName();
static List<PlaylistLocalItem> merge(
final List<PlaylistMetadataEntry> localPlaylists,
final List<PlaylistRemoteEntity> remotePlaylists) {
final List<PlaylistLocalItem> items = new ArrayList<>(
localPlaylists.size() + remotePlaylists.size());
items.addAll(localPlaylists);
items.addAll(remotePlaylists);
Collections.sort(items, (left, right) -> {
final String on1 = left.getOrderingName();
final String on2 = right.getOrderingName();
if (on1 == null) {
return on2 == null ? 0 : 1;
} else {
return on2 == null ? -1 : on1.compareToIgnoreCase(on2);
}
});
return items;
}
}

View File

@@ -7,18 +7,19 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
public class PlaylistMetadataEntry implements PlaylistLocalItem {
final public static String PLAYLIST_STREAM_COUNT = "streamCount";
public static final String PLAYLIST_STREAM_COUNT = "streamCount";
@ColumnInfo(name = PLAYLIST_ID)
final public long uid;
public final long uid;
@ColumnInfo(name = PLAYLIST_NAME)
final public String name;
public final String name;
@ColumnInfo(name = PLAYLIST_THUMBNAIL_URL)
final public String thumbnailUrl;
public final String thumbnailUrl;
@ColumnInfo(name = PLAYLIST_STREAM_COUNT)
final public long streamCount;
public final long streamCount;
public PlaylistMetadataEntry(long uid, String name, String thumbnailUrl, long streamCount) {
public PlaylistMetadataEntry(final long uid, final String name, final String thumbnailUrl,
final long streamCount) {
this.uid = uid;
this.name = name;
this.thumbnailUrl = thumbnailUrl;

View File

@@ -1,60 +0,0 @@
package org.schabi.newpipe.database.playlist;
import androidx.room.ColumnInfo;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType;
public class PlaylistStreamEntry implements LocalItem {
@ColumnInfo(name = StreamEntity.STREAM_ID)
final public long uid;
@ColumnInfo(name = StreamEntity.STREAM_SERVICE_ID)
final public int serviceId;
@ColumnInfo(name = StreamEntity.STREAM_URL)
final public String url;
@ColumnInfo(name = StreamEntity.STREAM_TITLE)
final public String title;
@ColumnInfo(name = StreamEntity.STREAM_TYPE)
final public StreamType streamType;
@ColumnInfo(name = StreamEntity.STREAM_DURATION)
final public long duration;
@ColumnInfo(name = StreamEntity.STREAM_UPLOADER)
final public String uploader;
@ColumnInfo(name = StreamEntity.STREAM_THUMBNAIL_URL)
final public String thumbnailUrl;
@ColumnInfo(name = PlaylistStreamEntity.JOIN_STREAM_ID)
final public long streamId;
@ColumnInfo(name = PlaylistStreamEntity.JOIN_INDEX)
final public int joinIndex;
public PlaylistStreamEntry(long uid, int serviceId, String url, String title,
StreamType streamType, long duration, String uploader,
String thumbnailUrl, long streamId, int joinIndex) {
this.uid = uid;
this.serviceId = serviceId;
this.url = url;
this.title = title;
this.streamType = streamType;
this.duration = duration;
this.uploader = uploader;
this.thumbnailUrl = thumbnailUrl;
this.streamId = streamId;
this.joinIndex = joinIndex;
}
public StreamInfoItem toStreamInfoItem() throws IllegalArgumentException {
StreamInfoItem item = new StreamInfoItem(serviceId, url, title, streamType);
item.setThumbnailUrl(thumbnailUrl);
item.setUploaderName(uploader);
item.setDuration(duration);
return item;
}
@Override
public LocalItemType getLocalItemType() {
return LocalItemType.PLAYLIST_STREAM_ITEM;
}
}

View File

@@ -0,0 +1,34 @@
package org.schabi.newpipe.database.playlist
import androidx.room.ColumnInfo
import androidx.room.Embedded
import org.schabi.newpipe.database.LocalItem
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity
import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.extractor.stream.StreamInfoItem
class PlaylistStreamEntry(
@Embedded
val streamEntity: StreamEntity,
@ColumnInfo(name = PlaylistStreamEntity.JOIN_STREAM_ID)
val streamId: Long,
@ColumnInfo(name = PlaylistStreamEntity.JOIN_INDEX)
val joinIndex: Int
) : LocalItem {
@Throws(IllegalArgumentException::class)
fun toStreamInfoItem(): StreamInfoItem {
val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType)
item.duration = streamEntity.duration
item.uploaderName = streamEntity.uploader
item.thumbnailUrl = streamEntity.thumbnailUrl
return item
}
override fun getLocalItemType(): LocalItem.LocalItemType {
return LocalItem.LocalItemType.PLAYLIST_STREAM_ITEM
}
}

View File

@@ -24,13 +24,13 @@ public abstract class PlaylistDAO implements BasicDAO<PlaylistEntity> {
public abstract int deleteAll();
@Override
public Flowable<List<PlaylistEntity>> listByService(int serviceId) {
public Flowable<List<PlaylistEntity>> listByService(final int serviceId) {
throw new UnsupportedOperationException();
}
@Query("SELECT * FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId")
public abstract Flowable<List<PlaylistEntity>> getPlaylist(final long playlistId);
public abstract Flowable<List<PlaylistEntity>> getPlaylist(long playlistId);
@Query("DELETE FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId")
public abstract int deletePlaylist(final long playlistId);
public abstract int deletePlaylist(long playlistId);
}

View File

@@ -27,22 +27,21 @@ public abstract class PlaylistRemoteDAO implements BasicDAO<PlaylistRemoteEntity
public abstract int deleteAll();
@Override
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE +
" WHERE " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE
+ " WHERE " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
public abstract Flowable<List<PlaylistRemoteEntity>> listByService(int serviceId);
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE " +
REMOTE_PLAYLIST_URL + " = :url AND " +
REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE "
+ REMOTE_PLAYLIST_URL + " = :url AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
public abstract Flowable<List<PlaylistRemoteEntity>> getPlaylist(long serviceId, String url);
@Query("SELECT " + REMOTE_PLAYLIST_ID + " FROM " + REMOTE_PLAYLIST_TABLE +
" WHERE " +
REMOTE_PLAYLIST_URL + " = :url AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
@Query("SELECT " + REMOTE_PLAYLIST_ID + " FROM " + REMOTE_PLAYLIST_TABLE
+ " WHERE " + REMOTE_PLAYLIST_URL + " = :url "
+ "AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
abstract Long getPlaylistIdInternal(long serviceId, String url);
@Transaction
public long upsert(PlaylistRemoteEntity playlist) {
public long upsert(final PlaylistRemoteEntity playlist) {
final Long playlistId = getPlaylistIdInternal(playlist.getServiceId(), playlist.getUrl());
if (playlistId == null) {
@@ -54,7 +53,7 @@ public abstract class PlaylistRemoteDAO implements BasicDAO<PlaylistRemoteEntity
}
}
@Query("DELETE FROM " + REMOTE_PLAYLIST_TABLE +
" WHERE " + REMOTE_PLAYLIST_ID + " = :playlistId")
public abstract int deletePlaylist(final long playlistId);
@Query("DELETE FROM " + REMOTE_PLAYLIST_TABLE
+ " WHERE " + REMOTE_PLAYLIST_ID + " = :playlistId")
public abstract int deletePlaylist(long playlistId);
}

View File

@@ -14,9 +14,16 @@ import java.util.List;
import io.reactivex.Flowable;
import static org.schabi.newpipe.database.playlist.PlaylistMetadataEntry.PLAYLIST_STREAM_COUNT;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.*;
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.*;
import static org.schabi.newpipe.database.stream.model.StreamEntity.*;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_INDEX;
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_PLAYLIST_ID;
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_STREAM_ID;
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_TABLE;
@Dao
public abstract class PlaylistStreamDAO implements BasicDAO<PlaylistStreamEntity> {
@@ -29,40 +36,39 @@ public abstract class PlaylistStreamDAO implements BasicDAO<PlaylistStreamEntity
public abstract int deleteAll();
@Override
public Flowable<List<PlaylistStreamEntity>> listByService(int serviceId) {
public Flowable<List<PlaylistStreamEntity>> listByService(final int serviceId) {
throw new UnsupportedOperationException();
}
@Query("DELETE FROM " + PLAYLIST_STREAM_JOIN_TABLE +
" WHERE " + JOIN_PLAYLIST_ID + " = :playlistId")
public abstract void deleteBatch(final long playlistId);
@Query("DELETE FROM " + PLAYLIST_STREAM_JOIN_TABLE
+ " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId")
public abstract void deleteBatch(long playlistId);
@Query("SELECT COALESCE(MAX(" + JOIN_INDEX + "), -1)" +
" FROM " + PLAYLIST_STREAM_JOIN_TABLE +
" WHERE " + JOIN_PLAYLIST_ID + " = :playlistId")
public abstract Flowable<Integer> getMaximumIndexOf(final long playlistId);
@Query("SELECT COALESCE(MAX(" + JOIN_INDEX + "), -1)"
+ " FROM " + PLAYLIST_STREAM_JOIN_TABLE
+ " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId")
public abstract Flowable<Integer> getMaximumIndexOf(long playlistId);
@Transaction
@Query("SELECT * FROM " + STREAM_TABLE + " INNER JOIN " +
@Query("SELECT * FROM " + STREAM_TABLE + " INNER JOIN "
// get ids of streams of the given playlist
"(SELECT " + JOIN_STREAM_ID + "," + JOIN_INDEX +
" FROM " + PLAYLIST_STREAM_JOIN_TABLE +
" WHERE " + JOIN_PLAYLIST_ID + " = :playlistId)" +
+ "(SELECT " + JOIN_STREAM_ID + "," + JOIN_INDEX
+ " FROM " + PLAYLIST_STREAM_JOIN_TABLE
+ " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId)"
// then merge with the stream metadata
" ON " + STREAM_ID + " = " + JOIN_STREAM_ID +
" ORDER BY " + JOIN_INDEX + " ASC")
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID
+ " ORDER BY " + JOIN_INDEX + " ASC")
public abstract Flowable<List<PlaylistStreamEntry>> getOrderedStreamsOf(long playlistId);
@Transaction
@Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", " +
PLAYLIST_THUMBNAIL_URL + ", " +
"COALESCE(COUNT(" + JOIN_PLAYLIST_ID + "), 0) AS " + PLAYLIST_STREAM_COUNT +
@Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", " + PLAYLIST_THUMBNAIL_URL + ", "
+ "COALESCE(COUNT(" + JOIN_PLAYLIST_ID + "), 0) AS " + PLAYLIST_STREAM_COUNT
" FROM " + PLAYLIST_TABLE +
" LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE +
" ON " + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID +
" GROUP BY " + JOIN_PLAYLIST_ID +
" ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC")
+ " FROM " + PLAYLIST_TABLE
+ " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE
+ " ON " + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID
+ " GROUP BY " + JOIN_PLAYLIST_ID
+ " ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC")
public abstract Flowable<List<PlaylistMetadataEntry>> getPlaylistMetadata();
}

View File

@@ -11,10 +11,10 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST
@Entity(tableName = PLAYLIST_TABLE,
indices = {@Index(value = {PLAYLIST_NAME})})
public class PlaylistEntity {
final public static String PLAYLIST_TABLE = "playlists";
final public static String PLAYLIST_ID = "uid";
final public static String PLAYLIST_NAME = "name";
final public static String PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
public static final String PLAYLIST_TABLE = "playlists";
public static final String PLAYLIST_ID = "uid";
public static final String PLAYLIST_NAME = "name";
public static final String PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = PLAYLIST_ID)
@@ -26,7 +26,7 @@ public class PlaylistEntity {
@ColumnInfo(name = PLAYLIST_THUMBNAIL_URL)
private String thumbnailUrl;
public PlaylistEntity(String name, String thumbnailUrl) {
public PlaylistEntity(final String name, final String thumbnailUrl) {
this.name = name;
this.thumbnailUrl = thumbnailUrl;
}
@@ -35,7 +35,7 @@ public class PlaylistEntity {
return uid;
}
public void setUid(long uid) {
public void setUid(final long uid) {
this.uid = uid;
}
@@ -43,7 +43,7 @@ public class PlaylistEntity {
return name;
}
public void setName(String name) {
public void setName(final String name) {
this.name = name;
}
@@ -51,7 +51,7 @@ public class PlaylistEntity {
return thumbnailUrl;
}
public void setThumbnailUrl(String thumbnailUrl) {
public void setThumbnailUrl(final String thumbnailUrl) {
this.thumbnailUrl = thumbnailUrl;
}
}

View File

@@ -24,14 +24,14 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.RE
@Index(value = {REMOTE_PLAYLIST_SERVICE_ID, REMOTE_PLAYLIST_URL}, unique = true)
})
public class PlaylistRemoteEntity implements PlaylistLocalItem {
final public static String REMOTE_PLAYLIST_TABLE = "remote_playlists";
final public static String REMOTE_PLAYLIST_ID = "uid";
final public static String REMOTE_PLAYLIST_SERVICE_ID = "service_id";
final public static String REMOTE_PLAYLIST_NAME = "name";
final public static String REMOTE_PLAYLIST_URL = "url";
final public static String REMOTE_PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
final public static String REMOTE_PLAYLIST_UPLOADER_NAME = "uploader";
final public static String REMOTE_PLAYLIST_STREAM_COUNT = "stream_count";
public static final String REMOTE_PLAYLIST_TABLE = "remote_playlists";
public static final String REMOTE_PLAYLIST_ID = "uid";
public static final String REMOTE_PLAYLIST_SERVICE_ID = "service_id";
public static final String REMOTE_PLAYLIST_NAME = "name";
public static final String REMOTE_PLAYLIST_URL = "url";
public static final String REMOTE_PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
public static final String REMOTE_PLAYLIST_UPLOADER_NAME = "uploader";
public static final String REMOTE_PLAYLIST_STREAM_COUNT = "stream_count";
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = REMOTE_PLAYLIST_ID)
@@ -55,8 +55,9 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
@ColumnInfo(name = REMOTE_PLAYLIST_STREAM_COUNT)
private Long streamCount;
public PlaylistRemoteEntity(int serviceId, String name, String url, String thumbnailUrl,
String uploader, Long streamCount) {
public PlaylistRemoteEntity(final int serviceId, final String name, final String url,
final String thumbnailUrl, final String uploader,
final Long streamCount) {
this.serviceId = serviceId;
this.name = name;
this.url = url;
@@ -68,7 +69,8 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
@Ignore
public PlaylistRemoteEntity(final PlaylistInfo info) {
this(info.getServiceId(), info.getName(), info.getUrl(),
info.getThumbnailUrl() == null ? info.getUploaderAvatarUrl() : info.getThumbnailUrl(),
info.getThumbnailUrl() == null
? info.getUploaderAvatarUrl() : info.getThumbnailUrl(),
info.getUploaderName(), info.getStreamCount());
}
@@ -90,7 +92,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
return uid;
}
public void setUid(long uid) {
public void setUid(final long uid) {
this.uid = uid;
}
@@ -98,7 +100,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
return serviceId;
}
public void setServiceId(int serviceId) {
public void setServiceId(final int serviceId) {
this.serviceId = serviceId;
}
@@ -106,7 +108,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
return name;
}
public void setName(String name) {
public void setName(final String name) {
this.name = name;
}
@@ -114,7 +116,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
return thumbnailUrl;
}
public void setThumbnailUrl(String thumbnailUrl) {
public void setThumbnailUrl(final String thumbnailUrl) {
this.thumbnailUrl = thumbnailUrl;
}
@@ -122,7 +124,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
return url;
}
public void setUrl(String url) {
public void setUrl(final String url) {
this.url = url;
}
@@ -130,7 +132,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
return uploader;
}
public void setUploader(String uploader) {
public void setUploader(final String uploader) {
this.uploader = uploader;
}
@@ -138,7 +140,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
return streamCount;
}
public void setStreamCount(Long streamCount) {
public void setStreamCount(final Long streamCount) {
this.streamCount = streamCount;
}

View File

@@ -30,11 +30,10 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PL
onDelete = CASCADE, onUpdate = CASCADE, deferred = true)
})
public class PlaylistStreamEntity {
final public static String PLAYLIST_STREAM_JOIN_TABLE = "playlist_stream_join";
final public static String JOIN_PLAYLIST_ID = "playlist_id";
final public static String JOIN_STREAM_ID = "stream_id";
final public static String JOIN_INDEX = "join_index";
public static final String PLAYLIST_STREAM_JOIN_TABLE = "playlist_stream_join";
public static final String JOIN_PLAYLIST_ID = "playlist_id";
public static final String JOIN_STREAM_ID = "stream_id";
public static final String JOIN_INDEX = "join_index";
@ColumnInfo(name = JOIN_PLAYLIST_ID)
private long playlistUid;
@@ -55,23 +54,23 @@ public class PlaylistStreamEntity {
return playlistUid;
}
public void setPlaylistUid(final long playlistUid) {
this.playlistUid = playlistUid;
}
public long getStreamUid() {
return streamUid;
}
public void setStreamUid(final long streamUid) {
this.streamUid = streamUid;
}
public int getIndex() {
return index;
}
public void setPlaylistUid(long playlistUid) {
this.playlistUid = playlistUid;
}
public void setStreamUid(long streamUid) {
this.streamUid = streamUid;
}
public void setIndex(int index) {
public void setIndex(final int index) {
this.index = index;
}
}

View File

@@ -1,69 +0,0 @@
package org.schabi.newpipe.database.stream;
import androidx.room.ColumnInfo;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.history.model.StreamHistoryEntity;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType;
import java.util.Date;
public class StreamStatisticsEntry implements LocalItem {
final public static String STREAM_LATEST_DATE = "latestAccess";
final public static String STREAM_WATCH_COUNT = "watchCount";
@ColumnInfo(name = StreamEntity.STREAM_ID)
final public long uid;
@ColumnInfo(name = StreamEntity.STREAM_SERVICE_ID)
final public int serviceId;
@ColumnInfo(name = StreamEntity.STREAM_URL)
final public String url;
@ColumnInfo(name = StreamEntity.STREAM_TITLE)
final public String title;
@ColumnInfo(name = StreamEntity.STREAM_TYPE)
final public StreamType streamType;
@ColumnInfo(name = StreamEntity.STREAM_DURATION)
final public long duration;
@ColumnInfo(name = StreamEntity.STREAM_UPLOADER)
final public String uploader;
@ColumnInfo(name = StreamEntity.STREAM_THUMBNAIL_URL)
final public String thumbnailUrl;
@ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID)
final public long streamId;
@ColumnInfo(name = StreamStatisticsEntry.STREAM_LATEST_DATE)
final public Date latestAccessDate;
@ColumnInfo(name = StreamStatisticsEntry.STREAM_WATCH_COUNT)
final public long watchCount;
public StreamStatisticsEntry(long uid, int serviceId, String url, String title,
StreamType streamType, long duration, String uploader,
String thumbnailUrl, long streamId, Date latestAccessDate,
long watchCount) {
this.uid = uid;
this.serviceId = serviceId;
this.url = url;
this.title = title;
this.streamType = streamType;
this.duration = duration;
this.uploader = uploader;
this.thumbnailUrl = thumbnailUrl;
this.streamId = streamId;
this.latestAccessDate = latestAccessDate;
this.watchCount = watchCount;
}
public StreamInfoItem toStreamInfoItem() {
StreamInfoItem item = new StreamInfoItem(serviceId, url, title, streamType);
item.setDuration(duration);
item.setUploaderName(uploader);
item.setThumbnailUrl(thumbnailUrl);
return item;
}
@Override
public LocalItemType getLocalItemType() {
return LocalItemType.STATISTIC_STREAM_ITEM;
}
}

View File

@@ -0,0 +1,42 @@
package org.schabi.newpipe.database.stream
import androidx.room.ColumnInfo
import androidx.room.Embedded
import java.util.Date
import org.schabi.newpipe.database.LocalItem
import org.schabi.newpipe.database.history.model.StreamHistoryEntity
import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.extractor.stream.StreamInfoItem
class StreamStatisticsEntry(
@Embedded
val streamEntity: StreamEntity,
@ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID)
val streamId: Long,
@ColumnInfo(name = STREAM_LATEST_DATE)
val latestAccessDate: Date,
@ColumnInfo(name = STREAM_WATCH_COUNT)
val watchCount: Long
) : LocalItem {
fun toStreamInfoItem(): StreamInfoItem {
val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType)
item.duration = streamEntity.duration
item.uploaderName = streamEntity.uploader
item.thumbnailUrl = streamEntity.thumbnailUrl
return item
}
override fun getLocalItemType(): LocalItem.LocalItemType {
return LocalItem.LocalItemType.STATISTIC_STREAM_ITEM
}
companion object {
const val STREAM_LATEST_DATE = "latestAccess"
const val STREAM_WATCH_COUNT = "watchCount"
}
}

View File

@@ -1,98 +0,0 @@
package org.schabi.newpipe.database.stream.dao;
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import androidx.room.Transaction;
import org.schabi.newpipe.database.BasicDAO;
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.database.history.model.StreamHistoryEntity;
import java.util.ArrayList;
import java.util.List;
import io.reactivex.Flowable;
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_SERVICE_ID;
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_URL;
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE;
@Dao
public abstract class StreamDAO implements BasicDAO<StreamEntity> {
@Override
@Query("SELECT * FROM " + STREAM_TABLE)
public abstract Flowable<List<StreamEntity>> getAll();
@Override
@Query("DELETE FROM " + STREAM_TABLE)
public abstract int deleteAll();
@Override
@Query("SELECT * FROM " + STREAM_TABLE + " WHERE " + STREAM_SERVICE_ID + " = :serviceId")
public abstract Flowable<List<StreamEntity>> listByService(int serviceId);
@Query("SELECT * FROM " + STREAM_TABLE + " WHERE " +
STREAM_URL + " = :url AND " +
STREAM_SERVICE_ID + " = :serviceId")
public abstract Flowable<List<StreamEntity>> getStream(long serviceId, String url);
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract void silentInsertAllInternal(final List<StreamEntity> streams);
@Query("SELECT " + STREAM_ID + " FROM " + STREAM_TABLE + " WHERE " +
STREAM_URL + " = :url AND " +
STREAM_SERVICE_ID + " = :serviceId")
abstract Long getStreamIdInternal(long serviceId, String url);
@Transaction
public long upsert(StreamEntity stream) {
final Long streamIdCandidate = getStreamIdInternal(stream.getServiceId(), stream.getUrl());
if (streamIdCandidate == null) {
return insert(stream);
} else {
stream.setUid(streamIdCandidate);
update(stream);
return streamIdCandidate;
}
}
@Transaction
public List<Long> upsertAll(List<StreamEntity> streams) {
silentInsertAllInternal(streams);
final List<Long> streamIds = new ArrayList<>(streams.size());
for (StreamEntity stream : streams) {
final Long streamId = getStreamIdInternal(stream.getServiceId(), stream.getUrl());
if (streamId == null) {
throw new IllegalStateException("StreamID cannot be null just after insertion.");
}
streamIds.add(streamId);
stream.setUid(streamId);
}
update(streams);
return streamIds;
}
@Query("DELETE FROM " + STREAM_TABLE + " WHERE " + STREAM_ID +
" NOT IN " +
"(SELECT DISTINCT " + STREAM_ID + " FROM " + STREAM_TABLE +
" LEFT JOIN " + STREAM_HISTORY_TABLE +
" ON " + STREAM_ID + " = " +
StreamHistoryEntity.STREAM_HISTORY_TABLE + "." + StreamHistoryEntity.JOIN_STREAM_ID +
" LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE +
" ON " + STREAM_ID + " = " +
PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE + "." + PlaylistStreamEntity.JOIN_STREAM_ID +
")")
public abstract int deleteOrphans();
}

View File

@@ -0,0 +1,140 @@
package org.schabi.newpipe.database.stream.dao
import androidx.room.ColumnInfo
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import io.reactivex.Flowable
import java.util.Date
import org.schabi.newpipe.database.BasicDAO
import org.schabi.newpipe.database.stream.model.StreamEntity
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.AUDIO_LIVE_STREAM
import org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM
@Dao
abstract class StreamDAO : BasicDAO<StreamEntity> {
@Query("SELECT * FROM streams")
abstract override fun getAll(): Flowable<List<StreamEntity>>
@Query("DELETE FROM streams")
abstract override fun deleteAll(): Int
@Query("SELECT * FROM streams WHERE service_id = :serviceId")
abstract override fun listByService(serviceId: Int): Flowable<List<StreamEntity>>
@Query("SELECT * FROM streams WHERE url = :url AND service_id = :serviceId")
abstract fun getStream(serviceId: Long, url: String): Flowable<List<StreamEntity>>
@Insert(onConflict = OnConflictStrategy.IGNORE)
internal abstract fun silentInsertInternal(stream: StreamEntity): Long
@Insert(onConflict = OnConflictStrategy.IGNORE)
internal abstract fun silentInsertAllInternal(streams: List<StreamEntity>): List<Long>
@Query("""
SELECT uid, stream_type, textual_upload_date, upload_date, is_upload_date_approximation, duration
FROM streams WHERE url = :url AND service_id = :serviceId
""")
internal abstract fun getMinimalStreamForCompare(serviceId: Int, url: String): StreamCompareFeed?
@Transaction
open fun upsert(newerStream: StreamEntity): Long {
val uid = silentInsertInternal(newerStream)
if (uid != -1L) {
newerStream.uid = uid
return uid
}
compareAndUpdateStream(newerStream)
update(newerStream)
return newerStream.uid
}
@Transaction
open fun upsertAll(streams: List<StreamEntity>): List<Long> {
val insertUidList = silentInsertAllInternal(streams)
val streamIds = ArrayList<Long>(streams.size)
for ((index, uid) in insertUidList.withIndex()) {
val newerStream = streams[index]
if (uid != -1L) {
streamIds.add(uid)
newerStream.uid = uid
continue
}
compareAndUpdateStream(newerStream)
streamIds.add(newerStream.uid)
}
update(streams)
return streamIds
}
private fun compareAndUpdateStream(newerStream: StreamEntity) {
val existentMinimalStream = getMinimalStreamForCompare(newerStream.serviceId, newerStream.url)
?: throw IllegalStateException("Stream cannot be null just after insertion.")
newerStream.uid = existentMinimalStream.uid
val isNewerStreamLive = newerStream.streamType == AUDIO_LIVE_STREAM || newerStream.streamType == LIVE_STREAM
if (!isNewerStreamLive) {
// 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.
val hasBetterPrecision =
newerStream.uploadDate != null && newerStream.isUploadDateApproximation != true
if (existentMinimalStream.uploadDate != null && !hasBetterPrecision) {
newerStream.uploadDate = existentMinimalStream.uploadDate
newerStream.textualUploadDate = existentMinimalStream.textualUploadDate
newerStream.isUploadDateApproximation = existentMinimalStream.isUploadDateApproximation
}
if (existentMinimalStream.duration > 0 && newerStream.duration < 0) {
newerStream.duration = existentMinimalStream.duration
}
}
}
@Query("""
DELETE FROM streams WHERE
NOT EXISTS (SELECT 1 FROM stream_history sh
WHERE sh.stream_id = streams.uid)
AND NOT EXISTS (SELECT 1 FROM playlist_stream_join ps
WHERE ps.stream_id = streams.uid)
AND NOT EXISTS (SELECT 1 FROM feed f
WHERE f.stream_id = streams.uid)
""")
abstract fun deleteOrphans(): Int
/**
* Minimal entry class used when comparing/updating an existent stream.
*/
internal data class StreamCompareFeed(
@ColumnInfo(name = STREAM_ID)
var uid: Long = 0,
@ColumnInfo(name = StreamEntity.STREAM_TYPE)
var streamType: StreamType,
@ColumnInfo(name = StreamEntity.STREAM_TEXTUAL_UPLOAD_DATE)
var textualUploadDate: String? = null,
@ColumnInfo(name = StreamEntity.STREAM_UPLOAD_DATE)
var uploadDate: Date? = null,
@ColumnInfo(name = StreamEntity.STREAM_IS_UPLOAD_DATE_APPROXIMATION)
var isUploadDateApproximation: Boolean? = null,
@ColumnInfo(name = StreamEntity.STREAM_DURATION)
var duration: Long
)
}

View File

@@ -27,21 +27,21 @@ public abstract class StreamStateDAO implements BasicDAO<StreamStateEntity> {
public abstract int deleteAll();
@Override
public Flowable<List<StreamStateEntity>> listByService(int serviceId) {
public Flowable<List<StreamStateEntity>> listByService(final int serviceId) {
throw new UnsupportedOperationException();
}
@Query("SELECT * FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
public abstract Flowable<List<StreamStateEntity>> getState(final long streamId);
public abstract Flowable<List<StreamStateEntity>> getState(long streamId);
@Query("DELETE FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
public abstract int deleteState(final long streamId);
public abstract int deleteState(long streamId);
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract void silentInsertInternal(final StreamStateEntity streamState);
abstract void silentInsertInternal(StreamStateEntity streamState);
@Transaction
public long upsert(StreamStateEntity stream) {
public long upsert(final StreamStateEntity stream) {
silentInsertInternal(stream);
return update(stream);
}

View File

@@ -1,153 +0,0 @@
package org.schabi.newpipe.database.stream.model;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.Ignore;
import androidx.room.Index;
import androidx.room.PrimaryKey;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.util.Constants;
import java.io.Serializable;
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_SERVICE_ID;
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_URL;
@Entity(tableName = STREAM_TABLE,
indices = {@Index(value = {STREAM_SERVICE_ID, STREAM_URL}, unique = true)})
public class StreamEntity implements Serializable {
final public static String STREAM_TABLE = "streams";
final public static String STREAM_ID = "uid";
final public static String STREAM_SERVICE_ID = "service_id";
final public static String STREAM_URL = "url";
final public static String STREAM_TITLE = "title";
final public static String STREAM_TYPE = "stream_type";
final public static String STREAM_DURATION = "duration";
final public static String STREAM_UPLOADER = "uploader";
final public static String STREAM_THUMBNAIL_URL = "thumbnail_url";
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = STREAM_ID)
private long uid = 0;
@ColumnInfo(name = STREAM_SERVICE_ID)
private int serviceId = Constants.NO_SERVICE_ID;
@ColumnInfo(name = STREAM_URL)
private String url;
@ColumnInfo(name = STREAM_TITLE)
private String title;
@ColumnInfo(name = STREAM_TYPE)
private StreamType streamType;
@ColumnInfo(name = STREAM_DURATION)
private Long duration;
@ColumnInfo(name = STREAM_UPLOADER)
private String uploader;
@ColumnInfo(name = STREAM_THUMBNAIL_URL)
private String thumbnailUrl;
public StreamEntity(final int serviceId, final String title, final String url,
final StreamType streamType, final String thumbnailUrl, final String uploader,
final long duration) {
this.serviceId = serviceId;
this.title = title;
this.url = url;
this.streamType = streamType;
this.thumbnailUrl = thumbnailUrl;
this.uploader = uploader;
this.duration = duration;
}
@Ignore
public StreamEntity(final StreamInfoItem item) {
this(item.getServiceId(), item.getName(), item.getUrl(), item.getStreamType(), item.getThumbnailUrl(),
item.getUploaderName(), item.getDuration());
}
@Ignore
public StreamEntity(final StreamInfo info) {
this(info.getServiceId(), info.getName(), info.getUrl(), info.getStreamType(), info.getThumbnailUrl(),
info.getUploaderName(), info.getDuration());
}
@Ignore
public StreamEntity(final PlayQueueItem item) {
this(item.getServiceId(), item.getTitle(), item.getUrl(), item.getStreamType(),
item.getThumbnailUrl(), item.getUploader(), item.getDuration());
}
public long getUid() {
return uid;
}
public void setUid(long uid) {
this.uid = uid;
}
public int getServiceId() {
return serviceId;
}
public void setServiceId(int serviceId) {
this.serviceId = serviceId;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public StreamType getStreamType() {
return streamType;
}
public void setStreamType(StreamType type) {
this.streamType = type;
}
public Long getDuration() {
return duration;
}
public void setDuration(Long duration) {
this.duration = duration;
}
public String getUploader() {
return uploader;
}
public void setUploader(String uploader) {
this.uploader = uploader;
}
public String getThumbnailUrl() {
return thumbnailUrl;
}
public void setThumbnailUrl(String thumbnailUrl) {
this.thumbnailUrl = thumbnailUrl;
}
}

View File

@@ -0,0 +1,121 @@
package org.schabi.newpipe.database.stream.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.Index
import androidx.room.PrimaryKey
import java.io.Serializable
import java.util.Calendar
import java.util.Date
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_URL
import org.schabi.newpipe.extractor.localization.DateWrapper
import org.schabi.newpipe.extractor.stream.StreamInfo
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.player.playqueue.PlayQueueItem
@Entity(tableName = STREAM_TABLE,
indices = [
Index(value = [STREAM_SERVICE_ID, STREAM_URL], unique = true)
]
)
data class StreamEntity(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = STREAM_ID)
var uid: Long = 0,
@ColumnInfo(name = STREAM_SERVICE_ID)
var serviceId: Int,
@ColumnInfo(name = STREAM_URL)
var url: String,
@ColumnInfo(name = STREAM_TITLE)
var title: String,
@ColumnInfo(name = STREAM_TYPE)
var streamType: StreamType,
@ColumnInfo(name = STREAM_DURATION)
var duration: Long,
@ColumnInfo(name = STREAM_UPLOADER)
var uploader: String,
@ColumnInfo(name = STREAM_THUMBNAIL_URL)
var thumbnailUrl: String? = null,
@ColumnInfo(name = STREAM_VIEWS)
var viewCount: Long? = null,
@ColumnInfo(name = STREAM_TEXTUAL_UPLOAD_DATE)
var textualUploadDate: String? = null,
@ColumnInfo(name = STREAM_UPLOAD_DATE)
var uploadDate: Date? = null,
@ColumnInfo(name = STREAM_IS_UPLOAD_DATE_APPROXIMATION)
var isUploadDateApproximation: Boolean? = null
) : Serializable {
@Ignore
constructor(item: StreamInfoItem) : this(
serviceId = item.serviceId, url = item.url, title = item.name,
streamType = item.streamType, duration = item.duration, uploader = item.uploaderName,
thumbnailUrl = item.thumbnailUrl, viewCount = item.viewCount,
textualUploadDate = item.textualUploadDate, uploadDate = item.uploadDate?.date()?.time,
isUploadDateApproximation = item.uploadDate?.isApproximation
)
@Ignore
constructor(info: StreamInfo) : this(
serviceId = info.serviceId, url = info.url, title = info.name,
streamType = info.streamType, duration = info.duration, uploader = info.uploaderName,
thumbnailUrl = info.thumbnailUrl, viewCount = info.viewCount,
textualUploadDate = info.textualUploadDate, uploadDate = info.uploadDate?.date()?.time,
isUploadDateApproximation = info.uploadDate?.isApproximation
)
@Ignore
constructor(item: PlayQueueItem) : this(
serviceId = item.serviceId, url = item.url, title = item.title,
streamType = item.streamType, duration = item.duration, uploader = item.uploader,
thumbnailUrl = item.thumbnailUrl
)
fun toStreamInfoItem(): StreamInfoItem {
val item = StreamInfoItem(serviceId, url, title, streamType)
item.duration = duration
item.uploaderName = uploader
item.thumbnailUrl = thumbnailUrl
if (viewCount != null) item.viewCount = viewCount as Long
item.textualUploadDate = textualUploadDate
item.uploadDate = uploadDate?.let {
DateWrapper(Calendar.getInstance().apply { time = it }, isUploadDateApproximation
?: false)
}
return item
}
companion object {
const val STREAM_TABLE = "streams"
const val STREAM_ID = "uid"
const val STREAM_SERVICE_ID = "service_id"
const val STREAM_URL = "url"
const val STREAM_TITLE = "title"
const val STREAM_TYPE = "stream_type"
const val STREAM_DURATION = "duration"
const val STREAM_UPLOADER = "uploader"
const val STREAM_THUMBNAIL_URL = "thumbnail_url"
const val STREAM_VIEWS = "view_count"
const val STREAM_TEXTUAL_UPLOAD_DATE = "textual_upload_date"
const val STREAM_UPLOAD_DATE = "upload_date"
const val STREAM_IS_UPLOAD_DATE_APPROXIMATION = "is_upload_date_approximation"
}
}

View File

@@ -1,10 +1,9 @@
package org.schabi.newpipe.database.stream.model;
import androidx.annotation.Nullable;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.ForeignKey;
import androidx.annotation.Nullable;
import java.util.concurrent.TimeUnit;
@@ -21,14 +20,17 @@ import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_
onDelete = CASCADE, onUpdate = CASCADE)
})
public class StreamStateEntity {
final public static String STREAM_STATE_TABLE = "stream_state";
final public static String JOIN_STREAM_ID = "stream_id";
final public static String STREAM_PROGRESS_TIME = "progress_time";
public static final String STREAM_STATE_TABLE = "stream_state";
public static final String JOIN_STREAM_ID = "stream_id";
public static final String STREAM_PROGRESS_TIME = "progress_time";
/** Playback state will not be saved, if playback time less than this threshold */
/**
* Playback state will not be saved, if playback time is less than this threshold.
*/
private static final int PLAYBACK_SAVE_THRESHOLD_START_SECONDS = 5;
/** Playback state will not be saved, if time left less than this threshold */
/**
* Playback state will not be saved, if time left is less than this threshold.
*/
private static final int PLAYBACK_SAVE_THRESHOLD_END_SECONDS = 10;
@ColumnInfo(name = JOIN_STREAM_ID)
@@ -37,7 +39,7 @@ public class StreamStateEntity {
@ColumnInfo(name = STREAM_PROGRESS_TIME)
private long progressTime;
public StreamStateEntity(long streamUid, long progressTime) {
public StreamStateEntity(final long streamUid, final long progressTime) {
this.streamUid = streamUid;
this.progressTime = progressTime;
}
@@ -46,7 +48,7 @@ public class StreamStateEntity {
return streamUid;
}
public void setStreamUid(long streamUid) {
public void setStreamUid(final long streamUid) {
this.streamUid = streamUid;
}
@@ -54,21 +56,23 @@ public class StreamStateEntity {
return progressTime;
}
public void setProgressTime(long progressTime) {
public void setProgressTime(final long progressTime) {
this.progressTime = progressTime;
}
public boolean isValid(int durationInSeconds) {
public boolean isValid(final int durationInSeconds) {
final int seconds = (int) TimeUnit.MILLISECONDS.toSeconds(progressTime);
return seconds > PLAYBACK_SAVE_THRESHOLD_START_SECONDS
&& seconds < durationInSeconds - PLAYBACK_SAVE_THRESHOLD_END_SECONDS;
}
@Override
public boolean equals(@Nullable Object obj) {
public boolean equals(@Nullable final Object obj) {
if (obj instanceof StreamStateEntity) {
return ((StreamStateEntity) obj).streamUid == streamUid
&& ((StreamStateEntity) obj).progressTime == progressTime;
} else return false;
} else {
return false;
}
}
}

View File

@@ -1,69 +0,0 @@
package org.schabi.newpipe.database.subscription;
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import androidx.room.Transaction;
import org.schabi.newpipe.database.BasicDAO;
import java.util.List;
import io.reactivex.Flowable;
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_SERVICE_ID;
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_TABLE;
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_UID;
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_URL;
@Dao
public abstract class SubscriptionDAO implements BasicDAO<SubscriptionEntity> {
@Override
@Query("SELECT * FROM " + SUBSCRIPTION_TABLE)
public abstract Flowable<List<SubscriptionEntity>> getAll();
@Override
@Query("DELETE FROM " + SUBSCRIPTION_TABLE)
public abstract int deleteAll();
@Override
@Query("SELECT * FROM " + SUBSCRIPTION_TABLE + " WHERE " + SUBSCRIPTION_SERVICE_ID + " = :serviceId")
public abstract Flowable<List<SubscriptionEntity>> listByService(int serviceId);
@Query("SELECT * FROM " + SUBSCRIPTION_TABLE + " WHERE " +
SUBSCRIPTION_URL + " LIKE :url AND " +
SUBSCRIPTION_SERVICE_ID + " = :serviceId")
public abstract Flowable<List<SubscriptionEntity>> getSubscription(int serviceId, String url);
@Query("SELECT " + SUBSCRIPTION_UID + " FROM " + SUBSCRIPTION_TABLE + " WHERE " +
SUBSCRIPTION_URL + " LIKE :url AND " +
SUBSCRIPTION_SERVICE_ID + " = :serviceId")
abstract Long getSubscriptionIdInternal(int serviceId, String url);
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract Long insertInternal(final SubscriptionEntity entities);
@Transaction
public List<SubscriptionEntity> upsertAll(List<SubscriptionEntity> entities) {
for (SubscriptionEntity entity : entities) {
Long uid = insertInternal(entity);
if (uid != -1) {
entity.setUid(uid);
continue;
}
uid = getSubscriptionIdInternal(entity.getServiceId(), entity.getUrl());
entity.setUid(uid);
if (uid == -1) {
throw new IllegalStateException("Invalid subscription id (-1)");
}
update(entity);
}
return entities;
}
}

View File

@@ -0,0 +1,103 @@
package org.schabi.newpipe.database.subscription
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import io.reactivex.Flowable
import io.reactivex.Maybe
import org.schabi.newpipe.database.BasicDAO
@Dao
abstract class SubscriptionDAO : BasicDAO<SubscriptionEntity> {
@Query("SELECT COUNT(*) FROM subscriptions")
abstract fun rowCount(): Flowable<Long>
@Query("SELECT * FROM subscriptions WHERE service_id = :serviceId")
abstract override fun listByService(serviceId: Int): Flowable<List<SubscriptionEntity>>
@Query("SELECT * FROM subscriptions ORDER BY name COLLATE NOCASE ASC")
abstract override fun getAll(): Flowable<List<SubscriptionEntity>>
@Query("""
SELECT * FROM subscriptions
WHERE name LIKE '%' || :filter || '%'
ORDER BY name COLLATE NOCASE ASC
""")
abstract fun getSubscriptionsFiltered(filter: String): Flowable<List<SubscriptionEntity>>
@Query("""
SELECT * FROM subscriptions s
LEFT JOIN feed_group_subscription_join fgs
ON s.uid = fgs.subscription_id
WHERE (fgs.subscription_id IS NULL OR fgs.group_id = :currentGroupId)
ORDER BY name COLLATE NOCASE ASC
""")
abstract fun getSubscriptionsOnlyUngrouped(
currentGroupId: Long
): Flowable<List<SubscriptionEntity>>
@Query("""
SELECT * FROM subscriptions s
LEFT JOIN feed_group_subscription_join fgs
ON s.uid = fgs.subscription_id
WHERE (fgs.subscription_id IS NULL OR fgs.group_id = :currentGroupId)
AND s.name LIKE '%' || :filter || '%'
ORDER BY name COLLATE NOCASE ASC
""")
abstract fun getSubscriptionsOnlyUngroupedFiltered(
currentGroupId: Long,
filter: String
): Flowable<List<SubscriptionEntity>>
@Query("SELECT * FROM subscriptions WHERE url LIKE :url AND service_id = :serviceId")
abstract fun getSubscriptionFlowable(serviceId: Int, url: String): Flowable<List<SubscriptionEntity>>
@Query("SELECT * FROM subscriptions WHERE url LIKE :url AND service_id = :serviceId")
abstract fun getSubscription(serviceId: Int, url: String): Maybe<SubscriptionEntity>
@Query("SELECT * FROM subscriptions WHERE uid = :subscriptionId")
abstract fun getSubscription(subscriptionId: Long): SubscriptionEntity
@Query("DELETE FROM subscriptions")
abstract override fun deleteAll(): Int
@Query("DELETE FROM subscriptions WHERE url LIKE :url AND service_id = :serviceId")
abstract fun deleteSubscription(serviceId: Int, url: String): Int
@Query("SELECT uid FROM subscriptions WHERE url LIKE :url AND service_id = :serviceId")
internal abstract fun getSubscriptionIdInternal(serviceId: Int, url: String): Long?
@Insert(onConflict = OnConflictStrategy.IGNORE)
internal abstract fun silentInsertAllInternal(entities: List<SubscriptionEntity>): List<Long>
@Transaction
open fun upsertAll(entities: List<SubscriptionEntity>): List<SubscriptionEntity> {
val insertUidList = silentInsertAllInternal(entities)
insertUidList.forEachIndexed { index: Int, uidFromInsert: Long ->
val entity = entities[index]
if (uidFromInsert != -1L) {
entity.uid = uidFromInsert
} else {
val subscriptionIdFromDb = getSubscriptionIdInternal(entity.serviceId, entity.url)
?: throw IllegalStateException("Subscription cannot be null just after insertion.")
entity.uid = subscriptionIdFromDb
update(entity)
}
}
return entities
}
}

View File

@@ -1,11 +1,11 @@
package org.schabi.newpipe.database.subscription;
import androidx.annotation.NonNull;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.Ignore;
import androidx.room.Index;
import androidx.room.PrimaryKey;
import androidx.annotation.NonNull;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
@@ -18,15 +18,14 @@ import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCR
@Entity(tableName = SUBSCRIPTION_TABLE,
indices = {@Index(value = {SUBSCRIPTION_SERVICE_ID, SUBSCRIPTION_URL}, unique = true)})
public class SubscriptionEntity {
final static String SUBSCRIPTION_UID = "uid";
final static String SUBSCRIPTION_TABLE = "subscriptions";
final static String SUBSCRIPTION_SERVICE_ID = "service_id";
final static String SUBSCRIPTION_URL = "url";
final static String SUBSCRIPTION_NAME = "name";
final static String SUBSCRIPTION_AVATAR_URL = "avatar_url";
final static String SUBSCRIPTION_SUBSCRIBER_COUNT = "subscriber_count";
final static String SUBSCRIPTION_DESCRIPTION = "description";
public static final String SUBSCRIPTION_UID = "uid";
public static final String SUBSCRIPTION_TABLE = "subscriptions";
public static final String SUBSCRIPTION_SERVICE_ID = "service_id";
public static final String SUBSCRIPTION_URL = "url";
public static final String SUBSCRIPTION_NAME = "name";
public static final String SUBSCRIPTION_AVATAR_URL = "avatar_url";
public static final String SUBSCRIPTION_SUBSCRIBER_COUNT = "subscriber_count";
public static final String SUBSCRIPTION_DESCRIPTION = "description";
@PrimaryKey(autoGenerate = true)
private long uid = 0;
@@ -49,11 +48,21 @@ public class SubscriptionEntity {
@ColumnInfo(name = SUBSCRIPTION_DESCRIPTION)
private String description;
@Ignore
public static SubscriptionEntity from(@NonNull final ChannelInfo info) {
SubscriptionEntity result = new SubscriptionEntity();
result.setServiceId(info.getServiceId());
result.setUrl(info.getUrl());
result.setData(info.getName(), info.getAvatarUrl(), info.getDescription(),
info.getSubscriberCount());
return result;
}
public long getUid() {
return uid;
}
public void setUid(long uid) {
public void setUid(final long uid) {
this.uid = uid;
}
@@ -61,7 +70,7 @@ public class SubscriptionEntity {
return serviceId;
}
public void setServiceId(int serviceId) {
public void setServiceId(final int serviceId) {
this.serviceId = serviceId;
}
@@ -69,7 +78,7 @@ public class SubscriptionEntity {
return url;
}
public void setUrl(String url) {
public void setUrl(final String url) {
this.url = url;
}
@@ -77,7 +86,7 @@ public class SubscriptionEntity {
return name;
}
public void setName(String name) {
public void setName(final String name) {
this.name = name;
}
@@ -85,7 +94,7 @@ public class SubscriptionEntity {
return avatarUrl;
}
public void setAvatarUrl(String avatarUrl) {
public void setAvatarUrl(final String avatarUrl) {
this.avatarUrl = avatarUrl;
}
@@ -93,7 +102,7 @@ public class SubscriptionEntity {
return subscriberCount;
}
public void setSubscriberCount(Long subscriberCount) {
public void setSubscriberCount(final Long subscriberCount) {
this.subscriberCount = subscriberCount;
}
@@ -101,19 +110,16 @@ public class SubscriptionEntity {
return description;
}
public void setDescription(String description) {
public void setDescription(final String description) {
this.description = description;
}
@Ignore
public void setData(final String name,
final String avatarUrl,
final String description,
final Long subscriberCount) {
this.setName(name);
this.setAvatarUrl(avatarUrl);
this.setDescription(description);
this.setSubscriberCount(subscriberCount);
public void setData(final String n, final String au, final String d, final Long sc) {
this.setName(n);
this.setAvatarUrl(au);
this.setDescription(d);
this.setSubscriberCount(sc);
}
@Ignore
@@ -125,12 +131,54 @@ public class SubscriptionEntity {
return item;
}
@Ignore
public static SubscriptionEntity from(@NonNull ChannelInfo info) {
SubscriptionEntity result = new SubscriptionEntity();
result.setServiceId(info.getServiceId());
result.setUrl(info.getUrl());
result.setData(info.getName(), info.getAvatarUrl(), info.getDescription(), info.getSubscriberCount());
// TODO: Remove these generated methods by migrating this class to a data class from Kotlin.
@Override
@SuppressWarnings("EqualsReplaceableByObjectsCall")
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final SubscriptionEntity that = (SubscriptionEntity) o;
if (uid != that.uid) {
return false;
}
if (serviceId != that.serviceId) {
return false;
}
if (!url.equals(that.url)) {
return false;
}
if (name != null ? !name.equals(that.name) : that.name != null) {
return false;
}
if (avatarUrl != null ? !avatarUrl.equals(that.avatarUrl) : that.avatarUrl != null) {
return false;
}
if (subscriberCount != null
? !subscriberCount.equals(that.subscriberCount)
: that.subscriberCount != null) {
return false;
}
return description != null
? description.equals(that.description)
: that.description == null;
}
@Override
public int hashCode() {
int result = (int) (uid ^ (uid >>> 32));
result = 31 * result + serviceId;
result = 31 * result + url.hashCode();
result = 31 * result + (name != null ? name.hashCode() : 0);
result = 31 * result + (avatarUrl != null ? avatarUrl.hashCode() : 0);
result = 31 * result + (subscriberCount != null ? subscriberCount.hashCode() : 0);
result = 31 * result + (description != null ? description.hashCode() : 0);
return result;
}
}

View File

@@ -3,32 +3,37 @@ package org.schabi.newpipe.download;
import android.app.FragmentTransaction;
import android.content.Intent;
import android.os.Bundle;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.ViewTreeObserver;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import org.schabi.newpipe.R;
import org.schabi.newpipe.settings.SettingsActivity;
import org.schabi.newpipe.util.AndroidTvUtils;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.views.FocusOverlayView;
import us.shandian.giga.service.DownloadManagerService;
import us.shandian.giga.ui.fragment.MissionsFragment;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
public class DownloadActivity extends AppCompatActivity {
private static final String MISSIONS_FRAGMENT_TAG = "fragment_tag";
@Override
protected void onCreate(Bundle savedInstanceState) {
protected void onCreate(final Bundle savedInstanceState) {
// Service
Intent i = new Intent();
i.setClass(this, DownloadManagerService.class);
startService(i);
assureCorrectAppLanguage(this);
ThemeHelper.setTheme(this);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_downloader);
@@ -43,13 +48,18 @@ public class DownloadActivity extends AppCompatActivity {
actionBar.setDisplayShowTitleEnabled(true);
}
getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
getWindow().getDecorView().getViewTreeObserver()
.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
updateFragments();
getWindow().getDecorView().getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
});
if (AndroidTvUtils.isTv(this)) {
FocusOverlayView.setupFocusObserver(this);
}
}
private void updateFragments() {
@@ -62,7 +72,7 @@ public class DownloadActivity extends AppCompatActivity {
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
public boolean onCreateOptionsMenu(final Menu menu) {
super.onCreateOptionsMenu(menu);
MenuInflater inflater = getMenuInflater();
@@ -72,17 +82,11 @@ public class DownloadActivity extends AppCompatActivity {
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home: {
case android.R.id.home:
onBackPressed();
return true;
}
case R.id.action_settings: {
Intent intent = new Intent(this, SettingsActivity.class);
startActivity(intent);
return true;
}
default:
return super.onOptionsItemSelected(item);
}

View File

@@ -11,15 +11,6 @@ import android.os.Bundle;
import android.os.Environment;
import android.os.IBinder;
import android.preference.PreferenceManager;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.fragment.app.DialogFragment;
import androidx.documentfile.provider.DocumentFile;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.view.menu.ActionMenuItemView;
import androidx.appcompat.widget.Toolbar;
import android.util.Log;
import android.util.SparseArray;
import android.view.LayoutInflater;
@@ -34,10 +25,21 @@ import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.view.menu.ActionMenuItemView;
import androidx.appcompat.widget.Toolbar;
import androidx.documentfile.provider.DocumentFile;
import androidx.fragment.app.DialogFragment;
import com.nononsenseapps.filepicker.Utils;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.RouterActivity;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.localization.Localization;
@@ -77,25 +79,35 @@ import us.shandian.giga.service.DownloadManagerService;
import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder;
import us.shandian.giga.service.MissionState;
public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener {
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
public class DownloadDialog extends DialogFragment
implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener {
private static final String TAG = "DialogFragment";
private static final boolean DEBUG = MainActivity.DEBUG;
private static final int REQUEST_DOWNLOAD_SAVE_AS = 0x1230;
@State
protected StreamInfo currentInfo;
StreamInfo currentInfo;
@State
protected StreamSizeWrapper<AudioStream> wrappedAudioStreams = StreamSizeWrapper.empty();
StreamSizeWrapper<AudioStream> wrappedAudioStreams = StreamSizeWrapper.empty();
@State
protected StreamSizeWrapper<VideoStream> wrappedVideoStreams = StreamSizeWrapper.empty();
StreamSizeWrapper<VideoStream> wrappedVideoStreams = StreamSizeWrapper.empty();
@State
protected StreamSizeWrapper<SubtitlesStream> wrappedSubtitleStreams = StreamSizeWrapper.empty();
StreamSizeWrapper<SubtitlesStream> wrappedSubtitleStreams = StreamSizeWrapper.empty();
@State
protected int selectedVideoIndex = 0;
int selectedVideoIndex = 0;
@State
protected int selectedAudioIndex = 0;
int selectedAudioIndex = 0;
@State
protected int selectedSubtitleIndex = 0;
int selectedSubtitleIndex = 0;
private StoredDirectoryHelper mainStorageAudio = null;
private StoredDirectoryHelper mainStorageVideo = null;
private DownloadManager downloadManager = null;
private ActionMenuItemView okButton = null;
private Context context;
private boolean askForSavePath;
private StreamItemAdapter<AudioStream, Stream> audioStreamsAdapter;
private StreamItemAdapter<VideoStream, AudioStream> videoStreamsAdapter;
@@ -111,15 +123,16 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
private SharedPreferences prefs;
public static DownloadDialog newInstance(StreamInfo info) {
public static DownloadDialog newInstance(final StreamInfo info) {
DownloadDialog dialog = new DownloadDialog();
dialog.setInfo(info);
return dialog;
}
public static DownloadDialog newInstance(Context context, StreamInfo info) {
final ArrayList<VideoStream> streamsList = new ArrayList<>(ListHelper.getSortedStreamVideosList(context,
info.getVideoStreams(), info.getVideoOnlyStreams(), false));
public static DownloadDialog newInstance(final Context context, final StreamInfo info) {
final ArrayList<VideoStream> streamsList = new ArrayList<>(ListHelper
.getSortedStreamVideosList(context, info.getVideoStreams(),
info.getVideoOnlyStreams(), false));
final int selectedStreamIndex = ListHelper.getDefaultResolutionIndex(context, streamsList);
final DownloadDialog instance = newInstance(info);
@@ -131,57 +144,61 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
return instance;
}
private void setInfo(StreamInfo info) {
private void setInfo(final StreamInfo info) {
this.currentInfo = info;
}
public void setAudioStreams(List<AudioStream> audioStreams) {
public void setAudioStreams(final List<AudioStream> audioStreams) {
setAudioStreams(new StreamSizeWrapper<>(audioStreams, getContext()));
}
public void setAudioStreams(StreamSizeWrapper<AudioStream> wrappedAudioStreams) {
this.wrappedAudioStreams = wrappedAudioStreams;
public void setAudioStreams(final StreamSizeWrapper<AudioStream> was) {
this.wrappedAudioStreams = was;
}
public void setVideoStreams(List<VideoStream> videoStreams) {
public void setVideoStreams(final List<VideoStream> videoStreams) {
setVideoStreams(new StreamSizeWrapper<>(videoStreams, getContext()));
}
public void setVideoStreams(StreamSizeWrapper<VideoStream> wrappedVideoStreams) {
this.wrappedVideoStreams = wrappedVideoStreams;
}
public void setSubtitleStreams(List<SubtitlesStream> subtitleStreams) {
setSubtitleStreams(new StreamSizeWrapper<>(subtitleStreams, getContext()));
}
public void setSubtitleStreams(StreamSizeWrapper<SubtitlesStream> wrappedSubtitleStreams) {
this.wrappedSubtitleStreams = wrappedSubtitleStreams;
}
public void setSelectedVideoStream(int selectedVideoIndex) {
this.selectedVideoIndex = selectedVideoIndex;
}
public void setSelectedAudioStream(int selectedAudioIndex) {
this.selectedAudioIndex = selectedAudioIndex;
}
public void setSelectedSubtitleStream(int selectedSubtitleIndex) {
this.selectedSubtitleIndex = selectedSubtitleIndex;
}
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (DEBUG)
Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
public void setVideoStreams(final StreamSizeWrapper<VideoStream> wvs) {
this.wrappedVideoStreams = wvs;
}
if (!PermissionHelper.checkStoragePermissions(getActivity(), PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
public void setSubtitleStreams(final List<SubtitlesStream> subtitleStreams) {
setSubtitleStreams(new StreamSizeWrapper<>(subtitleStreams, getContext()));
}
public void setSubtitleStreams(
final StreamSizeWrapper<SubtitlesStream> wss) {
this.wrappedSubtitleStreams = wss;
}
public void setSelectedVideoStream(final int svi) {
this.selectedVideoIndex = svi;
}
public void setSelectedAudioStream(final int sai) {
this.selectedAudioIndex = sai;
}
public void setSelectedSubtitleStream(final int ssi) {
this.selectedSubtitleIndex = ssi;
}
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (DEBUG) {
Log.d(TAG, "onCreate() called with: "
+ "savedInstanceState = [" + savedInstanceState + "]");
}
if (!PermissionHelper.checkStoragePermissions(getActivity(),
PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
getDialog().dismiss();
return;
}
@@ -195,17 +212,23 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
List<VideoStream> videoStreams = wrappedVideoStreams.getStreamsList();
for (int i = 0; i < videoStreams.size(); i++) {
if (!videoStreams.get(i).isVideoOnly()) continue;
AudioStream audioStream = SecondaryStreamHelper.getAudioStreamFor(wrappedAudioStreams.getStreamsList(), videoStreams.get(i));
if (!videoStreams.get(i).isVideoOnly()) {
continue;
}
AudioStream audioStream = SecondaryStreamHelper
.getAudioStreamFor(wrappedAudioStreams.getStreamsList(), videoStreams.get(i));
if (audioStream != null) {
secondaryStreams.append(i, new SecondaryStreamHelper<>(wrappedAudioStreams, audioStream));
secondaryStreams
.append(i, new SecondaryStreamHelper<>(wrappedAudioStreams, audioStream));
} else if (DEBUG) {
Log.w(TAG, "No audio stream candidates for video format " + videoStreams.get(i).getFormat().name());
Log.w(TAG, "No audio stream candidates for video format "
+ videoStreams.get(i).getFormat().name());
}
}
this.videoStreamsAdapter = new StreamItemAdapter<>(context, wrappedVideoStreams, secondaryStreams);
this.videoStreamsAdapter = new StreamItemAdapter<>(context, wrappedVideoStreams,
secondaryStreams);
this.audioStreamsAdapter = new StreamItemAdapter<>(context, wrappedAudioStreams);
this.subtitleStreamsAdapter = new StreamItemAdapter<>(context, wrappedSubtitleStreams);
@@ -214,7 +237,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
context.bindService(intent, new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName cname, IBinder service) {
public void onServiceConnected(final ComponentName cname, final IBinder service) {
DownloadManagerBinder mgr = (DownloadManagerBinder) service;
mainStorageAudio = mgr.getMainStorageAudio();
@@ -228,25 +251,34 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
}
@Override
public void onServiceDisconnected(ComponentName name) {
public void onServiceDisconnected(final ComponentName name) {
// nothing to do
}
}, Context.BIND_AUTO_CREATE);
}
/*//////////////////////////////////////////////////////////////////////////
// Inits
//////////////////////////////////////////////////////////////////////////*/
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
if (DEBUG)
Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]");
public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container,
final Bundle savedInstanceState) {
if (DEBUG) {
Log.d(TAG, "onCreateView() called with: "
+ "inflater = [" + inflater + "], container = [" + container + "], "
+ "savedInstanceState = [" + savedInstanceState + "]");
}
return inflater.inflate(R.layout.download_dialog, container);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
nameEditText = view.findViewById(R.id.file_name);
nameEditText.setText(FilenameUtils.createFilename(getContext(), currentInfo.getName()));
selectedAudioIndex = ListHelper.getDefaultAudioFormat(getContext(), currentInfo.getAudioStreams());
selectedAudioIndex = ListHelper
.getDefaultAudioFormat(getContext(), currentInfo.getAudioStreams());
selectedSubtitleIndex = getSubtitleIndexBy(subtitleStreamsAdapter.getAll());
@@ -268,21 +300,20 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
threadsCountTextView.setText(String.valueOf(threads));
threadsSeekBar.setProgress(threads - 1);
threadsSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekbar, int progress, boolean fromUser) {
progress++;
prefs.edit().putInt(getString(R.string.default_download_threads), progress).apply();
threadsCountTextView.setText(String.valueOf(progress));
public void onProgressChanged(final SeekBar seekbar, final int progress,
final boolean fromUser) {
final int newProgress = progress + 1;
prefs.edit().putInt(getString(R.string.default_download_threads), newProgress)
.apply();
threadsCountTextView.setText(String.valueOf(newProgress));
}
@Override
public void onStartTrackingTouch(SeekBar p1) {
}
public void onStartTrackingTouch(final SeekBar p1) { }
@Override
public void onStopTrackingTouch(SeekBar p1) {
}
public void onStopTrackingTouch(final SeekBar p1) { }
});
fetchStreamsSize();
@@ -291,17 +322,20 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
private void fetchStreamsSize() {
disposables.clear();
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedVideoStreams).subscribe(result -> {
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedVideoStreams)
.subscribe(result -> {
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.video_button) {
setupVideoSpinner();
}
}));
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedAudioStreams).subscribe(result -> {
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedAudioStreams)
.subscribe(result -> {
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.audio_button) {
setupAudioSpinner();
}
}));
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedSubtitleStreams).subscribe(result -> {
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedSubtitleStreams)
.subscribe(result -> {
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.subtitle_button) {
setupSubtitleSpinner();
}
@@ -314,14 +348,22 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
disposables.clear();
}
/*//////////////////////////////////////////////////////////////////////////
// Radio group Video&Audio options - Listener
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
public void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState);
Icepick.saveInstanceState(this, outState);
}
/*//////////////////////////////////////////////////////////////////////////
// Streams Spinner Listener
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_DOWNLOAD_SAVE_AS && resultCode == Activity.RESULT_OK) {
@@ -332,7 +374,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
if (FilePickerActivityHelper.isOwnFileUri(context, data.getData())) {
File file = Utils.getFileForUri(data.getData());
checkSelectedDownload(null, Uri.fromFile(file), file.getName(), StoredFileHelper.DEFAULT_MIME);
checkSelectedDownload(null, Uri.fromFile(file), file.getName(),
StoredFileHelper.DEFAULT_MIME);
return;
}
@@ -343,39 +386,46 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
}
// check if the selected file was previously used
checkSelectedDownload(null, data.getData(), docFile.getName(), docFile.getType());
checkSelectedDownload(null, data.getData(), docFile.getName(),
docFile.getType());
}
}
/*//////////////////////////////////////////////////////////////////////////
// Inits
//////////////////////////////////////////////////////////////////////////*/
private void initToolbar(Toolbar toolbar) {
if (DEBUG) Log.d(TAG, "initToolbar() called with: toolbar = [" + toolbar + "]");
boolean isLight = ThemeHelper.isLightThemeSelected(getActivity());
private void initToolbar(final Toolbar toolbar) {
if (DEBUG) {
Log.d(TAG, "initToolbar() called with: toolbar = [" + toolbar + "]");
}
toolbar.setTitle(R.string.download_dialog_title);
toolbar.setNavigationIcon(isLight ? R.drawable.ic_arrow_back_black_24dp : R.drawable.ic_arrow_back_white_24dp);
toolbar.setNavigationIcon(
ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_arrow_back));
toolbar.inflateMenu(R.menu.dialog_url);
toolbar.setNavigationOnClickListener(v -> getDialog().dismiss());
toolbar.setNavigationOnClickListener(v -> requireDialog().dismiss());
toolbar.setNavigationContentDescription(R.string.cancel);
okButton = toolbar.findViewById(R.id.okay);
okButton.setEnabled(false);// disable until the download service connection is done
okButton.setEnabled(false); // disable until the download service connection is done
toolbar.setOnMenuItemClickListener(item -> {
if (item.getItemId() == R.id.okay) {
prepareSelectedDownload();
if (getActivity() instanceof RouterActivity) {
getActivity().finish();
}
return true;
}
return false;
});
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
private void setupAudioSpinner() {
if (getContext() == null) return;
if (getContext() == null) {
return;
}
streamsSpinner.setAdapter(audioStreamsAdapter);
streamsSpinner.setSelection(selectedAudioIndex);
@@ -383,7 +433,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
}
private void setupVideoSpinner() {
if (getContext() == null) return;
if (getContext() == null) {
return;
}
streamsSpinner.setAdapter(videoStreamsAdapter);
streamsSpinner.setSelection(selectedVideoIndex);
@@ -391,21 +443,21 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
}
private void setupSubtitleSpinner() {
if (getContext() == null) return;
if (getContext() == null) {
return;
}
streamsSpinner.setAdapter(subtitleStreamsAdapter);
streamsSpinner.setSelection(selectedSubtitleIndex);
setRadioButtonsState(true);
}
/*//////////////////////////////////////////////////////////////////////////
// Radio group Video&Audio options - Listener
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCheckedChanged(RadioGroup group, @IdRes int checkedId) {
if (DEBUG)
Log.d(TAG, "onCheckedChanged() called with: group = [" + group + "], checkedId = [" + checkedId + "]");
public void onCheckedChanged(final RadioGroup group, @IdRes final int checkedId) {
if (DEBUG) {
Log.d(TAG, "onCheckedChanged() called with: "
+ "group = [" + group + "], checkedId = [" + checkedId + "]");
}
boolean flag = true;
switch (checkedId) {
@@ -424,14 +476,14 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
threadsSeekBar.setEnabled(flag);
}
/*//////////////////////////////////////////////////////////////////////////
// Streams Spinner Listener
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (DEBUG)
Log.d(TAG, "onItemSelected() called with: parent = [" + parent + "], view = [" + view + "], position = [" + position + "], id = [" + id + "]");
public void onItemSelected(final AdapterView<?> parent, final View view,
final int position, final long id) {
if (DEBUG) {
Log.d(TAG, "onItemSelected() called with: "
+ "parent = [" + parent + "], view = [" + view + "], "
+ "position = [" + position + "], id = [" + id + "]");
}
switch (radioStreamsGroup.getCheckedRadioButtonId()) {
case R.id.audio_button:
selectedAudioIndex = position;
@@ -446,13 +498,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
public void onNothingSelected(final AdapterView<?> parent) {
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
protected void setupDownloadOptions() {
setRadioButtonsState(false);
@@ -477,30 +525,36 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
subtitleButton.setChecked(true);
setupSubtitleSpinner();
} else {
Toast.makeText(getContext(), R.string.no_streams_available_download, Toast.LENGTH_SHORT).show();
Toast.makeText(getContext(), R.string.no_streams_available_download,
Toast.LENGTH_SHORT).show();
getDialog().dismiss();
}
}
private void setRadioButtonsState(boolean enabled) {
private void setRadioButtonsState(final boolean enabled) {
radioStreamsGroup.findViewById(R.id.audio_button).setEnabled(enabled);
radioStreamsGroup.findViewById(R.id.video_button).setEnabled(enabled);
radioStreamsGroup.findViewById(R.id.subtitle_button).setEnabled(enabled);
}
private int getSubtitleIndexBy(List<SubtitlesStream> streams) {
private int getSubtitleIndexBy(final List<SubtitlesStream> streams) {
final Localization preferredLocalization = NewPipe.getPreferredLocalization();
int candidate = 0;
for (int i = 0; i < streams.size(); i++) {
final Locale streamLocale = streams.get(i).getLocale();
final boolean languageEquals = streamLocale.getLanguage() != null && preferredLocalization.getLanguageCode() != null &&
streamLocale.getLanguage().equals(new Locale(preferredLocalization.getLanguageCode()).getLanguage());
final boolean countryEquals = streamLocale.getCountry() != null && streamLocale.getCountry().equals(preferredLocalization.getCountryCode());
final boolean languageEquals = streamLocale.getLanguage() != null
&& preferredLocalization.getLanguageCode() != null
&& streamLocale.getLanguage()
.equals(new Locale(preferredLocalization.getLanguageCode()).getLanguage());
final boolean countryEquals = streamLocale.getCountry() != null
&& streamLocale.getCountry().equals(preferredLocalization.getCountryCode());
if (languageEquals) {
if (countryEquals) return i;
if (countryEquals) {
return i;
}
candidate = i;
}
@@ -509,35 +563,30 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
return candidate;
}
StoredDirectoryHelper mainStorageAudio = null;
StoredDirectoryHelper mainStorageVideo = null;
DownloadManager downloadManager = null;
ActionMenuItemView okButton = null;
Context context;
boolean askForSavePath;
private String getNameEditText() {
String str = nameEditText.getText().toString().trim();
return FilenameUtils.createFilename(context, str.isEmpty() ? currentInfo.getName() : str);
}
private void showFailedDialog(@StringRes int msg) {
private void showFailedDialog(@StringRes final int msg) {
assureCorrectAppLanguage(getContext());
new AlertDialog.Builder(context)
.setTitle(R.string.general_error)
.setMessage(msg)
.setNegativeButton(android.R.string.ok, null)
.setNegativeButton(getString(R.string.finish), null)
.create()
.show();
}
private void showErrorActivity(Exception e) {
private void showErrorActivity(final Exception e) {
ErrorActivity.reportError(
context,
Collections.singletonList(e),
null,
null,
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "-", "-", R.string.general_error)
ErrorActivity.ErrorInfo
.make(UserAction.SOMETHING_ELSE, "-", "-", R.string.general_error)
);
}
@@ -555,8 +604,16 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
case R.id.audio_button:
mainStorage = mainStorageAudio;
format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
mime = format.mimeType;
filename += format.suffix;
switch (format) {
case WEBMA_OPUS:
mime = "audio/ogg";
filename += "opus";
break;
default:
mime = format.mimeType;
filename += format.suffix;
break;
}
break;
case R.id.video_button:
mainStorage = mainStorageVideo;
@@ -565,7 +622,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
filename += format.suffix;
break;
case R.id.subtitle_button:
mainStorage = mainStorageVideo;// subtitle & video files go together
mainStorage = mainStorageVideo; // subtitle & video files go together
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
mime = format.mimeType;
filename += format == MediaFormat.TTML ? MediaFormat.SRT.suffix : format.suffix;
@@ -580,23 +637,25 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
// * save path not defined (via download settings)
// * the user checked the "ask where to download" option
if (!askForSavePath)
Toast.makeText(context, getString(R.string.no_available_dir), Toast.LENGTH_LONG).show();
if (!askForSavePath) {
Toast.makeText(context, getString(R.string.no_available_dir),
Toast.LENGTH_LONG).show();
}
if (NewPipeSettings.useStorageAccessFramework(context)) {
StoredFileHelper.requestSafWithFileCreation(this, REQUEST_DOWNLOAD_SAVE_AS, filename, mime);
StoredFileHelper.requestSafWithFileCreation(this, REQUEST_DOWNLOAD_SAVE_AS,
filename, mime);
} else {
File initialSavePath;
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.audio_button)
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.audio_button) {
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MUSIC);
else
} else {
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MOVIES);
}
initialSavePath = new File(initialSavePath, filename);
startActivityForResult(
FilePickerActivityHelper.chooseFileToSave(context, initialSavePath.getAbsolutePath()),
REQUEST_DOWNLOAD_SAVE_AS
);
startActivityForResult(FilePickerActivityHelper.chooseFileToSave(context,
initialSavePath.getAbsolutePath()), REQUEST_DOWNLOAD_SAVE_AS);
}
return;
@@ -606,7 +665,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
checkSelectedDownload(mainStorage, mainStorage.findFile(filename), filename, mime);
}
private void checkSelectedDownload(StoredDirectoryHelper mainStorage, Uri targetFile, String filename, String mime) {
private void checkSelectedDownload(final StoredDirectoryHelper mainStorage,
final Uri targetFile, final String filename,
final String mime) {
StoredFileHelper storage;
try {
@@ -615,10 +676,12 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
storage = new StoredFileHelper(context, null, targetFile, "");
} else if (targetFile == null) {
// the file does not exist, but it is probably used in a pending download
storage = new StoredFileHelper(mainStorage.getUri(), filename, mime, mainStorage.getTag());
storage = new StoredFileHelper(mainStorage.getUri(), filename, mime,
mainStorage.getTag());
} else {
// the target filename is already use, attempt to use it
storage = new StoredFileHelper(context, mainStorage.getUri(), targetFile, mainStorage.getTag());
storage = new StoredFileHelper(context, mainStorage.getUri(), targetFile,
mainStorage.getTag());
}
} catch (Exception e) {
showErrorActivity(e);
@@ -722,24 +785,28 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
} else {
try {
// try take (or steal) the file
storageNew = new StoredFileHelper(context, mainStorage.getUri(), targetFile, mainStorage.getTag());
storageNew = new StoredFileHelper(context, mainStorage.getUri(),
targetFile, mainStorage.getTag());
} catch (IOException e) {
Log.e(TAG, "Failed to take (or steal) the file in " + targetFile.toString());
Log.e(TAG, "Failed to take (or steal) the file in "
+ targetFile.toString());
storageNew = null;
}
}
if (storageNew != null && storageNew.canWrite())
if (storageNew != null && storageNew.canWrite()) {
continueSelectedDownload(storageNew);
else
} else {
showFailedDialog(R.string.error_file_creation);
}
break;
case PendingRunning:
storageNew = mainStorage.createUniqueFile(filename, mime);
if (storageNew == null)
if (storageNew == null) {
showFailedDialog(R.string.error_file_creation);
else
} else {
continueSelectedDownload(storageNew);
}
break;
}
});
@@ -747,7 +814,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
askDialog.create().show();
}
private void continueSelectedDownload(@NonNull StoredFileHelper storage) {
private void continueSelectedDownload(@NonNull final StoredFileHelper storage) {
if (!storage.canWrite()) {
showFailedDialog(R.string.permission_denied);
return;
@@ -755,7 +822,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
// check if the selected file has to be overwritten, by simply checking its length
try {
if (storage.length() > 0) storage.truncate();
if (storage.length() > 0) {
storage.truncate();
}
} catch (IOException e) {
Log.e(TAG, "failed to truncate the file: " + storage.getUri().toString(), e);
showFailedDialog(R.string.overwrite_failed);
@@ -795,13 +864,15 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
if (secondary != null) {
secondaryStream = secondary.getStream();
if (selectedStream.getFormat() == MediaFormat.MPEG_4)
if (selectedStream.getFormat() == MediaFormat.MPEG_4) {
psName = Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER;
else
} else {
psName = Postprocessing.ALGORITHM_WEBM_MUXER;
}
psArgs = null;
long videoSize = wrappedVideoStreams.getSizeInBytes((VideoStream) selectedStream);
long videoSize = wrappedVideoStreams
.getSizeInBytes((VideoStream) selectedStream);
// set nearLength, only, if both sizes are fetched or known. This probably
// does not work on slow networks but is later updated in the downloader
@@ -811,7 +882,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
}
break;
case R.id.subtitle_button:
threads = 1;// use unique thread for subtitles due small file size
threads = 1; // use unique thread for subtitles due small file size
kind = 's';
selectedStream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex);
@@ -819,8 +890,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
psArgs = new String[]{
selectedStream.getFormat().getSuffix(),
"false",// ignore empty frames
"false",// detect youtube duplicate lines
"false" // ignore empty frames
};
}
break;
@@ -839,14 +909,12 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
urls = new String[]{
selectedStream.getUrl(), secondaryStream.getUrl()
};
recoveryInfo = new MissionRecoveryInfo[]{
new MissionRecoveryInfo(selectedStream), new MissionRecoveryInfo(secondaryStream)
};
recoveryInfo = new MissionRecoveryInfo[]{new MissionRecoveryInfo(selectedStream),
new MissionRecoveryInfo(secondaryStream)};
}
DownloadManagerService.startMission(
context, urls, storage, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength, recoveryInfo
);
DownloadManagerService.startMission(context, urls, storage, kind, threads,
currentInfo.getUrl(), psName, psArgs, nearLength, recoveryInfo);
dismiss();
}

View File

@@ -1,11 +1,11 @@
package org.schabi.newpipe.fragments;
/**
* Indicates that the current fragment can handle back presses
* Indicates that the current fragment can handle back presses.
*/
public interface BackPressable {
/**
* A back press was delegated to this fragment
* A back press was delegated to this fragment.
*
* @return if the back press was handled
*/

View File

@@ -1,9 +1,8 @@
package org.schabi.newpipe.fragments;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import android.util.Log;
import android.view.View;
import android.widget.Button;
@@ -11,19 +10,23 @@ import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import com.jakewharton.rxbinding2.view.RxView;
import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.ReCaptchaActivity;
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ExceptionUtils;
import org.schabi.newpipe.util.InfoCache;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -35,22 +38,21 @@ import io.reactivex.android.schedulers.AndroidSchedulers;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public abstract class BaseStateFragment<I> extends BaseFragment implements ViewContract<I> {
@State
protected AtomicBoolean wasLoading = new AtomicBoolean();
protected AtomicBoolean isLoading = new AtomicBoolean();
@Nullable
protected View emptyStateView;
private View emptyStateView;
@Nullable
protected ProgressBar loadingProgressBar;
private ProgressBar loadingProgressBar;
protected View errorPanelRoot;
protected Button errorButtonRetry;
protected TextView errorTextView;
private Button errorButtonRetry;
private TextView errorTextView;
@Override
public void onViewCreated(View rootView, Bundle savedInstanceState) {
public void onViewCreated(final View rootView, final Bundle savedInstanceState) {
super.onViewCreated(rootView, savedInstanceState);
doInitialLoadLogic();
}
@@ -61,14 +63,12 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
wasLoading.set(isLoading.get());
}
/*//////////////////////////////////////////////////////////////////////////
// Init
//////////////////////////////////////////////////////////////////////////*/
@Override
protected void initViews(View rootView, Bundle savedInstanceState) {
protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
emptyStateView = rootView.findViewById(R.id.empty_state_view);
@@ -104,8 +104,10 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
startLoading(true);
}
protected void startLoading(boolean forceLoad) {
if (DEBUG) Log.d(TAG, "startLoading() called with: forceLoad = [" + forceLoad + "]");
protected void startLoading(final boolean forceLoad) {
if (DEBUG) {
Log.d(TAG, "startLoading() called with: forceLoad = [" + forceLoad + "]");
}
showLoading();
isLoading.set(true);
}
@@ -116,42 +118,62 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
@Override
public void showLoading() {
if (emptyStateView != null) animateView(emptyStateView, false, 150);
if (loadingProgressBar != null) animateView(loadingProgressBar, true, 400);
if (emptyStateView != null) {
animateView(emptyStateView, false, 150);
}
if (loadingProgressBar != null) {
animateView(loadingProgressBar, true, 400);
}
animateView(errorPanelRoot, false, 150);
}
@Override
public void hideLoading() {
if (emptyStateView != null) animateView(emptyStateView, false, 150);
if (loadingProgressBar != null) animateView(loadingProgressBar, false, 0);
if (emptyStateView != null) {
animateView(emptyStateView, false, 150);
}
if (loadingProgressBar != null) {
animateView(loadingProgressBar, false, 0);
}
animateView(errorPanelRoot, false, 150);
}
@Override
public void showEmptyState() {
isLoading.set(false);
if (emptyStateView != null) animateView(emptyStateView, true, 200);
if (loadingProgressBar != null) animateView(loadingProgressBar, false, 0);
if (emptyStateView != null) {
animateView(emptyStateView, true, 200);
}
if (loadingProgressBar != null) {
animateView(loadingProgressBar, false, 0);
}
animateView(errorPanelRoot, false, 150);
}
@Override
public void showError(String message, boolean showRetryButton) {
if (DEBUG) Log.d(TAG, "showError() called with: message = [" + message + "], showRetryButton = [" + showRetryButton + "]");
public void showError(final String message, final boolean showRetryButton) {
if (DEBUG) {
Log.d(TAG, "showError() called with: "
+ "message = [" + message + "], showRetryButton = [" + showRetryButton + "]");
}
isLoading.set(false);
InfoCache.getInstance().clearCache();
hideLoading();
errorTextView.setText(message);
if (showRetryButton) animateView(errorButtonRetry, true, 600);
else animateView(errorButtonRetry, false, 0);
if (showRetryButton) {
animateView(errorButtonRetry, true, 600);
} else {
animateView(errorButtonRetry, false, 0);
}
animateView(errorPanelRoot, true, 300);
}
@Override
public void handleResult(I result) {
if (DEBUG) Log.d(TAG, "handleResult() called with: result = [" + result + "]");
public void handleResult(final I result) {
if (DEBUG) {
Log.d(TAG, "handleResult() called with: result = [" + result + "]");
}
hideLoading();
}
@@ -160,37 +182,52 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
//////////////////////////////////////////////////////////////////////////*/
/**
* Default implementation handles some general exceptions
* Default implementation handles some general exceptions.
*
* @return if the exception was handled
* @param exception The exception that should be handled
* @return If the exception was handled
*/
protected boolean onError(Throwable exception) {
if (DEBUG) Log.d(TAG, "onError() called with: exception = [" + exception + "]");
protected boolean onError(final Throwable exception) {
if (DEBUG) {
Log.d(TAG, "onError() called with: exception = [" + exception + "]");
}
isLoading.set(false);
if (isDetached() || isRemoving()) {
if (DEBUG) Log.w(TAG, "onError() is detached or removing = [" + exception + "]");
if (DEBUG) {
Log.w(TAG, "onError() is detached or removing = [" + exception + "]");
}
return true;
}
if (ExtractorHelper.isInterruptedCaused(exception)) {
if (DEBUG) Log.w(TAG, "onError() isInterruptedCaused! = [" + exception + "]");
if (ExceptionUtils.isInterruptedCaused(exception)) {
if (DEBUG) {
Log.w(TAG, "onError() isInterruptedCaused! = [" + exception + "]");
}
return true;
}
if (exception instanceof ReCaptchaException) {
onReCaptchaException((ReCaptchaException) exception);
return true;
} else if (exception instanceof IOException) {
} else if (exception instanceof ContentNotAvailableException) {
showError(getString(R.string.content_not_available), false);
return true;
} else if (ExceptionUtils.isNetworkRelated(exception)) {
showError(getString(R.string.network_error), true);
return true;
} else if (exception instanceof ContentNotSupportedException) {
showError(getString(R.string.content_not_supported), false);
return true;
}
return false;
}
public void onReCaptchaException(ReCaptchaException exception) {
if (DEBUG) Log.d(TAG, "onReCaptchaException() called");
public void onReCaptchaException(final ReCaptchaException exception) {
if (DEBUG) {
Log.d(TAG, "onReCaptchaException() called");
}
Toast.makeText(activity, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show();
// Starting ReCaptcha Challenge Activity
Intent intent = new Intent(activity, ReCaptchaActivity.class);
@@ -200,33 +237,58 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
showError(getString(R.string.recaptcha_request_toast), false);
}
public void onUnrecoverableError(Throwable exception, UserAction userAction, String serviceName, String request, @StringRes int errorId) {
onUnrecoverableError(Collections.singletonList(exception), userAction, serviceName, request, errorId);
public void onUnrecoverableError(final Throwable exception, final UserAction userAction,
final String serviceName, final String request,
@StringRes final int errorId) {
onUnrecoverableError(Collections.singletonList(exception), userAction, serviceName,
request, errorId);
}
public void onUnrecoverableError(List<Throwable> exception, UserAction userAction, String serviceName, String request, @StringRes int errorId) {
if (DEBUG) Log.d(TAG, "onUnrecoverableError() called with: exception = [" + exception + "]");
public void onUnrecoverableError(final List<Throwable> exception, final UserAction userAction,
final String serviceName, final String request,
@StringRes final int errorId) {
if (DEBUG) {
Log.d(TAG, "onUnrecoverableError() called with: exception = [" + exception + "]");
}
if (serviceName == null) serviceName = "none";
if (request == null) request = "none";
ErrorActivity.reportError(getContext(), exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(userAction, serviceName, request, errorId));
ErrorActivity.reportError(getContext(), exception, MainActivity.class, null,
ErrorActivity.ErrorInfo.make(userAction, serviceName == null ? "none" : serviceName,
request == null ? "none" : request, errorId));
}
public void showSnackBarError(Throwable exception, UserAction userAction, String serviceName, String request, @StringRes int errorId) {
showSnackBarError(Collections.singletonList(exception), userAction, serviceName, request, errorId);
public void showSnackBarError(final Throwable exception, final UserAction userAction,
final String serviceName, final String request,
@StringRes final int errorId) {
showSnackBarError(Collections.singletonList(exception), userAction, serviceName, request,
errorId);
}
/**
* Show a SnackBar and only call ErrorActivity#reportError IF we a find a valid view (otherwise the error screen appears)
* Show a SnackBar and only call
* {@link ErrorActivity#reportError(Context, List, Class, View, ErrorActivity.ErrorInfo)}
* IF we a find a valid view (otherwise the error screen appears).
*
* @param exception List of the exceptions to show
* @param userAction The user action that caused the exception
* @param serviceName The service where the exception happened
* @param request The page that was requested
* @param errorId The ID of the error
*/
public void showSnackBarError(List<Throwable> exception, UserAction userAction, String serviceName, String request, @StringRes int errorId) {
public void showSnackBarError(final List<Throwable> exception, final UserAction userAction,
final String serviceName, final String request,
@StringRes final int errorId) {
if (DEBUG) {
Log.d(TAG, "showSnackBarError() called with: exception = [" + exception + "], userAction = [" + userAction + "], request = [" + request + "], errorId = [" + errorId + "]");
Log.d(TAG, "showSnackBarError() called with: "
+ "exception = [" + exception + "], userAction = [" + userAction + "], "
+ "request = [" + request + "], errorId = [" + errorId + "]");
}
View rootView = activity != null ? activity.findViewById(android.R.id.content) : null;
if (rootView == null && getView() != null) rootView = getView();
if (rootView == null) return;
if (rootView == null && getView() != null) {
rootView = getView();
}
if (rootView == null) {
return;
}
ErrorActivity.reportError(getContext(), exception, MainActivity.class, rootView,
ErrorActivity.ErrorInfo.make(userAction, serviceName, request, errorId));

View File

@@ -1,24 +1,26 @@
package org.schabi.newpipe.fragments;
import android.os.Bundle;
import androidx.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.Nullable;
import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.R;
public class BlankFragment extends BaseFragment {
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
final Bundle savedInstanceState) {
setTitle("NewPipe");
return inflater.inflate(R.layout.fragment_blank, container, false);
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
public void setUserVisibleHint(final boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
setTitle("NewPipe");
// leave this inline. Will make it harder for copy cats.

View File

@@ -1,17 +1,19 @@
package org.schabi.newpipe.fragments;
import android.os.Bundle;
import androidx.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.Nullable;
import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.R;
public class EmptyFragment extends BaseFragment {
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
final Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_empty, container, false);
}
}

View File

@@ -1,7 +1,9 @@
package org.schabi.newpipe.fragments;
import android.content.Context;
import android.content.res.ColorStateList;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
@@ -16,7 +18,7 @@ import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;
import androidx.fragment.app.FragmentStatePagerAdapterMenuWorkaround;
import androidx.viewpager.widget.ViewPager;
import com.google.android.material.tabs.TabLayout;
@@ -30,6 +32,8 @@ import org.schabi.newpipe.settings.tabs.Tab;
import org.schabi.newpipe.settings.tabs.TabsManager;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.views.ScrollableTabLayout;
import java.util.ArrayList;
import java.util.List;
@@ -37,26 +41,29 @@ import java.util.List;
public class MainFragment extends BaseFragment implements TabLayout.OnTabSelectedListener {
private ViewPager viewPager;
private SelectedTabsPagerAdapter pagerAdapter;
private TabLayout tabLayout;
private ScrollableTabLayout tabLayout;
private List<Tab> tabsList = new ArrayList<>();
private TabsManager tabsManager;
private boolean hasTabsChanged = false;
private boolean previousYoutubeRestrictedModeEnabled;
private String youtubeRestrictedModeEnabledKey;
/*//////////////////////////////////////////////////////////////////////////
// Fragment's LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreate(Bundle savedInstanceState) {
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
tabsManager = TabsManager.getManager(activity);
tabsManager.setSavedTabsListener(() -> {
if (DEBUG) {
Log.d(TAG, "TabsManager.SavedTabsChangeListener: onTabsChanged called, isResumed = " + isResumed());
Log.d(TAG, "TabsManager.SavedTabsChangeListener: "
+ "onTabsChanged called, isResumed = " + isResumed());
}
if (isResumed()) {
setupTabs();
@@ -64,20 +71,29 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
hasTabsChanged = true;
}
});
youtubeRestrictedModeEnabledKey = getString(R.string.youtube_restricted_mode_enabled);
previousYoutubeRestrictedModeEnabled =
PreferenceManager.getDefaultSharedPreferences(getContext())
.getBoolean(youtubeRestrictedModeEnabledKey, false);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_main, container, false);
}
@Override
protected void initViews(View rootView, Bundle savedInstanceState) {
protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
tabLayout = rootView.findViewById(R.id.main_tab_layout);
viewPager = rootView.findViewById(R.id.pager);
tabLayout.setTabIconTint(ColorStateList.valueOf(
ThemeHelper.resolveColorFromAttr(requireContext(), R.attr.colorAccent)));
tabLayout.setupWithViewPager(viewPager);
tabLayout.addOnTabSelectedListener(this);
@@ -88,14 +104,24 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
public void onResume() {
super.onResume();
if (hasTabsChanged) setupTabs();
boolean youtubeRestrictedModeEnabled =
PreferenceManager.getDefaultSharedPreferences(getContext())
.getBoolean(youtubeRestrictedModeEnabledKey, false);
if (previousYoutubeRestrictedModeEnabled != youtubeRestrictedModeEnabled) {
previousYoutubeRestrictedModeEnabled = youtubeRestrictedModeEnabled;
setupTabs();
} else if (hasTabsChanged) {
setupTabs();
}
}
@Override
public void onDestroy() {
super.onDestroy();
tabsManager.unsetSavedTabsListener();
if (viewPager != null) viewPager.setAdapter(null);
if (viewPager != null) {
viewPager.setAdapter(null);
}
}
/*//////////////////////////////////////////////////////////////////////////
@@ -103,9 +129,12 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]");
if (DEBUG) {
Log.d(TAG, "onCreateOptionsMenu() called with: "
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
}
inflater.inflate(R.menu.main_fragment_menu, menu);
ActionBar supportActionBar = activity.getSupportActionBar();
@@ -115,7 +144,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case R.id.action_search:
try {
@@ -135,16 +164,18 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
// Tabs
//////////////////////////////////////////////////////////////////////////*/
public void setupTabs() {
private void setupTabs() {
tabsList.clear();
tabsList.addAll(tabsManager.getTabs());
if (pagerAdapter == null || !pagerAdapter.sameTabs(tabsList)) {
pagerAdapter = new SelectedTabsPagerAdapter(requireContext(), getChildFragmentManager(), tabsList);
pagerAdapter = new SelectedTabsPagerAdapter(requireContext(),
getChildFragmentManager(), tabsList);
}
// Clear previous tabs/fragments and set new adapter
viewPager.setAdapter(pagerAdapter);
viewPager.setAdapter(null);
viewPager.setOffscreenPageLimit(tabsList.size());
viewPager.setAdapter(pagerAdapter);
updateTabsIconAndDescription();
updateTitleForTab(viewPager.getCurrentItem());
@@ -163,38 +194,45 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
}
}
private void updateTitleForTab(int tabPosition) {
private void updateTitleForTab(final int tabPosition) {
setTitle(tabsList.get(tabPosition).getTabName(requireContext()));
}
@Override
public void onTabSelected(TabLayout.Tab selectedTab) {
if (DEBUG) Log.d(TAG, "onTabSelected() called with: selectedTab = [" + selectedTab + "]");
public void onTabSelected(final TabLayout.Tab selectedTab) {
if (DEBUG) {
Log.d(TAG, "onTabSelected() called with: selectedTab = [" + selectedTab + "]");
}
updateTitleForTab(selectedTab.getPosition());
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
public void onTabUnselected(final TabLayout.Tab tab) { }
@Override
public void onTabReselected(TabLayout.Tab tab) {
if (DEBUG) Log.d(TAG, "onTabReselected() called with: tab = [" + tab + "]");
public void onTabReselected(final TabLayout.Tab tab) {
if (DEBUG) {
Log.d(TAG, "onTabReselected() called with: tab = [" + tab + "]");
}
updateTitleForTab(tab.getPosition());
}
private static class SelectedTabsPagerAdapter extends FragmentStatePagerAdapter {
private static final class SelectedTabsPagerAdapter
extends FragmentStatePagerAdapterMenuWorkaround {
private final Context context;
private final List<Tab> internalTabsList;
private SelectedTabsPagerAdapter(Context context, FragmentManager fragmentManager, List<Tab> tabsList) {
private SelectedTabsPagerAdapter(final Context context,
final FragmentManager fragmentManager,
final List<Tab> tabsList) {
super(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
this.context = context;
this.internalTabsList = new ArrayList<>(tabsList);
}
@NonNull
@Override
public Fragment getItem(int position) {
public Fragment getItem(final int position) {
final Tab tab = internalTabsList.get(position);
Throwable throwable = null;
@@ -206,8 +244,8 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
}
if (throwable != null) {
ErrorActivity.reportError(context, throwable, null, null,
ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash));
ErrorActivity.reportError(context, throwable, null, null, ErrorActivity.ErrorInfo
.make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash));
return new BlankFragment();
}
@@ -219,7 +257,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
}
@Override
public int getItemPosition(Object object) {
public int getItemPosition(final Object object) {
// Causes adapter to reload all Fragments when
// notifyDataSetChanged is called
return POSITION_NONE;
@@ -230,7 +268,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
return internalTabsList.size();
}
public boolean sameTabs(List<Tab> tabsToCompare) {
public boolean sameTabs(final List<Tab> tabsToCompare) {
return internalTabsList.equals(tabsToCompare);
}
}

View File

@@ -9,12 +9,13 @@ import androidx.recyclerview.widget.StaggeredGridLayoutManager;
* if the view is scrolled below the last item.
*/
public abstract class OnScrollBelowItemsListener extends RecyclerView.OnScrollListener {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
public void onScrolled(final RecyclerView recyclerView, final int dx, final int dy) {
super.onScrolled(recyclerView, dx, dy);
if (dy > 0) {
int pastVisibleItems = 0, visibleItemCount, totalItemCount;
int pastVisibleItems = 0;
int visibleItemCount;
int totalItemCount;
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
visibleItemCount = layoutManager.getChildCount();
@@ -22,10 +23,14 @@ public abstract class OnScrollBelowItemsListener extends RecyclerView.OnScrollLi
// Already covers the GridLayoutManager case
if (layoutManager instanceof LinearLayoutManager) {
pastVisibleItems = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
pastVisibleItems = ((LinearLayoutManager) layoutManager)
.findFirstVisibleItemPosition();
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
int[] positions = ((StaggeredGridLayoutManager) layoutManager).findFirstVisibleItemPositions(null);
if (positions != null && positions.length > 0) pastVisibleItems = positions[0];
int[] positions = ((StaggeredGridLayoutManager) layoutManager)
.findFirstVisibleItemPositions(null);
if (positions != null && positions.length > 0) {
pastVisibleItems = positions[0];
}
}
if ((visibleItemCount + pastVisibleItems) >= totalItemCount) {

View File

@@ -2,8 +2,11 @@ package org.schabi.newpipe.fragments;
public interface ViewContract<I> {
void showLoading();
void hideLoading();
void showEmptyState();
void showError(String message, boolean showRetryButton);
void handleResult(I result);

View File

@@ -6,8 +6,8 @@ import java.io.Serializable;
class StackItem implements Serializable {
private final int serviceId;
private String title;
private String url;
private String title;
private PlayQueue playQueue;
StackItem(final int serviceId, final String url, final String title, final PlayQueue playQueue) {
@@ -17,10 +17,6 @@ class StackItem implements Serializable {
this.playQueue = playQueue;
}
public void setTitle(String title) {
this.title = title;
}
public void setUrl(String url) {
this.url = url;
}
@@ -37,6 +33,10 @@ class StackItem implements Serializable {
return title;
}
public void setTitle(final String title) {
this.title = title;
}
public String getUrl() {
return url;
}

View File

@@ -1,27 +1,27 @@
package org.schabi.newpipe.fragments.detail;
import android.view.ViewGroup;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;
public class TabAdaptor extends FragmentPagerAdapter {
private final List<Fragment> mFragmentList = new ArrayList<>();
private final List<String> mFragmentTitleList = new ArrayList<>();
private final FragmentManager fragmentManager;
public TabAdaptor(FragmentManager fm) {
public TabAdaptor(final FragmentManager fm) {
super(fm);
this.fragmentManager = fm;
}
@Override
public Fragment getItem(int position) {
public Fragment getItem(final int position) {
return mFragmentList.get(position);
}
@@ -30,7 +30,7 @@ public class TabAdaptor extends FragmentPagerAdapter {
return mFragmentList.size();
}
public void addFragment(Fragment fragment, String title) {
public void addFragment(final Fragment fragment, final String title) {
mFragmentList.add(fragment);
mFragmentTitleList.add(title);
}
@@ -40,46 +40,49 @@ public class TabAdaptor extends FragmentPagerAdapter {
mFragmentTitleList.clear();
}
public void removeItem(int position){
public void removeItem(final int position) {
mFragmentList.remove(position == 0 ? 0 : position - 1);
mFragmentTitleList.remove(position == 0 ? 0 : position - 1);
}
public void updateItem(int position, Fragment fragment){
public void updateItem(final int position, final Fragment fragment) {
mFragmentList.set(position, fragment);
}
public void updateItem(String title, Fragment fragment){
public void updateItem(final String title, final Fragment fragment) {
int index = mFragmentTitleList.indexOf(title);
if(index != -1){
if (index != -1) {
updateItem(index, fragment);
}
}
@Override
public int getItemPosition(Object object) {
if (mFragmentList.contains(object)) return mFragmentList.indexOf(object);
else return POSITION_NONE;
public int getItemPosition(final Object object) {
if (mFragmentList.contains(object)) {
return mFragmentList.indexOf(object);
} else {
return POSITION_NONE;
}
}
public int getItemPositionByTitle(String title) {
public int getItemPositionByTitle(final String title) {
return mFragmentTitleList.indexOf(title);
}
@Nullable
public String getItemTitle(int position) {
public String getItemTitle(final int position) {
if (position < 0 || position >= mFragmentTitleList.size()) {
return null;
}
return mFragmentTitleList.get(position);
}
public void notifyDataSetUpdate(){
public void notifyDataSetUpdate() {
notifyDataSetChanged();
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
public void destroyItem(final ViewGroup container, final int position, final Object object) {
fragmentManager.beginTransaction().remove((Fragment) object).commitNowAllowingStateLoss();
}

View File

@@ -5,6 +5,7 @@ import android.app.Activity;
import android.content.*;
import android.content.pm.ActivityInfo;
import android.database.ContentObserver;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
@@ -31,11 +32,13 @@ import androidx.viewpager.widget.ViewPager;
import android.text.Html;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import android.text.util.Linkify;
import android.util.DisplayMetrics;
import android.util.Log;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.content.res.AppCompatResources;
import com.nostra13.universalimageloader.core.assist.FailReason;
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
@@ -46,11 +49,10 @@ import org.schabi.newpipe.download.DownloadDialog;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.Description;
import org.schabi.newpipe.extractor.stream.Stream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamType;
@@ -63,20 +65,34 @@ import org.schabi.newpipe.fragments.list.videos.RelatedVideosFragment;
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.player.*;
import org.schabi.newpipe.player.event.OnKeyDownListener;
import org.schabi.newpipe.player.event.PlayerEventListener;
import org.schabi.newpipe.player.event.PlayerServiceEventListener;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.playqueue.*;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.*;
import org.schabi.newpipe.util.AndroidTvUtils;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.InfoCache;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.ShareUtils;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.views.AnimatedProgressBar;
import org.schabi.newpipe.views.LargeTextMovementMethod;
import java.io.Serializable;
import java.util.*;
import java.util.concurrent.TimeUnit;
import icepick.State;
import io.noties.markwon.Markwon;
import io.noties.markwon.linkify.LinkifyPlugin;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
@@ -86,6 +102,7 @@ import io.reactivex.schedulers.Schedulers;
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS;
import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired;
import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET;
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public class VideoDetailFragment
@@ -95,7 +112,8 @@ public class VideoDetailFragment
View.OnClickListener,
View.OnLongClickListener,
PlayerEventListener,
PlayerServiceEventListener {
PlayerServiceEventListener,
OnKeyDownListener {
public static final String AUTO_PLAY = "auto_play";
private static final int RELATED_STREAMS_UPDATE_FLAG = 0x1;
@@ -112,6 +130,9 @@ public class VideoDetailFragment
private static final String RELATED_TAB_TAG = "NEXT VIDEO";
private static final String EMPTY_TAB_TAG = "EMPTY TAB";
private static final String INFO_KEY = "info_key";
private static final String STACK_KEY = "stack_key";
private boolean showRelatedStreams;
private boolean showComments;
private String selectedTabTag;
@@ -174,6 +195,8 @@ public class VideoDetailFragment
private View uploaderRootLayout;
private TextView uploaderTextView;
private ImageView uploaderThumb;
private TextView subChannelTextView;
private ImageView subChannelThumb;
private TextView thumbsUpTextView;
private ImageView thumbsUpImageView;
@@ -300,13 +323,13 @@ public class VideoDetailFragment
return instance;
}
/*//////////////////////////////////////////////////////////////////////////
// Fragment's Lifecycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void
onCreate(Bundle savedInstanceState) {
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
showRelatedStreams = PreferenceManager.getDefaultSharedPreferences(activity)
@@ -336,19 +359,22 @@ public class VideoDetailFragment
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container,
final Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_video_detail, container, false);
}
@Override
public void onPause() {
super.onPause();
if (currentWorker != null) currentWorker.dispose();
if (currentWorker != null) {
currentWorker.dispose();
}
setupBrightness(true);
PreferenceManager.getDefaultSharedPreferences(getContext())
.edit()
.putString(getString(R.string.stream_info_selected_tab_key), pageAdapter.getItemTitle(viewPager.getCurrentItem()))
.putString(getString(R.string.stream_info_selected_tab_key),
pageAdapter.getItemTitle(viewPager.getCurrentItem()))
.apply();
}
@@ -395,8 +421,12 @@ public class VideoDetailFragment
activity.unregisterReceiver(broadcastReceiver);
activity.getContentResolver().unregisterContentObserver(settingsContentObserver);
if (positionSubscriber != null) positionSubscriber.dispose();
if (currentWorker != null) currentWorker.dispose();
if (positionSubscriber != null) {
positionSubscriber.dispose();
}
if (currentWorker != null) {
currentWorker.dispose();
}
disposables.clear();
positionSubscriber = null;
currentWorker = null;
@@ -404,13 +434,16 @@ public class VideoDetailFragment
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
case ReCaptchaActivity.RECAPTCHA_REQUEST:
if (resultCode == Activity.RESULT_OK) {
NavigationHelper.openVideoDetailFragment(getFragmentManager(), serviceId, url, name);
} else Log.e(TAG, "ReCaptcha failed");
NavigationHelper
.openVideoDetailFragment(getFragmentManager(), serviceId, url, name);
} else {
Log.e(TAG, "ReCaptcha failed");
}
break;
default:
Log.e(TAG, "Request code from activity not supported [" + requestCode + "]");
@@ -419,7 +452,8 @@ public class VideoDetailFragment
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences,
final String key) {
if (key.equals(getString(R.string.show_next_video_key))) {
showRelatedStreams = sharedPreferences.getBoolean(key, true);
updateFlags |= RELATED_STREAMS_UPDATE_FLAG;
@@ -433,11 +467,8 @@ public class VideoDetailFragment
// State Saving
//////////////////////////////////////////////////////////////////////////*/
private static final String INFO_KEY = "info_key";
private static final String STACK_KEY = "stack_key";
@Override
public void onSaveInstanceState(Bundle outState) {
public void onSaveInstanceState(final Bundle outState) {
super.onSaveInstanceState(outState);
// Check if the next video label and video is visible,
@@ -453,7 +484,7 @@ public class VideoDetailFragment
}
@Override
protected void onRestoreInstanceState(@NonNull Bundle savedState) {
protected void onRestoreInstanceState(@NonNull final Bundle savedState) {
super.onRestoreInstanceState(savedState);
Serializable serializable = savedState.getSerializable(INFO_KEY);
@@ -476,8 +507,10 @@ public class VideoDetailFragment
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onClick(View v) {
if (isLoading.get() || currentInfo == null) return;
public void onClick(final View v) {
if (isLoading.get() || currentInfo == null) {
return;
}
switch (v.getId()) {
case R.id.detail_controls_background:
@@ -499,7 +532,18 @@ public class VideoDetailFragment
}
break;
case R.id.detail_uploader_root_layout:
openChannel();
if (TextUtils.isEmpty(currentInfo.getSubChannelUrl())) {
if (!TextUtils.isEmpty(currentInfo.getUploaderUrl())) {
openChannel(currentInfo.getUploaderUrl(), currentInfo.getUploaderName());
}
if (DEBUG) {
Log.i(TAG, "Can't open sub-channel because we got no channel URL");
}
} else {
openChannel(currentInfo.getSubChannelUrl(),
currentInfo.getSubChannelName());
}
break;
case R.id.detail_thumbnail_root_layout:
openVideoPlayer();
@@ -527,9 +571,23 @@ public class VideoDetailFragment
}
}
private void openChannel(final String subChannelUrl, final String subChannelName) {
try {
NavigationHelper.openChannelFragment(
getFragmentManager(),
currentInfo.getServiceId(),
subChannelUrl,
subChannelName);
} catch (Exception e) {
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
}
}
@Override
public boolean onLongClick(View v) {
if (isLoading.get() || currentInfo == null) return false;
public boolean onLongClick(final View v) {
if (isLoading.get() || currentInfo == null) {
return false;
}
switch (v.getId()) {
case R.id.detail_controls_background:
@@ -543,7 +601,19 @@ public class VideoDetailFragment
break;
case R.id.overlay_thumbnail:
case R.id.overlay_metadata_layout:
openChannel();
if (currentInfo != null)
openChannel(currentInfo.getUploaderUrl(), currentInfo.getUploaderName());
break;
case R.id.detail_uploader_root_layout:
if (TextUtils.isEmpty(currentInfo.getSubChannelUrl())) {
Log.w(TAG,
"Can't open parent channel because we got no parent channel URL");
} else {
openChannel(currentInfo.getUploaderUrl(), currentInfo.getUploaderName());
}
break;
case R.id.detail_title_root_layout:
ShareUtils.copyToClipboard(getContext(), videoTitleTextView.getText().toString());
break;
}
@@ -554,11 +624,16 @@ public class VideoDetailFragment
if (videoDescriptionRootLayout.getVisibility() == View.VISIBLE) {
videoTitleTextView.setMaxLines(1);
videoDescriptionRootLayout.setVisibility(View.GONE);
videoTitleToggleArrow.setImageResource(R.drawable.arrow_down);
videoDescriptionView.setFocusable(false);
videoTitleToggleArrow.setImageResource(
ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_expand_more));
} else {
videoTitleTextView.setMaxLines(10);
videoDescriptionRootLayout.setVisibility(View.VISIBLE);
videoTitleToggleArrow.setImageResource(R.drawable.arrow_up);
videoDescriptionView.setFocusable(true);
videoDescriptionView.setMovementMethod(new LargeTextMovementMethod());
videoTitleToggleArrow.setImageResource(
ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_expand_less));
}
}
@@ -567,7 +642,7 @@ public class VideoDetailFragment
//////////////////////////////////////////////////////////////////////////*/
@Override
protected void initViews(View rootView, Bundle savedInstanceState) {
protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
thumbnailBackgroundButton = rootView.findViewById(R.id.detail_thumbnail_root_layout);
thumbnailImageView = rootView.findViewById(R.id.detail_thumbnail_image_view);
@@ -592,8 +667,6 @@ public class VideoDetailFragment
videoDescriptionRootLayout = rootView.findViewById(R.id.detail_description_root_layout);
videoUploadDateView = rootView.findViewById(R.id.detail_upload_date_view);
videoDescriptionView = rootView.findViewById(R.id.detail_description_view);
videoDescriptionView.setMovementMethod(LinkMovementMethod.getInstance());
videoDescriptionView.setAutoLinkMask(Linkify.WEB_URLS);
thumbsUpTextView = rootView.findViewById(R.id.detail_thumbs_up_count_view);
thumbsUpImageView = rootView.findViewById(R.id.detail_thumbs_up_img_view);
@@ -604,6 +677,8 @@ public class VideoDetailFragment
uploaderRootLayout = rootView.findViewById(R.id.detail_uploader_root_layout);
uploaderTextView = rootView.findViewById(R.id.detail_uploader_text_view);
uploaderThumb = rootView.findViewById(R.id.detail_uploader_thumbnail_view);
subChannelTextView = rootView.findViewById(R.id.detail_sub_channel_text_view);
subChannelThumb = rootView.findViewById(R.id.detail_sub_channel_thumbnail_view);
overlay = rootView.findViewById(R.id.overlay_layout);
overlayMetadata = rootView.findViewById(R.id.overlay_metadata_layout);
@@ -624,14 +699,28 @@ public class VideoDetailFragment
relatedStreamsLayout = rootView.findViewById(R.id.relatedStreamsLayout);
setHeightThumbnail();
thumbnailBackgroundButton.requestFocus();
if (AndroidTvUtils.isTv(getContext())) {
// remove ripple effects from detail controls
final int transparent = getResources().getColor(R.color.transparent_background_color);
detailControlsAddToPlaylist.setBackgroundColor(transparent);
detailControlsBackground.setBackgroundColor(transparent);
detailControlsPopup.setBackgroundColor(transparent);
detailControlsDownload.setBackgroundColor(transparent);
}
}
@Override
protected void initListeners() {
super.initListeners();
videoTitleRoot.setOnClickListener(this);
videoTitleRoot.setOnLongClickListener(this);
uploaderRootLayout.setOnClickListener(this);
uploaderRootLayout.setOnLongClickListener(this);
videoTitleRoot.setOnClickListener(this);
thumbnailBackgroundButton.setOnClickListener(this);
detailControlsBackground.setOnClickListener(this);
detailControlsPopup.setOnClickListener(this);
@@ -674,25 +763,31 @@ public class VideoDetailFragment
};
}
private void initThumbnailViews(@NonNull StreamInfo info) {
private void initThumbnailViews(@NonNull final StreamInfo info) {
thumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark);
if (!TextUtils.isEmpty(info.getThumbnailUrl())) {
final String infoServiceName = NewPipe.getNameOfService(info.getServiceId());
final ImageLoadingListener loadingListener = new SimpleImageLoadingListener() {
final ImageLoadingListener onFailListener = new SimpleImageLoadingListener() {
@Override
public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
public void onLoadingFailed(final String imageUri, final View view,
final FailReason failReason) {
showSnackBarError(failReason.getCause(), UserAction.LOAD_IMAGE,
infoServiceName, imageUri, R.string.could_not_load_thumbnails);
}
};
imageLoader.displayImage(info.getThumbnailUrl(), thumbnailImageView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, loadingListener);
IMAGE_LOADER.displayImage(info.getThumbnailUrl(), thumbnailImageView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, onFailListener);
}
if (!TextUtils.isEmpty(info.getSubChannelAvatarUrl())) {
IMAGE_LOADER.displayImage(info.getSubChannelAvatarUrl(), subChannelThumb,
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
}
if (!TextUtils.isEmpty(info.getUploaderAvatarUrl())) {
imageLoader.displayImage(info.getUploaderAvatarUrl(), uploaderThumb,
IMAGE_LOADER.displayImage(info.getUploaderAvatarUrl(), uploaderThumb,
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
}
}
@@ -707,20 +802,16 @@ public class VideoDetailFragment
*/
protected final LinkedList<StackItem> stack = new LinkedList<>();
/*public void setTitleToUrl(int serviceId, String videoUrl, String name) {
if (name != null && !name.isEmpty()) {
for (StackItem stackItem : stack) {
if (stack.peek().getServiceId() == serviceId
&& stackItem.getUrl().equals(videoUrl)) {
stackItem.setTitle(name);
}
}
}
}*/
@Override
public boolean onKeyDown(final int keyCode) {
return player != null && player.onKeyDown(keyCode);
}
@Override
public boolean onBackPressed() {
if (DEBUG) Log.d(TAG, "onBackPressed() called");
if (DEBUG) {
Log.d(TAG, "onBackPressed() called");
}
// If we are in fullscreen mode just exit from it via first back press
if (player != null && player.isFullscreen()) {
@@ -782,23 +873,28 @@ public class VideoDetailFragment
protected void doInitialLoadLogic() {
if (wasCleared()) return;
if (currentInfo == null) prepareAndLoadInfo();
else prepareAndHandleInfo(currentInfo, false);
if (currentInfo == null) {
prepareAndLoadInfo();
} else {
prepareAndHandleInfo(currentInfo, false);
}
}
public void selectAndLoadVideo(final int serviceId, final String videoUrl, final String name, final PlayQueue playQueue) {
public void selectAndLoadVideo(final int sid, final String videoUrl, final String title, final PlayQueue playQueue) {
// Situation when user switches from players to main player. All needed data is here, we can start watching
if (this.playQueue != null && this.playQueue.equals(playQueue)) {
openVideoPlayer();
return;
}
setInitialData(serviceId, videoUrl, name, playQueue);
setInitialData(sid, videoUrl, title, playQueue);
startLoading(false, true);
}
public void prepareAndHandleInfo(final StreamInfo info, final boolean scrollToTop) {
if (DEBUG) Log.d(TAG, "prepareAndHandleInfo() called with: info = ["
+ info + "], scrollToTop = [" + scrollToTop + "]");
private void prepareAndHandleInfo(final StreamInfo info, final boolean scrollToTop) {
if (DEBUG) {
Log.d(TAG, "prepareAndHandleInfo() called with: "
+ "info = [" + info + "], scrollToTop = [" + scrollToTop + "]");
}
showLoading();
initTabs();
@@ -820,7 +916,11 @@ public class VideoDetailFragment
initTabs();
currentInfo = null;
if (currentWorker != null) currentWorker.dispose();
if (currentWorker != null) {
currentWorker.dispose();
}
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
runWorker(forceLoad, stack.isEmpty());
}
@@ -836,20 +936,27 @@ public class VideoDetailFragment
}
private void runWorker(final boolean forceLoad, final boolean addToBackStack) {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
currentWorker = ExtractorHelper.getStreamInfo(serviceId, url, forceLoad)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe((@NonNull StreamInfo result) -> {
.subscribe((@NonNull final StreamInfo result) -> {
isLoading.set(false);
hideMainPlayer();
handleResult(result);
showContent();
if (addToBackStack) {
if (playQueue == null) playQueue = new SinglePlayQueue(result);
stack.push(new StackItem(serviceId, url, name, playQueue));
if (result.getAgeLimit() != NO_AGE_LIMIT && !prefs.getBoolean(
getString(R.string.show_age_restricted_content), false)) {
hideAgeRestrictedContent();
} else {
currentInfo = result;
handleResult(result);
showContent();
if (addToBackStack) {
if (playQueue == null) playQueue = new SinglePlayQueue(result);
stack.push(new StackItem(serviceId, url, name, playQueue));
}
if (isAutoplayEnabled()) openVideoPlayer();
}
if (isAutoplayEnabled()) openVideoPlayer();
}, (@NonNull Throwable throwable) -> {
}, (@NonNull final Throwable throwable) -> {
isLoading.set(false);
onError(throwable);
});
@@ -879,8 +986,10 @@ public class VideoDetailFragment
if (pageAdapter.getCount() < 2) {
tabLayout.setVisibility(View.GONE);
} else {
final int position = pageAdapter.getItemPositionByTitle(selectedTabTag);
if (position != -1) viewPager.setCurrentItem(position);
int position = pageAdapter.getItemPositionByTitle(selectedTabTag);
if (position != -1) {
viewPager.setCurrentItem(position);
}
tabLayout.setVisibility(View.VISIBLE);
}
}
@@ -1001,22 +1110,6 @@ public class VideoDetailFragment
return queue;
}
private void openChannel() {
if (TextUtils.isEmpty(currentInfo.getUploaderUrl())) {
Log.w(TAG, "Can't open channel because we got no channel URL");
} else {
try {
NavigationHelper.openChannelFragment(
getFragmentManager(),
currentInfo.getServiceId(),
currentInfo.getUploaderUrl(),
currentInfo.getUploaderName());
} catch (Exception e) {
ErrorActivity.reportUiError(activity, e);
}
}
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
@@ -1029,12 +1122,12 @@ public class VideoDetailFragment
@NonNull final StreamInfo info,
@NonNull final Stream selectedStream) {
NavigationHelper.playOnExternalPlayer(context, currentInfo.getName(),
currentInfo.getUploaderName(), selectedStream);
currentInfo.getSubChannelName(), selectedStream);
final HistoryRecordManager recordManager = new HistoryRecordManager(requireContext());
disposables.add(recordManager.onViewed(info).onErrorComplete()
.subscribe(
ignored -> {/* successful */},
ignored -> { /* successful */ },
error -> Log.e(TAG, "Register view failure: ", error)
));
}
@@ -1101,28 +1194,42 @@ public class VideoDetailFragment
return sortedVideoStreams != null ? sortedVideoStreams.get(selectedVideoStreamIndex) : null;
}
private void prepareDescription(final String descriptionHtml) {
if (TextUtils.isEmpty(descriptionHtml)) {
private void prepareDescription(final Description description) {
if (TextUtils.isEmpty(description.getContent())
|| description == Description.emptyDescription) {
return;
}
disposables.add(Single.just(descriptionHtml)
.map((@io.reactivex.annotations.NonNull String description) -> {
final Spanned parsedDescription;
if (Build.VERSION.SDK_INT >= 24) {
parsedDescription = Html.fromHtml(description, 0);
} else {
//noinspection deprecation
parsedDescription = Html.fromHtml(description);
}
return parsedDescription;
})
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe((@io.reactivex.annotations.NonNull Spanned spanned) -> {
videoDescriptionView.setText(spanned);
videoDescriptionView.setVisibility(View.VISIBLE);
}));
if (description.getType() == Description.HTML) {
disposables.add(Single.just(description.getContent())
.map((@io.reactivex.annotations.NonNull String descriptionText) -> {
Spanned parsedDescription;
if (Build.VERSION.SDK_INT >= 24) {
parsedDescription = Html.fromHtml(descriptionText, 0);
} else {
//noinspection deprecation
parsedDescription = Html.fromHtml(descriptionText);
}
return parsedDescription;
})
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe((@io.reactivex.annotations.NonNull Spanned spanned) -> {
videoDescriptionView.setText(spanned);
videoDescriptionView.setVisibility(View.VISIBLE);
}));
} else if (description.getType() == Description.MARKDOWN) {
final Markwon markwon = Markwon.builder(getContext())
.usePlugin(LinkifyPlugin.create())
.build();
markwon.setMarkdown(videoDescriptionView, description.getContent());
videoDescriptionView.setVisibility(View.VISIBLE);
} else {
//== Description.PLAIN_TEXT
videoDescriptionView.setAutoLinkMask(Linkify.WEB_URLS);
videoDescriptionView.setText(description.getContent(), TextView.BufferType.SPANNABLE);
videoDescriptionView.setVisibility(View.VISIBLE);
}
}
/**
@@ -1154,27 +1261,31 @@ public class VideoDetailFragment
contentRootLayoutHiding.setVisibility(View.VISIBLE);
}
protected void setInitialData(final int serviceId, final String url, final String name, final PlayQueue playQueue) {
this.serviceId = serviceId;
this.url = url;
protected void setInitialData(final int sid, final String u, final String name, final PlayQueue playQueue) {
this.serviceId = sid;
this.url = u;
this.name = !TextUtils.isEmpty(name) ? name : "";
this.playQueue = playQueue;
}
private void setErrorImage(final int imageResource) {
if (thumbnailImageView == null || activity == null) return;
if (thumbnailImageView == null || activity == null) {
return;
}
thumbnailImageView.setImageDrawable(ContextCompat.getDrawable(activity, imageResource));
thumbnailImageView.setImageDrawable(
AppCompatResources.getDrawable(requireContext(), imageResource));
animateView(thumbnailImageView, false, 0, 0,
() -> animateView(thumbnailImageView, true, 500));
}
@Override
public void showError(String message, boolean showRetryButton) {
public void showError(final String message, final boolean showRetryButton) {
showError(message, showRetryButton, R.drawable.not_available_monkey);
}
protected void showError(String message, boolean showRetryButton, @DrawableRes int imageError) {
protected void showError(final String message, final boolean showRetryButton,
@DrawableRes final int imageError) {
super.showError(message, showRetryButton);
setErrorImage(imageError);
}
@@ -1236,7 +1347,6 @@ public class VideoDetailFragment
animateView(videoTitleTextView, true, 0);
videoDescriptionRootLayout.setVisibility(View.GONE);
videoTitleToggleArrow.setImageResource(R.drawable.arrow_down);
videoTitleToggleArrow.setVisibility(View.GONE);
videoTitleRoot.setClickable(false);
@@ -1248,14 +1358,14 @@ public class VideoDetailFragment
}
}
imageLoader.cancelDisplayTask(thumbnailImageView);
imageLoader.cancelDisplayTask(uploaderThumb);
IMAGE_LOADER.cancelDisplayTask(thumbnailImageView);
IMAGE_LOADER.cancelDisplayTask(subChannelThumb);
thumbnailImageView.setImageBitmap(null);
uploaderThumb.setImageBitmap(null);
subChannelThumb.setImageBitmap(null);
}
@Override
public void handleResult(@NonNull StreamInfo info) {
public void handleResult(@NonNull final StreamInfo info) {
super.handleResult(info);
currentInfo = info;
@@ -1263,11 +1373,13 @@ public class VideoDetailFragment
if (showRelatedStreams) {
if (null == relatedStreamsLayout) { //phone
pageAdapter.updateItem(RELATED_TAB_TAG, RelatedVideosFragment.getInstance(info));
pageAdapter.updateItem(RELATED_TAB_TAG,
RelatedVideosFragment.getInstance(info));
pageAdapter.notifyDataSetUpdate();
} else { //tablet
getChildFragmentManager().beginTransaction()
.replace(R.id.relatedStreamsLayout, RelatedVideosFragment.getInstance(info))
.replace(R.id.relatedStreamsLayout,
RelatedVideosFragment.getInstance(info))
.commitNow();
relatedStreamsLayout.setVisibility(player != null && player.isFullscreen() ? View.GONE : View.VISIBLE);
}
@@ -1276,22 +1388,28 @@ public class VideoDetailFragment
animateView(thumbnailPlayButton, true, 200);
videoTitleTextView.setText(name);
if (!TextUtils.isEmpty(info.getUploaderName())) {
uploaderTextView.setText(info.getUploaderName());
uploaderTextView.setVisibility(View.VISIBLE);
uploaderTextView.setSelected(true);
if (!TextUtils.isEmpty(info.getSubChannelName())) {
displayBothUploaderAndSubChannel(info);
} else if (!TextUtils.isEmpty(info.getUploaderName())) {
displayUploaderAsSubChannel(info);
} else {
uploaderTextView.setVisibility(View.GONE);
uploaderThumb.setVisibility(View.GONE);
}
uploaderThumb.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.buddy));
Drawable buddyDrawable = AppCompatResources.getDrawable(activity, R.drawable.buddy);
subChannelThumb.setImageDrawable(buddyDrawable);
uploaderThumb.setImageDrawable(buddyDrawable);
if (info.getViewCount() >= 0) {
if (info.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) {
videoCountView.setText(Localization.listeningCount(activity, info.getViewCount()));
} else if (info.getStreamType().equals(StreamType.LIVE_STREAM)) {
videoCountView.setText(Localization.watchingCount(activity, info.getViewCount()));
videoCountView.setText(Localization
.localizeWatchingCount(activity, info.getViewCount()));
} else {
videoCountView.setText(Localization.localizeViewCount(activity, info.getViewCount()));
videoCountView.setText(Localization
.localizeViewCount(activity, info.getViewCount()));
}
videoCountView.setVisibility(View.VISIBLE);
} else {
@@ -1307,7 +1425,8 @@ public class VideoDetailFragment
thumbsDisabledTextView.setVisibility(View.VISIBLE);
} else {
if (info.getDislikeCount() >= 0) {
thumbsDownTextView.setText(Localization.shortCount(activity, info.getDislikeCount()));
thumbsDownTextView.setText(Localization
.shortCount(activity, info.getDislikeCount()));
thumbsDownTextView.setVisibility(View.VISIBLE);
thumbsDownImageView.setVisibility(View.VISIBLE);
} else {
@@ -1342,12 +1461,14 @@ public class VideoDetailFragment
videoDescriptionView.setVisibility(View.GONE);
videoTitleRoot.setClickable(true);
videoTitleToggleArrow.setImageResource(
ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_expand_more));
videoTitleToggleArrow.setVisibility(View.VISIBLE);
videoTitleToggleArrow.setImageResource(R.drawable.arrow_down);
videoDescriptionRootLayout.setVisibility(View.GONE);
if (info.getUploadDate() != null) {
videoUploadDateView.setText(Localization.localizeUploadDate(activity, info.getUploadDate().date().getTime()));
videoUploadDateView.setText(Localization
.localizeUploadDate(activity, info.getUploadDate().date().getTime()));
videoUploadDateView.setVisibility(View.VISIBLE);
} else {
videoUploadDateView.setText(null);
@@ -1382,16 +1503,54 @@ public class VideoDetailFragment
detailControlsDownload.setVisibility(View.GONE);
break;
default:
if (info.getAudioStreams().isEmpty()) detailControlsBackground.setVisibility(View.GONE);
if (!info.getVideoStreams().isEmpty()
|| !info.getVideoOnlyStreams().isEmpty()) break;
if (info.getAudioStreams().isEmpty()) {
detailControlsBackground.setVisibility(View.GONE);
}
if (!info.getVideoStreams().isEmpty() || !info.getVideoOnlyStreams().isEmpty()) {
break;
}
detailControlsPopup.setVisibility(View.GONE);
thumbnailPlayButton.setImageResource(R.drawable.ic_headset_white_24dp);
thumbnailPlayButton.setImageResource(R.drawable.ic_headset_shadow);
break;
}
}
private void hideAgeRestrictedContent() {
showError(getString(R.string.restricted_video), false);
if (relatedStreamsLayout != null) { // tablet
relatedStreamsLayout.setVisibility(View.INVISIBLE);
}
viewPager.setVisibility(View.GONE);
tabLayout.setVisibility(View.GONE);
}
private void displayUploaderAsSubChannel(final StreamInfo info) {
subChannelTextView.setText(info.getUploaderName());
subChannelTextView.setVisibility(View.VISIBLE);
subChannelTextView.setSelected(true);
uploaderTextView.setVisibility(View.GONE);
}
private void displayBothUploaderAndSubChannel(final StreamInfo info) {
subChannelTextView.setText(info.getSubChannelName());
subChannelTextView.setVisibility(View.VISIBLE);
subChannelTextView.setSelected(true);
subChannelThumb.setVisibility(View.VISIBLE);
if (!TextUtils.isEmpty(info.getUploaderName())) {
uploaderTextView.setText(
String.format(getString(R.string.video_detail_by), info.getUploaderName()));
uploaderTextView.setVisibility(View.VISIBLE);
uploaderTextView.setSelected(true);
} else {
uploaderTextView.setVisibility(View.GONE);
}
}
public void openDownloadDialog() {
try {
@@ -1423,24 +1582,20 @@ public class VideoDetailFragment
//////////////////////////////////////////////////////////////////////////*/
@Override
protected boolean onError(Throwable exception) {
if (super.onError(exception)) return true;
else if (exception instanceof ContentNotAvailableException) {
showError(getString(R.string.content_not_available), false);
} else {
final int errorId = exception instanceof YoutubeStreamExtractor.DecryptException
? R.string.youtube_signature_decryption_error
: exception instanceof ParsingException
? R.string.parsing_error
: R.string.general_error;
onUnrecoverableError(exception,
UserAction.REQUESTED_STREAM,
NewPipe.getNameOfService(serviceId),
url,
errorId);
protected boolean onError(final Throwable exception) {
if (super.onError(exception)) {
return true;
}
int errorId = exception instanceof YoutubeStreamExtractor.DecryptException
? R.string.youtube_signature_decryption_error
: exception instanceof ExtractionException
? R.string.parsing_error
: R.string.general_error;
onUnrecoverableError(exception, UserAction.REQUESTED_STREAM,
NewPipe.getNameOfService(serviceId), url, errorId);
return true;
}
@@ -1449,9 +1604,9 @@ public class VideoDetailFragment
positionSubscriber.dispose();
}
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
final boolean playbackResumeEnabled =
prefs.getBoolean(activity.getString(R.string.enable_watch_history_key), true)
&& prefs.getBoolean(activity.getString(R.string.enable_playback_resume_key), true);
final boolean playbackResumeEnabled = prefs
.getBoolean(activity.getString(R.string.enable_watch_history_key), true)
&& prefs.getBoolean(activity.getString(R.string.enable_playback_resume_key), true);
final boolean showPlaybackPosition = prefs.getBoolean(
activity.getString(R.string.enable_playback_state_lists_key), true);
if (!playbackResumeEnabled) {
@@ -1459,6 +1614,12 @@ public class VideoDetailFragment
|| playQueue.getItem().getRecoveryPosition() == RECOVERY_UNSET || !showPlaybackPosition) {
positionView.setVisibility(View.INVISIBLE);
detailPositionView.setVisibility(View.GONE);
// TODO: Remove this check when separation of concerns is done.
// (live streams weren't getting updated because they are mixed)
if (!info.getStreamType().equals(StreamType.LIVE_STREAM)
&& !info.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) {
return;
}
} else {
// Show saved position from backStack if user allows it
showPlaybackProgress(playQueue.getItem().getRecoveryPosition(),
@@ -1466,9 +1627,12 @@ public class VideoDetailFragment
animateView(positionView, true, 500);
animateView(detailPositionView, true, 500);
}
return;
}
return;
}
final HistoryRecordManager recordManager = new HistoryRecordManager(requireContext());
// TODO: Separate concerns when updating database data.
// (move the updating part to when the loading happens)
positionSubscriber = recordManager.loadStreamState(info)
.subscribeOn(Schedulers.io())
.onErrorComplete()
@@ -1478,7 +1642,9 @@ public class VideoDetailFragment
animateView(positionView, true, 500);
animateView(detailPositionView, true, 500);
}, e -> {
if (DEBUG) e.printStackTrace();
if (DEBUG) {
e.printStackTrace();
}
}, () -> {
positionView.setVisibility(View.GONE);
detailPositionView.setVisibility(View.GONE);
@@ -1489,7 +1655,7 @@ public class VideoDetailFragment
final int progressSeconds = (int) TimeUnit.MILLISECONDS.toSeconds(progress);
final int durationSeconds = (int) TimeUnit.MILLISECONDS.toSeconds(duration);
positionView.setMax(durationSeconds);
positionView.setProgress(progressSeconds);
positionView.setProgressAnimated(progressSeconds);
detailPositionView.setText(Localization.getDurationString(progressSeconds));
if (positionView.getVisibility() != View.VISIBLE) {
animateView(positionView, true, 100);
@@ -1862,12 +2028,12 @@ public class VideoDetailFragment
overlayChannelTextView.setText(TextUtils.isEmpty(uploader) ? "" : uploader);
overlayThumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark);
if (!TextUtils.isEmpty(thumbnailUrl))
imageLoader.displayImage(thumbnailUrl, overlayThumbnailImageView,
IMAGE_LOADER.displayImage(thumbnailUrl, overlayThumbnailImageView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, null);
}
private void setOverlayPlayPauseImage() {
final int attr = player != null && player.getPlayer().getPlayWhenReady() ? R.attr.pause : R.attr.play;
final int attr = player != null && player.getPlayer().getPlayWhenReady() ? R.attr.ic_pause : R.attr.ic_play_arrow;
overlayPlayPauseButton.setImageResource(ThemeHelper.resolveResourceIdFromAttr(activity, attr));
}

View File

@@ -7,17 +7,17 @@ import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.Bundle;
import android.preference.PreferenceManager;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
@@ -34,13 +34,21 @@ import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
import org.schabi.newpipe.util.StateSaver;
import org.schabi.newpipe.util.StreamDialogEntry;
import org.schabi.newpipe.views.SuperScrollLayoutManager;
import java.util.List;
import java.util.Queue;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implements ListViewContract<I, N>, StateSaver.WriteRead, SharedPreferences.OnSharedPreferenceChangeListener {
public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
implements ListViewContract<I, N>, StateSaver.WriteRead,
SharedPreferences.OnSharedPreferenceChangeListener {
private static final int LIST_MODE_UPDATE_FLAG = 0x32;
protected StateSaver.SavedState savedState;
private boolean useDefaultStateSaving = true;
private int updateFlags = 0;
/*//////////////////////////////////////////////////////////////////////////
// Views
@@ -48,18 +56,19 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
protected InfoListAdapter infoListAdapter;
protected RecyclerView itemsList;
private int updateFlags = 0;
private static final int LIST_MODE_UPDATE_FLAG = 0x32;
private int focusedPosition = -1;
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onAttach(Context context) {
public void onAttach(final Context context) {
super.onAttach(context);
infoListAdapter = new InfoListAdapter(activity);
if (infoListAdapter == null) {
infoListAdapter = new InfoListAdapter(activity);
}
}
@Override
@@ -68,7 +77,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
}
@Override
public void onCreate(Bundle savedInstanceState) {
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
PreferenceManager.getDefaultSharedPreferences(activity)
@@ -78,7 +87,9 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
@Override
public void onDestroy() {
super.onDestroy();
StateSaver.onDestroy(savedState);
if (useDefaultStateSaving) {
StateSaver.onDestroy(savedState);
}
PreferenceManager.getDefaultSharedPreferences(activity)
.unregisterOnSharedPreferenceChangeListener(this);
}
@@ -90,8 +101,9 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
if (updateFlags != 0) {
if ((updateFlags & LIST_MODE_UPDATE_FLAG) != 0) {
final boolean useGrid = isGridLayout();
itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager());
infoListAdapter.setGridItemVariants(useGrid);
itemsList.setLayoutManager(useGrid
? getGridLayoutManager() : getListLayoutManager());
infoListAdapter.setUseGridVariant(useGrid);
infoListAdapter.notifyDataSetChanged();
}
updateFlags = 0;
@@ -102,7 +114,15 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
// State Saving
//////////////////////////////////////////////////////////////////////////*/
protected StateSaver.SavedState savedState;
/**
* If the default implementation of {@link StateSaver.WriteRead} should be used.
*
* @see StateSaver
* @param useDefaultStateSaving Whether the default implementation should be used
*/
public void setUseDefaultStateSaving(final boolean useDefaultStateSaving) {
this.useDefaultStateSaving = useDefaultStateSaving;
}
@Override
public String generateSuffix() {
@@ -110,28 +130,81 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
return "." + infoListAdapter.getItemsList().size() + ".list";
}
private int getFocusedPosition() {
try {
final View focusedItem = itemsList.getFocusedChild();
final RecyclerView.ViewHolder itemHolder =
itemsList.findContainingViewHolder(focusedItem);
return itemHolder.getAdapterPosition();
} catch (NullPointerException e) {
return -1;
}
}
@Override
public void writeTo(Queue<Object> objectsToSave) {
public void writeTo(final Queue<Object> objectsToSave) {
if (!useDefaultStateSaving) {
return;
}
objectsToSave.add(infoListAdapter.getItemsList());
objectsToSave.add(getFocusedPosition());
}
@Override
@SuppressWarnings("unchecked")
public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception {
public void readFrom(@NonNull final Queue<Object> savedObjects) throws Exception {
if (!useDefaultStateSaving) {
return;
}
infoListAdapter.getItemsList().clear();
infoListAdapter.getItemsList().addAll((List<InfoItem>) savedObjects.poll());
restoreFocus((Integer) savedObjects.poll());
}
private void restoreFocus(final Integer position) {
if (position == null || position < 0) {
return;
}
itemsList.post(() -> {
RecyclerView.ViewHolder focusedHolder =
itemsList.findViewHolderForAdapterPosition(position);
if (focusedHolder != null) {
focusedHolder.itemView.requestFocus();
}
});
}
@Override
public void onSaveInstanceState(Bundle bundle) {
public void onSaveInstanceState(final Bundle bundle) {
super.onSaveInstanceState(bundle);
savedState = StateSaver.tryToSave(activity.isChangingConfigurations(), savedState, bundle, this);
if (useDefaultStateSaving) {
savedState = StateSaver
.tryToSave(activity.isChangingConfigurations(), savedState, bundle, this);
}
}
@Override
protected void onRestoreInstanceState(@NonNull Bundle bundle) {
protected void onRestoreInstanceState(@NonNull final Bundle bundle) {
super.onRestoreInstanceState(bundle);
savedState = StateSaver.tryToRestore(bundle, this);
if (useDefaultStateSaving) {
savedState = StateSaver.tryToRestore(bundle, this);
}
}
@Override
public void onStop() {
focusedPosition = getFocusedPosition();
super.onStop();
}
@Override
public void onStart() {
super.onStart();
restoreFocus(focusedPosition);
}
/*//////////////////////////////////////////////////////////////////////////
@@ -147,36 +220,39 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
}
protected RecyclerView.LayoutManager getListLayoutManager() {
return new LinearLayoutManager(activity);
return new SuperScrollLayoutManager(activity);
}
protected RecyclerView.LayoutManager getGridLayoutManager() {
final Resources resources = activity.getResources();
int width = resources.getDimensionPixelSize(R.dimen.video_item_grid_thumbnail_image_width);
width += (24 * resources.getDisplayMetrics().density);
final int spanCount = (int) Math.floor(resources.getDisplayMetrics().widthPixels / (double)width);
final int spanCount = (int) Math.floor(resources.getDisplayMetrics().widthPixels
/ (double) width);
final GridLayoutManager lm = new GridLayoutManager(activity, spanCount);
lm.setSpanSizeLookup(infoListAdapter.getSpanSizeLookup(spanCount));
return lm;
}
@Override
protected void initViews(View rootView, Bundle savedInstanceState) {
protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
final boolean useGrid = isGridLayout();
itemsList = rootView.findViewById(R.id.items_list);
itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager());
infoListAdapter.setGridItemVariants(useGrid);
infoListAdapter.setUseGridVariant(useGrid);
infoListAdapter.setFooter(getListFooter());
infoListAdapter.setHeader(getListHeader());
itemsList.setAdapter(infoListAdapter);
}
protected void onItemSelected(InfoItem selectedItem) {
if (DEBUG) Log.d(TAG, "onItemSelected() called with: selectedItem = [" + selectedItem + "]");
protected void onItemSelected(final InfoItem selectedItem) {
if (DEBUG) {
Log.d(TAG, "onItemSelected() called with: selectedItem = [" + selectedItem + "]");
}
}
@Override
@@ -184,19 +260,19 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
super.initListeners();
infoListAdapter.setOnStreamSelectedListener(new OnClickGesture<StreamInfoItem>() {
@Override
public void selected(StreamInfoItem selectedItem) {
public void selected(final StreamInfoItem selectedItem) {
onStreamSelected(selectedItem);
}
@Override
public void held(StreamInfoItem selectedItem) {
public void held(final StreamInfoItem selectedItem) {
showStreamDialog(selectedItem);
}
});
infoListAdapter.setOnChannelSelectedListener(new OnClickGesture<ChannelInfoItem>() {
@Override
public void selected(ChannelInfoItem selectedItem) {
public void selected(final ChannelInfoItem selectedItem) {
try {
onItemSelected(selectedItem);
NavigationHelper.openChannelFragment(getFM(),
@@ -211,7 +287,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
infoListAdapter.setOnPlaylistSelectedListener(new OnClickGesture<PlaylistInfoItem>() {
@Override
public void selected(PlaylistInfoItem selectedItem) {
public void selected(final PlaylistInfoItem selectedItem) {
try {
onItemSelected(selectedItem);
NavigationHelper.openPlaylistFragment(getFM(),
@@ -226,7 +302,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
infoListAdapter.setOnCommentsSelectedListener(new OnClickGesture<CommentsInfoItem>() {
@Override
public void selected(CommentsInfoItem selectedItem) {
public void selected(final CommentsInfoItem selectedItem) {
onItemSelected(selectedItem);
}
});
@@ -234,13 +310,13 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
itemsList.clearOnScrollListeners();
itemsList.addOnScrollListener(new OnScrollBelowItemsListener() {
@Override
public void onScrolledDown(RecyclerView recyclerView) {
public void onScrolledDown(final RecyclerView recyclerView) {
onScrollToBottom();
}
});
}
private void onStreamSelected(StreamInfoItem selectedItem) {
private void onStreamSelected(final StreamInfoItem selectedItem) {
onItemSelected(selectedItem);
NavigationHelper.openVideoDetailFragment(getFM(),
selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName());
@@ -253,12 +329,12 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
}
protected void showStreamDialog(final StreamInfoItem item) {
final Context context = getContext();
final Activity activity = getActivity();
if (context == null || context.getResources() == null || activity == null) return;
if (context == null || context.getResources() == null || activity == null) {
return;
}
if (item.getStreamType() == StreamType.AUDIO_STREAM) {
StreamDialogEntry.setEnabledEntries(
@@ -276,8 +352,8 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
StreamDialogEntry.share);
}
new InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context), (dialog, which) ->
StreamDialogEntry.clickOn(which, this, item)).show();
new InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context),
(dialog, which) -> StreamDialogEntry.clickOn(which, this, item)).show();
}
/*//////////////////////////////////////////////////////////////////////////
@@ -285,8 +361,11 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]");
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
if (DEBUG) {
Log.d(TAG, "onCreateOptionsMenu() called with: "
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
}
super.onCreateOptionsMenu(menu, inflater);
ActionBar supportActionBar = activity.getSupportActionBar();
if (supportActionBar != null) {
@@ -324,7 +403,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
}
@Override
public void showError(String message, boolean showRetryButton) {
public void showError(final String message, final boolean showRetryButton) {
super.showError(message, showRetryButton);
showListFooter(false);
animateView(itemsList, false, 200);
@@ -346,25 +425,28 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
}
@Override
public void handleNextItems(N result) {
public void handleNextItems(final N result) {
isLoading.set(false);
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences,
final String key) {
if (key.equals(getString(R.string.list_view_mode_key))) {
updateFlags |= LIST_MODE_UPDATE_FLAG;
}
}
protected boolean isGridLayout() {
final String list_mode = PreferenceManager.getDefaultSharedPreferences(activity).getString(getString(R.string.list_view_mode_key), getString(R.string.list_view_mode_value));
if ("auto".equals(list_mode)) {
final String listMode = PreferenceManager.getDefaultSharedPreferences(activity)
.getString(getString(R.string.list_view_mode_key),
getString(R.string.list_view_mode_value));
if ("auto".equals(listMode)) {
final Configuration configuration = getResources().getConfiguration();
return configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
&& configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE);
} else {
return "grid".equals(list_mode);
return "grid".equals(listMode);
}
}
}

View File

@@ -9,7 +9,9 @@ import androidx.annotation.NonNull;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.ListInfo;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.views.NewPipeRecyclerView;
import java.util.Queue;
@@ -21,7 +23,6 @@ import io.reactivex.schedulers.Schedulers;
public abstract class BaseListInfoFragment<I extends ListInfo>
extends BaseListFragment<I, ListExtractor.InfoItemsPage> {
@State
protected int serviceId = Constants.NO_SERVICE_ID;
@State
@@ -30,11 +31,11 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
protected String url;
protected I currentInfo;
protected String currentNextPageUrl;
protected Page currentNextPage;
protected Disposable currentWorker;
@Override
protected void initViews(View rootView, Bundle savedInstanceState) {
protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
setTitle(name);
showListFooter(hasMoreItems());
@@ -43,7 +44,9 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
@Override
public void onPause() {
super.onPause();
if (currentWorker != null) currentWorker.dispose();
if (currentWorker != null) {
currentWorker.dispose();
}
}
@Override
@@ -73,18 +76,18 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
//////////////////////////////////////////////////////////////////////////*/
@Override
public void writeTo(Queue<Object> objectsToSave) {
public void writeTo(final Queue<Object> objectsToSave) {
super.writeTo(objectsToSave);
objectsToSave.add(currentInfo);
objectsToSave.add(currentNextPageUrl);
objectsToSave.add(currentNextPage);
}
@Override
@SuppressWarnings("unchecked")
public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception {
public void readFrom(@NonNull final Queue<Object> savedObjects) throws Exception {
super.readFrom(savedObjects);
currentInfo = (I) savedObjects.poll();
currentNextPageUrl = (String) savedObjects.poll();
currentNextPage = (Page) savedObjects.poll();
}
/*//////////////////////////////////////////////////////////////////////////
@@ -92,10 +95,14 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
//////////////////////////////////////////////////////////////////////////*/
protected void doInitialLoadLogic() {
if (DEBUG) Log.d(TAG, "doInitialLoadLogic() called");
if (DEBUG) {
Log.d(TAG, "doInitialLoadLogic() called");
}
if (currentInfo == null) {
startLoading(false);
} else handleResult(currentInfo);
} else {
handleResult(currentInfo);
}
}
/**
@@ -103,43 +110,56 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
* You can use the default implementations from {@link org.schabi.newpipe.util.ExtractorHelper}.
*
* @param forceLoad allow or disallow the result to come from the cache
* @return Rx {@link Single} containing the {@link ListInfo}
*/
protected abstract Single<I> loadResult(boolean forceLoad);
@Override
public void startLoading(boolean forceLoad) {
public void startLoading(final boolean forceLoad) {
super.startLoading(forceLoad);
showListFooter(false);
infoListAdapter.clearStreamItemList();
currentInfo = null;
if (currentWorker != null) currentWorker.dispose();
if (currentWorker != null) {
currentWorker.dispose();
}
currentWorker = loadResult(forceLoad)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe((@NonNull I result) -> {
isLoading.set(false);
currentInfo = result;
currentNextPageUrl = result.getNextPageUrl();
currentNextPage = result.getNextPage();
handleResult(result);
}, (@NonNull Throwable throwable) -> onError(throwable));
}
/**
* Implement the logic to load more items<br/>
* You can use the default implementations from {@link org.schabi.newpipe.util.ExtractorHelper}
* Implement the logic to load more items.
* <p>You can use the default implementations
* from {@link org.schabi.newpipe.util.ExtractorHelper}.</p>
*
* @return Rx {@link Single} containing the {@link ListExtractor.InfoItemsPage}
*/
protected abstract Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic();
protected void loadMoreItems() {
isLoading.set(true);
if (currentWorker != null) currentWorker.dispose();
if (currentWorker != null) {
currentWorker.dispose();
}
forbidDownwardFocusScroll();
currentWorker = loadMoreItemsLogic()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe((@io.reactivex.annotations.NonNull ListExtractor.InfoItemsPage InfoItemsPage) -> {
.doFinally(this::allowDownwardFocusScroll)
.subscribe((@io.reactivex.annotations.NonNull
ListExtractor.InfoItemsPage InfoItemsPage) -> {
isLoading.set(false);
handleNextItems(InfoItemsPage);
}, (@io.reactivex.annotations.NonNull Throwable throwable) -> {
@@ -148,10 +168,22 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
});
}
private void forbidDownwardFocusScroll() {
if (itemsList instanceof NewPipeRecyclerView) {
((NewPipeRecyclerView) itemsList).setFocusScrollAllowed(false);
}
}
private void allowDownwardFocusScroll() {
if (itemsList instanceof NewPipeRecyclerView) {
((NewPipeRecyclerView) itemsList).setFocusScrollAllowed(true);
}
}
@Override
public void handleNextItems(ListExtractor.InfoItemsPage result) {
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
super.handleNextItems(result);
currentNextPageUrl = result.getNextPageUrl();
currentNextPage = result.getNextPage();
infoListAdapter.addInfoItemList(result.getItems());
showListFooter(hasMoreItems());
@@ -159,7 +191,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
@Override
protected boolean hasMoreItems() {
return !TextUtils.isEmpty(currentNextPageUrl);
return Page.isValid(currentNextPage);
}
/*//////////////////////////////////////////////////////////////////////////
@@ -167,7 +199,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
//////////////////////////////////////////////////////////////////////////*/
@Override
public void handleResult(@NonNull I result) {
public void handleResult(@NonNull final I result) {
super.handleResult(result);
name = result.getName();
@@ -188,9 +220,9 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
// Utils
//////////////////////////////////////////////////////////////////////////*/
protected void setInitialData(int serviceId, String url, String name) {
this.serviceId = serviceId;
this.url = url;
this.name = !TextUtils.isEmpty(name) ? name : "";
protected void setInitialData(final int sid, final String u, final String title) {
this.serviceId = sid;
this.url = u;
this.name = !TextUtils.isEmpty(title) ? title : "";
}
}

View File

@@ -4,12 +4,9 @@ import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.appcompat.app.ActionBar;
import android.text.TextUtils;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@@ -21,6 +18,12 @@ import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import com.jakewharton.rxbinding2.view.RxView;
import org.schabi.newpipe.R;
@@ -29,13 +32,14 @@ import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.local.subscription.SubscriptionService;
import org.schabi.newpipe.local.subscription.SubscriptionManager;
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.ExtractorHelper;
@@ -43,8 +47,10 @@ import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ShareUtils;
import org.schabi.newpipe.util.ThemeHelper;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -62,42 +68,43 @@ import static org.schabi.newpipe.util.AnimationUtils.animateBackgroundColor;
import static org.schabi.newpipe.util.AnimationUtils.animateTextColor;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
implements View.OnClickListener {
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
private final CompositeDisposable disposables = new CompositeDisposable();
private Disposable subscribeButtonMonitor;
private SubscriptionService subscriptionService;
/*//////////////////////////////////////////////////////////////////////////
// Views
//////////////////////////////////////////////////////////////////////////*/
private SubscriptionManager subscriptionManager;
private View headerRootLayout;
private ImageView headerChannelBanner;
private ImageView headerAvatarView;
private TextView headerTitleView;
private ImageView headerSubChannelAvatarView;
private TextView headerSubChannelTitleView;
private TextView headerSubscribersTextView;
private Button headerSubscribeButton;
private View playlistCtrl;
private LinearLayout headerPlayAllButton;
private LinearLayout headerPopupButton;
private LinearLayout headerBackgroundButton;
private MenuItem menuRssButton;
private TextView contentNotSupportedTextView;
private TextView kaomojiTextView;
private TextView noVideosTextView;
public static ChannelFragment getInstance(int serviceId, String url, String name) {
public static ChannelFragment getInstance(final int serviceId, final String url,
final String name) {
ChannelFragment instance = new ChannelFragment();
instance.setInitialData(serviceId, url, name);
return instance;
}
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
public void setUserVisibleHint(final boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (activity != null
&& useAsFrontPage
@@ -106,22 +113,40 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
}
}
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onAttach(Context context) {
public void onAttach(final Context context) {
super.onAttach(context);
subscriptionService = SubscriptionService.getInstance(activity);
subscriptionManager = new SubscriptionManager(activity);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_channel, container, false);
}
@Override
public void onViewCreated(final View rootView, final Bundle savedInstanceState) {
super.onViewCreated(rootView, savedInstanceState);
contentNotSupportedTextView = rootView.findViewById(R.id.error_content_not_supported);
kaomojiTextView = rootView.findViewById(R.id.channel_kaomoji);
noVideosTextView = rootView.findViewById(R.id.channel_no_videos);
}
@Override
public void onDestroy() {
super.onDestroy();
if (disposables != null) disposables.clear();
if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose();
if (disposables != null) {
disposables.clear();
}
if (subscribeButtonMonitor != null) {
subscribeButtonMonitor.dispose();
}
}
/*//////////////////////////////////////////////////////////////////////////
@@ -129,14 +154,18 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
//////////////////////////////////////////////////////////////////////////*/
protected View getListHeader() {
headerRootLayout = activity.getLayoutInflater().inflate(R.layout.channel_header, itemsList, false);
headerRootLayout = activity.getLayoutInflater()
.inflate(R.layout.channel_header, itemsList, false);
headerChannelBanner = headerRootLayout.findViewById(R.id.channel_banner_image);
headerAvatarView = headerRootLayout.findViewById(R.id.channel_avatar_view);
headerTitleView = headerRootLayout.findViewById(R.id.channel_title_view);
headerSubscribersTextView = headerRootLayout.findViewById(R.id.channel_subscriber_view);
headerSubscribeButton = headerRootLayout.findViewById(R.id.channel_subscribe_button);
playlistCtrl = headerRootLayout.findViewById(R.id.playlist_control);
headerSubChannelAvatarView =
headerRootLayout.findViewById(R.id.sub_channel_avatar_view);
headerSubChannelTitleView =
headerRootLayout.findViewById(R.id.sub_channel_title_view);
headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_all_button);
headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button);
@@ -145,12 +174,20 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
return headerRootLayout;
}
@Override
protected void initListeners() {
super.initListeners();
headerSubChannelTitleView.setOnClickListener(this);
headerSubChannelAvatarView.setOnClickListener(this);
}
/*//////////////////////////////////////////////////////////////////////////
// Menu
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
ActionBar supportActionBar = activity.getSupportActionBar();
if (useAsFrontPage && supportActionBar != null) {
@@ -158,8 +195,10 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
} else {
inflater.inflate(R.menu.menu_channel, menu);
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu +
"], inflater = [" + inflater + "]");
if (DEBUG) {
Log.d(TAG, "onCreateOptionsMenu() called with: "
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
}
menuRssButton = menu.findItem(R.id.menu_item_rss);
}
}
@@ -173,19 +212,22 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case R.id.action_settings:
NavigationHelper.openSettings(requireContext());
break;
case R.id.menu_item_rss:
openRssFeed();
break;
case R.id.menu_item_openInBrowser:
if (currentInfo != null) {
ShareUtils.openUrlInBrowser(this.getContext(), currentInfo.getOriginalUrl());
ShareUtils.openUrlInBrowser(requireContext(), currentInfo.getOriginalUrl());
}
break;
case R.id.menu_item_share:
if (currentInfo != null) {
ShareUtils.shareUrl(this.getContext(), name, currentInfo.getOriginalUrl());
ShareUtils.shareUrl(requireContext(), name, currentInfo.getOriginalUrl());
}
break;
default:
@@ -198,19 +240,17 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
// Channel Subscription
//////////////////////////////////////////////////////////////////////////*/
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
private void monitorSubscription(final ChannelInfo info) {
final Consumer<Throwable> onError = (Throwable throwable) -> {
animateView(headerSubscribeButton, false, 100);
showSnackBarError(throwable, UserAction.SUBSCRIPTION,
NewPipe.getNameOfService(currentInfo.getServiceId()),
"Get subscription status",
0);
animateView(headerSubscribeButton, false, 100);
showSnackBarError(throwable, UserAction.SUBSCRIPTION,
NewPipe.getNameOfService(currentInfo.getServiceId()),
"Get subscription status", 0);
};
final Observable<List<SubscriptionEntity>> observable = subscriptionService.subscriptionTable()
.getSubscription(info.getServiceId(), info.getUrl())
final Observable<List<SubscriptionEntity>> observable = subscriptionManager
.subscriptionTable()
.getSubscriptionFlowable(info.getServiceId(), info.getUrl())
.toObservable();
disposables.add(observable
@@ -218,34 +258,40 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
.subscribe(getSubscribeUpdateMonitor(info), onError));
disposables.add(observable
// Some updates are very rapid (when calling the updateSubscription(info), for example)
// so only update the UI for the latest emission ("sync" the subscribe button's state)
// Some updates are very rapid
// (for example when calling the updateSubscription(info))
// so only update the UI for the latest emission
// ("sync" the subscribe button's state)
.debounce(100, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe((List<SubscriptionEntity> subscriptionEntities) ->
updateSubscribeButton(!subscriptionEntities.isEmpty())
, onError));
updateSubscribeButton(!subscriptionEntities.isEmpty()), onError));
}
private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription) {
private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription,
final ChannelInfo info) {
return (@NonNull Object o) -> {
subscriptionService.subscriptionTable().insert(subscription);
subscriptionManager.insertSubscription(subscription, info);
return o;
};
}
private Function<Object, Object> mapOnUnsubscribe(final SubscriptionEntity subscription) {
return (@NonNull Object o) -> {
subscriptionService.subscriptionTable().delete(subscription);
subscriptionManager.deleteSubscription(subscription);
return o;
};
}
private void updateSubscription(final ChannelInfo info) {
if (DEBUG) Log.d(TAG, "updateSubscription() called with: info = [" + info + "]");
if (DEBUG) {
Log.d(TAG, "updateSubscription() called with: info = [" + info + "]");
}
final Action onComplete = () -> {
if (DEBUG) Log.d(TAG, "Updated subscription: " + info.getUrl());
if (DEBUG) {
Log.d(TAG, "Updated subscription: " + info.getUrl());
}
};
final Consumer<Throwable> onError = (@NonNull Throwable throwable) ->
@@ -255,15 +301,18 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
"Updating Subscription for " + info.getUrl(),
R.string.subscription_update_failed);
disposables.add(subscriptionService.updateChannelInfo(info)
disposables.add(subscriptionManager.updateChannelInfo(info)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(onComplete, onError));
}
private Disposable monitorSubscribeButton(final Button subscribeButton, final Function<Object, Object> action) {
private Disposable monitorSubscribeButton(final Button subscribeButton,
final Function<Object, Object> action) {
final Consumer<Object> onNext = (@NonNull Object o) -> {
if (DEBUG) Log.d(TAG, "Changed subscription status to this channel!");
if (DEBUG) {
Log.d(TAG, "Changed subscription status to this channel!");
}
};
final Consumer<Throwable> onError = (@NonNull Throwable throwable) ->
@@ -284,12 +333,18 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
private Consumer<List<SubscriptionEntity>> getSubscribeUpdateMonitor(final ChannelInfo info) {
return (List<SubscriptionEntity> subscriptionEntities) -> {
if (DEBUG)
Log.d(TAG, "subscriptionService.subscriptionTable.doOnNext() called with: subscriptionEntities = [" + subscriptionEntities + "]");
if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose();
if (DEBUG) {
Log.d(TAG, "subscriptionManager.subscriptionTable.doOnNext() called with: "
+ "subscriptionEntities = [" + subscriptionEntities + "]");
}
if (subscribeButtonMonitor != null) {
subscribeButtonMonitor.dispose();
}
if (subscriptionEntities.isEmpty()) {
if (DEBUG) Log.d(TAG, "No subscription to this channel!");
if (DEBUG) {
Log.d(TAG, "No subscription to this channel!");
}
SubscriptionEntity channel = new SubscriptionEntity();
channel.setServiceId(info.getServiceId());
channel.setUrl(info.getUrl());
@@ -297,34 +352,45 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
info.getAvatarUrl(),
info.getDescription(),
info.getSubscriberCount());
subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnSubscribe(channel));
subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton,
mapOnSubscribe(channel, info));
} else {
if (DEBUG) Log.d(TAG, "Found subscription to this channel!");
if (DEBUG) {
Log.d(TAG, "Found subscription to this channel!");
}
final SubscriptionEntity subscription = subscriptionEntities.get(0);
subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnUnsubscribe(subscription));
subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton,
mapOnUnsubscribe(subscription));
}
};
}
private void updateSubscribeButton(boolean isSubscribed) {
if (DEBUG) Log.d(TAG, "updateSubscribeButton() called with: isSubscribed = [" + isSubscribed + "]");
private void updateSubscribeButton(final boolean isSubscribed) {
if (DEBUG) {
Log.d(TAG, "updateSubscribeButton() called with: "
+ "isSubscribed = [" + isSubscribed + "]");
}
boolean isButtonVisible = headerSubscribeButton.getVisibility() == View.VISIBLE;
int backgroundDuration = isButtonVisible ? 300 : 0;
int textDuration = isButtonVisible ? 200 : 0;
int subscribeBackground = ContextCompat.getColor(activity, R.color.subscribe_background_color);
int subscribeBackground = ThemeHelper
.resolveColorFromAttr(activity, R.attr.colorPrimary);
int subscribeText = ContextCompat.getColor(activity, R.color.subscribe_text_color);
int subscribedBackground = ContextCompat.getColor(activity, R.color.subscribed_background_color);
int subscribedBackground = ContextCompat
.getColor(activity, R.color.subscribed_background_color);
int subscribedText = ContextCompat.getColor(activity, R.color.subscribed_text_color);
if (!isSubscribed) {
headerSubscribeButton.setText(R.string.subscribe_button_title);
animateBackgroundColor(headerSubscribeButton, backgroundDuration, subscribedBackground, subscribeBackground);
animateBackgroundColor(headerSubscribeButton, backgroundDuration, subscribedBackground,
subscribeBackground);
animateTextColor(headerSubscribeButton, textDuration, subscribedText, subscribeText);
} else {
headerSubscribeButton.setText(R.string.subscribed_button_title);
animateBackgroundColor(headerSubscribeButton, backgroundDuration, subscribeBackground, subscribedBackground);
animateBackgroundColor(headerSubscribeButton, backgroundDuration, subscribeBackground,
subscribedBackground);
animateTextColor(headerSubscribeButton, textDuration, subscribeText, subscribedText);
}
@@ -337,14 +403,42 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
@Override
protected Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
return ExtractorHelper.getMoreChannelItems(serviceId, url, currentNextPageUrl);
return ExtractorHelper.getMoreChannelItems(serviceId, url, currentNextPage);
}
@Override
protected Single<ChannelInfo> loadResult(boolean forceLoad) {
protected Single<ChannelInfo> loadResult(final boolean forceLoad) {
return ExtractorHelper.getChannelInfo(serviceId, url, forceLoad);
}
/*//////////////////////////////////////////////////////////////////////////
// OnClick
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onClick(final View v) {
if (isLoading.get() || currentInfo == null) {
return;
}
switch (v.getId()) {
case R.id.sub_channel_avatar_view:
case R.id.sub_channel_title_view:
if (!TextUtils.isEmpty(currentInfo.getParentChannelUrl())) {
try {
NavigationHelper.openChannelFragment(getFragmentManager(),
currentInfo.getServiceId(), currentInfo.getParentChannelUrl(),
currentInfo.getParentChannelName());
} catch (Exception e) {
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
}
} else if (DEBUG) {
Log.i(TAG, "Can't open parent channel because we got no channel URL");
}
break;
}
}
/*//////////////////////////////////////////////////////////////////////////
// Contract
//////////////////////////////////////////////////////////////////////////*/
@@ -353,47 +447,100 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
public void showLoading() {
super.showLoading();
imageLoader.cancelDisplayTask(headerChannelBanner);
imageLoader.cancelDisplayTask(headerAvatarView);
IMAGE_LOADER.cancelDisplayTask(headerChannelBanner);
IMAGE_LOADER.cancelDisplayTask(headerAvatarView);
IMAGE_LOADER.cancelDisplayTask(headerSubChannelAvatarView);
animateView(headerSubscribeButton, false, 100);
}
@Override
public void handleResult(@NonNull ChannelInfo result) {
public void handleResult(@NonNull final ChannelInfo result) {
super.handleResult(result);
headerRootLayout.setVisibility(View.VISIBLE);
imageLoader.displayImage(result.getBannerUrl(), headerChannelBanner,
IMAGE_LOADER.displayImage(result.getBannerUrl(), headerChannelBanner,
ImageDisplayConstants.DISPLAY_BANNER_OPTIONS);
imageLoader.displayImage(result.getAvatarUrl(), headerAvatarView,
IMAGE_LOADER.displayImage(result.getAvatarUrl(), headerAvatarView,
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
IMAGE_LOADER.displayImage(result.getParentChannelAvatarUrl(), headerSubChannelAvatarView,
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
headerSubscribersTextView.setVisibility(View.VISIBLE);
if (result.getSubscriberCount() >= 0) {
headerSubscribersTextView.setText(Localization.localizeSubscribersCount(activity, result.getSubscriberCount()));
headerSubscribersTextView.setText(Localization
.shortSubscriberCount(activity, result.getSubscriberCount()));
} else {
headerSubscribersTextView.setText(R.string.subscribers_count_not_available);
}
if (menuRssButton != null) menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl()));
if (!TextUtils.isEmpty(currentInfo.getParentChannelName())) {
headerSubChannelTitleView.setText(String.format(
getString(R.string.channel_created_by),
currentInfo.getParentChannelName())
);
headerSubChannelTitleView.setVisibility(View.VISIBLE);
headerSubChannelAvatarView.setVisibility(View.VISIBLE);
} else {
headerSubChannelTitleView.setVisibility(View.GONE);
}
if (menuRssButton != null) {
menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl()));
}
playlistCtrl.setVisibility(View.VISIBLE);
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
List<Throwable> errors = new ArrayList<>(result.getErrors());
if (!errors.isEmpty()) {
// handling ContentNotSupportedException not to show the error but an appropriate string
// so that crashes won't be sent uselessly and the user will understand what happened
for (Iterator<Throwable> it = errors.iterator(); it.hasNext();) {
Throwable throwable = it.next();
if (throwable instanceof ContentNotSupportedException) {
showContentNotSupported();
it.remove();
}
}
if (!errors.isEmpty()) {
showSnackBarError(errors, UserAction.REQUESTED_CHANNEL,
NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
}
}
if (disposables != null) disposables.clear();
if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose();
if (disposables != null) {
disposables.clear();
}
if (subscribeButtonMonitor != null) {
subscribeButtonMonitor.dispose();
}
updateSubscription(result);
monitorSubscription(result);
headerPlayAllButton.setOnClickListener(
view -> NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), true));
headerPopupButton.setOnClickListener(
view -> NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
headerBackgroundButton.setOnClickListener(
view -> NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
headerPlayAllButton.setOnClickListener(view -> NavigationHelper
.playOnMainPlayer(activity, getPlayQueue(), true));
headerPopupButton.setOnClickListener(view -> NavigationHelper
.playOnPopupPlayer(activity, getPlayQueue(), false));
headerBackgroundButton.setOnClickListener(view -> NavigationHelper
.playOnBackgroundPlayer(activity, getPlayQueue(), false));
headerPopupButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue(), true);
return true;
});
headerBackgroundButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue(), true);
return true;
});
}
private void showContentNotSupported() {
contentNotSupportedTextView.setVisibility(View.VISIBLE);
kaomojiTextView.setText("(︶︹︺)");
kaomojiTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 45f);
noVideosTextView.setVisibility(View.GONE);
}
private PlayQueue getPlayQueue() {
@@ -407,17 +554,12 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
streamItems.add((StreamInfoItem) i);
}
}
return new ChannelPlayQueue(
currentInfo.getServiceId(),
currentInfo.getUrl(),
currentInfo.getNextPageUrl(),
streamItems,
index
);
return new ChannelPlayQueue(currentInfo.getServiceId(), currentInfo.getUrl(),
currentInfo.getNextPage(), streamItems, index);
}
@Override
public void handleNextItems(ListExtractor.InfoItemsPage result) {
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
super.handleNextItems(result);
if (!result.getErrors().isEmpty()) {
@@ -434,19 +576,17 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
//////////////////////////////////////////////////////////////////////////*/
@Override
protected boolean onError(Throwable exception) {
if (super.onError(exception)) return true;
if (exception instanceof ContentNotAvailableException) {
showError(getString(R.string.content_not_available), false);
} else {
int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error;
onUnrecoverableError(exception,
UserAction.REQUESTED_CHANNEL,
NewPipe.getNameOfService(serviceId),
url,
errorId);
protected boolean onError(final Throwable exception) {
if (super.onError(exception)) {
return true;
}
int errorId = exception instanceof ExtractionException
? R.string.parsing_error : R.string.general_error;
onUnrecoverableError(exception, UserAction.REQUESTED_CHANNEL,
NewPipe.getNameOfService(serviceId), url, errorId);
return true;
}
@@ -455,8 +595,10 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
//////////////////////////////////////////////////////////////////////////*/
@Override
public void setTitle(String title) {
public void setTitle(final String title) {
super.setTitle(title);
if (!useAsFrontPage) headerTitleView.setText(title);
if (!useAsFrontPage) {
headerTitleView.setText(title);
}
}
}

View File

@@ -2,14 +2,15 @@ package org.schabi.newpipe.fragments.list.comments;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe;
@@ -23,17 +24,12 @@ import io.reactivex.Single;
import io.reactivex.disposables.CompositeDisposable;
public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
private CompositeDisposable disposables = new CompositeDisposable();
/*//////////////////////////////////////////////////////////////////////////
// Views
//////////////////////////////////////////////////////////////////////////*/
private boolean mIsVisibleToUser = false;
public static CommentsFragment getInstance(int serviceId, String url, String name) {
public static CommentsFragment getInstance(final int serviceId, final String url,
final String name) {
CommentsFragment instance = new CommentsFragment();
instance.setInitialData(serviceId, url, name);
return instance;
@@ -44,39 +40,42 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
//////////////////////////////////////////////////////////////////////////*/
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
public void setUserVisibleHint(final boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
mIsVisibleToUser = isVisibleToUser;
}
@Override
public void onAttach(Context context) {
public void onAttach(final Context context) {
super.onAttach(context);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_comments, container, false);
}
@Override
public void onDestroy() {
super.onDestroy();
if (disposables != null) disposables.clear();
if (disposables != null) {
disposables.clear();
}
}
/*//////////////////////////////////////////////////////////////////////////
// Load and handle
//////////////////////////////////////////////////////////////////////////*/
@Override
protected Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
return ExtractorHelper.getMoreCommentItems(serviceId, currentInfo, currentNextPageUrl);
return ExtractorHelper.getMoreCommentItems(serviceId, currentInfo, currentNextPage);
}
@Override
protected Single<CommentsInfo> loadResult(boolean forceLoad) {
protected Single<CommentsInfo> loadResult(final boolean forceLoad) {
return ExtractorHelper.getCommentsInfo(serviceId, url, forceLoad);
}
@@ -90,27 +89,28 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
}
@Override
public void handleResult(@NonNull CommentsInfo result) {
public void handleResult(@NonNull final CommentsInfo result) {
super.handleResult(result);
AnimationUtils.slideUp(getView(),120, 150, 0.06f);
AnimationUtils.slideUp(getView(), 120, 150, 0.06f);
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.REQUESTED_COMMENTS, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
showSnackBarError(result.getErrors(), UserAction.REQUESTED_COMMENTS,
NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
}
if (disposables != null) disposables.clear();
if (disposables != null) {
disposables.clear();
}
}
@Override
public void handleNextItems(ListExtractor.InfoItemsPage result) {
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
super.handleNextItems(result);
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(),
UserAction.REQUESTED_COMMENTS,
NewPipe.getNameOfService(serviceId),
"Get next page of: " + url,
showSnackBarError(result.getErrors(), UserAction.REQUESTED_COMMENTS,
NewPipe.getNameOfService(serviceId), "Get next page of: " + url,
R.string.general_error);
}
}
@@ -120,11 +120,14 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
//////////////////////////////////////////////////////////////////////////*/
@Override
protected boolean onError(Throwable exception) {
if (super.onError(exception)) return true;
protected boolean onError(final Throwable exception) {
if (super.onError(exception)) {
return true;
}
hideLoading();
showSnackBarError(exception, UserAction.REQUESTED_COMMENTS, NewPipe.getNameOfService(serviceId), url, R.string.error_unable_to_load_comments);
showSnackBarError(exception, UserAction.REQUESTED_COMMENTS,
NewPipe.getNameOfService(serviceId), url, R.string.error_unable_to_load_comments);
return true;
}
@@ -133,14 +136,10 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
//////////////////////////////////////////////////////////////////////////*/
@Override
public void setTitle(String title) {
return;
}
public void setTitle(final String title) { }
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
return;
}
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { }
@Override
protected boolean isGridLayout() {

View File

@@ -10,9 +10,8 @@ import org.schabi.newpipe.util.KioskTranslator;
import org.schabi.newpipe.util.ServiceHelper;
public class DefaultKioskFragment extends KioskFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (serviceId < 0) {
@@ -25,7 +24,9 @@ public class DefaultKioskFragment extends KioskFragment {
super.onResume();
if (serviceId != ServiceHelper.getSelectedServiceId(requireContext())) {
if (currentWorker != null) currentWorker.dispose();
if (currentWorker != null) {
currentWorker.dispose();
}
updateSelectedDefaultKiosk();
reloadContent();
}
@@ -43,9 +44,10 @@ public class DefaultKioskFragment extends KioskFragment {
name = kioskTranslatedName;
currentInfo = null;
currentNextPageUrl = null;
currentNextPage = null;
} catch (ExtractionException e) {
onUnrecoverableError(e, UserAction.REQUESTED_KIOSK, "none", "Loading default kiosk from selected service", 0);
onUnrecoverableError(e, UserAction.REQUESTED_KIOSK, "none",
"Loading default kiosk from selected service", 0);
}
}
}

View File

@@ -1,17 +1,16 @@
package org.schabi.newpipe.fragments.list.kiosk;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import android.preference.PreferenceManager;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe;
@@ -33,45 +32,45 @@ import static org.schabi.newpipe.util.AnimationUtils.animateView;
/**
* Created by Christian Schabesberger on 23.09.17.
*
* <p>
* Copyright (C) Christian Schabesberger 2017 <chris.schabesberger@mailbox.org>
* KioskFragment.java is part of NewPipe.
*
* </p>
* <p>
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* </p>
* <p>
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* </p>
* <p>
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
* </p>
*/
public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
@State
protected String kioskId = "";
protected String kioskTranslatedName;
String kioskId = "";
String kioskTranslatedName;
@State
protected ContentCountry contentCountry;
ContentCountry contentCountry;
/*//////////////////////////////////////////////////////////////////////////
// Views
//////////////////////////////////////////////////////////////////////////*/
public static KioskFragment getInstance(int serviceId)
throws ExtractionException {
public static KioskFragment getInstance(final int serviceId) throws ExtractionException {
return getInstance(serviceId, NewPipe.getService(serviceId)
.getKioskList()
.getDefaultKioskId());
.getKioskList().getDefaultKioskId());
}
public static KioskFragment getInstance(int serviceId, String kioskId)
public static KioskFragment getInstance(final int serviceId, final String kioskId)
throws ExtractionException {
KioskFragment instance = new KioskFragment();
StreamingService service = NewPipe.getService(serviceId);
@@ -88,7 +87,7 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreate(Bundle savedInstanceState) {
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
kioskTranslatedName = KioskTranslator.getTranslatedKioskName(kioskId, activity);
@@ -97,9 +96,9 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
public void setUserVisibleHint(final boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if(useAsFrontPage && isVisibleToUser && activity != null) {
if (useAsFrontPage && isVisibleToUser && activity != null) {
try {
setTitle(kioskTranslatedName);
} catch (Exception e) {
@@ -111,7 +110,9 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_kiosk, container, false);
}
@@ -129,7 +130,7 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
ActionBar supportActionBar = activity.getSupportActionBar();
if (supportActionBar != null && useAsFrontPage) {
@@ -142,18 +143,14 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
//////////////////////////////////////////////////////////////////////////*/
@Override
public Single<KioskInfo> loadResult(boolean forceReload) {
public Single<KioskInfo> loadResult(final boolean forceReload) {
contentCountry = Localization.getPreferredContentCountry(requireContext());
return ExtractorHelper.getKioskInfo(serviceId,
url,
forceReload);
return ExtractorHelper.getKioskInfo(serviceId, url, forceReload);
}
@Override
public Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
return ExtractorHelper.getMoreKioskItems(serviceId,
url,
currentNextPageUrl);
return ExtractorHelper.getMoreKioskItems(serviceId, url, currentNextPage);
}
/*//////////////////////////////////////////////////////////////////////////
@@ -181,13 +178,13 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
}
@Override
public void handleNextItems(ListExtractor.InfoItemsPage result) {
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
super.handleNextItems(result);
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(),
UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(serviceId)
, "Get next page of: " + url, 0);
UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(serviceId),
"Get next page of: " + url, 0);
}
}
}

View File

@@ -3,9 +3,6 @@ package org.schabi.newpipe.fragments.list.playlist;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
@@ -17,6 +14,10 @@ import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.schabi.newpipe.NewPipeDatabase;
@@ -38,6 +39,7 @@ import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ShareUtils;
import org.schabi.newpipe.util.StreamDialogEntry;
@@ -57,7 +59,6 @@ import io.reactivex.disposables.Disposables;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
private CompositeDisposable disposables;
private Subscription bookmarkReactor;
private AtomicBoolean isBookmarkButtonReady;
@@ -82,7 +83,8 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
private MenuItem playlistBookmarkButton;
public static PlaylistFragment getInstance(int serviceId, String url, String name) {
public static PlaylistFragment getInstance(final int serviceId, final String url,
final String name) {
PlaylistFragment instance = new PlaylistFragment();
instance.setInitialData(serviceId, url, name);
return instance;
@@ -93,17 +95,18 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreate(Bundle savedInstanceState) {
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
disposables = new CompositeDisposable();
isBookmarkButtonReady = new AtomicBoolean(false);
remotePlaylistManager = new RemotePlaylistManager(NewPipeDatabase.getInstance(
requireContext()));
remotePlaylistManager = new RemotePlaylistManager(NewPipeDatabase
.getInstance(requireContext()));
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_playlist, container, false);
}
@@ -112,7 +115,8 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
//////////////////////////////////////////////////////////////////////////*/
protected View getListHeader() {
headerRootLayout = activity.getLayoutInflater().inflate(R.layout.playlist_header, itemsList, false);
headerRootLayout = activity.getLayoutInflater()
.inflate(R.layout.playlist_header, itemsList, false);
headerTitleView = headerRootLayout.findViewById(R.id.playlist_title_view);
headerUploaderLayout = headerRootLayout.findViewById(R.id.uploader_layout);
headerUploaderName = headerRootLayout.findViewById(R.id.uploader_name);
@@ -129,21 +133,23 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
}
@Override
protected void initViews(View rootView, Bundle savedInstanceState) {
protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
infoListAdapter.useMiniItemVariants(true);
infoListAdapter.setUseMiniVariant(true);
}
private PlayQueue getPlayQueueStartingAt(StreamInfoItem infoItem) {
private PlayQueue getPlayQueueStartingAt(final StreamInfoItem infoItem) {
return getPlayQueue(Math.max(infoListAdapter.getItemsList().indexOf(infoItem), 0));
}
@Override
protected void showStreamDialog(StreamInfoItem item) {
protected void showStreamDialog(final StreamInfoItem item) {
final Context context = getContext();
final Activity activity = getActivity();
if (context == null || context.getResources() == null || activity == null) return;
if (context == null || context.getResources() == null || activity == null) {
return;
}
if (item.getStreamType() == StreamType.AUDIO_STREAM) {
StreamDialogEntry.setEnabledEntries(
@@ -160,21 +166,25 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
StreamDialogEntry.append_playlist,
StreamDialogEntry.share);
StreamDialogEntry.start_here_on_popup.setCustomAction(
(fragment, infoItem) -> NavigationHelper.playOnPopupPlayer(context, getPlayQueueStartingAt(infoItem), true));
StreamDialogEntry.start_here_on_popup.setCustomAction((fragment, infoItem) ->
NavigationHelper.playOnPopupPlayer(context,
getPlayQueueStartingAt(infoItem), true));
}
StreamDialogEntry.start_here_on_background.setCustomAction(
(fragment, infoItem) -> NavigationHelper.playOnBackgroundPlayer(context, getPlayQueueStartingAt(infoItem), true));
StreamDialogEntry.start_here_on_background.setCustomAction((fragment, infoItem) ->
NavigationHelper.playOnBackgroundPlayer(context,
getPlayQueueStartingAt(infoItem), true));
new InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context), (dialog, which) ->
StreamDialogEntry.clickOn(which, this, item)).show();
new InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context),
(dialog, which) -> StreamDialogEntry.clickOn(which, this, item)).show();
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu +
"], inflater = [" + inflater + "]");
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
if (DEBUG) {
Log.d(TAG, "onCreateOptionsMenu() called with: "
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
}
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.menu_playlist, menu);
@@ -185,10 +195,16 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
@Override
public void onDestroyView() {
super.onDestroyView();
if (isBookmarkButtonReady != null) isBookmarkButtonReady.set(false);
if (isBookmarkButtonReady != null) {
isBookmarkButtonReady.set(false);
}
if (disposables != null) disposables.clear();
if (bookmarkReactor != null) bookmarkReactor.cancel();
if (disposables != null) {
disposables.clear();
}
if (bookmarkReactor != null) {
bookmarkReactor.cancel();
}
bookmarkReactor = null;
}
@@ -197,7 +213,9 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
public void onDestroy() {
super.onDestroy();
if (disposables != null) disposables.dispose();
if (disposables != null) {
disposables.dispose();
}
disposables = null;
remotePlaylistManager = null;
@@ -211,22 +229,25 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
@Override
protected Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
return ExtractorHelper.getMorePlaylistItems(serviceId, url, currentNextPageUrl);
return ExtractorHelper.getMorePlaylistItems(serviceId, url, currentNextPage);
}
@Override
protected Single<PlaylistInfo> loadResult(boolean forceLoad) {
protected Single<PlaylistInfo> loadResult(final boolean forceLoad) {
return ExtractorHelper.getPlaylistInfo(serviceId, url, forceLoad);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case R.id.action_settings:
NavigationHelper.openSettings(requireContext());
break;
case R.id.menu_item_openInBrowser:
ShareUtils.openUrlInBrowser(this.getContext(), url);
ShareUtils.openUrlInBrowser(requireContext(), url);
break;
case R.id.menu_item_share:
ShareUtils.shareUrl(this.getContext(), name, url);
ShareUtils.shareUrl(requireContext(), name, url);
break;
case R.id.menu_item_bookmark:
onBookmarkClicked();
@@ -248,7 +269,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
animateView(headerRootLayout, false, 200);
animateView(itemsList, false, 100);
imageLoader.cancelDisplayTask(headerUploaderAvatar);
IMAGE_LOADER.cancelDisplayTask(headerUploaderAvatar);
animateView(headerUploaderLayout, false, 200);
}
@@ -259,7 +280,8 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
animateView(headerRootLayout, true, 100);
animateView(headerUploaderLayout, true, 300);
headerUploaderLayout.setOnClickListener(null);
if (!TextUtils.isEmpty(result.getUploaderName())) { // If we have an uploader : Put them into the ui
// If we have an uploader put them into the UI
if (!TextUtils.isEmpty(result.getUploaderName())) {
headerUploaderName.setText(result.getUploaderName());
if (!TextUtils.isEmpty(result.getUploaderUrl())) {
headerUploaderLayout.setOnClickListener(v -> {
@@ -273,19 +295,20 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
}
});
}
} else { // Else : say we have no uploader
} else { // Otherwise say we have no uploader
headerUploaderName.setText(R.string.playlist_no_uploader);
}
playlistCtrl.setVisibility(View.VISIBLE);
imageLoader.displayImage(result.getUploaderAvatarUrl(), headerUploaderAvatar,
IMAGE_LOADER.displayImage(result.getUploaderAvatarUrl(), headerUploaderAvatar,
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
headerStreamCount.setText(getResources().getQuantityString(R.plurals.videos,
(int) result.getStreamCount(), (int) result.getStreamCount()));
headerStreamCount.setText(Localization
.localizeStreamCount(getContext(), result.getStreamCount()));
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST,
NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
}
remotePlaylistManager.getPlaylist(result)
@@ -318,27 +341,27 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
private PlayQueue getPlayQueue(final int index) {
final List<StreamInfoItem> infoItems = new ArrayList<>();
for(InfoItem i : infoListAdapter.getItemsList()) {
if(i instanceof StreamInfoItem) {
for (InfoItem i : infoListAdapter.getItemsList()) {
if (i instanceof StreamInfoItem) {
infoItems.add((StreamInfoItem) i);
}
}
return new PlaylistPlayQueue(
currentInfo.getServiceId(),
currentInfo.getUrl(),
currentInfo.getNextPageUrl(),
currentInfo.getNextPage(),
infoItems,
index
);
}
@Override
public void handleNextItems(ListExtractor.InfoItemsPage result) {
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
super.handleNextItems(result);
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(serviceId)
, "Get next page of: " + url, 0);
showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST,
NewPipe.getNameOfService(serviceId), "Get next page of: " + url, 0);
}
}
@@ -347,15 +370,15 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
//////////////////////////////////////////////////////////////////////////*/
@Override
protected boolean onError(Throwable exception) {
if (super.onError(exception)) return true;
protected boolean onError(final Throwable exception) {
if (super.onError(exception)) {
return true;
}
int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error;
onUnrecoverableError(exception,
UserAction.REQUESTED_PLAYLIST,
NewPipe.getNameOfService(serviceId),
url,
errorId);
int errorId = exception instanceof ExtractionException
? R.string.parsing_error : R.string.general_error;
onUnrecoverableError(exception, UserAction.REQUESTED_PLAYLIST,
NewPipe.getNameOfService(serviceId), url, errorId);
return true;
}
@@ -363,13 +386,18 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
// Utils
//////////////////////////////////////////////////////////////////////////*/
private Flowable<Integer> getUpdateProcessor(@NonNull List<PlaylistRemoteEntity> playlists,
@NonNull PlaylistInfo result) {
private Flowable<Integer> getUpdateProcessor(
@NonNull final List<PlaylistRemoteEntity> playlists,
@NonNull final PlaylistInfo result) {
final Flowable<Integer> noItemToUpdate = Flowable.just(/*noItemToUpdate=*/-1);
if (playlists.isEmpty()) return noItemToUpdate;
if (playlists.isEmpty()) {
return noItemToUpdate;
}
final PlaylistRemoteEntity playlistEntity = playlists.get(0);
if (playlistEntity.isIdenticalTo(result)) return noItemToUpdate;
final PlaylistRemoteEntity playlistRemoteEntity = playlists.get(0);
if (playlistRemoteEntity.isIdenticalTo(result)) {
return noItemToUpdate;
}
return remotePlaylistManager.onUpdate(playlists.get(0).getUid(), result).toFlowable();
}
@@ -377,56 +405,59 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
private Subscriber<List<PlaylistRemoteEntity>> getPlaylistBookmarkSubscriber() {
return new Subscriber<List<PlaylistRemoteEntity>>() {
@Override
public void onSubscribe(Subscription s) {
if (bookmarkReactor != null) bookmarkReactor.cancel();
public void onSubscribe(final Subscription s) {
if (bookmarkReactor != null) {
bookmarkReactor.cancel();
}
bookmarkReactor = s;
bookmarkReactor.request(1);
}
@Override
public void onNext(List<PlaylistRemoteEntity> playlist) {
public void onNext(final List<PlaylistRemoteEntity> playlist) {
playlistEntity = playlist.isEmpty() ? null : playlist.get(0);
updateBookmarkButtons();
isBookmarkButtonReady.set(true);
if (bookmarkReactor != null) bookmarkReactor.request(1);
if (bookmarkReactor != null) {
bookmarkReactor.request(1);
}
}
@Override
public void onError(Throwable t) {
public void onError(final Throwable t) {
PlaylistFragment.this.onError(t);
}
@Override
public void onComplete() {
}
public void onComplete() { }
};
}
@Override
public void setTitle(String title) {
public void setTitle(final String title) {
super.setTitle(title);
headerTitleView.setText(title);
}
private void onBookmarkClicked() {
if (isBookmarkButtonReady == null || !isBookmarkButtonReady.get() ||
remotePlaylistManager == null)
if (isBookmarkButtonReady == null || !isBookmarkButtonReady.get()
|| remotePlaylistManager == null) {
return;
}
final Disposable action;
if (currentInfo != null && playlistEntity == null) {
action = remotePlaylistManager.onBookmark(currentInfo)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> {/* Do nothing */}, this::onError);
.subscribe(ignored -> { /* Do nothing */ }, this::onError);
} else if (playlistEntity != null) {
action = remotePlaylistManager.deletePlaylist(playlistEntity.getUid())
.observeOn(AndroidSchedulers.mainThread())
.doFinally(() -> playlistEntity = null)
.subscribe(ignored -> {/* Do nothing */}, this::onError);
.subscribe(ignored -> { /* Do nothing */ }, this::onError);
} else {
action = Disposables.empty();
}
@@ -435,13 +466,15 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
}
private void updateBookmarkButtons() {
if (playlistBookmarkButton == null || activity == null) return;
if (playlistBookmarkButton == null || activity == null) {
return;
}
final int iconAttr = playlistEntity == null ?
R.attr.ic_playlist_add : R.attr.ic_playlist_check;
final int iconAttr = playlistEntity == null
? R.attr.ic_playlist_add : R.attr.ic_playlist_check;
final int titleRes = playlistEntity == null ?
R.string.bookmark_playlist : R.string.unbookmark_playlist;
final int titleRes = playlistEntity == null
? R.string.bookmark_playlist : R.string.unbookmark_playlist;
playlistBookmarkButton.setIcon(ThemeHelper.resolveResourceIdFromAttr(activity, iconAttr));
playlistBookmarkButton.setTitle(titleRes);

View File

@@ -6,14 +6,8 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.RecyclerView;
import androidx.appcompat.widget.TooltipCompat;
import androidx.recyclerview.widget.ItemTouchHelper;
import android.text.Editable;
import android.text.Html;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
@@ -30,31 +24,37 @@ import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.TooltipCompat;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.ReCaptchaActivity;
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.search.SearchExtractor;
import org.schabi.newpipe.extractor.search.SearchInfo;
import org.schabi.newpipe.util.FireTvUtils;
import org.schabi.newpipe.fragments.BackPressable;
import org.schabi.newpipe.fragments.list.BaseListFragment;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.AndroidTvUtils;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ServiceHelper;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@@ -73,14 +73,13 @@ import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import io.reactivex.subjects.PublishSubject;
import static android.text.Html.escapeHtml;
import static androidx.recyclerview.widget.ItemTouchHelper.Callback.makeMovementFlags;
import static java.util.Arrays.asList;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public class SearchFragment
extends BaseListFragment<SearchInfo, ListExtractor.InfoItemsPage>
public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.InfoItemsPage>
implements BackPressable {
/*//////////////////////////////////////////////////////////////////////////
// Search
//////////////////////////////////////////////////////////////////////////*/
@@ -92,44 +91,51 @@ public class SearchFragment
private static final int THRESHOLD_NETWORK_SUGGESTION = 1;
/**
* How much time have to pass without emitting a item (i.e. the user stop typing) to fetch/show the suggestions, in milliseconds.
* How much time have to pass without emitting a item (i.e. the user stop typing)
* to fetch/show the suggestions, in milliseconds.
*/
private static final int SUGGESTIONS_DEBOUNCE = 120; //ms
private final PublishSubject<String> suggestionPublisher = PublishSubject.create();
@State
protected int filterItemCheckedId = -1;
int filterItemCheckedId = -1;
@State
protected int serviceId = Constants.NO_SERVICE_ID;
// this three represet the current search query
// these three represents the current search query
@State
protected String searchString;
String searchString;
/**
* No content filter should add like contentfilter = all
* No content filter should add like contentFilter = all
* be aware of this when implementing an extractor.
*/
@State
protected String[] contentFilter = new String[0];
String[] contentFilter = new String[0];
@State
protected String sortFilter;
// these represtent the last search
String sortFilter;
// these represents the last search
@State
protected String lastSearchedString;
String lastSearchedString;
@State
protected boolean wasSearchFocused = false;
String searchSuggestion;
@State
boolean isCorrectedSearch;
@State
boolean wasSearchFocused = false;
private Map<Integer, String> menuItemToFilterName;
private StreamingService service;
private String currentPageUrl;
private String nextPageUrl;
private Page nextPage;
private String contentCountry;
private boolean isSuggestionsEnabled = true;
private final PublishSubject<String> suggestionPublisher = PublishSubject.create();
private Disposable searchDisposable;
private Disposable suggestionDisposable;
private final CompositeDisposable disposables = new CompositeDisposable();
@@ -145,12 +151,16 @@ public class SearchFragment
private EditText searchEditText;
private View searchClear;
private TextView correctSuggestion;
private View suggestionsPanel;
private RecyclerView suggestionsRecyclerView;
/*////////////////////////////////////////////////////////////////////////*/
public static SearchFragment getInstance(int serviceId, String searchString) {
private TextWatcher textWatcher;
public static SearchFragment getInstance(final int serviceId, final String searchString) {
SearchFragment searchFragment = new SearchFragment();
searchFragment.setQuery(serviceId, searchString, new String[0], "");
@@ -173,33 +183,37 @@ public class SearchFragment
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onAttach(Context context) {
public void onAttach(final Context context) {
super.onAttach(context);
suggestionListAdapter = new SuggestionListAdapter(activity);
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
boolean isSearchHistoryEnabled = preferences.getBoolean(getString(R.string.enable_search_history_key), true);
boolean isSearchHistoryEnabled = preferences
.getBoolean(getString(R.string.enable_search_history_key), true);
suggestionListAdapter.setShowSuggestionHistory(isSearchHistoryEnabled);
historyRecordManager = new HistoryRecordManager(context);
}
@Override
public void onCreate(Bundle savedInstanceState) {
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
isSuggestionsEnabled = preferences.getBoolean(getString(R.string.show_search_suggestions_key), true);
contentCountry = preferences.getString(getString(R.string.content_country_key), getString(R.string.default_country_value));
isSuggestionsEnabled = preferences
.getBoolean(getString(R.string.show_search_suggestions_key), true);
contentCountry = preferences.getString(getString(R.string.content_country_key),
getString(R.string.default_localization_key));
}
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_search, container, false);
}
@Override
public void onViewCreated(View rootView, Bundle savedInstanceState) {
public void onViewCreated(final View rootView, final Bundle savedInstanceState) {
super.onViewCreated(rootView, savedInstanceState);
showSearchOnStart();
initSearchListeners();
@@ -211,15 +225,23 @@ public class SearchFragment
wasSearchFocused = searchEditText.hasFocus();
if (searchDisposable != null) searchDisposable.dispose();
if (suggestionDisposable != null) suggestionDisposable.dispose();
if (disposables != null) disposables.clear();
if (searchDisposable != null) {
searchDisposable.dispose();
}
if (suggestionDisposable != null) {
suggestionDisposable.dispose();
}
if (disposables != null) {
disposables.clear();
}
hideKeyboardSearch();
}
@Override
public void onResume() {
if (DEBUG) Log.d(TAG, "onResume() called");
if (DEBUG) {
Log.d(TAG, "onResume() called");
}
super.onResume();
try {
@@ -245,7 +267,11 @@ public class SearchFragment
}
}
if (suggestionDisposable == null || suggestionDisposable.isDisposed()) initSuggestionObserver();
handleSearchSuggestion();
if (suggestionDisposable == null || suggestionDisposable.isDisposed()) {
initSuggestionObserver();
}
if (TextUtils.isEmpty(searchString) || wasSearchFocused) {
showKeyboardSearch();
@@ -259,7 +285,9 @@ public class SearchFragment
@Override
public void onDestroyView() {
if (DEBUG) Log.d(TAG, "onDestroyView() called");
if (DEBUG) {
Log.d(TAG, "onDestroyView() called");
}
unsetSearchListeners();
super.onDestroyView();
}
@@ -267,19 +295,27 @@ public class SearchFragment
@Override
public void onDestroy() {
super.onDestroy();
if (searchDisposable != null) searchDisposable.dispose();
if (suggestionDisposable != null) suggestionDisposable.dispose();
if (disposables != null) disposables.clear();
if (searchDisposable != null) {
searchDisposable.dispose();
}
if (suggestionDisposable != null) {
suggestionDisposable.dispose();
}
if (disposables != null) {
disposables.clear();
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
switch (requestCode) {
case ReCaptchaActivity.RECAPTCHA_REQUEST:
if (resultCode == Activity.RESULT_OK
&& !TextUtils.isEmpty(searchString)) {
search(searchString, contentFilter, sortFilter);
} else Log.e(TAG, "ReCaptcha failed");
} else {
Log.e(TAG, "ReCaptcha failed");
}
break;
default:
@@ -293,25 +329,27 @@ public class SearchFragment
//////////////////////////////////////////////////////////////////////////*/
@Override
protected void initViews(View rootView, Bundle savedInstanceState) {
protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
suggestionsPanel = rootView.findViewById(R.id.suggestions_panel);
suggestionsRecyclerView = rootView.findViewById(R.id.suggestions_list);
suggestionsRecyclerView.setAdapter(suggestionListAdapter);
new ItemTouchHelper(new ItemTouchHelper.Callback() {
@Override
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
public int getMovementFlags(@NonNull final RecyclerView recyclerView,
@NonNull final RecyclerView.ViewHolder viewHolder) {
return getSuggestionMovementFlags(recyclerView, viewHolder);
}
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder,
@NonNull RecyclerView.ViewHolder viewHolder1) {
public boolean onMove(@NonNull final RecyclerView recyclerView,
@NonNull final RecyclerView.ViewHolder viewHolder,
@NonNull final RecyclerView.ViewHolder viewHolder1) {
return false;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int i) {
public void onSwiped(@NonNull final RecyclerView.ViewHolder viewHolder, final int i) {
onSuggestionItemSwiped(viewHolder, i);
}
}).attachToRecyclerView(suggestionsRecyclerView);
@@ -319,6 +357,8 @@ public class SearchFragment
searchToolbarContainer = activity.findViewById(R.id.toolbar_search_container);
searchEditText = searchToolbarContainer.findViewById(R.id.toolbar_search_edit_text);
searchClear = searchToolbarContainer.findViewById(R.id.toolbar_search_clear);
correctSuggestion = rootView.findViewById(R.id.correct_suggestion);
}
/*//////////////////////////////////////////////////////////////////////////
@@ -326,21 +366,19 @@ public class SearchFragment
//////////////////////////////////////////////////////////////////////////*/
@Override
public void writeTo(Queue<Object> objectsToSave) {
public void writeTo(final Queue<Object> objectsToSave) {
super.writeTo(objectsToSave);
objectsToSave.add(currentPageUrl);
objectsToSave.add(nextPageUrl);
objectsToSave.add(nextPage);
}
@Override
public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception {
public void readFrom(@NonNull final Queue<Object> savedObjects) throws Exception {
super.readFrom(savedObjects);
currentPageUrl = (String) savedObjects.poll();
nextPageUrl = (String) savedObjects.poll();
nextPage = (Page) savedObjects.poll();
}
@Override
public void onSaveInstanceState(Bundle bundle) {
public void onSaveInstanceState(final Bundle bundle) {
searchString = searchEditText != null
? searchEditText.getText().toString()
: searchString;
@@ -372,7 +410,7 @@ public class SearchFragment
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
ActionBar supportActionBar = activity.getSupportActionBar();
@@ -386,13 +424,20 @@ public class SearchFragment
int itemId = 0;
boolean isFirstItem = true;
final Context c = getContext();
for(String filter : service.getSearchQHFactory().getAvailableContentFilter()) {
for (String filter : service.getSearchQHFactory().getAvailableContentFilter()) {
if (filter.equals("music_songs")) {
MenuItem musicItem = menu.add(2,
itemId++,
0,
"YouTube Music");
musicItem.setEnabled(false);
}
menuItemToFilterName.put(itemId, filter);
MenuItem item = menu.add(1,
itemId++,
0,
ServiceHelper.getTranslatedFilterString(filter, c));
if(isFirstItem) {
if (isFirstItem) {
item.setChecked(true);
isFirstItem = false;
}
@@ -403,19 +448,20 @@ public class SearchFragment
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
List<String> contentFilter = new ArrayList<>(1);
contentFilter.add(menuItemToFilterName.get(item.getItemId()));
changeContentFilter(item, contentFilter);
public boolean onOptionsItemSelected(final MenuItem item) {
List<String> cf = new ArrayList<>(1);
cf.add(menuItemToFilterName.get(item.getItemId()));
changeContentFilter(item, cf);
return true;
}
private void restoreFilterChecked(Menu menu, int itemId) {
private void restoreFilterChecked(final Menu menu, final int itemId) {
if (itemId != -1) {
MenuItem item = menu.findItem(itemId);
if (item == null) return;
if (item == null) {
return;
}
item.setChecked(true);
}
@@ -425,13 +471,13 @@ public class SearchFragment
// Search
//////////////////////////////////////////////////////////////////////////*/
private TextWatcher textWatcher;
private void showSearchOnStart() {
if (DEBUG) Log.d(TAG, "showSearchOnStart() called, searchQuery → "
+ searchString
+ ", lastSearchedQuery → "
+ lastSearchedString);
if (DEBUG) {
Log.d(TAG, "showSearchOnStart() called, searchQuery → "
+ searchString
+ ", lastSearchedQuery → "
+ lastSearchedString);
}
searchEditText.setText(searchString);
if (TextUtils.isEmpty(searchString) || TextUtils.isEmpty(searchEditText.getText())) {
@@ -451,14 +497,20 @@ public class SearchFragment
}
private void initSearchListeners() {
if (DEBUG) Log.d(TAG, "initSearchListeners() called");
if (DEBUG) {
Log.d(TAG, "initSearchListeners() called");
}
searchClear.setOnClickListener(v -> {
if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]");
if (DEBUG) {
Log.d(TAG, "onClick() called with: v = [" + v + "]");
}
if (TextUtils.isEmpty(searchEditText.getText())) {
NavigationHelper.gotoMainFragment(getFragmentManager());
return;
}
correctSuggestion.setVisibility(View.GONE);
searchEditText.setText("");
suggestionListAdapter.setItems(new ArrayList<>());
showKeyboardSearch();
@@ -467,53 +519,65 @@ public class SearchFragment
TooltipCompat.setTooltipText(searchClear, getString(R.string.clear));
searchEditText.setOnClickListener(v -> {
if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]");
if (DEBUG) {
Log.d(TAG, "onClick() called with: v = [" + v + "]");
}
if (isSuggestionsEnabled && errorPanelRoot.getVisibility() != View.VISIBLE) {
showSuggestionsPanel();
}
if(FireTvUtils.isFireTv()){
if (AndroidTvUtils.isTv(getContext())) {
showKeyboardSearch();
}
});
searchEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> {
if (DEBUG) Log.d(TAG, "onFocusChange() called with: v = [" + v + "], hasFocus = [" + hasFocus + "]");
if (isSuggestionsEnabled && hasFocus && errorPanelRoot.getVisibility() != View.VISIBLE) {
if (DEBUG) {
Log.d(TAG, "onFocusChange() called with: "
+ "v = [" + v + "], hasFocus = [" + hasFocus + "]");
}
if (isSuggestionsEnabled && hasFocus
&& errorPanelRoot.getVisibility() != View.VISIBLE) {
showSuggestionsPanel();
}
});
suggestionListAdapter.setListener(new SuggestionListAdapter.OnSuggestionItemSelected() {
@Override
public void onSuggestionItemSelected(SuggestionItem item) {
public void onSuggestionItemSelected(final SuggestionItem item) {
search(item.query, new String[0], "");
searchEditText.setText(item.query);
}
@Override
public void onSuggestionItemInserted(SuggestionItem item) {
public void onSuggestionItemInserted(final SuggestionItem item) {
searchEditText.setText(item.query);
searchEditText.setSelection(searchEditText.getText().length());
}
@Override
public void onSuggestionItemLongClick(SuggestionItem item) {
if (item.fromHistory) showDeleteSuggestionDialog(item);
public void onSuggestionItemLongClick(final SuggestionItem item) {
if (item.fromHistory) {
showDeleteSuggestionDialog(item);
}
}
});
if (textWatcher != null) searchEditText.removeTextChangedListener(textWatcher);
if (textWatcher != null) {
searchEditText.removeTextChangedListener(textWatcher);
}
textWatcher = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
public void beforeTextChanged(final CharSequence s, final int start,
final int count, final int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
public void onTextChanged(final CharSequence s, final int start,
final int before, final int count) {
}
@Override
public void afterTextChanged(Editable s) {
public void afterTextChanged(final Editable s) {
String newText = searchEditText.getText().toString();
suggestionPublisher.onNext(newText);
}
@@ -522,48 +586,62 @@ public class SearchFragment
searchEditText.setOnEditorActionListener(
(TextView v, int actionId, KeyEvent event) -> {
if (DEBUG) {
Log.d(TAG, "onEditorAction() called with: v = [" + v + "], actionId = [" + actionId + "], event = [" + event + "]");
Log.d(TAG, "onEditorAction() called with: v = [" + v + "], "
+ "actionId = [" + actionId + "], event = [" + event + "]");
}
if(actionId == EditorInfo.IME_ACTION_PREVIOUS){
if (actionId == EditorInfo.IME_ACTION_PREVIOUS) {
hideKeyboardSearch();
} else if (event != null
&& (event.getKeyCode() == KeyEvent.KEYCODE_ENTER
|| event.getAction() == EditorInfo.IME_ACTION_SEARCH)) {
|| event.getAction() == EditorInfo.IME_ACTION_SEARCH)) {
search(searchEditText.getText().toString(), new String[0], "");
return true;
}
return false;
});
if (suggestionDisposable == null || suggestionDisposable.isDisposed())
if (suggestionDisposable == null || suggestionDisposable.isDisposed()) {
initSuggestionObserver();
}
}
private void unsetSearchListeners() {
if (DEBUG) Log.d(TAG, "unsetSearchListeners() called");
if (DEBUG) {
Log.d(TAG, "unsetSearchListeners() called");
}
searchClear.setOnClickListener(null);
searchClear.setOnLongClickListener(null);
searchEditText.setOnClickListener(null);
searchEditText.setOnFocusChangeListener(null);
searchEditText.setOnEditorActionListener(null);
if (textWatcher != null) searchEditText.removeTextChangedListener(textWatcher);
if (textWatcher != null) {
searchEditText.removeTextChangedListener(textWatcher);
}
textWatcher = null;
}
private void showSuggestionsPanel() {
if (DEBUG) Log.d(TAG, "showSuggestionsPanel() called");
if (DEBUG) {
Log.d(TAG, "showSuggestionsPanel() called");
}
animateView(suggestionsPanel, AnimationUtils.Type.LIGHT_SLIDE_AND_ALPHA, true, 200);
}
private void hideSuggestionsPanel() {
if (DEBUG) Log.d(TAG, "hideSuggestionsPanel() called");
if (DEBUG) {
Log.d(TAG, "hideSuggestionsPanel() called");
}
animateView(suggestionsPanel, AnimationUtils.Type.LIGHT_SLIDE_AND_ALPHA, false, 200);
}
private void showKeyboardSearch() {
if (DEBUG) Log.d(TAG, "showKeyboardSearch() called");
if (searchEditText == null) return;
if (DEBUG) {
Log.d(TAG, "showKeyboardSearch() called");
}
if (searchEditText == null) {
return;
}
if (searchEditText.requestFocus()) {
InputMethodManager imm = (InputMethodManager) activity.getSystemService(
@@ -573,19 +651,26 @@ public class SearchFragment
}
private void hideKeyboardSearch() {
if (DEBUG) Log.d(TAG, "hideKeyboardSearch() called");
if (searchEditText == null) return;
if (DEBUG) {
Log.d(TAG, "hideKeyboardSearch() called");
}
if (searchEditText == null) {
return;
}
InputMethodManager imm = (InputMethodManager) activity.getSystemService(
Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(searchEditText.getWindowToken(), InputMethodManager.RESULT_UNCHANGED_SHOWN);
InputMethodManager imm = (InputMethodManager) activity
.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(searchEditText.getWindowToken(),
InputMethodManager.RESULT_UNCHANGED_SHOWN);
searchEditText.clearFocus();
}
private void showDeleteSuggestionDialog(final SuggestionItem item) {
if (activity == null || historyRecordManager == null || suggestionPublisher == null ||
searchEditText == null || disposables == null) return;
if (activity == null || historyRecordManager == null || suggestionPublisher == null
|| searchEditText == null || disposables == null) {
return;
}
final String query = item.query;
new AlertDialog.Builder(activity)
.setTitle(query)
@@ -619,20 +704,20 @@ public class SearchFragment
return false;
}
public void giveSearchEditTextFocus() {
showKeyboardSearch();
}
private void initSuggestionObserver() {
if (DEBUG) Log.d(TAG, "initSuggestionObserver() called");
if (suggestionDisposable != null) suggestionDisposable.dispose();
if (DEBUG) {
Log.d(TAG, "initSuggestionObserver() called");
}
if (suggestionDisposable != null) {
suggestionDisposable.dispose();
}
final Observable<String> observable = suggestionPublisher
.debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS)
.startWith(searchString != null
? searchString
: "")
.filter(searchString -> isSuggestionsEnabled);
.filter(ss -> isSuggestionsEnabled);
suggestionDisposable = observable
.switchMap(query -> {
@@ -641,13 +726,15 @@ public class SearchFragment
final Observable<List<SuggestionItem>> local = flowable.toObservable()
.map(searchHistoryEntries -> {
List<SuggestionItem> result = new ArrayList<>();
for (SearchHistoryEntry entry : searchHistoryEntries)
for (SearchHistoryEntry entry : searchHistoryEntries) {
result.add(new SuggestionItem(true, entry.getSearch()));
}
return result;
});
if (query.length() < THRESHOLD_NETWORK_SUGGESTION) {
// Only pass through if the query length is equal or greater than THRESHOLD_NETWORK_SUGGESTION
// Only pass through if the query length
// is equal or greater than THRESHOLD_NETWORK_SUGGESTION
return local.materialize();
}
@@ -664,7 +751,9 @@ public class SearchFragment
return Observable.zip(local, network, (localResult, networkResult) -> {
List<SuggestionItem> result = new ArrayList<>();
if (localResult.size() > 0) result.addAll(localResult);
if (localResult.size() > 0) {
result.addAll(localResult);
}
// Remove duplicates
final Iterator<SuggestionItem> iterator = networkResult.iterator();
@@ -678,7 +767,9 @@ public class SearchFragment
}
}
if (networkResult.size() > 0) result.addAll(networkResult);
if (networkResult.size() > 0) {
result.addAll(networkResult);
}
return result;
}).materialize();
})
@@ -688,12 +779,7 @@ public class SearchFragment
if (listNotification.isOnNext()) {
handleSuggestions(listNotification.getValue());
} else if (listNotification.isOnError()) {
Throwable error = listNotification.getError();
if (!ExtractorHelper.hasAssignableCauseThrowable(error,
IOException.class, SocketException.class,
InterruptedException.class, InterruptedIOException.class)) {
onSuggestionError(error);
}
onSuggestionError(listNotification.getError());
}
});
}
@@ -703,17 +789,21 @@ public class SearchFragment
// no-op
}
private void search(final String searchString, String[] contentFilter, String sortFilter) {
if (DEBUG) Log.d(TAG, "search() called with: query = [" + searchString + "]");
if (searchString.isEmpty()) return;
private void search(final String ss, final String[] cf, final String sf) {
if (DEBUG) {
Log.d(TAG, "search() called with: query = [" + ss + "]");
}
if (ss.isEmpty()) {
return;
}
try {
final StreamingService service = NewPipe.getServiceByUrl(searchString);
if (service != null) {
final StreamingService streamingService = NewPipe.getServiceByUrl(ss);
if (streamingService != null) {
showLoading();
disposables.add(Observable
.fromCallable(() ->
NavigationHelper.getIntentByLink(activity, service, searchString))
NavigationHelper.getIntentByLink(activity, streamingService, ss))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(intent -> {
@@ -723,36 +813,41 @@ public class SearchFragment
showError(getString(R.string.url_not_supported_toast), false)));
return;
}
} catch (Exception e) {
} catch (Exception ignored) {
// Exception occurred, it's not a url
}
lastSearchedString = this.searchString;
this.searchString = searchString;
this.searchString = ss;
infoListAdapter.clearStreamItemList();
hideSuggestionsPanel();
hideKeyboardSearch();
historyRecordManager.onSearched(serviceId, searchString)
historyRecordManager.onSearched(serviceId, ss)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
ignored -> {},
ignored -> {
},
error -> showSnackBarError(error, UserAction.SEARCHED,
NewPipe.getNameOfService(serviceId), searchString, 0)
NewPipe.getNameOfService(serviceId), ss, 0)
);
suggestionPublisher.onNext(searchString);
suggestionPublisher.onNext(ss);
startLoading(false);
}
@Override
public void startLoading(boolean forceLoad) {
public void startLoading(final boolean forceLoad) {
super.startLoading(forceLoad);
if (disposables != null) disposables.clear();
if (searchDisposable != null) searchDisposable.dispose();
if (disposables != null) {
disposables.clear();
}
if (searchDisposable != null) {
searchDisposable.dispose();
}
searchDisposable = ExtractorHelper.searchFor(serviceId,
searchString,
Arrays.asList(contentFilter),
sortFilter)
searchString,
Arrays.asList(contentFilter),
sortFilter)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnEvent((searchResult, throwable) -> isLoading.set(false))
@@ -762,16 +857,20 @@ public class SearchFragment
@Override
protected void loadMoreItems() {
if(nextPageUrl == null || nextPageUrl.isEmpty()) return;
if (!Page.isValid(nextPage)) {
return;
}
isLoading.set(true);
showListFooter(true);
if (searchDisposable != null) searchDisposable.dispose();
if (searchDisposable != null) {
searchDisposable.dispose();
}
searchDisposable = ExtractorHelper.getMoreSearchItems(
serviceId,
searchString,
asList(contentFilter),
sortFilter,
nextPageUrl)
serviceId,
searchString,
asList(contentFilter),
sortFilter,
nextPage)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnEvent((nextItemsResult, throwable) -> isLoading.set(false))
@@ -785,7 +884,7 @@ public class SearchFragment
}
@Override
protected void onItemSelected(InfoItem selectedItem) {
protected void onItemSelected(final InfoItem selectedItem) {
super.onItemSelected(selectedItem);
hideKeyboardSearch();
}
@@ -794,22 +893,22 @@ public class SearchFragment
// Utils
//////////////////////////////////////////////////////////////////////////*/
private void changeContentFilter(MenuItem item, List<String> contentFilter) {
private void changeContentFilter(final MenuItem item, final List<String> cf) {
this.filterItemCheckedId = item.getItemId();
item.setChecked(true);
this.contentFilter = new String[] {contentFilter.get(0)};
this.contentFilter = new String[]{cf.get(0)};
if (!TextUtils.isEmpty(searchString)) {
search(searchString, this.contentFilter, sortFilter);
}
}
private void setQuery(int serviceId, String searchString, String[] contentfilter, String sortFilter) {
this.serviceId = serviceId;
private void setQuery(final int sid, final String ss, final String[] cf, final String sf) {
this.serviceId = sid;
this.searchString = searchString;
this.contentFilter = contentfilter;
this.sortFilter = sortFilter;
this.contentFilter = cf;
this.sortFilter = sf;
}
/*//////////////////////////////////////////////////////////////////////////
@@ -817,7 +916,9 @@ public class SearchFragment
//////////////////////////////////////////////////////////////////////////*/
public void handleSuggestions(@NonNull final List<SuggestionItem> suggestions) {
if (DEBUG) Log.d(TAG, "handleSuggestions() called with: suggestions = [" + suggestions + "]");
if (DEBUG) {
Log.d(TAG, "handleSuggestions() called with: suggestions = [" + suggestions + "]");
}
suggestionsRecyclerView.smoothScrollToPosition(0);
suggestionsRecyclerView.post(() -> suggestionListAdapter.setItems(suggestions));
@@ -826,9 +927,13 @@ public class SearchFragment
}
}
public void onSuggestionError(Throwable exception) {
if (DEBUG) Log.d(TAG, "onSuggestionError() called with: exception = [" + exception + "]");
if (super.onError(exception)) return;
public void onSuggestionError(final Throwable exception) {
if (DEBUG) {
Log.d(TAG, "onSuggestionError() called with: exception = [" + exception + "]");
}
if (super.onError(exception)) {
return;
}
int errorId = exception instanceof ParsingException
? R.string.parsing_error
@@ -848,7 +953,7 @@ public class SearchFragment
}
@Override
public void showError(String message, boolean showRetryButton) {
public void showError(final String message, final boolean showRetryButton) {
super.showError(message, showRetryButton);
hideSuggestionsPanel();
hideKeyboardSearch();
@@ -859,18 +964,22 @@ public class SearchFragment
//////////////////////////////////////////////////////////////////////////*/
@Override
public void handleResult(@NonNull SearchInfo result) {
public void handleResult(@NonNull final SearchInfo result) {
final List<Throwable> exceptions = result.getErrors();
if (!exceptions.isEmpty()
&& !(exceptions.size() == 1
&& exceptions.get(0) instanceof SearchExtractor.NothingFoundException)){
&& !(exceptions.size() == 1
&& exceptions.get(0) instanceof SearchExtractor.NothingFoundException)) {
showSnackBarError(result.getErrors(), UserAction.SEARCHED,
NewPipe.getNameOfService(serviceId), searchString, 0);
}
searchSuggestion = result.getSearchSuggestion();
isCorrectedSearch = result.isCorrectedSearch();
handleSearchSuggestion();
lastSearchedString = searchString;
nextPageUrl = result.getNextPageUrl();
currentPageUrl = result.getUrl();
nextPage = result.getNextPage();
if (infoListAdapter.getItemsList().size() == 0) {
if (!result.getRelatedItems().isEmpty()) {
@@ -885,24 +994,58 @@ public class SearchFragment
super.handleResult(result);
}
private void handleSearchSuggestion() {
if (TextUtils.isEmpty(searchSuggestion)) {
correctSuggestion.setVisibility(View.GONE);
} else {
final String helperText = getString(isCorrectedSearch
? R.string.search_showing_result_for
: R.string.did_you_mean);
final String highlightedSearchSuggestion =
"<b><i>" + escapeHtml(searchSuggestion) + "</i></b>";
correctSuggestion.setText(
Html.fromHtml(String.format(helperText, highlightedSearchSuggestion)));
correctSuggestion.setOnClickListener(v -> {
correctSuggestion.setVisibility(View.GONE);
search(searchSuggestion, contentFilter, sortFilter);
searchEditText.setText(searchSuggestion);
});
correctSuggestion.setOnLongClickListener(v -> {
searchEditText.setText(searchSuggestion);
searchEditText.setSelection(searchSuggestion.length());
showKeyboardSearch();
return true;
});
correctSuggestion.setVisibility(View.VISIBLE);
}
}
@Override
public void handleNextItems(ListExtractor.InfoItemsPage result) {
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
showListFooter(false);
currentPageUrl = result.getNextPageUrl();
infoListAdapter.addInfoItemList(result.getItems());
nextPageUrl = result.getNextPageUrl();
nextPage = result.getNextPage();
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.SEARCHED,
NewPipe.getNameOfService(serviceId)
, "\"" + searchString + "\" → page: " + nextPageUrl, 0);
NewPipe.getNameOfService(serviceId),
"\"" + searchString + "\" → pageUrl: " + nextPage.getUrl() + ", "
+ "pageIds: " + nextPage.getIds() + ", "
+ "pageCookies: " + nextPage.getCookies(), 0);
}
super.handleNextItems(result);
}
@Override
protected boolean onError(Throwable exception) {
if (super.onError(exception)) return true;
protected boolean onError(final Throwable exception) {
if (super.onError(exception)) {
return true;
}
if (exception instanceof SearchExtractor.NothingFoundException) {
infoListAdapter.clearStreamItemList();
@@ -922,13 +1065,20 @@ public class SearchFragment
// Suggestion item touch helper
//////////////////////////////////////////////////////////////////////////*/
public int getSuggestionMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
public int getSuggestionMovementFlags(@NonNull final RecyclerView recyclerView,
@NonNull final RecyclerView.ViewHolder viewHolder) {
final int position = viewHolder.getAdapterPosition();
if (position == RecyclerView.NO_POSITION) {
return 0;
}
final SuggestionItem item = suggestionListAdapter.getItem(position);
return item.fromHistory ? makeMovementFlags(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) : 0;
return item.fromHistory ? makeMovementFlags(0,
ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) : 0;
}
public void onSuggestionItemSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int i) {
public void onSuggestionItemSwiped(@NonNull final RecyclerView.ViewHolder viewHolder,
final int i) {
final int position = viewHolder.getAdapterPosition();
final String query = suggestionListAdapter.getItem(position).query;
final Disposable onDelete = historyRecordManager.deleteSearchHistory(query)

View File

@@ -1,10 +1,10 @@
package org.schabi.newpipe.fragments.list.search;
public class SuggestionItem {
public final boolean fromHistory;
final boolean fromHistory;
public final String query;
public SuggestionItem(boolean fromHistory, String query) {
public SuggestionItem(final boolean fromHistory, final String query) {
this.fromHistory = fromHistory;
this.query = query;
}

View File

@@ -2,36 +2,32 @@ package org.schabi.newpipe.fragments.list.search;
import android.content.Context;
import android.content.res.TypedArray;
import androidx.annotation.AttrRes;
import androidx.recyclerview.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.AttrRes;
import androidx.recyclerview.widget.RecyclerView;
import org.schabi.newpipe.R;
import java.util.ArrayList;
import java.util.List;
public class SuggestionListAdapter extends RecyclerView.Adapter<SuggestionListAdapter.SuggestionItemHolder> {
public class SuggestionListAdapter
extends RecyclerView.Adapter<SuggestionListAdapter.SuggestionItemHolder> {
private final ArrayList<SuggestionItem> items = new ArrayList<>();
private final Context context;
private OnSuggestionItemSelected listener;
private boolean showSuggestionHistory = true;
public interface OnSuggestionItemSelected {
void onSuggestionItemSelected(SuggestionItem item);
void onSuggestionItemInserted(SuggestionItem item);
void onSuggestionItemLongClick(SuggestionItem item);
}
public SuggestionListAdapter(Context context) {
public SuggestionListAdapter(final Context context) {
this.context = context;
}
public void setItems(List<SuggestionItem> items) {
public void setItems(final List<SuggestionItem> items) {
this.items.clear();
if (showSuggestionHistory) {
this.items.addAll(items);
@@ -46,36 +42,43 @@ public class SuggestionListAdapter extends RecyclerView.Adapter<SuggestionListAd
notifyDataSetChanged();
}
public void setListener(OnSuggestionItemSelected listener) {
public void setListener(final OnSuggestionItemSelected listener) {
this.listener = listener;
}
public void setShowSuggestionHistory(boolean v) {
public void setShowSuggestionHistory(final boolean v) {
showSuggestionHistory = v;
}
@Override
public SuggestionItemHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new SuggestionItemHolder(LayoutInflater.from(context).inflate(R.layout.item_search_suggestion, parent, false));
public SuggestionItemHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
return new SuggestionItemHolder(LayoutInflater.from(context)
.inflate(R.layout.item_search_suggestion, parent, false));
}
@Override
public void onBindViewHolder(SuggestionItemHolder holder, int position) {
public void onBindViewHolder(final SuggestionItemHolder holder, final int position) {
final SuggestionItem currentItem = getItem(position);
holder.updateFrom(currentItem);
holder.queryView.setOnClickListener(v -> {
if (listener != null) listener.onSuggestionItemSelected(currentItem);
if (listener != null) {
listener.onSuggestionItemSelected(currentItem);
}
});
holder.queryView.setOnLongClickListener(v -> {
if (listener != null) listener.onSuggestionItemLongClick(currentItem);
return true;
if (listener != null) {
listener.onSuggestionItemLongClick(currentItem);
}
return true;
});
holder.insertView.setOnClickListener(v -> {
if (listener != null) listener.onSuggestionItemInserted(currentItem);
if (listener != null) {
listener.onSuggestionItemInserted(currentItem);
}
});
}
SuggestionItem getItem(int position) {
SuggestionItem getItem(final int position) {
return items.get(position);
}
@@ -88,7 +91,15 @@ public class SuggestionListAdapter extends RecyclerView.Adapter<SuggestionListAd
return getItemCount() == 0;
}
public static class SuggestionItemHolder extends RecyclerView.ViewHolder {
public interface OnSuggestionItemSelected {
void onSuggestionItemSelected(SuggestionItem item);
void onSuggestionItemInserted(SuggestionItem item);
void onSuggestionItemLongClick(SuggestionItem item);
}
public static final class SuggestionItemHolder extends RecyclerView.ViewHolder {
private final TextView itemSuggestionQuery;
private final ImageView suggestionIcon;
private final View queryView;
@@ -98,7 +109,7 @@ public class SuggestionListAdapter extends RecyclerView.Adapter<SuggestionListAd
private final int historyResId;
private final int searchResId;
private SuggestionItemHolder(View rootView) {
private SuggestionItemHolder(final View rootView) {
super(rootView);
suggestionIcon = rootView.findViewById(R.id.item_suggestion_icon);
itemSuggestionQuery = rootView.findViewById(R.id.item_suggestion_query);
@@ -106,20 +117,21 @@ public class SuggestionListAdapter extends RecyclerView.Adapter<SuggestionListAd
queryView = rootView.findViewById(R.id.suggestion_search);
insertView = rootView.findViewById(R.id.suggestion_insert);
historyResId = resolveResourceIdFromAttr(rootView.getContext(), R.attr.history);
searchResId = resolveResourceIdFromAttr(rootView.getContext(), R.attr.search);
historyResId = resolveResourceIdFromAttr(rootView.getContext(), R.attr.ic_history);
searchResId = resolveResourceIdFromAttr(rootView.getContext(), R.attr.ic_search);
}
private void updateFrom(SuggestionItem item) {
suggestionIcon.setImageResource(item.fromHistory ? historyResId : searchResId);
itemSuggestionQuery.setText(item.query);
}
private static int resolveResourceIdFromAttr(Context context, @AttrRes int attr) {
private static int resolveResourceIdFromAttr(final Context context,
@AttrRes final int attr) {
TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{attr});
int attributeResourceId = a.getResourceId(0, 0);
a.recycle();
return attributeResourceId;
}
private void updateFrom(final SuggestionItem item) {
suggestionIcon.setImageResource(item.fromHistory ? historyResId : searchResId);
itemSuggestionQuery.setText(item.query);
}
}
}

View File

@@ -4,8 +4,6 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@@ -14,6 +12,9 @@ import android.view.ViewGroup;
import android.widget.CompoundButton;
import android.widget.Switch;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe;
@@ -28,53 +29,61 @@ import java.io.Serializable;
import io.reactivex.Single;
import io.reactivex.disposables.CompositeDisposable;
public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInfo> implements SharedPreferences.OnSharedPreferenceChangeListener{
public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInfo>
implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final String INFO_KEY = "related_info_key";
private CompositeDisposable disposables = new CompositeDisposable();
private RelatedStreamInfo relatedStreamInfo;
/*//////////////////////////////////////////////////////////////////////////
// Views
//////////////////////////////////////////////////////////////////////////*/
private View headerRootLayout;
private Switch aSwitch;
private boolean mIsVisibleToUser = false;
public static RelatedVideosFragment getInstance(StreamInfo info) {
public static RelatedVideosFragment getInstance(final StreamInfo info) {
RelatedVideosFragment instance = new RelatedVideosFragment();
instance.setInitialData(info);
return instance;
}
@Override
public void setUserVisibleHint(final boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
mIsVisibleToUser = isVisibleToUser;
}
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
mIsVisibleToUser = isVisibleToUser;
}
@Override
public void onAttach(Context context) {
public void onAttach(final Context context) {
super.onAttach(context);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_related_streams, container, false);
}
@Override
public void onDestroy() {
super.onDestroy();
if (disposables != null) disposables.clear();
if (disposables != null) {
disposables.clear();
}
}
protected View getListHeader(){
if(relatedStreamInfo != null && relatedStreamInfo.getNextStream() != null){
headerRootLayout = activity.getLayoutInflater().inflate(R.layout.related_streams_header, itemsList, false);
protected View getListHeader() {
if (relatedStreamInfo != null && relatedStreamInfo.getNextStream() != null) {
headerRootLayout = activity.getLayoutInflater()
.inflate(R.layout.related_streams_header, itemsList, false);
aSwitch = headerRootLayout.findViewById(R.id.autoplay_switch);
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getContext());
@@ -82,14 +91,14 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
aSwitch.setChecked(autoplay);
aSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
SharedPreferences.Editor prefEdit = PreferenceManager.getDefaultSharedPreferences(getContext()).edit();
prefEdit.putBoolean(getString(R.string.auto_queue_key), b);
prefEdit.apply();
public void onCheckedChanged(final CompoundButton compoundButton,
final boolean b) {
PreferenceManager.getDefaultSharedPreferences(getContext()).edit()
.putBoolean(getString(R.string.auto_queue_key), b).apply();
}
});
return headerRootLayout;
}else{
} else {
return null;
}
}
@@ -99,38 +108,44 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
return Single.fromCallable(() -> ListExtractor.InfoItemsPage.emptyPage());
}
@Override
protected Single<RelatedStreamInfo> loadResult(boolean forceLoad) {
return Single.fromCallable(() -> relatedStreamInfo);
}
/*//////////////////////////////////////////////////////////////////////////
// Contract
//////////////////////////////////////////////////////////////////////////*/
@Override
protected Single<RelatedStreamInfo> loadResult(final boolean forceLoad) {
return Single.fromCallable(() -> relatedStreamInfo);
}
@Override
public void showLoading() {
super.showLoading();
if(null != headerRootLayout) headerRootLayout.setVisibility(View.INVISIBLE);
if (headerRootLayout != null) {
headerRootLayout.setVisibility(View.INVISIBLE);
}
}
@Override
public void handleResult(@NonNull RelatedStreamInfo result) {
public void handleResult(@NonNull final RelatedStreamInfo result) {
super.handleResult(result);
if(null != headerRootLayout) headerRootLayout.setVisibility(View.VISIBLE);
AnimationUtils.slideUp(getView(),120, 96, 0.06f);
if (headerRootLayout != null) {
headerRootLayout.setVisibility(View.VISIBLE);
}
AnimationUtils.slideUp(getView(), 120, 96, 0.06f);
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.REQUESTED_STREAM, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
showSnackBarError(result.getErrors(), UserAction.REQUESTED_STREAM,
NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
}
if (disposables != null) disposables.clear();
if (disposables != null) {
disposables.clear();
}
}
@Override
public void handleNextItems(ListExtractor.InfoItemsPage result) {
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
super.handleNextItems(result);
if (!result.getErrors().isEmpty()) {
@@ -147,11 +162,14 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
//////////////////////////////////////////////////////////////////////////*/
@Override
protected boolean onError(Throwable exception) {
if (super.onError(exception)) return true;
protected boolean onError(final Throwable exception) {
if (super.onError(exception)) {
return true;
}
hideLoading();
showSnackBarError(exception, UserAction.REQUESTED_STREAM, NewPipe.getNameOfService(serviceId), url, R.string.general_error);
showSnackBarError(exception, UserAction.REQUESTED_STREAM,
NewPipe.getNameOfService(serviceId), url, R.string.general_error);
return true;
}
@@ -160,45 +178,47 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
//////////////////////////////////////////////////////////////////////////*/
@Override
public void setTitle(String title) {
public void setTitle(final String title) {
return;
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
return;
}
private void setInitialData(StreamInfo info) {
private void setInitialData(final StreamInfo info) {
super.setInitialData(info.getServiceId(), info.getUrl(), info.getName());
if(this.relatedStreamInfo == null) this.relatedStreamInfo = RelatedStreamInfo.getInfo(info);
if (this.relatedStreamInfo == null) {
this.relatedStreamInfo = RelatedStreamInfo.getInfo(info);
}
}
private static final String INFO_KEY = "related_info_key";
@Override
public void onSaveInstanceState(Bundle outState) {
public void onSaveInstanceState(final Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable(INFO_KEY, relatedStreamInfo);
}
@Override
protected void onRestoreInstanceState(@NonNull Bundle savedState) {
protected void onRestoreInstanceState(@NonNull final Bundle savedState) {
super.onRestoreInstanceState(savedState);
if (savedState != null) {
Serializable serializable = savedState.getSerializable(INFO_KEY);
if(serializable instanceof RelatedStreamInfo){
if (serializable instanceof RelatedStreamInfo) {
this.relatedStreamInfo = (RelatedStreamInfo) serializable;
}
}
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) {
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences,
final String s) {
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getContext());
Boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
if(null != aSwitch) aSwitch.setChecked(autoplay);
boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
if (null != aSwitch) {
aSwitch.setChecked(autoplay);
}
}
@Override

View File

@@ -1,10 +1,11 @@
package org.schabi.newpipe.info_list;
import android.content.Context;
import androidx.annotation.NonNull;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import com.nostra13.universalimageloader.core.ImageLoader;
import org.schabi.newpipe.extractor.InfoItem;
@@ -29,24 +30,26 @@ import org.schabi.newpipe.util.OnClickGesture;
* <p>
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* InfoItemBuilder.java is part of NewPipe.
* </p>
* <p>
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* </p>
* <p>
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* </p>
* <p>
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
* </p>
*/
public class InfoItemBuilder {
private static final String TAG = InfoItemBuilder.class.toString();
private final Context context;
private final ImageLoader imageLoader = ImageLoader.getInstance();
@@ -55,31 +58,39 @@ public class InfoItemBuilder {
private OnClickGesture<PlaylistInfoItem> onPlaylistSelectedListener;
private OnClickGesture<CommentsInfoItem> onCommentsSelectedListener;
public InfoItemBuilder(Context context) {
public InfoItemBuilder(final Context context) {
this.context = context;
}
public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem, final HistoryRecordManager historyRecordManager) {
public View buildView(@NonNull final ViewGroup parent, @NonNull final InfoItem infoItem,
final HistoryRecordManager historyRecordManager) {
return buildView(parent, infoItem, historyRecordManager, false);
}
public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem,
final HistoryRecordManager historyRecordManager, boolean useMiniVariant) {
public View buildView(@NonNull final ViewGroup parent, @NonNull final InfoItem infoItem,
final HistoryRecordManager historyRecordManager,
final boolean useMiniVariant) {
InfoItemHolder holder = holderFromInfoType(parent, infoItem.getInfoType(), useMiniVariant);
holder.updateFromItem(infoItem, historyRecordManager);
return holder.itemView;
}
private InfoItemHolder holderFromInfoType(@NonNull ViewGroup parent, @NonNull InfoItem.InfoType infoType, boolean useMiniVariant) {
private InfoItemHolder holderFromInfoType(@NonNull final ViewGroup parent,
@NonNull final InfoItem.InfoType infoType,
final boolean useMiniVariant) {
switch (infoType) {
case STREAM:
return useMiniVariant ? new StreamMiniInfoItemHolder(this, parent) : new StreamInfoItemHolder(this, parent);
return useMiniVariant ? new StreamMiniInfoItemHolder(this, parent)
: new StreamInfoItemHolder(this, parent);
case CHANNEL:
return useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent) : new ChannelInfoItemHolder(this, parent);
return useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent)
: new ChannelInfoItemHolder(this, parent);
case PLAYLIST:
return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent) : new PlaylistInfoItemHolder(this, parent);
return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent)
: new PlaylistInfoItemHolder(this, parent);
case COMMENT:
return useMiniVariant ? new CommentsMiniInfoItemHolder(this, parent) : new CommentsInfoItemHolder(this, parent);
return useMiniVariant ? new CommentsMiniInfoItemHolder(this, parent)
: new CommentsInfoItemHolder(this, parent);
default:
throw new RuntimeException("InfoType not expected = " + infoType.name());
}
@@ -97,7 +108,7 @@ public class InfoItemBuilder {
return onStreamSelectedListener;
}
public void setOnStreamSelectedListener(OnClickGesture<StreamInfoItem> listener) {
public void setOnStreamSelectedListener(final OnClickGesture<StreamInfoItem> listener) {
this.onStreamSelectedListener = listener;
}
@@ -105,7 +116,7 @@ public class InfoItemBuilder {
return onChannelSelectedListener;
}
public void setOnChannelSelectedListener(OnClickGesture<ChannelInfoItem> listener) {
public void setOnChannelSelectedListener(final OnClickGesture<ChannelInfoItem> listener) {
this.onChannelSelectedListener = listener;
}
@@ -113,7 +124,7 @@ public class InfoItemBuilder {
return onPlaylistSelectedListener;
}
public void setOnPlaylistSelectedListener(OnClickGesture<PlaylistInfoItem> listener) {
public void setOnPlaylistSelectedListener(final OnClickGesture<PlaylistInfoItem> listener) {
this.onPlaylistSelectedListener = listener;
}
@@ -121,8 +132,8 @@ public class InfoItemBuilder {
return onCommentsSelectedListener;
}
public void setOnCommentsSelectedListener(OnClickGesture<CommentsInfoItem> onCommentsSelectedListener) {
public void setOnCommentsSelectedListener(
final OnClickGesture<CommentsInfoItem> onCommentsSelectedListener) {
this.onCommentsSelectedListener = onCommentsSelectedListener;
}
}

View File

@@ -3,11 +3,12 @@ package org.schabi.newpipe.info_list;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;

View File

@@ -1,13 +1,14 @@
package org.schabi.newpipe.info_list;
import android.content.Context;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
import org.schabi.newpipe.extractor.InfoItem;
@@ -83,92 +84,101 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
private View header = null;
private View footer = null;
public class HFHolder extends RecyclerView.ViewHolder {
public View view;
public HFHolder(View v) {
super(v);
view = v;
}
}
public InfoListAdapter(Context context) {
public InfoListAdapter(final Context context) {
this.recordManager = new HistoryRecordManager(context);
infoItemBuilder = new InfoItemBuilder(context);
infoItemList = new ArrayList<>();
}
public void setOnStreamSelectedListener(OnClickGesture<StreamInfoItem> listener) {
public void setOnStreamSelectedListener(final OnClickGesture<StreamInfoItem> listener) {
infoItemBuilder.setOnStreamSelectedListener(listener);
}
public void setOnChannelSelectedListener(OnClickGesture<ChannelInfoItem> listener) {
public void setOnChannelSelectedListener(final OnClickGesture<ChannelInfoItem> listener) {
infoItemBuilder.setOnChannelSelectedListener(listener);
}
public void setOnPlaylistSelectedListener(OnClickGesture<PlaylistInfoItem> listener) {
public void setOnPlaylistSelectedListener(final OnClickGesture<PlaylistInfoItem> listener) {
infoItemBuilder.setOnPlaylistSelectedListener(listener);
}
public void setOnCommentsSelectedListener(OnClickGesture<CommentsInfoItem> listener) {
public void setOnCommentsSelectedListener(final OnClickGesture<CommentsInfoItem> listener) {
infoItemBuilder.setOnCommentsSelectedListener(listener);
}
public void useMiniItemVariants(boolean useMiniVariant) {
public void setUseMiniVariant(final boolean useMiniVariant) {
this.useMiniVariant = useMiniVariant;
}
public void setGridItemVariants(boolean useGridVariant) {
public void setUseGridVariant(final boolean useGridVariant) {
this.useGridVariant = useGridVariant;
}
public void addInfoItemList(@Nullable final List<InfoItem> data) {
public void addInfoItemList(@Nullable final List<? extends InfoItem> data) {
if (data == null) {
return;
}
if (DEBUG) Log.d(TAG, "addInfoItemList() before > infoItemList.size() = " +
infoItemList.size() + ", data.size() = " + data.size());
if (DEBUG) {
Log.d(TAG, "addInfoItemList() before > infoItemList.size() = "
+ infoItemList.size() + ", data.size() = " + data.size());
}
int offsetStart = sizeConsideringHeaderOffset();
infoItemList.addAll(data);
if (DEBUG) Log.d(TAG, "addInfoItemList() after > offsetStart = " + offsetStart +
", infoItemList.size() = " + infoItemList.size() +
", header = " + header + ", footer = " + footer +
", showFooter = " + showFooter);
if (DEBUG) {
Log.d(TAG, "addInfoItemList() after > offsetStart = " + offsetStart + ", "
+ "infoItemList.size() = " + infoItemList.size() + ", "
+ "header = " + header + ", footer = " + footer + ", "
+ "showFooter = " + showFooter);
}
notifyItemRangeInserted(offsetStart, data.size());
if (footer != null && showFooter) {
int footerNow = sizeConsideringHeaderOffset();
notifyItemMoved(offsetStart, footerNow);
if (DEBUG) Log.d(TAG, "addInfoItemList() footer from " + offsetStart +
" to " + footerNow);
if (DEBUG) {
Log.d(TAG, "addInfoItemList() footer from " + offsetStart
+ " to " + footerNow);
}
}
}
public void addInfoItem(@Nullable InfoItem data) {
public void setInfoItemList(final List<? extends InfoItem> data) {
infoItemList.clear();
infoItemList.addAll(data);
notifyDataSetChanged();
}
public void addInfoItem(@Nullable final InfoItem data) {
if (data == null) {
return;
}
if (DEBUG) Log.d(TAG, "addInfoItem() before > infoItemList.size() = " +
infoItemList.size() + ", thread = " + Thread.currentThread());
if (DEBUG) {
Log.d(TAG, "addInfoItem() before > infoItemList.size() = "
+ infoItemList.size() + ", thread = " + Thread.currentThread());
}
int positionInserted = sizeConsideringHeaderOffset();
infoItemList.add(data);
if (DEBUG) Log.d(TAG, "addInfoItem() after > position = " + positionInserted +
", infoItemList.size() = " + infoItemList.size() +
", header = " + header + ", footer = " + footer +
", showFooter = " + showFooter);
if (DEBUG) {
Log.d(TAG, "addInfoItem() after > position = " + positionInserted + ", "
+ "infoItemList.size() = " + infoItemList.size() + ", "
+ "header = " + header + ", footer = " + footer + ", "
+ "showFooter = " + showFooter);
}
notifyItemInserted(positionInserted);
if (footer != null && showFooter) {
int footerNow = sizeConsideringHeaderOffset();
notifyItemMoved(positionInserted, footerNow);
if (DEBUG) Log.d(TAG, "addInfoItem() footer from " + positionInserted +
" to " + footerNow);
if (DEBUG) {
Log.d(TAG, "addInfoItem() footer from " + positionInserted
+ " to " + footerNow);
}
}
}
@@ -180,29 +190,39 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
notifyDataSetChanged();
}
public void setHeader(View header) {
public void setHeader(final View header) {
boolean changed = header != this.header;
this.header = header;
if (changed) notifyDataSetChanged();
if (changed) {
notifyDataSetChanged();
}
}
public void setFooter(View view) {
public void setFooter(final View view) {
this.footer = view;
}
public void showFooter(boolean show) {
if (DEBUG) Log.d(TAG, "showFooter() called with: show = [" + show + "]");
if (show == showFooter) return;
public void showFooter(final boolean show) {
if (DEBUG) {
Log.d(TAG, "showFooter() called with: show = [" + show + "]");
}
if (show == showFooter) {
return;
}
showFooter = show;
if (show) notifyItemInserted(sizeConsideringHeaderOffset());
else notifyItemRemoved(sizeConsideringHeaderOffset());
if (show) {
notifyItemInserted(sizeConsideringHeaderOffset());
} else {
notifyItemRemoved(sizeConsideringHeaderOffset());
}
}
private int sizeConsideringHeaderOffset() {
int i = infoItemList.size() + (header != null ? 1 : 0);
if (DEBUG) Log.d(TAG, "sizeConsideringHeaderOffset() called → " + i);
if (DEBUG) {
Log.d(TAG, "sizeConsideringHeaderOffset() called → " + i);
}
return i;
}
@@ -213,18 +233,27 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
@Override
public int getItemCount() {
int count = infoItemList.size();
if (header != null) count++;
if (footer != null && showFooter) count++;
if (header != null) {
count++;
}
if (footer != null && showFooter) {
count++;
}
if (DEBUG) {
Log.d(TAG, "getItemCount() called, count = " + count + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter);
Log.d(TAG, "getItemCount() called with: "
+ "count = " + count + ", infoItemList.size() = " + infoItemList.size() + ", "
+ "header = " + header + ", footer = " + footer + ", "
+ "showFooter = " + showFooter);
}
return count;
}
@Override
public int getItemViewType(int position) {
if (DEBUG) Log.d(TAG, "getItemViewType() called with: position = [" + position + "]");
if (DEBUG) {
Log.d(TAG, "getItemViewType() called with: position = [" + position + "]");
}
if (header != null && position == 0) {
return HEADER_TYPE;
@@ -237,11 +266,14 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
final InfoItem item = infoItemList.get(position);
switch (item.getInfoType()) {
case STREAM:
return useGridVariant ? GRID_STREAM_HOLDER_TYPE : useMiniVariant ? MINI_STREAM_HOLDER_TYPE : STREAM_HOLDER_TYPE;
return useGridVariant ? GRID_STREAM_HOLDER_TYPE : useMiniVariant
? MINI_STREAM_HOLDER_TYPE : STREAM_HOLDER_TYPE;
case CHANNEL:
return useGridVariant ? GRID_CHANNEL_HOLDER_TYPE : useMiniVariant ? MINI_CHANNEL_HOLDER_TYPE : CHANNEL_HOLDER_TYPE;
return useGridVariant ? GRID_CHANNEL_HOLDER_TYPE : useMiniVariant
? MINI_CHANNEL_HOLDER_TYPE : CHANNEL_HOLDER_TYPE;
case PLAYLIST:
return useGridVariant ? GRID_PLAYLIST_HOLDER_TYPE : useMiniVariant ? MINI_PLAYLIST_HOLDER_TYPE : PLAYLIST_HOLDER_TYPE;
return useGridVariant ? GRID_PLAYLIST_HOLDER_TYPE : useMiniVariant
? MINI_PLAYLIST_HOLDER_TYPE : PLAYLIST_HOLDER_TYPE;
case COMMENT:
return useMiniVariant ? MINI_COMMENT_HOLDER_TYPE : COMMENT_HOLDER_TYPE;
default:
@@ -251,9 +283,12 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int type) {
if (DEBUG)
Log.d(TAG, "onCreateViewHolder() called with: parent = [" + parent + "], type = [" + type + "]");
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent,
final int type) {
if (DEBUG) {
Log.d(TAG, "onCreateViewHolder() called with: "
+ "parent = [" + parent + "], type = [" + type + "]");
}
switch (type) {
case HEADER_TYPE:
return new HFHolder(header);
@@ -287,28 +322,38 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
if (DEBUG) Log.d(TAG, "onBindViewHolder() called with: holder = [" + holder.getClass().getSimpleName() + "], position = [" + position + "]");
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, int position) {
if (DEBUG) {
Log.d(TAG, "onBindViewHolder() called with: "
+ "holder = [" + holder.getClass().getSimpleName() + "], "
+ "position = [" + position + "]");
}
if (holder instanceof InfoItemHolder) {
// If header isn't null, offset the items by -1
if (header != null) position--;
if (header != null) {
position--;
}
((InfoItemHolder) holder).updateFromItem(infoItemList.get(position), recordManager);
} else if (holder instanceof HFHolder && position == 0 && header != null) {
((HFHolder) holder).view = header;
} else if (holder instanceof HFHolder && position == sizeConsideringHeaderOffset() && footer != null && showFooter) {
} else if (holder instanceof HFHolder && position == sizeConsideringHeaderOffset()
&& footer != null && showFooter) {
((HFHolder) holder).view = footer;
}
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List<Object> payloads) {
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position,
@NonNull final List<Object> payloads) {
if (!payloads.isEmpty() && holder instanceof InfoItemHolder) {
for (Object payload : payloads) {
if (payload instanceof StreamStateEntity) {
((InfoItemHolder) holder).updateState(infoItemList.get(header == null ? position : position - 1), recordManager);
((InfoItemHolder) holder).updateState(infoItemList
.get(header == null ? position : position - 1), recordManager);
} else if (payload instanceof Boolean) {
((InfoItemHolder) holder).updateState(infoItemList.get(header == null ? position : position - 1), recordManager);
((InfoItemHolder) holder).updateState(infoItemList
.get(header == null ? position : position - 1), recordManager);
}
}
} else {
@@ -319,10 +364,19 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final int spanCount) {
return new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
public int getSpanSize(final int position) {
final int type = getItemViewType(position);
return type == HEADER_TYPE || type == FOOTER_TYPE ? spanCount : 1;
}
};
}
public class HFHolder extends RecyclerView.ViewHolder {
public View view;
HFHolder(final View v) {
super(v);
view = v;
}
}
}

View File

@@ -6,8 +6,8 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.info_list.InfoItemBuilder;
public class ChannelGridInfoItemHolder extends ChannelMiniInfoItemHolder {
public ChannelGridInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
super(infoItemBuilder, R.layout.list_channel_grid_item, parent);
}
public ChannelGridInfoItemHolder(final InfoItemBuilder infoItemBuilder,
final ViewGroup parent) {
super(infoItemBuilder, R.layout.list_channel_grid_item, parent);
}
}

View File

@@ -31,18 +31,21 @@ import org.schabi.newpipe.util.Localization;
*/
public class ChannelInfoItemHolder extends ChannelMiniInfoItemHolder {
public final TextView itemChannelDescriptionView;
private final TextView itemChannelDescriptionView;
public ChannelInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
public ChannelInfoItemHolder(final InfoItemBuilder infoItemBuilder, final ViewGroup parent) {
super(infoItemBuilder, R.layout.list_channel_item, parent);
itemChannelDescriptionView = itemView.findViewById(R.id.itemChannelDescriptionView);
}
@Override
public void updateFromItem(final InfoItem infoItem, final HistoryRecordManager historyRecordManager) {
public void updateFromItem(final InfoItem infoItem,
final HistoryRecordManager historyRecordManager) {
super.updateFromItem(infoItem, historyRecordManager);
if (!(infoItem instanceof ChannelInfoItem)) return;
if (!(infoItem instanceof ChannelInfoItem)) {
return;
}
final ChannelInfoItem item = (ChannelInfoItem) infoItem;
itemChannelDescriptionView.setText(item.getDescription());

View File

@@ -16,9 +16,10 @@ import de.hdodenhof.circleimageview.CircleImageView;
public class ChannelMiniInfoItemHolder extends InfoItemHolder {
public final CircleImageView itemThumbnailView;
public final TextView itemTitleView;
public final TextView itemAdditionalDetailView;
private final TextView itemAdditionalDetailView;
ChannelMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) {
ChannelMiniInfoItemHolder(final InfoItemBuilder infoItemBuilder, final int layoutId,
final ViewGroup parent) {
super(infoItemBuilder, layoutId, parent);
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
@@ -26,13 +27,17 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder {
itemAdditionalDetailView = itemView.findViewById(R.id.itemAdditionalDetails);
}
public ChannelMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
public ChannelMiniInfoItemHolder(final InfoItemBuilder infoItemBuilder,
final ViewGroup parent) {
this(infoItemBuilder, R.layout.list_channel_mini_item, parent);
}
@Override
public void updateFromItem(final InfoItem infoItem, final HistoryRecordManager historyRecordManager) {
if (!(infoItem instanceof ChannelInfoItem)) return;
public void updateFromItem(final InfoItem infoItem,
final HistoryRecordManager historyRecordManager) {
if (!(infoItem instanceof ChannelInfoItem)) {
return;
}
final ChannelInfoItem item = (ChannelInfoItem) infoItem;
itemTitleView.setText(item.getName());

View File

@@ -30,23 +30,24 @@ import org.schabi.newpipe.local.history.HistoryRecordManager;
*/
public class CommentsInfoItemHolder extends CommentsMiniInfoItemHolder {
public final TextView itemTitleView;
public CommentsInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
public CommentsInfoItemHolder(final InfoItemBuilder infoItemBuilder, final ViewGroup parent) {
super(infoItemBuilder, R.layout.list_comments_item, parent);
itemTitleView = itemView.findViewById(R.id.itemTitleView);
}
@Override
public void updateFromItem(final InfoItem infoItem, final HistoryRecordManager historyRecordManager) {
public void updateFromItem(final InfoItem infoItem,
final HistoryRecordManager historyRecordManager) {
super.updateFromItem(infoItem, historyRecordManager);
if (!(infoItem instanceof CommentsInfoItem)) return;
if (!(infoItem instanceof CommentsInfoItem)) {
return;
}
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
itemTitleView.setText(item.getAuthorName());
itemTitleView.setText(item.getUploaderName());
}
}

View File

@@ -1,21 +1,26 @@
package org.schabi.newpipe.info_list.holder;
import androidx.appcompat.app.AppCompatActivity;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import android.text.style.URLSpan;
import android.text.util.Linkify;
import android.view.ViewGroup;
import android.widget.TextView;
import org.jsoup.helper.StringUtil;
import androidx.appcompat.app.AppCompatActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.util.AndroidTvUtils;
import org.schabi.newpipe.util.CommentTextOnTouchListener;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ShareUtils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -23,35 +28,41 @@ import java.util.regex.Pattern;
import de.hdodenhof.circleimageview.CircleImageView;
public class CommentsMiniInfoItemHolder extends InfoItemHolder {
private static final int COMMENT_DEFAULT_LINES = 2;
private static final int COMMENT_EXPANDED_LINES = 1000;
private static final Pattern PATTERN = Pattern.compile("(\\d+:)?(\\d+)?:(\\d+)");
public final CircleImageView itemThumbnailView;
private final TextView itemContentView;
private final TextView itemLikesCountView;
private final TextView itemDislikesCountView;
private final TextView itemPublishedTime;
private static final int commentDefaultLines = 2;
private static final int commentExpandedLines = 1000;
private String commentText;
private String streamUrl;
private static final Pattern pattern = Pattern.compile("(\\d+:)?(\\d+)?:(\\d+)");
private final Linkify.TransformFilter timestampLink = new Linkify.TransformFilter() {
@Override
public String transformUrl(Matcher match, String url) {
public String transformUrl(final Matcher match, final String url) {
int timestamp = 0;
String hours = match.group(1);
String minutes = match.group(2);
String seconds = match.group(3);
if(hours != null) timestamp += (Integer.parseInt(hours.replace(":", ""))*3600);
if(minutes != null) timestamp += (Integer.parseInt(minutes.replace(":", ""))*60);
if(seconds != null) timestamp += (Integer.parseInt(seconds));
if (hours != null) {
timestamp += (Integer.parseInt(hours.replace(":", "")) * 3600);
}
if (minutes != null) {
timestamp += (Integer.parseInt(minutes.replace(":", "")) * 60);
}
if (seconds != null) {
timestamp += (Integer.parseInt(seconds));
}
return streamUrl + url.replace(match.group(0), "#timestamp=" + timestamp);
}
};
CommentsMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) {
CommentsMiniInfoItemHolder(final InfoItemBuilder infoItemBuilder, final int layoutId,
final ViewGroup parent) {
super(infoItemBuilder, layoutId, parent);
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
@@ -61,37 +72,29 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
itemContentView = itemView.findViewById(R.id.itemCommentContentView);
}
public CommentsMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
public CommentsMiniInfoItemHolder(final InfoItemBuilder infoItemBuilder,
final ViewGroup parent) {
this(infoItemBuilder, R.layout.list_comments_mini_item, parent);
}
@Override
public void updateFromItem(final InfoItem infoItem, final HistoryRecordManager historyRecordManager) {
if (!(infoItem instanceof CommentsInfoItem)) return;
public void updateFromItem(final InfoItem infoItem,
final HistoryRecordManager historyRecordManager) {
if (!(infoItem instanceof CommentsInfoItem)) {
return;
}
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
itemBuilder.getImageLoader()
.displayImage(item.getAuthorThumbnail(),
.displayImage(item.getUploaderAvatarUrl(),
itemThumbnailView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
itemThumbnailView.setOnClickListener(view -> {
if(StringUtil.isBlank(item.getAuthorEndpoint())) return;
try {
final AppCompatActivity activity = (AppCompatActivity) itemBuilder.getContext();
NavigationHelper.openChannelFragment(
activity.getSupportFragmentManager(),
item.getServiceId(),
item.getAuthorEndpoint(),
item.getAuthorName());
} catch (Exception e) {
ErrorActivity.reportUiError((AppCompatActivity) itemBuilder.getContext(), e);
}
});
itemThumbnailView.setOnClickListener(view -> openCommentAuthor(item));
streamUrl = item.getUrl();
itemContentView.setLines(commentDefaultLines);
itemContentView.setLines(COMMENT_DEFAULT_LINES);
commentText = item.getCommentText();
itemContentView.setText(commentText);
itemContentView.setOnTouchListener(CommentTextOnTouchListener.INSTANCE);
@@ -108,10 +111,10 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
itemLikesCountView.setText("-");
}
if (item.getPublishedTime() != null) {
itemPublishedTime.setText(Localization.relativeTime(item.getPublishedTime().date()));
if (item.getUploadDate() != null) {
itemPublishedTime.setText(Localization.relativeTime(item.getUploadDate().date()));
} else {
itemPublishedTime.setText(item.getTextualPublishedTime());
itemPublishedTime.setText(item.getTextualUploadDate());
}
itemView.setOnClickListener(view -> {
@@ -120,36 +123,102 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
itemBuilder.getOnCommentsSelectedListener().selected(item);
}
});
itemView.setOnLongClickListener(view -> {
if (AndroidTvUtils.isTv(itemBuilder.getContext())) {
openCommentAuthor(item);
} else {
ShareUtils.copyToClipboard(itemBuilder.getContext(), commentText);
}
return true;
});
}
private void openCommentAuthor(final CommentsInfoItem item) {
if (TextUtils.isEmpty(item.getUploaderUrl())) {
return;
}
try {
final AppCompatActivity activity = (AppCompatActivity) itemBuilder.getContext();
NavigationHelper.openChannelFragment(
activity.getSupportFragmentManager(),
item.getServiceId(),
item.getUploaderUrl(),
item.getUploaderName());
} catch (Exception e) {
ErrorActivity.reportUiError((AppCompatActivity) itemBuilder.getContext(), e);
}
}
private void allowLinkFocus() {
itemContentView.setMovementMethod(LinkMovementMethod.getInstance());
}
private void denyLinkFocus() {
itemContentView.setMovementMethod(null);
}
private boolean shouldFocusLinks() {
if (itemView.isInTouchMode()) {
return false;
}
URLSpan[] urls = itemContentView.getUrls();
return urls != null && urls.length != 0;
}
private void determineLinkFocus() {
if (shouldFocusLinks()) {
allowLinkFocus();
} else {
denyLinkFocus();
}
}
private void ellipsize() {
if (itemContentView.getLineCount() > commentDefaultLines){
int endOfLastLine = itemContentView.getLayout().getLineEnd(commentDefaultLines - 1);
int end = itemContentView.getText().toString().lastIndexOf(' ', endOfLastLine -2);
if(end == -1) end = Math.max(endOfLastLine -2, 0);
boolean hasEllipsis = false;
if (itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) {
int endOfLastLine = itemContentView.getLayout().getLineEnd(COMMENT_DEFAULT_LINES - 1);
int end = itemContentView.getText().toString().lastIndexOf(' ', endOfLastLine - 2);
if (end == -1) {
end = Math.max(endOfLastLine - 2, 0);
}
String newVal = itemContentView.getText().subSequence(0, end) + "";
itemContentView.setText(newVal);
hasEllipsis = true;
}
linkify();
if (hasEllipsis) {
denyLinkFocus();
} else {
determineLinkFocus();
}
}
private void toggleEllipsize() {
if (itemContentView.getText().toString().equals(commentText)) {
if (itemContentView.getLineCount() > commentDefaultLines) ellipsize();
if (itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) {
ellipsize();
}
} else {
expand();
}
}
private void expand() {
itemContentView.setMaxLines(commentExpandedLines);
itemContentView.setMaxLines(COMMENT_EXPANDED_LINES);
itemContentView.setText(commentText);
linkify();
determineLinkFocus();
}
private void linkify(){
private void linkify() {
Linkify.addLinks(itemContentView, Linkify.WEB_URLS);
Linkify.addLinks(itemContentView, pattern, null, null, timestampLink);
itemContentView.setMovementMethod(null);
Linkify.addLinks(itemContentView, PATTERN, null, null, timestampLink);
}
}

View File

@@ -1,9 +1,10 @@
package org.schabi.newpipe.info_list.holder;
import androidx.recyclerview.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.recyclerview.widget.RecyclerView;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
@@ -31,13 +32,15 @@ import org.schabi.newpipe.local.history.HistoryRecordManager;
public abstract class InfoItemHolder extends RecyclerView.ViewHolder {
protected final InfoItemBuilder itemBuilder;
public InfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) {
public InfoItemHolder(final InfoItemBuilder infoItemBuilder, final int layoutId,
final ViewGroup parent) {
super(LayoutInflater.from(infoItemBuilder.getContext()).inflate(layoutId, parent, false));
this.itemBuilder = infoItemBuilder;
}
public abstract void updateFromItem(final InfoItem infoItem, final HistoryRecordManager historyRecordManager);
public abstract void updateFromItem(InfoItem infoItem,
HistoryRecordManager historyRecordManager);
public void updateState(final InfoItem infoItem, final HistoryRecordManager historyRecordManager) {
}
public void updateState(final InfoItem infoItem,
final HistoryRecordManager historyRecordManager) { }
}

View File

@@ -6,8 +6,8 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.info_list.InfoItemBuilder;
public class PlaylistGridInfoItemHolder extends PlaylistMiniInfoItemHolder {
public PlaylistGridInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
super(infoItemBuilder, R.layout.list_playlist_grid_item, parent);
}
}
public PlaylistGridInfoItemHolder(final InfoItemBuilder infoItemBuilder,
final ViewGroup parent) {
super(infoItemBuilder, R.layout.list_playlist_grid_item, parent);
}
}

View File

@@ -6,8 +6,7 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.info_list.InfoItemBuilder;
public class PlaylistInfoItemHolder extends PlaylistMiniInfoItemHolder {
public PlaylistInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
public PlaylistInfoItemHolder(final InfoItemBuilder infoItemBuilder, final ViewGroup parent) {
super(infoItemBuilder, R.layout.list_playlist_item, parent);
}
}

View File

@@ -10,14 +10,16 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
public final ImageView itemThumbnailView;
public final TextView itemStreamCountView;
private final TextView itemStreamCountView;
public final TextView itemTitleView;
public final TextView itemUploaderView;
public PlaylistMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) {
public PlaylistMiniInfoItemHolder(final InfoItemBuilder infoItemBuilder, final int layoutId,
final ViewGroup parent) {
super(infoItemBuilder, layoutId, parent);
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
@@ -26,22 +28,27 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
itemUploaderView = itemView.findViewById(R.id.itemUploaderView);
}
public PlaylistMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
public PlaylistMiniInfoItemHolder(final InfoItemBuilder infoItemBuilder,
final ViewGroup parent) {
this(infoItemBuilder, R.layout.list_playlist_mini_item, parent);
}
@Override
public void updateFromItem(final InfoItem infoItem, final HistoryRecordManager historyRecordManager) {
if (!(infoItem instanceof PlaylistInfoItem)) return;
public void updateFromItem(final InfoItem infoItem,
final HistoryRecordManager historyRecordManager) {
if (!(infoItem instanceof PlaylistInfoItem)) {
return;
}
final PlaylistInfoItem item = (PlaylistInfoItem) infoItem;
itemTitleView.setText(item.getName());
itemStreamCountView.setText(String.valueOf(item.getStreamCount()));
itemStreamCountView.setText(Localization
.localizeStreamCountMini(itemStreamCountView.getContext(), item.getStreamCount()));
itemUploaderView.setText(item.getUploaderName());
itemBuilder.getImageLoader()
.displayImage(item.getThumbnailUrl(), itemThumbnailView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
itemView.setOnClickListener(view -> {
if (itemBuilder.getOnPlaylistSelectedListener() != null) {

View File

@@ -5,9 +5,8 @@ import android.view.ViewGroup;
import org.schabi.newpipe.R;
import org.schabi.newpipe.info_list.InfoItemBuilder;
public class StreamGridInfoItemHolder extends StreamMiniInfoItemHolder {
public StreamGridInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
super(infoItemBuilder, R.layout.list_stream_grid_item, parent);
}
public class StreamGridInfoItemHolder extends StreamInfoItemHolder {
public StreamGridInfoItemHolder(final InfoItemBuilder infoItemBuilder, final ViewGroup parent) {
super(infoItemBuilder, R.layout.list_stream_grid_item, parent);
}
}

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