Merge pull request #3478 from TeamNewPipe/release_0.19.3

Release 0.19.3
This commit is contained in:
Tobias Groza 2020-05-03 21:22:44 +02:00 committed by GitHub
commit dc8d1b0993
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
332 changed files with 18350 additions and 11041 deletions

View File

@ -12,57 +12,53 @@ add a comment to it. You'll see exactly what is sent, the system is 100% transpa
## Issue reporting/feature requests
* Search the [existing issues](https://github.com/TeamNewPipe/NewPipe/issues) first to make sure your issue/feature
hasn't been reported/requested before
* Check whether your issue/feature is already fixed/implemented
* Check if the issue still exists in the latest release/beta version
* If you are an Android/Java developer, you are always welcome to fix/implement an issue/a feature yourself. PRs welcome!
hasn't been reported/requested before.
* Check whether your issue/feature is already fixed/implemented.
* Check if the issue still exists in the latest release/beta version.
* If you are an Android/Java developer, you are always welcome to fix an issue or implement a feature yourself. PRs welcome!
* We use English for development. Issues in other languages will be closed and ignored.
* Please only add *one* issue at a time. Do not put multiple issues into one thread.
* When reporting a bug please give us a context, and a description how to reproduce it.
* Issues that only contain a generated bug report, but no description might be closed.
* Follow the template! Issues or feature requests not matching the template might be closed.
## Bug Fixing
* If you want to help NewPipe to become free of bugs (this is our utopic goal for NewPipe), you can send us an email to
tnp@newpipe.schabi.org to let me know that you intend to help. We'll send you further instructions. You may, on request,
register at our [Sentry](https://sentry.schabi.org) instance (see section "Crash reporting" for more information.
<a href="mailto:tnp@newpipe.schabi.org">tnp@newpipe.schabi.org</a> to let us know that you intend to help. We'll send you further instructions. You may, on request,
register at our [Sentry](https://sentry.schabi.org) instance (see section "Crash reporting" for more information).
## Translation
* NewPipe can be translated via [Weblate](https://hosted.weblate.org/projects/newpipe/strings/). You can log in there
* NewPipe is translated via [Weblate](https://hosted.weblate.org/projects/newpipe/strings/). You can log in there
with your GitHub account.
* If the language you want to translate is not on Weblate, you can add it: see [How to add a new language](https://github.com/TeamNewPipe/NewPipe/wiki/How-to-add-a-new-language-to-NewPipe) in the wiki.
## Code contribution
* Stick to NewPipe's style conventions (well, just look the other code and then do it the same way :))
* Do not bring non-free software (e.g., binary blobs) into the project. Also, make sure you do not introduce Google
* Stick to NewPipe's style conventions: follow [checkStyle](https://github.com/checkstyle/checkstyle). It will run each time you build the project.
* Do not bring non-free software (e.g. binary blobs) into the project. Also, make sure you do not introduce Google
libraries.
* Stick to [F-Droid contribution guidelines](https://f-droid.org/wiki/page/Inclusion_Policy)
* Make changes on a separate branch, not on the master branch. This is commonly known as *feature branch workflow*. You
may then send your changes as a pull request on GitHub. Patches to the email address mentioned in this document might
not be considered, GitHub is the primary platform. (This only affects you if you are a member of TeamNewPipe)
* Stick to [F-Droid contribution guidelines](https://f-droid.org/wiki/page/Inclusion_Policy).
* Make changes on a separate branch with a meaningful name, not on the master neither dev branch. This is commonly known as *feature branch workflow*. You
may then send your changes as a pull request (PR) on GitHub.
* When submitting changes, you confirm that your code is licensed under the terms of the
[GNU General Public License v3](https://www.gnu.org/licenses/gpl-3.0.html).
* Please test (compile and run) your code before you submit changes! Ideally, provide test feedback in the PR
description. Untested code will **not** be merged!
* Try to figure out yourself why builds on our CI fail.
* Make sure your PR is up-to-date with the rest of the code. Often, a simple click on "Update branch" will do the job,
but if not, you are asked to merge the master branch manually and resolve the problems on your own. That will make the
but if not, you are asked to rebase the dev branch manually and resolve the problems on your own. You can find help [on the wiki](https://github.com/TeamNewPipe/NewPipe/wiki/How-to-merge-a-PR). That will make the
maintainers' jobs way easier.
* Please show intention to maintain your features and code after you contributed it. Unmaintained code is a hassle for
the core developers, and just adds work. If you do not intend to maintain features you contributed, please think again
about submission, or clearly state that in the description of your PR.
* Respond yourselves if someone requests changes or otherwise raises issues about your PRs.
* Check if your contributions align with the [fdroid inclusion guidelines](https://f-droid.org/en/docs/Inclusion_Policy/).
* Check if your submission can be build with the current fdroid build server setup.
* Send PR that only cover one specific issue/solution/bug. Do not send PRs that are huge and consists of multiple
independent solutions.
## Communication
* WE DO NOW HAVE A MAILING LIST: [newpipe@list.schabi.org](https://list.schabi.org/cgi-bin/mailman/listinfo/newpipe).
* There is an IRC channel on Freenode which is regularly visited by the core team and other developers:
[#newpipe](irc:irc.freenode.net/newpipe). [Click here for Webchat](https://webchat.freenode.net/?channels=newpipe)!
* If you want to get in touch with the core team or one of our other contributors you can send an email to
tnp(at)schabi.org. Please do not send issue reports, they will be ignored and remain unanswered! Use the GitHub issue
<a href="mailto:tnp@newpipe.schabi.org">tnp@newpipe.schabi.org</a>. Please do not send issue reports, they will be ignored and remain unanswered! Use the GitHub issue
tracker described above!
* Feel free to post suggestions, changes, ideas etc. on GitHub, IRC or the mailing list!
* Feel free to post suggestions, changes, ideas etc. on GitHub or IRC!

View File

@ -7,38 +7,40 @@ assignees: ''
---
<!--
Oh no, a bug! It happens. Thanks for reporting an issue with NewPipe.
Oh no, a bug! It happens. Thanks for reporting an issue with NewPipe. If this is your first bug report, read the following information before proceeding:
Use this template to notify us if you found a bug.
To make it easier for us to help you please enter detailed information below.
Please note, we only support the latest version of NewPipe and the master branch. Make sure you have that version installed. If you don't, upgrade & reproduce the problem before opening the issue. The release page (https://github.com/TeamNewPipe/NewPipe/releases/latest) is the go-to place to get this version. In order to check your app version, open the left drawer and click on "About".
Please note, we only support the latest version of NewPipe. In order to check your app version, open the left drawer and click on "About". If you don't have the latest version, upgrade to it and reproduce the problem before opening the issue. The release page (https://github.com/TeamNewPipe/NewPipe/releases/latest) is where you can get it.
P.S.: Our contribution guidelines might be a nice document to read before you fill out the report :) You can find it at https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md
-->
### Version
<!-- Which version are you using? -->
-
To make it easier for us to help you please enter detailed information in the template we have provided below. If a section isn't relevant, just delete it, though it would be helpful to still provide as much detail as possible.
-->
<!-- The comments between these brackets won't show up in the submitted issue (as you can see in the preview). -->
### Version
<!-- Which version are you using? Hopefully the latest! We just told you that above! -->
-
### Steps to reproduce the bug
<!-- If you can't reproduce it, please try to give as many details as possible on how you think you got to the bug. -->
Steps to reproduce the behavior:
<!--
1. Go to '...'
2. Press on '....'
3. Swipe down to '....'
-->
<!-- If you can't cause the bug to show up again reliably (and hence don't have a proper set of steps to give us), please still try to give as many details as possible on how you think you encountered the bug. -->
### Expected behavior
Tell us what you expected to happen.
<!-- Tell us what you expect to happen. -->
### Actual behaviour
Tell us what happens instead.
<!-- Tell us what happens instead. -->
### Screenshots/Screen records
If applicable, add screenshots or a screen recording to help explain your problem. GitHub should support uploading them directly in the issue field. If your file is too big, feel free to paste a link from an image/video hoster here instead.
### Screenshots/Screen recordings
<!-- If applicable, add screenshots or a screen recording to help explain your problem. GitHub supports uploading them directly in the issue text box. If your file is too big for Github to accept, feel free to paste a link from an image/video hoster here instead. -->
### Logs
If your bug includes a crash, please head over to the [incredible bugreport to markdown converter](https://teamnewpipe.github.io/CrashReportToMarkdown/). Copy the result. Paste it here:
<!-- If your bug includes a crash (where you're shown the Error Report page with a bunch of info), copy it to the clipboard (there is a share button for this), head over to our bug report to markdown converter at https://teamnewpipe.github.io/CrashReportToMarkdown/ and paste it. Copy the converted text (it is MUCH easier to read this way) from there and paste it here: -->
<!-- That's right, here! -->

View File

@ -7,22 +7,33 @@ assignees: ''
---
<!-- Hey. Our contribution guidelines (https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md) might be an appropriate
document to read before you fill out the request :) -->
#### Is your feature request related to a problem? Please describe it
A clear and concise description of what the problem is.
Example: *I want to do X, but there is no way to do it.*
#### Describe the solution you'd like
A clear and concise description of what you want to happen.
<!-- The comments between these brackets won't show up in the submitted issue (as you can see in the preview). -->
#### Describe the feature you want
<!-- A clear and concise description of what you want to happen. PLEASE MAKE SURE it is one feature ONLY. You should open separate issues for separate feature requests, because those issues will be used to track their status.
Example: *I think it would be nice if you add feature Y which makes X possible.*
#### (Optional) Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered.
Example: *I considered Z, but that didn't turn out to be a good idea because...*
Optionally, also describe alternatives you've considered.
Example: *Z is also a good alternative. Not as good as Y, but at least...* or *I considered Z, but that didn't turn out to be a good idea because...* -->
<!-- Write below this -->
#### Is your feature request related to a problem? Please describe it
<!-- A clear and concise description of what the problem is. Maybe the developers could brainstorm and come up with a better solution to your problem. If they exist, link to related Issues and/or PRs for developers to keep track easier.
Example: *I want to do X, but there is no way to do it.* -->
<!-- Write below this -->
#### Additional context
Add any other context or screenshots about the feature request here.
Example: *Here's a photo of my cat!*
<!-- Add any other context, like screenshots, about the feature request here.
Example: *Here's a photo of my cat!* -->
<!-- Write below this -->
#### How will you/everyone benefit from this feature?
Convince us! How does it change your NewPipe experience and/or your life?
<!-- Convince us! How does it change your NewPipe experience and/or your life?
The better this paragraph is, the more likely a developer will think about working on it.
Example: *This feature will help us colonize the galaxy! -->
<!-- Write below this -->

View File

@ -1,10 +1,12 @@
<!-- Hey there. Thank you so much for improving NewPipe. Please take a moment to fill out the following suggestion on how to structure this PR description. Having roughly the same layout helps everyone considerably :)-->
#### What is it?
- [ ] Bug fix
- [ ] Feature
- [ ] Bug fix (user facing)
- [ ] Feature (user facing)
- [ ] Code base improvement (dev facing)
- [ ] Meta improvement to the project (dev facing)
#### Long description of the changes in your PR
#### Description of the changes in your PR
<!-- While bullet points are the norm in this section, feel free to write a text instead if you can't fit it in a list -->
- record videos
- create clones

View File

@ -4,10 +4,10 @@
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://f-droid.org/wiki/images/0/06/F-Droid-button_get-it-on.png"></a></p>
<p align="center">
<a href="https://github.com/TeamNewPipe/NewPipe" alt="GitHub release"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a>
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub release"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a>
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="License: GPLv3"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg"></a>
<a href="https://travis-ci.org/TeamNewPipe/NewPipe" alt="Build Status"><img src="https://travis-ci.org/TeamNewPipe/NewPipe.svg"></a>
<a href="https://hosted.weblate.org/engage/NewPipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/NewPipe/-/svg-badge.svg"></a>
<a href="https://hosted.weblate.org/engage/newpipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
<a href="http://webchat.freenode.net/?channels=%23newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
<a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
</p>

View File

@ -2,6 +2,7 @@ apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: 'checkstyle'
android {
compileSdkVersion 28
@ -12,8 +13,8 @@ android {
resValue "string", "app_name", "NewPipe"
minSdkVersion 19
targetSdkVersion 28
versionCode 920
versionName "0.19.2"
versionCode 930
versionName "0.19.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
@ -26,12 +27,6 @@ android {
}
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
multiDexEnabled true
debuggable true
@ -49,6 +44,16 @@ android {
archivesBaseName = 'NewPipe_' + normalizedWorkingBranch
}
}
// Keep the release build type at the end of the list to override 'archivesBaseName' of
// debug build. This seems to be a Gradle bug, therefore
// TODO: update Gradle version
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
archivesBaseName = 'app'
}
}
lintOptions {
@ -82,18 +87,62 @@ ext {
icepickLibVersion = '3.2.0'
stethoLibVersion = '1.5.0'
markwonVersion = '4.2.1'
checkstyleVersion = '8.31'
}
checkstyle {
configFile rootProject.file('checkstyle.xml')
ignoreFailures false
showViolations true
toolVersion = "${checkstyleVersion}"
}
task runCheckstyle(type: Checkstyle) {
source 'src'
include '**/*.java'
exclude '**/gen/**'
exclude '**/R.java'
exclude '**/BuildConfig.java'
exclude 'main/java/us/shandian/giga/**'
// empty classpath
classpath = files()
showViolations true
reports {
xml.enabled true
html.enabled true
}
}
tasks.withType(Checkstyle).each {
checkstyleTask -> checkstyleTask.doLast {
reports.all { report ->
def outputFile = report.destination
if (outputFile.exists() && outputFile.text.contains("severity=\"error\"")) {
throw new GradleException("There were checkstyle errors! For more info check $outputFile")
}
}
}
}
afterEvaluate {
preDebugBuild.dependsOn runCheckstyle
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
debugImplementation "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation "android.arch.persistence.room:testing:1.1.1"
androidTestImplementation('androidx.test.espresso:espresso-core:3.1.0', {
exclude module: 'support-annotations'
})
implementation 'com.github.TeamNewPipe:NewPipeExtractor:69e0624e3'
implementation 'com.github.TeamNewPipe:NewPipeExtractor:fc3a69ed54b393e3e4e3a78ae6e89edc1d47c45a'
testImplementation 'junit:junit:4.12'
testImplementation 'org.mockito:mockito-core:2.23.0'
@ -113,7 +162,7 @@ dependencies {
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
// Originally in NewPipeExtractor
implementation 'com.grack:nanojson:1.1'
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
implementation 'org.jsoup:jsoup:1.9.2'
implementation 'ch.acra:acra:4.9.2' //4.11
@ -165,4 +214,4 @@ static String getGitWorkingBranch() {
// git was not found
return ""
}
}
}

View File

@ -30,8 +30,9 @@ class AppDatabaseTest {
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());
@get:Rule
val testHelper = MigrationTestHelper(InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java.canonicalName, FrameworkSQLiteOpenHelperFactory())
@Test
fun migrateDatabaseFrom2to3() {
@ -72,7 +73,7 @@ class AppDatabaseTest {
}
testHelper.runMigrationsAndValidate(AppDatabase.DATABASE_NAME, Migrations.DB_VER_3,
true, Migrations.MIGRATION_2_3);
true, Migrations.MIGRATION_2_3)
val migratedDatabaseV3 = getMigratedDatabase()
val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst()

View File

@ -1,8 +1,9 @@
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 org.junit.Test;
import org.junit.runner.RunWith;
@ -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

@ -3,6 +3,7 @@ package org.schabi.newpipe;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import androidx.annotation.NonNull;
import androidx.multidex.MultiDex;
@ -26,7 +27,7 @@ public class DebugApp extends App {
private static final String TAG = DebugApp.class.toString();
@Override
protected void attachBaseContext(Context base) {
protected void attachBaseContext(final Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}
@ -39,8 +40,10 @@ public class DebugApp extends App {
@Override
protected Downloader getDownloader() {
return DownloaderImpl.init(new OkHttpClient.Builder()
DownloaderImpl downloader = DownloaderImpl.init(new OkHttpClient.Builder()
.addNetworkInterceptor(new StethoInterceptor()));
setCookiesToDownloader(downloader);
return downloader;
}
private void initStetho() {

View File

@ -11,7 +11,10 @@
<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:banner="@mipmap/newpipe_tv_banner"
android:name=".App"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
@ -26,6 +29,7 @@
<intent-filter>
<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>

View File

@ -38,12 +38,15 @@ import java.util.ArrayList;
* 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.
* 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>
* <br><b>Check out the changes in:</b>
* <b>Check out the changes in:</b>
* </p>
* <ul>
* <li>{@link #saveState()}</li>
* <li>{@link #restoreState(Parcelable, ClassLoader)}</li>
@ -88,8 +91,8 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
private Fragment mCurrentPrimaryItem = null;
/**
* Constructor for {@link FragmentStatePagerAdapterMenuWorkaround} that sets the fragment manager for the
* adapter. This is the equivalent of calling
* 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}.
*
@ -101,7 +104,7 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
* {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT}
*/
@Deprecated
public FragmentStatePagerAdapterMenuWorkaround(@NonNull FragmentManager fm) {
public FragmentStatePagerAdapterMenuWorkaround(@NonNull final FragmentManager fm) {
this(fm, BEHAVIOR_SET_USER_VISIBLE_HINT);
}
@ -117,20 +120,21 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
* @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 FragmentManager fm,
@Behavior int behavior) {
public FragmentStatePagerAdapterMenuWorkaround(@NonNull final FragmentManager fm,
@Behavior final int behavior) {
mFragmentManager = fm;
mBehavior = behavior;
}
/**
* Return the Fragment associated with a specified position.
* @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 ViewGroup container) {
public void startUpdate(@NonNull final ViewGroup container) {
if (container.getId() == View.NO_ID) {
throw new IllegalStateException("ViewPager with adapter " + this
+ " requires a view id");
@ -140,7 +144,7 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
@SuppressWarnings("deprecation")
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
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
@ -157,7 +161,9 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
}
Fragment fragment = getItem(position);
if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
if (DEBUG) {
Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
}
if (mSavedState.size() > position) {
Fragment.SavedState fss = mSavedState.get(position);
if (fss != null) {
@ -183,14 +189,17 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
}
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
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());
if (DEBUG) {
Log.v(TAG, "Removing item #" + position + ": f=" + object
+ " v=" + ((Fragment) object).getView());
}
while (mSavedState.size() <= position) {
mSavedState.add(null);
}
@ -206,8 +215,9 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
@Override
@SuppressWarnings({"ReferenceEquality", "deprecation"})
public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
Fragment fragment = (Fragment)object;
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);
@ -235,7 +245,7 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
}
@Override
public void finishUpdate(@NonNull ViewGroup container) {
public void finishUpdate(@NonNull final ViewGroup container) {
if (mCurTransaction != null) {
mCurTransaction.commitNowAllowingStateLoss();
mCurTransaction = null;
@ -243,12 +253,12 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
}
@Override
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
return ((Fragment)object).getView() == view;
public boolean isViewFromObject(@NonNull final View view, @NonNull final Object object) {
return ((Fragment) object).getView() == view;
}
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
private final String SELECTED_FRAGMENT = "selected_fragment";
private final String selectedFragment = "selected_fragment";
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
@Override
@ -261,7 +271,7 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
mSavedState.toArray(fss);
state.putParcelableArray("states", fss);
}
for (int i=0; i<mFragments.size(); i++) {
for (int i = 0; i < mFragments.size(); i++) {
Fragment f = mFragments.get(i);
if (f != null && f.isAdded()) {
if (state == null) {
@ -273,7 +283,7 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// Check if it's the same fragment instance
if (f == mCurrentPrimaryItem) {
state.putString(SELECTED_FRAGMENT, key);
state.putString(selectedFragment, key);
}
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
}
@ -282,16 +292,16 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
}
@Override
public void restoreState(@Nullable Parcelable state, @Nullable ClassLoader loader) {
public void restoreState(@Nullable final Parcelable state, @Nullable final ClassLoader loader) {
if (state != null) {
Bundle bundle = (Bundle)state;
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]);
for (int i = 0; i < fss.length; i++) {
mSavedState.add((Fragment.SavedState) fss[i]);
}
}
Iterable<String> keys = bundle.keySet();
@ -304,7 +314,8 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
mFragments.add(null);
}
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
final boolean wasSelected = bundle.getString(SELECTED_FRAGMENT, "").equals(key);
final boolean wasSelected = bundle.getString(selectedFragment, "")
.equals(key);
f.setMenuVisibility(wasSelected);
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
mFragments.set(index, f);

View File

@ -1,24 +1,60 @@
package com.google.android.material.appbar;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.OverScroller;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
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);
}
@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) {
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
// remove reference to old nested scrolling child
@ -35,7 +71,8 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
@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);
@ -62,12 +99,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) {
// ?
}
@ -76,7 +115,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,10 +5,12 @@ 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;
@ -27,7 +29,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 +68,24 @@ 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;
private RefWatcher refWatcher;
@Nullable
public static RefWatcher getRefWatcher(final Context context) {
final App application = (App) context.getApplicationContext();
return application.refWatcher;
}
public static App getApp() {
return app;
}
@Override
protected void attachBaseContext(Context base) {
protected void attachBaseContext(final Context base) {
super.attachBaseContext(base);
initACRA();
@ -116,31 +127,46 @@ 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.setCookies(prefs.getString(key, ""));
}
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 +176,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
@ -191,7 +219,7 @@ public class App extends Application {
private void initACRA() {
try {
final ACRAConfiguration acraConfig = new ConfigurationBuilder(this)
.setReportSenderFactoryClasses(reportSenderFactoryClasses)
.setReportSenderFactoryClasses(REPORT_SENDER_FACTORY_CLASSES)
.setBuildConfigClass(BuildConfig.class)
.build();
ACRA.init(this, acraConfig);
@ -202,7 +230,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 +258,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,12 +279,6 @@ 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;
}
@ -264,8 +286,4 @@ public class App extends Application {
protected boolean isDisposedRxExceptionsReported() {
return false;
}
public static App getApp() {
return app;
}
}

View File

@ -2,13 +2,14 @@ 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;
@ -16,18 +17,16 @@ import icepick.Icepick;
import icepick.State;
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,51 @@ 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);
if (refWatcher != null) {
refWatcher.watch(this);
}
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
public void setUserVisibleHint(final boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
mIsVisibleToUser = isVisibleToUser;
}
@ -93,20 +100,20 @@ 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() {
}
protected void initListeners() { }
/*//////////////////////////////////////////////////////////////////////////
// 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

@ -3,6 +3,9 @@ package org.schabi.newpipe;
import android.os.Build;
import android.text.TextUtils;
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;
@ -26,9 +29,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,20 +37,22 @@ 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 10.0; WOW64; rv:68.0) Gecko/20100101 Firefox/68.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";
private static DownloaderImpl instance;
private 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();
}
@ -58,20 +60,72 @@ public class DownloaderImpl extends Downloader {
* 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;
}
/**
* 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 String getCookies() {
return mCookies;
}
public void setCookies(String cookies) {
public void setCookies(final String cookies) {
mCookies = cookies;
}
@ -81,7 +135,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,7 +146,7 @@ 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)
@ -122,7 +176,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();
@ -172,49 +227,7 @@ public class DownloaderImpl extends Downloader {
}
final String latestUrl = response.request().url().toString();
return new Response(response.code(), response.message(), response.headers().toMultimap(), responseBodyToReturn, latestUrl);
}
/**
* 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();
}
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

@ -33,6 +33,7 @@ import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.AdapterView;
@ -63,6 +64,7 @@ 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.report.ErrorActivity;
import org.schabi.newpipe.util.AndroidTvUtils;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.KioskTranslator;
import org.schabi.newpipe.util.Localization;
@ -73,6 +75,7 @@ import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.StateSaver;
import org.schabi.newpipe.util.TLSSocketFactoryCompat;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.views.FocusOverlayView;
import java.util.ArrayList;
import java.util.List;
@ -93,11 +96,11 @@ public class MainActivity extends AppCompatActivity {
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;
@ -108,8 +111,11 @@ 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) {
@ -123,10 +129,12 @@ public class MainActivity extends AppCompatActivity {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Window w = getWindow();
w.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
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();
}
@ -136,6 +144,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 {
@ -151,13 +163,15 @@ 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))
.add(R.id.menu_tabs_group, kioskId, 0, KioskTranslator
.getTranslatedKioskName(ks, this))
.setIcon(KioskTranslator.getKioskIcons(ks, this));
kioskId ++;
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_feed_title)
@ -180,20 +194,21 @@ public class MainActivity extends AppCompatActivity {
.add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.info));
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)) {
@ -206,7 +221,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);
@ -229,14 +244,16 @@ 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;
@ -259,19 +276,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;
@ -283,13 +301,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());
// 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() {
@ -299,7 +331,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 {
@ -313,55 +345,62 @@ public class MainActivity extends AppCompatActivity {
private void showServices() {
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) {
}
});
@ -379,9 +418,10 @@ 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))
.add(R.id.menu_tabs_group, kioskId, ORDER,
KioskTranslator.getTranslatedKioskName(ks, this))
.setIcon(KioskTranslator.getKioskIcons(ks, this));
kioskId ++;
kioskId++;
}
drawerItems.getMenu()
@ -420,15 +460,17 @@ public class MainActivity extends AppCompatActivity {
@Override
protected void onResume() {
assureCorrectAppLanguage(this);
Localization.init(getApplicationContext()); //change the date format to match the selected language on resume
// 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 {
final int selectedServiceId = ServiceHelper.getSelectedServiceId(this);
final String selectedServiceName = NewPipe.getService(selectedServiceId).getServiceInfo().getName();
final String selectedServiceName = NewPipe.getService(selectedServiceId)
.getServiceInfo().getName();
headerServiceView.setText(selectedServiceName);
headerServiceIcon.setImageResource(ServiceHelper.getIcon(selectedServiceId));
@ -441,15 +483,20 @@ 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);
}
@ -460,13 +507,18 @@ public class MainActivity extends AppCompatActivity {
}
@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);
@ -476,24 +528,40 @@ public class MainActivity extends AppCompatActivity {
@Override
public void onBackPressed() {
if (DEBUG) Log.d(TAG, "onBackPressed() called");
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) {
if (((BackPressable) fragment).onBackPressed()) return;
if (DEBUG) {
Log.d(TAG, "onBackPressed() called");
}
if (AndroidTvUtils.isTv(this)) {
View drawerPanel = findViewById(R.id.navigation);
if (drawer.isDrawerOpen(drawerPanel)) {
drawer.closeDrawers();
return;
}
}
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) {
if (((BackPressable) fragment).onBackPressed()) {
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;
}
}
@ -502,7 +570,8 @@ public class MainActivity extends AppCompatActivity {
NavigationHelper.openDownloads(this);
break;
case PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE:
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
Fragment fragment = getSupportFragmentManager()
.findFragmentById(R.id.fragment_holder);
if (fragment instanceof VideoDetailFragment) {
((VideoDetailFragment) fragment).openDownloadDialog();
}
@ -547,8 +616,10 @@ 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);
@ -557,8 +628,8 @@ public class MainActivity extends AppCompatActivity {
}
if (!(fragment instanceof SearchFragment)) {
findViewById(R.id.toolbar).findViewById(R.id.toolbar_search_container).setVisibility(View.GONE);
findViewById(R.id.toolbar).findViewById(R.id.toolbar_search_container)
.setVisibility(View.GONE);
}
ActionBar actionBar = getSupportActionBar();
@ -572,8 +643,10 @@ 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) {
@ -590,11 +663,15 @@ 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)) {
handleIntent(getIntent());
} else NavigationHelper.gotoMainFragment(getSupportFragmentManager());
} else {
NavigationHelper.gotoMainFragment(getSupportFragmentManager());
}
}
/*//////////////////////////////////////////////////////////////////////////
@ -602,12 +679,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) {
@ -622,26 +701,23 @@ 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);
NavigationHelper.openVideoDetailFragment(getSupportFragmentManager(), serviceId, url, title, autoPlay);
boolean autoPlay = intent
.getBooleanExtra(VideoDetailFragment.AUTO_PLAY, false);
NavigationHelper.openVideoDetailFragment(getSupportFragmentManager(),
serviceId, url, title, autoPlay);
break;
case CHANNEL:
NavigationHelper.openChannelFragment(getSupportFragmentManager(),
@ -658,7 +734,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(),

View File

@ -13,14 +13,13 @@ 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_1_2, MIGRATION_2_3)
@ -28,13 +27,14 @@ public final class NewPipeDatabase {
}
@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;
}
}
}

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,24 +1,31 @@
package org.schabi.newpipe;
import android.content.Intent;
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 androidx.annotation.NonNull;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
/*
* Created by beneth <bmauduit@beneth.fr> on 06.12.16.
@ -49,7 +56,7 @@ public class ReCaptchaActivity extends AppCompatActivity {
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);
@ -72,10 +79,33 @@ public class ReCaptchaActivity extends AppCompatActivity {
webSettings.setJavaScriptEnabled(true);
webView.setWebViewClient(new WebViewClient() {
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public void onPageFinished(WebView view, String url) {
public boolean shouldOverrideUrlLoading(final WebView view,
final WebResourceRequest request) {
String url = request.getUrl().toString();
if (MainActivity.DEBUG) {
Log.d(TAG, "shouldOverrideUrlLoading: request.url=" + url);
}
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);
handleCookies(url);
handleCookiesFromUrl(url);
}
});
@ -84,7 +114,8 @@ public class ReCaptchaActivity extends AppCompatActivity {
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();
}
@ -93,7 +124,7 @@ public class ReCaptchaActivity extends AppCompatActivity {
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
public boolean onCreateOptionsMenu(final Menu menu) {
getMenuInflater().inflate(R.menu.menu_recaptcha, menu);
ActionBar actionBar = getSupportActionBar();
@ -112,7 +143,7 @@ public class ReCaptchaActivity extends AppCompatActivity {
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
public boolean onOptionsItemSelected(final MenuItem item) {
int id = item.getItemId();
switch (id) {
case R.id.menu_item_done:
@ -124,8 +155,18 @@ public class ReCaptchaActivity extends AppCompatActivity {
}
private void saveCookiesAndFinish() {
handleCookies(webView.getUrl()); // try to get cookies of unclosed page
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().setCookies(foundCookies);
setResult(RESULT_OK);
@ -137,24 +178,60 @@ public class ReCaptchaActivity extends AppCompatActivity {
}
private void handleCookiesFromUrl(@Nullable final String url) {
if (MainActivity.DEBUG) {
Log.d(TAG, "handleCookiesFromUrl: url=" + (url == null ? "null" : url));
}
if (url == null) {
return;
}
private void handleCookies(String url) {
String cookies = CookieManager.getInstance().getCookie(url);
if (MainActivity.DEBUG) Log.d(TAG, "handleCookies: url=" + url + "; cookies=" + (cookies == null ? "null" : cookies));
if (cookies == null) return;
handleCookies(cookies);
addYoutubeCookies(cookies);
// add other methods to extract cookies here
// 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 addYoutubeCookies(@NonNull String cookies) {
if (cookies.contains("s_gl=") || cookies.contains("goojf=") || cookies.contains("VISITOR_INFO1_LIVE=")) {
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(String cookie) {
private void addCookie(final String cookie) {
if (foundCookies.contains(cookie)) {
return;
}

View File

@ -45,15 +45,19 @@ import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.AndroidTvUtils;
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.views.FocusOverlayView;
import org.schabi.newpipe.util.urlfinder.UrlFinder;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import icepick.Icepick;
@ -71,29 +75,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);
@ -106,14 +112,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);
}
@ -132,7 +138,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) {
@ -157,13 +163,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();
@ -175,8 +182,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);
@ -186,7 +196,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:
@ -204,20 +215,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) {
@ -239,7 +256,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) -> {
@ -250,7 +268,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();
}
};
@ -261,7 +281,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();
@ -270,10 +292,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;
@ -285,18 +310,21 @@ 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.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);
@ -315,48 +343,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),
returnList.add(new AdapterChoiceItem(getString(R.string.show_info_key),
getString(R.string.show_info),
resolveResourceIdFromAttr(context, R.attr.info)));
if (capabilities.contains(VIDEO) && !(isExtVideoEnabled && linkType != LinkType.STREAM)) {
returnList.add(new AdapterChoiceItem(getString(R.string.video_player_key), getString(R.string.video_player),
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),
returnList.add(new AdapterChoiceItem(getString(R.string.popup_player_key),
getString(R.string.popup_player),
resolveResourceIdFromAttr(context, R.attr.popup)));
}
if (capabilities.contains(AUDIO) && !(isExtAudioEnabled && linkType != LinkType.STREAM)) {
returnList.add(new AdapterChoiceItem(getString(R.string.background_player_key), getString(R.string.background_player),
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.download_key), getString(R.string.download),
returnList.add(new AdapterChoiceItem(getString(R.string.download_key),
getString(R.string.download),
resolveResourceIdFromAttr(context, R.attr.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);
@ -372,21 +416,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();
}
@ -414,7 +462,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);
@ -427,12 +476,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);
@ -450,7 +498,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();
@ -462,12 +512,73 @@ public class RouterActivity extends AppCompatActivity {
}
}
/*//////////////////////////////////////////////////////////////////////////
// Service Fetcher
//////////////////////////////////////////////////////////////////////////*/
private String removeHeadingGibberish(final String input) {
int start = 0;
for (int i = input.indexOf("://") - 1; i >= 0; i--) {
if (!input.substring(i, i + 1).matches("\\p{L}")) {
start = i + 1;
break;
}
}
return input.substring(start);
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
private String trim(final String input) {
if (input == null || input.length() < 1) {
return input;
} else {
String output = input;
while (output.length() > 0 && output.substring(0, 1).matches(REGEX_REMOVE_FROM_URL)) {
output = output.substring(1);
}
while (output.length() > 0
&& output.substring(output.length() - 1).matches(REGEX_REMOVE_FROM_URL)) {
output = output.substring(0, output.length() - 1);
}
return output;
}
}
/**
* Retrieves all Strings which look remotely like URLs from a text.
* Used if NewPipe was called through share menu.
*
* @param sharedText text to scan for URLs.
* @return potential URLs
*/
protected String[] getUris(final String sharedText) {
final Collection<String> result = new HashSet<>();
if (sharedText != null) {
final String[] array = sharedText.split("\\p{Space}");
for (String s : array) {
s = trim(s);
if (s.length() != 0) {
if (s.matches(".+://.+")) {
result.add(removeHeadingGibberish(s));
} else if (s.matches(".+\\..+")) {
result.add("http://" + s);
}
}
}
}
return result.toArray(new String[result.size()]);
}
private static class AdapterChoiceItem {
final String description, key;
final String description;
final String key;
@DrawableRes
final int icon;
AdapterChoiceItem(String key, String description, int icon) {
AdapterChoiceItem(final String key, final String description, final int icon) {
this.description = description;
this.key = key;
this.icon = icon;
@ -476,10 +587,12 @@ public class RouterActivity extends AppCompatActivity {
private static class Choice implements Serializable {
final int serviceId;
final String url, playerChoice;
final String url;
final String playerChoice;
final LinkType linkType;
Choice(int serviceId, LinkType linkType, String url, String playerChoice) {
Choice(final int serviceId, final LinkType linkType,
final String url, final String playerChoice) {
this.serviceId = serviceId;
this.linkType = linkType;
this.url = url;
@ -492,14 +605,10 @@ public class RouterActivity extends AppCompatActivity {
}
}
/*//////////////////////////////////////////////////////////////////////////
// Service Fetcher
//////////////////////////////////////////////////////////////////////////*/
public static class FetcherService extends IntentService {
private static final int ID = 456;
public static final String KEY_CHOICE = "key_choice";
private static final int ID = 456;
private Disposable fetcher;
public FetcherService() {
@ -513,16 +622,20 @@ public class RouterActivity extends AppCompatActivity {
}
@Override
protected void onHandleIntent(@Nullable Intent intent) {
if (intent == null) return;
protected void onHandleIntent(@Nullable final Intent intent) {
if (intent == null) {
return;
}
final Serializable serializable = intent.getSerializableExtra(KEY_CHOICE);
if (!(serializable instanceof Choice)) return;
if (!(serializable instanceof Choice)) {
return;
}
Choice playerChoice = (Choice) serializable;
handleChoice(playerChoice);
}
public void handleChoice(Choice choice) {
public void handleChoice(final Choice choice) {
Single<? extends Info> single = null;
UserAction userAction = UserAction.SOMETHING_ELSE;
@ -549,22 +662,27 @@ public class RouterActivity extends AppCompatActivity {
.observeOn(AndroidSchedulers.mainThread())
.subscribe(info -> {
resultHandler.accept(info);
if (fetcher != null) fetcher.dispose();
if (fetcher != null) {
fetcher.dispose();
}
}, throwable -> ExtractorHelper.handleGeneralException(this,
choice.serviceId, choice.url, throwable, finalUserAction, ", opened with " + choice.playerChoice));
choice.serviceId, choice.url, throwable, finalUserAction,
", opened with " + choice.playerChoice));
}
}
public Consumer<Info> getResultHandler(Choice choice) {
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);
;
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;
@ -590,7 +708,9 @@ public class RouterActivity extends AppCompatActivity {
}
if (info instanceof ChannelInfo || info instanceof PlaylistInfo) {
playQueue = info instanceof ChannelInfo ? new ChannelPlayQueue((ChannelInfo) info) : new PlaylistPlayQueue((PlaylistInfo) info);
playQueue = info instanceof ChannelInfo
? new ChannelPlayQueue((ChannelInfo) info)
: new PlaylistPlayQueue((PlaylistInfo) info);
if (playerChoice.equals(videoPlayerKey)) {
NavigationHelper.playOnMainPlayer(this, playQueue, true);
@ -607,7 +727,9 @@ public class RouterActivity extends AppCompatActivity {
public void onDestroy() {
super.onDestroy();
stopForeground(true);
if (fetcher != null) fetcher.dispose();
if (fetcher != null) {
fetcher.dispose();
}
}
private NotificationCompat.Builder createNotification() {
@ -615,8 +737,10 @@ public class RouterActivity extends AppCompatActivity {
.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));
.setContentTitle(
getString(R.string.preferred_player_fetcher_notification_title))
.setContentText(
getString(R.string.preferred_player_fetcher_notification_message));
}
}
@ -625,7 +749,7 @@ public class RouterActivity extends AppCompatActivity {
//////////////////////////////////////////////////////////////////////////*/
@Nullable
private String getUrl(Intent intent) {
private String getUrl(final Intent intent) {
String foundUrl = null;
if (intent.getData() != null) {
// Called from another app

View File

@ -4,21 +4,22 @@ 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;
@ -27,26 +28,41 @@ import org.schabi.newpipe.util.ThemeHelper;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
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 - 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)
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)
};
/**
@ -65,7 +81,7 @@ 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);
@ -88,10 +104,8 @@ public class AboutActivity extends AppCompatActivity {
tabLayout.setupWithViewPager(mViewPager);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
public boolean onOptionsItemSelected(final MenuItem item) {
int id = item.getItemId();
switch (id) {
@ -107,21 +121,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();
@ -129,40 +142,42 @@ 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 ->
openWebsite(context.getString(R.string.github_url), context));
View donationLink = rootView.findViewById(R.id.donation_link);
donationLink.setOnClickListener(v -> openWebsite(context.getString(R.string.donation_url), context));
donationLink.setOnClickListener(v ->
openWebsite(context.getString(R.string.donation_url), context));
View websiteLink = rootView.findViewById(R.id.website_link);
websiteLink.setOnClickListener(nv -> openWebsite(context.getString(R.string.website_url), context));
websiteLink.setOnClickListener(nv ->
openWebsite(context.getString(R.string.website_url), context));
View privacyPolicyLink = rootView.findViewById(R.id.privacy_policy_link);
privacyPolicyLink.setOnClickListener(v -> openWebsite(context.getString(R.string.privacy_policy_url), context));
privacyPolicyLink.setOnClickListener(v ->
openWebsite(context.getString(R.string.privacy_policy_url), context));
return rootView;
}
private void openWebsite(String url, Context context) {
private void openWebsite(final String url, final 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();
@ -179,7 +194,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

@ -5,26 +5,32 @@ 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 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;
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,23 +41,25 @@ 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) {
public 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) {
public int compare(final SoftwareComponent o1, final SoftwareComponent o2) {
return o1.getName().compareTo(o2.getName());
}
});
@ -59,7 +67,8 @@ public class LicenseFragment extends Fragment {
@Nullable
@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) {
View rootView = inflater.inflate(R.layout.fragment_licenses, container, false);
ViewGroup softwareComponentsView = rootView.findViewById(R.id.software_components);
@ -67,7 +76,8 @@ public class LicenseFragment extends Fragment {
licenseLink.setOnClickListener(new OnReadFullLicenseClickListener());
for (final SoftwareComponent component : softwareComponents) {
View componentView = inflater.inflate(R.layout.item_software_component, container, false);
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);
softwareName.setText(component.getName());
@ -79,7 +89,7 @@ public class LicenseFragment extends Fragment {
componentView.setTag(component);
componentView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
public void onClick(final View v) {
Context context = v.getContext();
if (context != null) {
showLicense(context, component.getLicense());
@ -93,7 +103,8 @@ public class LicenseFragment extends Fragment {
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
public void onCreateContextMenu(final ContextMenu menu, final View v,
final ContextMenu.ContextMenuInfo menuInfo) {
MenuInflater inflater = getActivity().getMenuInflater();
SoftwareComponent component = (SoftwareComponent) v.getTag();
menu.setHeaderTitle(component.getName());
@ -103,7 +114,7 @@ public class LicenseFragment extends Fragment {
}
@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;
if (component == null) {
@ -119,14 +130,14 @@ public class LicenseFragment extends Fragment {
return false;
}
private void openWebsite(String componentLink) {
private void openWebsite(final 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) {
public void onClick(final View v) {
LicenseFragment.showLicense(v.getContext(), StandardLicenses.GPL3);
}
}

View File

@ -2,30 +2,103 @@ package org.schabi.newpipe.about;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Resources;
import android.os.AsyncTask;
import android.webkit.WebView;
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.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);
}
private static String getFinishString(final Activity activity) {
return activity.getApplicationContext().getResources().getString(R.string.finish);
}
/**
* @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(final Context context, final 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()), 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
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(final 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(final Context context, final int color) {
return context.getResources().getString(color).substring(3);
}
@Nullable
private Activity getActivity() {
Activity activity = weakReference.get();
@ -38,13 +111,13 @@ 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) {
protected void onPostExecute(final Integer result) {
Activity activity = getActivity();
if (activity == null) {
return;
@ -63,74 +136,4 @@ public class LicenseFragmentHelper extends AsyncTask<Object, Void, Integer> {
alert.show();
}
private static String getFinishString(Activity activity) {
return activity.getApplicationContext().getResources().getString(R.string.finish);
}
/**
* @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

@ -46,14 +46,20 @@ public abstract class AppDatabase extends RoomDatabase {
public abstract SearchHistoryDAO searchHistoryDAO();
public abstract StreamDAO streamDAO();
public abstract StreamHistoryDAO streamHistoryDAO();
public abstract StreamStateDAO streamStateDAO();
public abstract PlaylistDAO playlistDAO();
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

@ -7,47 +7,52 @@ 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(FeedGroupIcon feedGroupIcon) {
public static Integer integerOf(final FeedGroupIcon feedGroupIcon) {
return feedGroupIcon.getId();
}
@TypeConverter
public static FeedGroupIcon feedGroupIconOf(Integer id) {
public static FeedGroupIcon feedGroupIconOf(final Integer id) {
for (FeedGroupIcon icon : FeedGroupIcon.values()) {
if (icon.getId() == id) return icon;
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,72 +1,103 @@
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 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_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");
}
}
@ -74,37 +105,60 @@ public class Migrations {
public static final Migration MIGRATION_2_3 = new Migration(DB_VER_2, DB_VER_3) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
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("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) " +
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 " +
+ "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");
+ "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)");
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 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 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)");
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

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

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

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

@ -80,7 +80,12 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
val isNewerStreamLive = newerStream.streamType == AUDIO_LIVE_STREAM || newerStream.streamType == LIVE_STREAM
if (!isNewerStreamLive) {
if (existentMinimalStream.uploadDate != null && existentMinimalStream.isUploadDateApproximation != true) {
// 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

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

@ -90,7 +90,8 @@ data class StreamEntity(
if (viewCount != null) item.viewCount = viewCount as Long
item.textualUploadDate = textualUploadDate
item.uploadDate = uploadDate?.let {
DateWrapper(Calendar.getInstance().apply { time = it }, isUploadDateApproximation ?: false)
DateWrapper(Calendar.getInstance().apply { time = it }, isUploadDateApproximation
?: false)
}
return item

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

@ -48,7 +48,7 @@ abstract class SubscriptionDAO : BasicDAO<SubscriptionEntity> {
entity.uid = uidFromInsert
} else {
val subscriptionIdFromDb = getSubscriptionIdInternal(entity.serviceId, entity.url)
?: throw IllegalStateException("Subscription cannot be null just after insertion.")
?: throw IllegalStateException("Subscription cannot be null just after insertion.")
entity.uid = subscriptionIdFromDb
update(entity)

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 {
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";
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
@ -124,13 +130,4 @@ public class SubscriptionEntity {
item.setDescription(getDescription());
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());
return result;
}
}

View File

@ -3,17 +3,19 @@ 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.util.NavigationHelper;
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;
@ -25,7 +27,7 @@ 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);
@ -46,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() {
@ -65,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();
@ -75,7 +82,7 @@ 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:
onBackPressed();

View File

@ -81,25 +81,33 @@ import us.shandian.giga.service.MissionState;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener {
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;
@ -115,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);
@ -135,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;
}
@ -199,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);
@ -218,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();
@ -232,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());
@ -272,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();
@ -295,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();
}
@ -318,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) {
@ -336,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;
}
@ -347,27 +386,27 @@ 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 + "]");
private void initToolbar(final Toolbar toolbar) {
if (DEBUG) {
Log.d(TAG, "initToolbar() called with: toolbar = [" + toolbar + "]");
}
boolean isLight = ThemeHelper.isLightThemeSelected(getActivity());
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(isLight ? R.drawable.ic_arrow_back_black_24dp
: R.drawable.ic_arrow_back_white_24dp);
toolbar.inflateMenu(R.menu.dialog_url);
toolbar.setNavigationOnClickListener(v -> getDialog().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) {
@ -381,8 +420,14 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
});
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
private void setupAudioSpinner() {
if (getContext() == null) return;
if (getContext() == null) {
return;
}
streamsSpinner.setAdapter(audioStreamsAdapter);
streamsSpinner.setSelection(selectedAudioIndex);
@ -390,7 +435,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);
@ -398,21 +445,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) {
@ -431,14 +478,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;
@ -453,13 +500,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);
@ -484,30 +527,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;
}
@ -516,20 +565,13 @@ 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)
@ -539,13 +581,14 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
.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)
);
}
@ -563,7 +606,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
case R.id.audio_button:
mainStorage = mainStorageAudio;
format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
switch(format) {
switch (format) {
case WEBMA_OPUS:
mime = "audio/ogg";
filename += "opus";
@ -581,7 +624,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;
@ -596,23 +639,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;
@ -622,7 +667,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 {
@ -631,10 +678,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);
@ -738,24 +787,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;
}
});
@ -763,7 +816,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;
@ -771,7 +824,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);
@ -811,13 +866,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
@ -827,7 +884,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);
@ -835,7 +892,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" // ignore empty frames
};
}
break;
@ -854,14 +911,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,6 +10,9 @@ 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;
@ -18,13 +20,13 @@ 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;
@ -36,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();
}
@ -62,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);
@ -105,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);
}
@ -117,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();
}
@ -161,21 +182,28 @@ 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;
}
@ -185,16 +213,21 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
} else if (exception instanceof ContentNotAvailableException) {
showError(getString(R.string.content_not_available), false);
return true;
} else if (exception instanceof IOException) {
} 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);
@ -204,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

@ -50,14 +50,15 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
//////////////////////////////////////////////////////////////////////////*/
@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();
@ -68,12 +69,14 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
}
@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);
@ -89,14 +92,18 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
public void onResume() {
super.onResume();
if (hasTabsChanged) setupTabs();
if (hasTabsChanged) {
setupTabs();
}
}
@Override
public void onDestroy() {
super.onDestroy();
tabsManager.unsetSavedTabsListener();
if (viewPager != null) viewPager.setAdapter(null);
if (viewPager != null) {
viewPager.setAdapter(null);
}
}
/*//////////////////////////////////////////////////////////////////////////
@ -104,9 +111,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();
@ -116,7 +126,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 {
@ -141,7 +151,8 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
tabsList.addAll(tabsManager.getTabs());
if (pagerAdapter == null || !pagerAdapter.sameTabs(tabsList)) {
pagerAdapter = new SelectedTabsPagerAdapter(requireContext(), getChildFragmentManager(), tabsList);
pagerAdapter = new SelectedTabsPagerAdapter(requireContext(),
getChildFragmentManager(), tabsList);
}
viewPager.setAdapter(null);
@ -165,31 +176,37 @@ 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 FragmentStatePagerAdapterMenuWorkaround {
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);
@ -197,7 +214,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
@NonNull
@Override
public Fragment getItem(int position) {
public Fragment getItem(final int position) {
final Tab tab = internalTabsList.get(position);
Throwable throwable = null;
@ -209,8 +226,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();
}
@ -222,7 +239,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;
@ -233,7 +250,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

@ -4,19 +4,15 @@ import java.io.Serializable;
class StackItem implements Serializable {
private final int serviceId;
private String title;
private final String url;
private String title;
StackItem(int serviceId, String url, String title) {
StackItem(final int serviceId, final String url, final String title) {
this.serviceId = serviceId;
this.url = url;
this.title = title;
}
public void setTitle(String title) {
this.title = title;
}
public int getServiceId() {
return serviceId;
}
@ -25,6 +21,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

@ -8,20 +8,9 @@ import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.tabs.TabLayout;
import androidx.fragment.app.Fragment;
import androidx.core.content.ContextCompat;
import androidx.viewpager.widget.ViewPager;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
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;
@ -41,6 +30,17 @@ import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.viewpager.widget.ViewPager;
import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.tabs.TabLayout;
import com.nostra13.universalimageloader.core.assist.FailReason;
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
@ -52,7 +52,6 @@ import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.ServiceList;
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;
@ -73,6 +72,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.report.ErrorActivity;
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.ImageDisplayConstants;
@ -86,6 +86,7 @@ import org.schabi.newpipe.util.ShareUtils;
import org.schabi.newpipe.util.StreamItemAdapter;
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
import org.schabi.newpipe.views.AnimatedProgressBar;
import org.schabi.newpipe.views.LargeTextMovementMethod;
import java.io.Serializable;
import java.util.Collection;
@ -103,14 +104,12 @@ import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS;
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public class VideoDetailFragment
extends BaseStateFragment<StreamInfo>
implements BackPressable,
SharedPreferences.OnSharedPreferenceChangeListener,
View.OnClickListener,
View.OnLongClickListener {
public class VideoDetailFragment extends BaseStateFragment<StreamInfo>
implements BackPressable, SharedPreferences.OnSharedPreferenceChangeListener,
View.OnClickListener, View.OnLongClickListener {
public static final String AUTO_PLAY = "auto_play";
private int updateFlags = 0;
@ -183,32 +182,41 @@ public class VideoDetailFragment
private ImageView thumbsDownImageView;
private TextView thumbsDisabledTextView;
private static final String COMMENTS_TAB_TAG = "COMMENTS";
private static final String RELATED_TAB_TAG = "NEXT VIDEO";
private static final String EMPTY_TAB_TAG = "EMPTY TAB";
private AppBarLayout appBarLayout;
private ViewPager viewPager;
private ViewPager viewPager;
private TabAdaptor pageAdapter;
private TabLayout tabLayout;
private FrameLayout relatedStreamsLayout;
/*////////////////////////////////////////////////////////////////////////*/
public static VideoDetailFragment getInstance(int serviceId, String videoUrl, String name) {
private static final String COMMENTS_TAB_TAG = "COMMENTS";
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";
/**
* Stack that contains the "navigation history".<br>
* The peek is the current video.
*/
private final LinkedList<StackItem> stack = new LinkedList<>();
public static VideoDetailFragment getInstance(final int serviceId, final String videoUrl,
final String name) {
VideoDetailFragment instance = new VideoDetailFragment();
instance.setInitialData(serviceId, videoUrl, name);
return instance;
}
/*//////////////////////////////////////////////////////////////////////////
// Fragment's Lifecycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void
onCreate(Bundle savedInstanceState) {
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
@ -226,17 +234,21 @@ 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();
}
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();
}
@ -246,9 +258,15 @@ public class VideoDetailFragment
if (updateFlags != 0) {
if (!isLoading.get() && currentInfo != null) {
if ((updateFlags & RELATED_STREAMS_UPDATE_FLAG) != 0) startLoading(false);
if ((updateFlags & RESOLUTIONS_MENU_UPDATE_FLAG) != 0) setupActionBar(currentInfo);
if ((updateFlags & COMMENTS_UPDATE_FLAG) != 0) startLoading(false);
if ((updateFlags & RELATED_STREAMS_UPDATE_FLAG) != 0) {
startLoading(false);
}
if ((updateFlags & RESOLUTIONS_MENU_UPDATE_FLAG) != 0) {
setupActionBar(currentInfo);
}
if ((updateFlags & COMMENTS_UPDATE_FLAG) != 0) {
startLoading(false);
}
}
if ((updateFlags & TOOLBAR_ITEMS_UPDATE_FLAG) != 0
@ -273,9 +291,15 @@ public class VideoDetailFragment
PreferenceManager.getDefaultSharedPreferences(activity)
.unregisterOnSharedPreferenceChangeListener(this);
if (positionSubscriber != null) positionSubscriber.dispose();
if (currentWorker != null) currentWorker.dispose();
if (disposables != null) disposables.clear();
if (positionSubscriber != null) {
positionSubscriber.dispose();
}
if (currentWorker != null) {
currentWorker.dispose();
}
if (disposables != null) {
disposables.clear();
}
positionSubscriber = null;
currentWorker = null;
disposables = null;
@ -283,20 +307,25 @@ public class VideoDetailFragment
@Override
public void onDestroyView() {
if (DEBUG) Log.d(TAG, "onDestroyView() called");
if (DEBUG) {
Log.d(TAG, "onDestroyView() called");
}
spinnerToolbar.setOnItemSelectedListener(null);
spinnerToolbar.setAdapter(null);
super.onDestroyView();
}
@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 + "]");
@ -305,7 +334,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;
@ -326,11 +356,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,
@ -345,7 +372,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);
@ -360,7 +387,6 @@ public class VideoDetailFragment
//noinspection unchecked
stack.addAll((Collection<? extends StackItem>) serializable);
}
}
/*//////////////////////////////////////////////////////////////////////////
@ -368,8 +394,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:
@ -395,14 +423,14 @@ public class VideoDetailFragment
Log.w(TAG, "Can't open channel because we got no channel URL");
} else {
try {
NavigationHelper.openChannelFragment(
getFragmentManager(),
currentInfo.getServiceId(),
currentInfo.getUploaderUrl(),
currentInfo.getUploaderName());
NavigationHelper.openChannelFragment(
getFragmentManager(),
currentInfo.getServiceId(),
currentInfo.getUploaderUrl(),
currentInfo.getUploaderName());
} catch (Exception e) {
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
}
}
}
break;
case R.id.detail_thumbnail_root_layout:
@ -420,8 +448,10 @@ public class VideoDetailFragment
}
@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:
@ -442,10 +472,13 @@ public class VideoDetailFragment
if (videoDescriptionRootLayout.getVisibility() == View.VISIBLE) {
videoTitleTextView.setMaxLines(1);
videoDescriptionRootLayout.setVisibility(View.GONE);
videoDescriptionView.setFocusable(false);
videoTitleToggleArrow.setImageResource(R.drawable.arrow_down);
} else {
videoTitleTextView.setMaxLines(10);
videoDescriptionRootLayout.setVisibility(View.VISIBLE);
videoDescriptionView.setFocusable(true);
videoDescriptionView.setMovementMethod(new LargeTextMovementMethod());
videoTitleToggleArrow.setImageResource(R.drawable.arrow_up);
}
}
@ -455,7 +488,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);
spinnerToolbar = activity.findViewById(R.id.toolbar).findViewById(R.id.toolbar_spinner);
@ -482,7 +515,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());
thumbsUpTextView = rootView.findViewById(R.id.detail_thumbs_up_count_view);
thumbsUpImageView = rootView.findViewById(R.id.detail_thumbs_up_img_view);
@ -505,6 +537,16 @@ public class VideoDetailFragment
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);
}
}
@ -544,41 +586,41 @@ 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 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,
IMAGE_LOADER.displayImage(info.getThumbnailUrl(), thumbnailImageView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, onFailListener);
}
if (!TextUtils.isEmpty(info.getUploaderAvatarUrl())) {
imageLoader.displayImage(info.getUploaderAvatarUrl(), uploaderThumb,
IMAGE_LOADER.displayImage(info.getUploaderAvatarUrl(), uploaderThumb,
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
}
}
/*//////////////////////////////////////////////////////////////////////////
// Menu
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
this.menu = menu;
public void onCreateOptionsMenu(final Menu m, final MenuInflater inflater) {
this.menu = m;
// CAUTION set item properties programmatically otherwise it would not be accepted by
// appcompat itemsinflater.inflate(R.menu.videoitem_detail, menu);
inflater.inflate(R.menu.video_detail_menu, menu);
inflater.inflate(R.menu.video_detail_menu, m);
updateMenuItemVisibility();
@ -590,7 +632,6 @@ public class VideoDetailFragment
}
private void updateMenuItemVisibility() {
// show kodi if set in settings
menu.findItem(R.id.action_play_with_kodi).setVisible(
PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(
@ -598,7 +639,7 @@ public class VideoDetailFragment
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
public boolean onOptionsItemSelected(final MenuItem item) {
int id = item.getItemId();
if (id == R.id.action_settings) {
NavigationHelper.openSettings(requireContext());
@ -611,24 +652,25 @@ public class VideoDetailFragment
}
switch (id) {
case R.id.menu_item_share: {
case R.id.menu_item_share:
if (currentInfo != null) {
ShareUtils.shareUrl(requireContext(), currentInfo.getName(), currentInfo.getOriginalUrl());
ShareUtils.shareUrl(requireContext(), currentInfo.getName(),
currentInfo.getOriginalUrl());
}
return true;
}
case R.id.menu_item_openInBrowser: {
case R.id.menu_item_openInBrowser:
if (currentInfo != null) {
ShareUtils.openUrlInBrowser(requireContext(), currentInfo.getOriginalUrl());
}
return true;
}
case R.id.action_play_with_kodi:
try {
NavigationHelper.playWithKore(activity, Uri.parse(
url.replace("https", "http")));
} catch (Exception e) {
if (DEBUG) Log.i(TAG, "Failed to start kore", e);
if (DEBUG) {
Log.i(TAG, "Failed to start kore", e);
}
KoreUtil.showInstallKoreDialog(activity);
}
return true;
@ -637,37 +679,39 @@ public class VideoDetailFragment
}
}
private void setupActionBarOnError(final String url) {
if (DEBUG) Log.d(TAG, "setupActionBarHandlerOnError() called with: url = [" + url + "]");
private void setupActionBarOnError(final String u) {
if (DEBUG) {
Log.d(TAG, "setupActionBarHandlerOnError() called with: url = [" + u + "]");
}
Log.e("-----", "missing code");
}
private void setupActionBar(final StreamInfo info) {
if (DEBUG) Log.d(TAG, "setupActionBarHandler() called with: info = [" + info + "]");
if (DEBUG) {
Log.d(TAG, "setupActionBarHandler() called with: info = [" + info + "]");
}
boolean isExternalPlayerEnabled = PreferenceManager.getDefaultSharedPreferences(activity)
.getBoolean(activity.getString(R.string.use_external_video_player_key), false);
sortedVideoStreams = ListHelper.getSortedStreamVideosList(
activity,
info.getVideoStreams(),
info.getVideoOnlyStreams(),
false);
selectedVideoStreamIndex = ListHelper.getDefaultResolutionIndex(activity, sortedVideoStreams);
sortedVideoStreams = ListHelper.getSortedStreamVideosList(activity, info.getVideoStreams(),
info.getVideoOnlyStreams(), false);
selectedVideoStreamIndex = ListHelper
.getDefaultResolutionIndex(activity, sortedVideoStreams);
final StreamItemAdapter<VideoStream, Stream> streamsAdapter =
new StreamItemAdapter<>(activity,
new StreamSizeWrapper<>(sortedVideoStreams, activity), isExternalPlayerEnabled);
final StreamItemAdapter<VideoStream, Stream> streamsAdapter = new StreamItemAdapter<>(
activity, new StreamSizeWrapper<>(sortedVideoStreams, activity),
isExternalPlayerEnabled);
spinnerToolbar.setAdapter(streamsAdapter);
spinnerToolbar.setSelection(selectedVideoStreamIndex);
spinnerToolbar.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) {
selectedVideoStreamIndex = position;
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
public void onNothingSelected(final AdapterView<?> parent) { }
});
}
@ -675,37 +719,31 @@ public class VideoDetailFragment
// OwnStack
//////////////////////////////////////////////////////////////////////////*/
/**
* Stack that contains the "navigation history".<br>
* The peek is the current video.
*/
protected final LinkedList<StackItem> stack = new LinkedList<>();
public void pushToStack(int serviceId, String videoUrl, String name) {
private void pushToStack(final int sid, final String videoUrl, final String title) {
if (DEBUG) {
Log.d(TAG, "pushToStack() called with: serviceId = ["
+ serviceId + "], videoUrl = [" + videoUrl + "], name = [" + name + "]");
+ sid + "], videoUrl = [" + videoUrl + "], title = [" + title + "]");
}
if (stack.size() > 0
&& stack.peek().getServiceId() == serviceId
&& stack.peek().getServiceId() == sid
&& stack.peek().getUrl().equals(videoUrl)) {
Log.d(TAG, "pushToStack() called with: serviceId == peek.serviceId = ["
+ serviceId + "], videoUrl == peek.getUrl = [" + videoUrl + "]");
+ sid + "], videoUrl == peek.getUrl = [" + videoUrl + "]");
return;
} else {
Log.d(TAG, "pushToStack() wasn't equal");
}
stack.push(new StackItem(serviceId, videoUrl, name));
stack.push(new StackItem(sid, videoUrl, title));
}
public void setTitleToUrl(int serviceId, String videoUrl, String name) {
if (name != null && !name.isEmpty()) {
private void setTitleToUrl(final int sid, final String videoUrl, final String title) {
if (title != null && !title.isEmpty()) {
for (StackItem stackItem : stack) {
if (stack.peek().getServiceId() == serviceId
if (stack.peek().getServiceId() == sid
&& stackItem.getUrl().equals(videoUrl)) {
stackItem.setTitle(name);
stackItem.setTitle(title);
}
}
}
@ -713,20 +751,21 @@ public class VideoDetailFragment
@Override
public boolean onBackPressed() {
if (DEBUG) Log.d(TAG, "onBackPressed() called");
if (DEBUG) {
Log.d(TAG, "onBackPressed() called");
}
// That means that we are on the start of the stack,
// return false to let the MainActivity handle the onBack
if (stack.size() <= 1) return false;
if (stack.size() <= 1) {
return false;
}
// Remove top
stack.pop();
// Get stack item from the new top
StackItem peek = stack.peek();
selectAndLoadVideo(peek.getServiceId(),
peek.getUrl(),
!TextUtils.isEmpty(peek.getTitle())
? peek.getTitle()
: "");
selectAndLoadVideo(peek.getServiceId(), peek.getUrl(),
!TextUtils.isEmpty(peek.getTitle()) ? peek.getTitle() : "");
return true;
}
@ -736,57 +775,72 @@ public class VideoDetailFragment
@Override
protected void doInitialLoadLogic() {
if (currentInfo == null) prepareAndLoadInfo();
else prepareAndHandleInfo(currentInfo, false);
if (currentInfo == null) {
prepareAndLoadInfo();
} else {
prepareAndHandleInfo(currentInfo, false);
}
}
public void selectAndLoadVideo(int serviceId, String videoUrl, String name) {
setInitialData(serviceId, videoUrl, name);
public void selectAndLoadVideo(final int sid, final String videoUrl, final String title) {
setInitialData(sid, videoUrl, title);
prepareAndLoadInfo();
}
public void prepareAndHandleInfo(final StreamInfo info, 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 + "]");
}
setInitialData(info.getServiceId(), info.getUrl(), info.getName());
pushToStack(serviceId, url, name);
showLoading();
initTabs();
if (scrollToTop) appBarLayout.setExpanded(true, true);
if (scrollToTop) {
appBarLayout.setExpanded(true, true);
}
handleResult(info);
showContent();
}
protected void prepareAndLoadInfo() {
private void prepareAndLoadInfo() {
appBarLayout.setExpanded(true, true);
pushToStack(serviceId, url, name);
startLoading(false);
}
@Override
public void startLoading(boolean forceLoad) {
public void startLoading(final boolean forceLoad) {
super.startLoading(forceLoad);
initTabs();
currentInfo = null;
if (currentWorker != null) currentWorker.dispose();
if (currentWorker != null) {
currentWorker.dispose();
}
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);
currentInfo = result;
handleResult(result);
showContent();
}, (@NonNull Throwable throwable) -> {
if (result.getAgeLimit() != NO_AGE_LIMIT && !prefs.getBoolean(
getString(R.string.show_age_restricted_content), false)) {
hideAgeRestrictedContent();
} else {
currentInfo = result;
handleResult(result);
showContent();
}
}, (@NonNull final Throwable throwable) -> {
isLoading.set(false);
onError(throwable);
});
}
private void initTabs() {
@ -795,26 +849,29 @@ public class VideoDetailFragment
}
pageAdapter.clearAllItems();
if(shouldShowComments()){
pageAdapter.addFragment(CommentsFragment.getInstance(serviceId, url, name), COMMENTS_TAB_TAG);
if (shouldShowComments()) {
pageAdapter.addFragment(CommentsFragment.getInstance(serviceId, url, name),
COMMENTS_TAB_TAG);
}
if(showRelatedStreams && null == relatedStreamsLayout){
if (showRelatedStreams && null == relatedStreamsLayout) {
//temp empty fragment. will be updated in handleResult
pageAdapter.addFragment(new Fragment(), RELATED_TAB_TAG);
}
if(pageAdapter.getCount() == 0){
if (pageAdapter.getCount() == 0) {
pageAdapter.addFragment(new EmptyFragment(), EMPTY_TAB_TAG);
}
pageAdapter.notifyDataSetUpdate();
if(pageAdapter.getCount() < 2){
if (pageAdapter.getCount() < 2) {
tabLayout.setVisibility(View.GONE);
}else{
} else {
int position = pageAdapter.getItemPositionByTitle(selectedTabTag);
if(position != -1) viewPager.setCurrentItem(position);
if (position != -1) {
viewPager.setCurrentItem(position);
}
tabLayout.setVisibility(View.VISIBLE);
}
}
@ -859,9 +916,8 @@ public class VideoDetailFragment
NavigationHelper.enqueueOnPopupPlayer(activity, itemQueue, false);
} else {
Toast.makeText(activity, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show();
final Intent intent = NavigationHelper.getPlayerIntent(
activity, PopupVideoPlayer.class, itemQueue, getSelectedVideoStream().resolution, true
);
final Intent intent = NavigationHelper.getPlayerIntent(activity,
PopupVideoPlayer.class, itemQueue, getSelectedVideoStream().resolution, true);
activity.startService(intent);
}
}
@ -900,7 +956,7 @@ public class VideoDetailFragment
// Utils
//////////////////////////////////////////////////////////////////////////*/
public void setAutoplay(boolean autoplay) {
public void setAutoplay(final boolean autoplay) {
this.autoPlayEnabled = autoplay;
}
@ -913,7 +969,7 @@ public class VideoDetailFragment
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)
));
}
@ -923,8 +979,9 @@ public class VideoDetailFragment
return sortedVideoStreams != null ? sortedVideoStreams.get(selectedVideoStreamIndex) : null;
}
private void prepareDescription(Description description) {
if (TextUtils.isEmpty(description.getContent()) || description == Description.emptyDescription) {
private void prepareDescription(final Description description) {
if (TextUtils.isEmpty(description.getContent())
|| description == Description.emptyDescription) {
return;
}
@ -975,14 +1032,16 @@ public class VideoDetailFragment
contentRootLayoutHiding.setVisibility(View.VISIBLE);
}
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 : "";
}
private void setErrorImage(final int imageResource) {
if (thumbnailImageView == null || activity == null) return;
if (thumbnailImageView == null || activity == null) {
return;
}
thumbnailImageView.setImageDrawable(ContextCompat.getDrawable(activity, imageResource));
animateView(thumbnailImageView, false, 0, 0,
@ -990,11 +1049,12 @@ public class VideoDetailFragment
}
@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);
}
@ -1009,7 +1069,7 @@ public class VideoDetailFragment
super.showLoading();
//if data is already cached, transition from VISIBLE -> INVISIBLE -> VISIBLE is not required
if(!ExtractorHelper.isCached(serviceId, url, InfoItem.InfoType.STREAM)){
if (!ExtractorHelper.isCached(serviceId, url, InfoItem.InfoType.STREAM)) {
contentRootLayoutHiding.setVisibility(View.INVISIBLE);
}
@ -1028,33 +1088,35 @@ public class VideoDetailFragment
videoTitleToggleArrow.setVisibility(View.GONE);
videoTitleRoot.setClickable(false);
if(relatedStreamsLayout != null){
if(showRelatedStreams){
if (relatedStreamsLayout != null) {
if (showRelatedStreams) {
relatedStreamsLayout.setVisibility(View.INVISIBLE);
}else{
} else {
relatedStreamsLayout.setVisibility(View.GONE);
}
}
imageLoader.cancelDisplayTask(thumbnailImageView);
imageLoader.cancelDisplayTask(uploaderThumb);
IMAGE_LOADER.cancelDisplayTask(thumbnailImageView);
IMAGE_LOADER.cancelDisplayTask(uploaderThumb);
thumbnailImageView.setImageBitmap(null);
uploaderThumb.setImageBitmap(null);
}
@Override
public void handleResult(@NonNull StreamInfo info) {
public void handleResult(@NonNull final StreamInfo info) {
super.handleResult(info);
setInitialData(info.getServiceId(), info.getOriginalUrl(), info.getName());
if(showRelatedStreams){
if(null == relatedStreamsLayout){ //phone
pageAdapter.updateItem(RELATED_TAB_TAG, RelatedVideosFragment.getInstance(currentInfo));
if (showRelatedStreams) {
if (null == relatedStreamsLayout) { //phone
pageAdapter.updateItem(RELATED_TAB_TAG,
RelatedVideosFragment.getInstance(currentInfo));
pageAdapter.notifyDataSetUpdate();
}else{ //tablet
} else { //tablet
getChildFragmentManager().beginTransaction()
.replace(R.id.relatedStreamsLayout, RelatedVideosFragment.getInstance(currentInfo))
.replace(R.id.relatedStreamsLayout,
RelatedVideosFragment.getInstance(currentInfo))
.commitNow();
relatedStreamsLayout.setVisibility(View.VISIBLE);
}
@ -1078,9 +1140,11 @@ public class VideoDetailFragment
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.localizeWatchingCount(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 {
@ -1096,7 +1160,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 {
@ -1136,7 +1201,8 @@ public class VideoDetailFragment
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);
@ -1168,9 +1234,12 @@ public class VideoDetailFragment
spinnerToolbar.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);
spinnerToolbar.setVisibility(View.GONE);
@ -1185,30 +1254,40 @@ public class VideoDetailFragment
}
}
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);
}
public void openDownloadDialog() {
try {
DownloadDialog downloadDialog = DownloadDialog.newInstance(currentInfo);
downloadDialog.setVideoStreams(sortedVideoStreams);
downloadDialog.setAudioStreams(currentInfo.getAudioStreams());
downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex);
downloadDialog.setSubtitleStreams(currentInfo.getSubtitles());
try {
DownloadDialog downloadDialog = DownloadDialog.newInstance(currentInfo);
downloadDialog.setVideoStreams(sortedVideoStreams);
downloadDialog.setAudioStreams(currentInfo.getAudioStreams());
downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex);
downloadDialog.setSubtitleStreams(currentInfo.getSubtitles());
downloadDialog.show(getActivity().getSupportFragmentManager(), "downloadDialog");
} catch (Exception e) {
ErrorActivity.ErrorInfo info = ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR,
ServiceList.all()
.get(currentInfo
.getServiceId())
.getServiceInfo()
.getName(), "",
R.string.could_not_setup_download_menu);
downloadDialog.show(getActivity().getSupportFragmentManager(), "downloadDialog");
} catch (Exception e) {
ErrorActivity.ErrorInfo info = ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR,
ServiceList.all()
.get(currentInfo
.getServiceId())
.getServiceInfo()
.getName(), "",
R.string.could_not_setup_download_menu);
ErrorActivity.reportError(getActivity(),
e,
getActivity().getClass(),
getActivity().findViewById(android.R.id.content), info);
}
ErrorActivity.reportError(getActivity(),
e,
getActivity().getClass(),
getActivity().findViewById(android.R.id.content), info);
}
}
/*//////////////////////////////////////////////////////////////////////////
@ -1216,12 +1295,16 @@ public class VideoDetailFragment
//////////////////////////////////////////////////////////////////////////*/
@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 YoutubeStreamExtractor.DecryptException ? R.string.youtube_signature_decryption_error
: exception instanceof ExtractionException ? R.string.parsing_error
: R.string.general_error;
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);
@ -1234,9 +1317,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);
if (!playbackResumeEnabled || info.getDuration() <= 0) {
positionView.setVisibility(View.INVISIBLE);
@ -1244,8 +1327,8 @@ public class VideoDetailFragment
// 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)) {
if (!info.getStreamType().equals(StreamType.LIVE_STREAM)
&& !info.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) {
return;
}
}
@ -1258,14 +1341,17 @@ public class VideoDetailFragment
.onErrorComplete()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(state -> {
final int seconds = (int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime());
final int seconds
= (int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime());
positionView.setMax((int) info.getDuration());
positionView.setProgressAnimated(seconds);
detailPositionView.setText(Localization.getDurationString(seconds));
animateView(positionView, true, 500);
animateView(detailPositionView, true, 500);
}, e -> {
if (DEBUG) e.printStackTrace();
if (DEBUG) {
e.printStackTrace();
}
}, () -> {
animateView(positionView, false, 500);
animateView(detailPositionView, false, 500);

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,16 +56,14 @@ 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);
if (infoListAdapter == null) {
@ -71,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)
@ -81,7 +87,9 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
@Override
public void onDestroy() {
super.onDestroy();
if (useDefaultStateSaving) StateSaver.onDestroy(savedState);
if (useDefaultStateSaving) {
StateSaver.onDestroy(savedState);
}
PreferenceManager.getDefaultSharedPreferences(activity)
.unregisterOnSharedPreferenceChangeListener(this);
}
@ -93,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;
@ -105,16 +114,14 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
// State Saving
//////////////////////////////////////////////////////////////////////////*/
protected StateSaver.SavedState savedState;
protected boolean useDefaultStateSaving = true;
/**
* If the default implementation of {@link StateSaver.WriteRead} should be used.
*
* @see StateSaver
* @param useDefaultStateSaving Whether the default implementation should be used
*/
public void useDefaultStateSaving(boolean useDefault) {
this.useDefaultStateSaving = useDefault;
public void setUseDefaultStateSaving(final boolean useDefaultStateSaving) {
this.useDefaultStateSaving = useDefaultStateSaving;
}
@Override
@ -123,30 +130,81 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
return "." + infoListAdapter.getItemsList().size() + ".list";
}
@Override
public void writeTo(Queue<Object> objectsToSave) {
if (useDefaultStateSaving) objectsToSave.add(infoListAdapter.getItemsList());
}
@Override
@SuppressWarnings("unchecked")
public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception {
if (useDefaultStateSaving) {
infoListAdapter.getItemsList().clear();
infoListAdapter.getItemsList().addAll((List<InfoItem>) savedObjects.poll());
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 onSaveInstanceState(Bundle bundle) {
super.onSaveInstanceState(bundle);
if (useDefaultStateSaving) savedState = StateSaver.tryToSave(activity.isChangingConfigurations(), savedState, bundle, this);
public void writeTo(final Queue<Object> objectsToSave) {
if (!useDefaultStateSaving) {
return;
}
objectsToSave.add(infoListAdapter.getItemsList());
objectsToSave.add(getFocusedPosition());
}
@Override
protected void onRestoreInstanceState(@NonNull Bundle bundle) {
@SuppressWarnings("unchecked")
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(final Bundle bundle) {
super.onSaveInstanceState(bundle);
if (useDefaultStateSaving) {
savedState = StateSaver
.tryToSave(activity.isChangingConfigurations(), savedState, bundle, this);
}
}
@Override
protected void onRestoreInstanceState(@NonNull final Bundle bundle) {
super.onRestoreInstanceState(bundle);
if (useDefaultStateSaving) 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);
}
/*//////////////////////////////////////////////////////////////////////////
@ -162,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
@ -199,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(),
@ -226,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(),
@ -241,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);
}
});
@ -249,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());
@ -268,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(
@ -291,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();
}
/*//////////////////////////////////////////////////////////////////////////
@ -300,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) {
@ -339,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);
@ -361,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

@ -10,6 +10,7 @@ import androidx.annotation.NonNull;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.ListInfo;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.views.NewPipeRecyclerView;
import java.util.Queue;
@ -21,7 +22,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
@ -34,7 +34,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
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 +43,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,7 +75,7 @@ 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);
@ -81,7 +83,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
@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();
@ -92,10 +94,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,18 +109,21 @@ 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())
@ -127,19 +136,29 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
}
/**
* 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,8 +167,20 @@ 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();
infoListAdapter.addInfoItemList(result.getItems());
@ -167,7 +198,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 +219,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,11 @@ 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.core.content.ContextCompat;
import com.jakewharton.rxbinding2.view.RxView;
import org.schabi.newpipe.R;
@ -29,7 +31,7 @@ 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;
@ -45,6 +47,7 @@ import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ShareUtils;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.TimeUnit;
@ -63,15 +66,15 @@ import static org.schabi.newpipe.util.AnimationUtils.animateTextColor;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
private final CompositeDisposable disposables = new CompositeDisposable();
private Disposable subscribeButtonMonitor;
private SubscriptionManager subscriptionManager;
/*//////////////////////////////////////////////////////////////////////////
// Views
//////////////////////////////////////////////////////////////////////////*/
private SubscriptionManager subscriptionManager;
private View headerRootLayout;
private ImageView headerChannelBanner;
private ImageView headerAvatarView;
@ -79,25 +82,23 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
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 +107,40 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
}
}
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onAttach(Context context) {
public void onAttach(final Context context) {
super.onAttach(context);
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,7 +148,8 @@ 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);
@ -150,7 +170,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
//////////////////////////////////////////////////////////////////////////*/
@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 +178,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,7 +195,7 @@ 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());
@ -201,18 +223,16 @@ 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 = subscriptionManager.subscriptionTable()
final Observable<List<SubscriptionEntity>> observable = subscriptionManager
.subscriptionTable()
.getSubscriptionFlowable(info.getServiceId(), info.getUrl())
.toObservable();
@ -221,17 +241,19 @@ 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, ChannelInfo info) {
private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription,
final ChannelInfo info) {
return (@NonNull Object o) -> {
subscriptionManager.insertSubscription(subscription, info);
return o;
@ -246,9 +268,13 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
}
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) ->
@ -264,9 +290,12 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
.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) ->
@ -287,12 +316,18 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
private Consumer<List<SubscriptionEntity>> getSubscribeUpdateMonitor(final ChannelInfo info) {
return (List<SubscriptionEntity> subscriptionEntities) -> {
if (DEBUG)
Log.d(TAG, "subscriptionManager.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());
@ -300,34 +335,45 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
info.getAvatarUrl(),
info.getDescription(),
info.getSubscriberCount());
subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnSubscribe(channel, info));
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 = ContextCompat
.getColor(activity, R.color.subscribe_background_color);
int subscribeText = ContextCompat.getColor(activity, R.color.subscribe_text_color);
int subscribedBackground = ContextCompat.getColor(activity, R.color.subscribed_background_color);
int 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);
}
@ -344,7 +390,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
}
@Override
protected Single<ChannelInfo> loadResult(boolean forceLoad) {
protected Single<ChannelInfo> loadResult(final boolean forceLoad) {
return ExtractorHelper.getChannelInfo(serviceId, url, forceLoad);
}
@ -356,47 +402,86 @@ 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);
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);
headerSubscribersTextView.setVisibility(View.VISIBLE);
if (result.getSubscriberCount() >= 0) {
headerSubscribersTextView.setText(Localization.shortSubscriberCount(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 (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(), false));
headerPopupButton.setOnClickListener(
view -> NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
headerBackgroundButton.setOnClickListener(
view -> NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
headerPlayAllButton.setOnClickListener(view -> NavigationHelper
.playOnMainPlayer(activity, getPlayQueue(), false));
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() {
@ -410,17 +495,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.getNextPageUrl(), streamItems, index);
}
@Override
public void handleNextItems(ListExtractor.InfoItemsPage result) {
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
super.handleNextItems(result);
if (!result.getErrors().isEmpty()) {
@ -437,8 +517,10 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
//////////////////////////////////////////////////////////////////////////*/
@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;
@ -454,8 +536,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,28 +40,31 @@ 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
//////////////////////////////////////////////////////////////////////////*/
@ -76,7 +75,7 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
}
@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();
}
@ -45,7 +46,8 @@ public class DefaultKioskFragment extends KioskFragment {
currentInfo = null;
currentNextPageUrl = 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, currentNextPageUrl);
}
/*//////////////////////////////////////////////////////////////////////////
@ -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;
@ -215,12 +233,12 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
}
@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());
@ -251,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);
}
@ -262,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 -> {
@ -276,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)
@ -321,8 +341,8 @@ 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);
}
}
@ -336,12 +356,12 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
}
@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);
}
}
@ -350,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;
}
@ -366,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();
}
@ -380,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();
}
@ -438,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,13 +6,6 @@ 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.TextUtils;
import android.text.TextWatcher;
@ -30,6 +23,14 @@ 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;
@ -40,7 +41,7 @@ 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.util.AndroidTvUtils;
import org.schabi.newpipe.fragments.BackPressable;
import org.schabi.newpipe.fragments.list.BaseListFragment;
import org.schabi.newpipe.local.history.HistoryRecordManager;
@ -52,9 +53,6 @@ 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;
@ -77,10 +75,8 @@ import static androidx.recyclerview.widget.ItemTouchHelper.Callback.makeMovement
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,35 +88,38 @@ 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;
boolean wasSearchFocused = false;
private Map<Integer, String> menuItemToFilterName;
private StreamingService service;
@ -129,7 +128,6 @@ public class SearchFragment
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();
@ -150,7 +148,9 @@ public class SearchFragment
/*////////////////////////////////////////////////////////////////////////*/
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 +173,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_localization_key));
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 +215,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 +257,9 @@ public class SearchFragment
}
}
if (suggestionDisposable == null || suggestionDisposable.isDisposed()) initSuggestionObserver();
if (suggestionDisposable == null || suggestionDisposable.isDisposed()) {
initSuggestionObserver();
}
if (TextUtils.isEmpty(searchString) || wasSearchFocused) {
showKeyboardSearch();
@ -259,7 +273,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 +283,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 +317,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);
@ -326,21 +352,21 @@ 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);
}
@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();
}
@Override
public void onSaveInstanceState(Bundle bundle) {
public void onSaveInstanceState(final Bundle bundle) {
searchString = searchEditText != null
? searchEditText.getText().toString()
: searchString;
@ -372,7 +398,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 +412,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 +436,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 +459,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,9 +485,13 @@ 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;
@ -467,53 +505,63 @@ 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 +570,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 +635,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)
@ -624,15 +693,19 @@ public class SearchFragment
}
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 +714,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 +739,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 +755,9 @@ public class SearchFragment
}
}
if (networkResult.size() > 0) result.addAll(networkResult);
if (networkResult.size() > 0) {
result.addAll(networkResult);
}
return result;
}).materialize();
})
@ -688,12 +767,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 +777,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 -> {
@ -728,31 +806,36 @@ public class SearchFragment
}
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 +845,20 @@ public class SearchFragment
@Override
protected void loadMoreItems() {
if(nextPageUrl == null || nextPageUrl.isEmpty()) return;
if (nextPageUrl == null || nextPageUrl.isEmpty()) {
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,
nextPageUrl)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnEvent((nextItemsResult, throwable) -> isLoading.set(false))
@ -785,7 +872,7 @@ public class SearchFragment
}
@Override
protected void onItemSelected(InfoItem selectedItem) {
protected void onItemSelected(final InfoItem selectedItem) {
super.onItemSelected(selectedItem);
hideKeyboardSearch();
}
@ -794,22 +881,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 +904,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 +915,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 +941,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,11 +952,11 @@ 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);
}
@ -886,7 +979,7 @@ public class SearchFragment
}
@Override
public void handleNextItems(ListExtractor.InfoItemsPage result) {
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
showListFooter(false);
currentPageUrl = result.getNextPageUrl();
infoListAdapter.addInfoItemList(result.getItems());
@ -894,15 +987,17 @@ public class SearchFragment
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.SEARCHED,
NewPipe.getNameOfService(serviceId)
, "\"" + searchString + "\" → page: " + nextPageUrl, 0);
NewPipe.getNameOfService(serviceId),
"\"" + searchString + "\" → page: " + nextPageUrl, 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 +1017,16 @@ 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();
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);
@ -110,16 +121,17 @@ public class SuggestionListAdapter extends RecyclerView.Adapter<SuggestionListAd
searchResId = resolveResourceIdFromAttr(rootView.getContext(), R.attr.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,42 +84,33 @@ 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;
}
@ -126,55 +118,67 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
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 setInfoItemList(List<? extends InfoItem> data) {
public void setInfoItemList(final List<? extends InfoItem> data) {
infoItemList.clear();
infoItemList.addAll(data);
notifyDataSetChanged();
}
public void addInfoItem(@Nullable InfoItem data) {
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);
}
}
}
@ -186,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;
}
@ -219,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;
@ -243,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:
@ -257,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);
@ -293,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 {
@ -325,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());
}
}

View File

@ -3,8 +3,9 @@ package org.schabi.newpipe.info_list.holder;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.text.method.LinkMovementMethod;
import android.text.style.URLSpan;
import android.text.util.Linkify;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;
@ -18,6 +19,7 @@ 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;
@ -29,35 +31,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);
@ -67,13 +75,17 @@ 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()
@ -81,23 +93,11 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
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);
@ -128,49 +128,104 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
});
itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
itemView.setOnLongClickListener(view -> {
if (!AndroidTvUtils.isTv(itemBuilder.getContext())) {
ClipboardManager clipboardManager = (ClipboardManager) itemBuilder.getContext()
.getSystemService(Context.CLIPBOARD_SERVICE);
clipboardManager.setPrimaryClip(ClipData.newPlainText(null,commentText));
Toast.makeText(itemBuilder.getContext(), R.string.msg_copied, Toast.LENGTH_SHORT).show();
return true;
.getSystemService(Context.CLIPBOARD_SERVICE);
clipboardManager.setPrimaryClip(ClipData.newPlainText(null, commentText));
Toast.makeText(itemBuilder.getContext(), R.string.msg_copied, Toast.LENGTH_SHORT)
.show();
} else {
openCommentAuthor(item);
}
return true;
});
}
private void openCommentAuthor(final CommentsInfoItem item) {
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);
}
}
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

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

View File

@ -20,39 +20,46 @@ import static org.schabi.newpipe.MainActivity.DEBUG;
* <p>
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* StreamInfoItemHolder.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 StreamInfoItemHolder extends StreamMiniInfoItemHolder {
public final TextView itemAdditionalDetails;
public StreamInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
public StreamInfoItemHolder(final InfoItemBuilder infoItemBuilder, final ViewGroup parent) {
this(infoItemBuilder, R.layout.list_stream_item, parent);
}
public StreamInfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) {
public StreamInfoItemHolder(final InfoItemBuilder infoItemBuilder, final int layoutId,
final ViewGroup parent) {
super(infoItemBuilder, layoutId, parent);
itemAdditionalDetails = itemView.findViewById(R.id.itemAdditionalDetails);
}
@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 StreamInfoItem)) return;
if (!(infoItem instanceof StreamInfoItem)) {
return;
}
final StreamInfoItem item = (StreamInfoItem) infoItem;
itemAdditionalDetails.setText(getStreamInfoDetailLine(item));
@ -62,11 +69,14 @@ public class StreamInfoItemHolder extends StreamMiniInfoItemHolder {
String viewsAndDate = "";
if (infoItem.getViewCount() >= 0) {
if (infoItem.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) {
viewsAndDate = Localization.listeningCount(itemBuilder.getContext(), infoItem.getViewCount());
viewsAndDate = Localization
.listeningCount(itemBuilder.getContext(), infoItem.getViewCount());
} else if (infoItem.getStreamType().equals(StreamType.LIVE_STREAM)) {
viewsAndDate = Localization.shortWatchingCount(itemBuilder.getContext(), infoItem.getViewCount());
viewsAndDate = Localization
.shortWatchingCount(itemBuilder.getContext(), infoItem.getViewCount());
} else {
viewsAndDate = Localization.shortViewCount(itemBuilder.getContext(), infoItem.getViewCount());
viewsAndDate = Localization
.shortViewCount(itemBuilder.getContext(), infoItem.getViewCount());
}
}
@ -84,10 +94,12 @@ public class StreamInfoItemHolder extends StreamMiniInfoItemHolder {
private String getFormattedRelativeUploadDate(final StreamInfoItem infoItem) {
if (infoItem.getUploadDate() != null) {
String formattedRelativeTime = Localization.relativeTime(infoItem.getUploadDate().date());
String formattedRelativeTime = Localization
.relativeTime(infoItem.getUploadDate().date());
if (DEBUG && PreferenceManager.getDefaultSharedPreferences(itemBuilder.getContext())
.getBoolean(itemBuilder.getContext().getString(R.string.show_original_time_ago_key), false)) {
.getBoolean(itemBuilder.getContext()
.getString(R.string.show_original_time_ago_key), false)) {
formattedRelativeTime += " (" + infoItem.getTextualUploadDate() + ")";
}
return formattedRelativeTime;

View File

@ -1,11 +1,12 @@
package org.schabi.newpipe.info_list.holder;
import androidx.core.content.ContextCompat;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.core.content.ContextCompat;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
import org.schabi.newpipe.extractor.InfoItem;
@ -21,14 +22,14 @@ import org.schabi.newpipe.views.AnimatedProgressBar;
import java.util.concurrent.TimeUnit;
public class StreamMiniInfoItemHolder extends InfoItemHolder {
public final ImageView itemThumbnailView;
public final TextView itemVideoTitleView;
public final TextView itemUploaderView;
public final TextView itemDurationView;
public final AnimatedProgressBar itemProgressView;
private final AnimatedProgressBar itemProgressView;
StreamMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) {
StreamMiniInfoItemHolder(final InfoItemBuilder infoItemBuilder, final int layoutId,
final ViewGroup parent) {
super(infoItemBuilder, layoutId, parent);
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
@ -38,13 +39,16 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
itemProgressView = itemView.findViewById(R.id.itemProgressView);
}
public StreamMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
public StreamMiniInfoItemHolder(final InfoItemBuilder infoItemBuilder, final ViewGroup parent) {
this(infoItemBuilder, R.layout.list_stream_mini_item, parent);
}
@Override
public void updateFromItem(final InfoItem infoItem, final HistoryRecordManager historyRecordManager) {
if (!(infoItem instanceof StreamInfoItem)) return;
public void updateFromItem(final InfoItem infoItem,
final HistoryRecordManager historyRecordManager) {
if (!(infoItem instanceof StreamInfoItem)) {
return;
}
final StreamInfoItem item = (StreamInfoItem) infoItem;
itemVideoTitleView.setText(item.getName());
@ -56,11 +60,13 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
R.color.duration_background_color));
itemDurationView.setVisibility(View.VISIBLE);
StreamStateEntity state2 = historyRecordManager.loadStreamState(infoItem).blockingGet()[0];
StreamStateEntity state2 = historyRecordManager.loadStreamState(infoItem)
.blockingGet()[0];
if (state2 != null) {
itemProgressView.setVisibility(View.VISIBLE);
itemProgressView.setMax((int) item.getDuration());
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state2.getProgressTime()));
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
.toSeconds(state2.getProgressTime()));
} else {
itemProgressView.setVisibility(View.GONE);
}
@ -103,16 +109,20 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
}
@Override
public void updateState(final InfoItem infoItem, final HistoryRecordManager historyRecordManager) {
public void updateState(final InfoItem infoItem,
final HistoryRecordManager historyRecordManager) {
final StreamInfoItem item = (StreamInfoItem) infoItem;
StreamStateEntity state = historyRecordManager.loadStreamState(infoItem).blockingGet()[0];
if (state != null && item.getDuration() > 0 && item.getStreamType() != StreamType.LIVE_STREAM) {
if (state != null && item.getDuration() > 0
&& item.getStreamType() != StreamType.LIVE_STREAM) {
itemProgressView.setMax((int) item.getDuration());
if (itemProgressView.getVisibility() == View.VISIBLE) {
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()));
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS
.toSeconds(state.getProgressTime()));
} else {
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()));
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
.toSeconds(state.getProgressTime()));
AnimationUtils.animateView(itemProgressView, true, 500);
}
} else if (itemProgressView.getVisibility() == View.VISIBLE) {
@ -134,4 +144,4 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
itemView.setLongClickable(false);
itemView.setOnLongClickListener(null);
}
}
}

View File

@ -5,16 +5,17 @@ import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.Bundle;
import android.preference.PreferenceManager;
import androidx.fragment.app.Fragment;
import androidx.appcompat.app.ActionBar;
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.appcompat.app.ActionBar;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.fragments.list.ListViewContract;
@ -25,10 +26,14 @@ import static org.schabi.newpipe.util.AnimationUtils.animateView;
* This fragment is design to be used with persistent data such as
* {@link org.schabi.newpipe.database.LocalItem}, and does not cache the data contained
* in the list adapter to avoid extra writes when the it exits or re-enters its lifecycle.
*
* <p>
* This fragment destroys its adapter and views when {@link Fragment#onDestroyView()} is
* called and is memory efficient when in backstack.
* */
* </p>
*
* @param <I> List of {@link org.schabi.newpipe.database.LocalItem}s
* @param <N> {@link Void}
*/
public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
implements ListViewContract<I, N>, SharedPreferences.OnSharedPreferenceChangeListener {
@ -36,21 +41,19 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
// Views
//////////////////////////////////////////////////////////////////////////*/
protected View headerRootView;
protected View footerRootView;
private static final int LIST_MODE_UPDATE_FLAG = 0x32;
private View headerRootView;
private View footerRootView;
protected LocalItemListAdapter itemListAdapter;
protected RecyclerView itemsList;
private int updateFlags = 0;
private static final int LIST_MODE_UPDATE_FLAG = 0x32;
/*//////////////////////////////////////////////////////////////////////////
// Lifecycle - Creation
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreate(Bundle savedInstanceState) {
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
PreferenceManager.getDefaultSharedPreferences(activity)
@ -70,8 +73,9 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
if (updateFlags != 0) {
if ((updateFlags & LIST_MODE_UPDATE_FLAG) != 0) {
final boolean useGrid = isGridLayout();
itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager());
itemListAdapter.setGridItemVariants(useGrid);
itemsList.setLayoutManager(
useGrid ? getGridLayoutManager() : getListLayoutManager());
itemListAdapter.setUseGridVariant(useGrid);
itemListAdapter.notifyDataSetChanged();
}
updateFlags = 0;
@ -94,7 +98,8 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
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(itemListAdapter.getSpanSizeLookup(spanCount));
return lm;
@ -105,7 +110,7 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
}
@Override
protected void initViews(View rootView, Bundle savedInstanceState) {
protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
itemListAdapter = new LocalItemListAdapter(activity);
@ -114,9 +119,11 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
itemsList = rootView.findViewById(R.id.items_list);
itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager());
itemListAdapter.setGridItemVariants(useGrid);
itemListAdapter.setHeader(headerRootView = getListHeader());
itemListAdapter.setFooter(footerRootView = getListFooter());
itemListAdapter.setUseGridVariant(useGrid);
headerRootView = getListHeader();
itemListAdapter.setHeader(headerRootView);
footerRootView = getListFooter();
itemListAdapter.setFooter(footerRootView);
itemsList.setAdapter(itemListAdapter);
}
@ -131,13 +138,17 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
//////////////////////////////////////////////////////////////////////////*/
@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 + "]");
}
final ActionBar supportActionBar = activity.getSupportActionBar();
if (supportActionBar == null) return;
if (supportActionBar == null) {
return;
}
supportActionBar.setDisplayShowTitleEnabled(true);
}
@ -158,7 +169,7 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
//////////////////////////////////////////////////////////////////////////*/
@Override
public void startLoading(boolean forceLoad) {
public void startLoading(final boolean forceLoad) {
super.startLoading(forceLoad);
resetFragment();
}
@ -166,24 +177,36 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
@Override
public void showLoading() {
super.showLoading();
if (itemsList != null) animateView(itemsList, false, 200);
if (headerRootView != null) animateView(headerRootView, false, 200);
if (itemsList != null) {
animateView(itemsList, false, 200);
}
if (headerRootView != null) {
animateView(headerRootView, false, 200);
}
}
@Override
public void hideLoading() {
super.hideLoading();
if (itemsList != null) animateView(itemsList, true, 200);
if (headerRootView != null) animateView(headerRootView, true, 200);
if (itemsList != null) {
animateView(itemsList, true, 200);
}
if (headerRootView != null) {
animateView(headerRootView, true, 200);
}
}
@Override
public void showError(String message, boolean showRetryButton) {
public void showError(final String message, final boolean showRetryButton) {
super.showError(message, showRetryButton);
showListFooter(false);
if (itemsList != null) animateView(itemsList, false, 200);
if (headerRootView != null) animateView(headerRootView, false, 200);
if (itemsList != null) {
animateView(itemsList, false, 200);
}
if (headerRootView != null) {
animateView(headerRootView, false, 200);
}
}
@Override
@ -194,14 +217,18 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
@Override
public void showListFooter(final boolean show) {
if (itemsList == null) return;
if (itemsList == null) {
return;
}
itemsList.post(() -> {
if (itemListAdapter != null) itemListAdapter.showFooter(show);
if (itemListAdapter != null) {
itemListAdapter.showFooter(show);
}
});
}
@Override
public void handleNextItems(N result) {
public void handleNextItems(final N result) {
isLoading.set(false);
}
@ -210,30 +237,35 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
//////////////////////////////////////////////////////////////////////////*/
protected void resetFragment() {
if (itemListAdapter != null) itemListAdapter.clearStreamItemList();
if (itemListAdapter != null) {
itemListAdapter.clearStreamItemList();
}
}
@Override
protected boolean onError(Throwable exception) {
protected boolean onError(final Throwable exception) {
resetFragment();
return super.onError(exception);
}
@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

@ -1,13 +1,14 @@
package org.schabi.newpipe.local;
import androidx.recyclerview.widget.RecyclerView;
import android.view.View;
import androidx.recyclerview.widget.RecyclerView;
public class HeaderFooterHolder extends RecyclerView.ViewHolder {
public View view;
public HeaderFooterHolder(View v) {
public HeaderFooterHolder(final View v) {
super(v);
view = v;
}
}
}

View File

@ -30,14 +30,12 @@ import org.schabi.newpipe.util.OnClickGesture;
*/
public class LocalItemBuilder {
private static final String TAG = LocalItemBuilder.class.toString();
private final Context context;
private final ImageLoader imageLoader = ImageLoader.getInstance();
private OnClickGesture<LocalItem> onSelectedListener;
public LocalItemBuilder(Context context) {
public LocalItemBuilder(final Context context) {
this.context = context;
}
@ -54,7 +52,7 @@ public class LocalItemBuilder {
return onSelectedListener;
}
public void setOnItemSelectedListener(OnClickGesture<LocalItem> listener) {
public void setOnItemSelectedListener(final OnClickGesture<LocalItem> listener) {
this.onSelectedListener = listener;
}
}

View File

@ -1,13 +1,14 @@
package org.schabi.newpipe.local;
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.LocalItem;
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
@ -50,7 +51,6 @@ import java.util.List;
*/
public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final String TAG = LocalItemListAdapter.class.getSimpleName();
private static final boolean DEBUG = false;
@ -63,8 +63,8 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
private static final int STREAM_PLAYLIST_GRID_HOLDER_TYPE = 0x1004;
private static final int LOCAL_PLAYLIST_HOLDER_TYPE = 0x2000;
private static final int REMOTE_PLAYLIST_HOLDER_TYPE = 0x2001;
private static final int LOCAL_PLAYLIST_GRID_HOLDER_TYPE = 0x2002;
private static final int REMOTE_PLAYLIST_GRID_HOLDER_TYPE = 0x2004;
private static final int LOCAL_PLAYLIST_GRID_HOLDER_TYPE = 0x2002;
private static final int REMOTE_PLAYLIST_GRID_HOLDER_TYPE = 0x2004;
private final LocalItemBuilder localItemBuilder;
private final ArrayList<LocalItem> localItems;
@ -76,7 +76,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
private View header = null;
private View footer = null;
public LocalItemListAdapter(Context context) {
public LocalItemListAdapter(final Context context) {
recordManager = new HistoryRecordManager(context);
localItemBuilder = new LocalItemBuilder(context);
localItems = new ArrayList<>();
@ -84,7 +84,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
Localization.getPreferredLocale(context));
}
public void setSelectedListener(OnClickGesture<LocalItem> listener) {
public void setSelectedListener(final OnClickGesture<LocalItem> listener) {
localItemBuilder.setOnItemSelectedListener(listener);
}
@ -92,28 +92,34 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
localItemBuilder.setOnItemSelectedListener(null);
}
public void addItems(@Nullable List<? extends LocalItem> data) {
public void addItems(@Nullable final List<? extends LocalItem> data) {
if (data == null) {
return;
}
if (DEBUG) Log.d(TAG, "addItems() before > localItems.size() = " +
localItems.size() + ", data.size() = " + data.size());
if (DEBUG) {
Log.d(TAG, "addItems() before > localItems.size() = "
+ localItems.size() + ", data.size() = " + data.size());
}
int offsetStart = sizeConsideringHeader();
localItems.addAll(data);
if (DEBUG) Log.d(TAG, "addItems() after > offsetStart = " + offsetStart +
", localItems.size() = " + localItems.size() +
", header = " + header + ", footer = " + footer +
", showFooter = " + showFooter);
if (DEBUG) {
Log.d(TAG, "addItems() after > offsetStart = " + offsetStart + ", "
+ "localItems.size() = " + localItems.size() + ", "
+ "header = " + header + ", footer = " + footer + ", "
+ "showFooter = " + showFooter);
}
notifyItemRangeInserted(offsetStart, data.size());
if (footer != null && showFooter) {
int footerNow = sizeConsideringHeader();
notifyItemMoved(offsetStart, footerNow);
if (DEBUG) Log.d(TAG, "addItems() footer from " + offsetStart +
" to " + footerNow);
if (DEBUG) {
Log.d(TAG, "addItems() footer from " + offsetStart
+ " to " + footerNow);
}
}
}
@ -123,12 +129,16 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
notifyItemRemoved(index + (header != null ? 1 : 0));
}
public boolean swapItems(int fromAdapterPosition, int toAdapterPosition) {
public boolean swapItems(final int fromAdapterPosition, final int toAdapterPosition) {
final int actualFrom = adapterOffsetWithoutHeader(fromAdapterPosition);
final int actualTo = adapterOffsetWithoutHeader(toAdapterPosition);
if (actualFrom < 0 || actualTo < 0) return false;
if (actualFrom >= localItems.size() || actualTo >= localItems.size()) return false;
if (actualFrom < 0 || actualTo < 0) {
return false;
}
if (actualFrom >= localItems.size() || actualTo >= localItems.size()) {
return false;
}
localItems.add(actualTo, localItems.remove(actualFrom));
notifyItemMoved(fromAdapterPosition, toAdapterPosition);
@ -143,27 +153,36 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
notifyDataSetChanged();
}
public void setGridItemVariants(boolean useGridVariant) {
public void setUseGridVariant(final boolean useGridVariant) {
this.useGridVariant = useGridVariant;
}
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(sizeConsideringHeader());
else notifyItemRemoved(sizeConsideringHeader());
if (show) {
notifyItemInserted(sizeConsideringHeader());
} else {
notifyItemRemoved(sizeConsideringHeader());
}
}
private int adapterOffsetWithoutHeader(final int offset) {
@ -181,21 +200,27 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
@Override
public int getItemCount() {
int count = localItems.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 +
", localItems.size() = " + localItems.size() +
", header = " + header + ", footer = " + footer +
", showFooter = " + showFooter);
Log.d(TAG, "getItemCount() called, count = " + count + ", "
+ "localItems.size() = " + localItems.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;
@ -208,23 +233,34 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
final LocalItem item = localItems.get(position);
switch (item.getLocalItemType()) {
case PLAYLIST_LOCAL_ITEM: return useGridVariant ? LOCAL_PLAYLIST_GRID_HOLDER_TYPE : LOCAL_PLAYLIST_HOLDER_TYPE;
case PLAYLIST_REMOTE_ITEM: return useGridVariant ? REMOTE_PLAYLIST_GRID_HOLDER_TYPE : REMOTE_PLAYLIST_HOLDER_TYPE;
case PLAYLIST_LOCAL_ITEM:
return useGridVariant
? LOCAL_PLAYLIST_GRID_HOLDER_TYPE : LOCAL_PLAYLIST_HOLDER_TYPE;
case PLAYLIST_REMOTE_ITEM:
return useGridVariant
? REMOTE_PLAYLIST_GRID_HOLDER_TYPE : REMOTE_PLAYLIST_HOLDER_TYPE;
case PLAYLIST_STREAM_ITEM: return useGridVariant ? STREAM_PLAYLIST_GRID_HOLDER_TYPE : STREAM_PLAYLIST_HOLDER_TYPE;
case STATISTIC_STREAM_ITEM: return useGridVariant ? STREAM_STATISTICS_GRID_HOLDER_TYPE : STREAM_STATISTICS_HOLDER_TYPE;
case PLAYLIST_STREAM_ITEM:
return useGridVariant
? STREAM_PLAYLIST_GRID_HOLDER_TYPE : STREAM_PLAYLIST_HOLDER_TYPE;
case STATISTIC_STREAM_ITEM:
return useGridVariant
? STREAM_STATISTICS_GRID_HOLDER_TYPE : STREAM_STATISTICS_HOLDER_TYPE;
default:
Log.e(TAG, "No holder type has been considered for item: [" +
item.getLocalItemType() + "]");
Log.e(TAG, "No holder type has been considered for item: ["
+ item.getLocalItemType() + "]");
return -1;
}
}
@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 HeaderFooterHolder(header);
@ -253,15 +289,21 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
}
@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 LocalItemHolder) {
// If header isn't null, offset the items by -1
if (header != null) position--;
if (header != null) {
position--;
}
((LocalItemHolder) holder).updateFromItem(localItems.get(position), recordManager, dateFormat);
((LocalItemHolder) holder)
.updateFromItem(localItems.get(position), recordManager, dateFormat);
} else if (holder instanceof HeaderFooterHolder && position == 0 && header != null) {
((HeaderFooterHolder) holder).view = header;
} else if (holder instanceof HeaderFooterHolder && position == sizeConsideringHeader()
@ -271,13 +313,16 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
}
@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 LocalItemHolder) {
for (Object payload : payloads) {
if (payload instanceof StreamStateEntity) {
((LocalItemHolder) holder).updateState(localItems.get(header == null ? position : position - 1), recordManager);
((LocalItemHolder) holder).updateState(localItems
.get(header == null ? position : position - 1), recordManager);
} else if (payload instanceof Boolean) {
((LocalItemHolder) holder).updateState(localItems.get(header == null ? position : position - 1), recordManager);
((LocalItemHolder) holder).updateState(localItems
.get(header == null ? position : position - 1), recordManager);
}
}
} else {
@ -288,7 +333,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
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;
}

View File

@ -5,15 +5,15 @@ import android.app.AlertDialog.Builder;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.Log;
import android.widget.EditText;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentManager;
import io.reactivex.disposables.Disposable;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.schabi.newpipe.NewPipeDatabase;
@ -39,10 +39,9 @@ import io.reactivex.Flowable;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
public final class BookmarkFragment
extends BaseLocalListFragment<List<PlaylistLocalItem>, Void> {
public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistLocalItem>, Void> {
@State
protected Parcelable itemsListState;
@ -55,10 +54,37 @@ public final class BookmarkFragment
// Fragment LifeCycle - Creation
///////////////////////////////////////////////////////////////////////////
private static List<PlaylistLocalItem> merge(
final List<PlaylistMetadataEntry> localPlaylists,
final List<PlaylistRemoteEntity> remotePlaylists) {
List<PlaylistLocalItem> items = new ArrayList<>(
localPlaylists.size() + remotePlaylists.size());
items.addAll(localPlaylists);
items.addAll(remotePlaylists);
Collections.sort(items, (left, right) -> {
String on1 = left.getOrderingName();
String on2 = right.getOrderingName();
if (on1 == null && on2 == null) {
return 0;
} else if (on1 != null && on2 == null) {
return -1;
} else if (on1 == null && on2 != null) {
return 1;
} else {
return on1.compareToIgnoreCase(on2);
}
});
return items;
}
@Override
public void onCreate(Bundle savedInstanceState) {
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (activity == null) return;
if (activity == null) {
return;
}
final AppDatabase database = NewPipeDatabase.getInstance(activity);
localPlaylistManager = new LocalPlaylistManager(database);
remotePlaylistManager = new RemotePlaylistManager(database);
@ -67,19 +93,18 @@ public final class BookmarkFragment
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
Bundle savedInstanceState) {
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
final Bundle savedInstanceState) {
if(!useAsFrontPage) {
if (!useAsFrontPage) {
setTitle(activity.getString(R.string.tab_bookmarks));
}
return inflater.inflate(R.layout.fragment_bookmarks, container, false);
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
public void setUserVisibleHint(final boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (activity != null && isVisibleToUser) {
setTitle(activity.getString(R.string.tab_bookmarks));
@ -91,7 +116,7 @@ public final class BookmarkFragment
///////////////////////////////////////////////////////////////////////////
@Override
protected void initViews(View rootView, Bundle savedInstanceState) {
protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
}
@ -101,7 +126,7 @@ public final class BookmarkFragment
itemListAdapter.setSelectedListener(new OnClickGesture<LocalItem>() {
@Override
public void selected(LocalItem selectedItem) {
public void selected(final LocalItem selectedItem) {
final FragmentManager fragmentManager = getFM();
if (selectedItem instanceof PlaylistMetadataEntry) {
@ -120,7 +145,7 @@ public final class BookmarkFragment
}
@Override
public void held(LocalItem selectedItem) {
public void held(final LocalItem selectedItem) {
if (selectedItem instanceof PlaylistMetadataEntry) {
showLocalDialog((PlaylistMetadataEntry) selectedItem);
} else if (selectedItem instanceof PlaylistRemoteEntity) {
@ -135,16 +160,14 @@ public final class BookmarkFragment
///////////////////////////////////////////////////////////////////////////
@Override
public void startLoading(boolean forceLoad) {
public void startLoading(final boolean forceLoad) {
super.startLoading(forceLoad);
Flowable.combineLatest(
localPlaylistManager.getPlaylists(),
remotePlaylistManager.getPlaylists(),
BookmarkFragment::merge
).onBackpressureLatest()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getPlaylistsSubscriber());
Flowable.combineLatest(localPlaylistManager.getPlaylists(),
remotePlaylistManager.getPlaylists(), BookmarkFragment::merge)
.onBackpressureLatest()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getPlaylistsSubscriber());
}
///////////////////////////////////////////////////////////////////////////
@ -161,8 +184,12 @@ public final class BookmarkFragment
public void onDestroyView() {
super.onDestroyView();
if (disposables != null) disposables.clear();
if (databaseSubscription != null) databaseSubscription.cancel();
if (disposables != null) {
disposables.clear();
}
if (databaseSubscription != null) {
databaseSubscription.cancel();
}
databaseSubscription = null;
}
@ -170,7 +197,9 @@ public final class BookmarkFragment
@Override
public void onDestroy() {
super.onDestroy();
if (disposables != null) disposables.dispose();
if (disposables != null) {
disposables.dispose();
}
disposables = null;
localPlaylistManager = null;
@ -185,32 +214,35 @@ public final class BookmarkFragment
private Subscriber<List<PlaylistLocalItem>> getPlaylistsSubscriber() {
return new Subscriber<List<PlaylistLocalItem>>() {
@Override
public void onSubscribe(Subscription s) {
public void onSubscribe(final Subscription s) {
showLoading();
if (databaseSubscription != null) databaseSubscription.cancel();
if (databaseSubscription != null) {
databaseSubscription.cancel();
}
databaseSubscription = s;
databaseSubscription.request(1);
}
@Override
public void onNext(List<PlaylistLocalItem> subscriptions) {
public void onNext(final List<PlaylistLocalItem> subscriptions) {
handleResult(subscriptions);
if (databaseSubscription != null) databaseSubscription.request(1);
if (databaseSubscription != null) {
databaseSubscription.request(1);
}
}
@Override
public void onError(Throwable exception) {
public void onError(final Throwable exception) {
BookmarkFragment.this.onError(exception);
}
@Override
public void onComplete() {
}
public void onComplete() { }
};
}
@Override
public void handleResult(@NonNull List<PlaylistLocalItem> result) {
public void handleResult(@NonNull final List<PlaylistLocalItem> result) {
super.handleResult(result);
itemListAdapter.clearStreamItemList();
@ -227,13 +259,16 @@ public final class BookmarkFragment
}
hideLoading();
}
///////////////////////////////////////////////////////////////////////////
// Fragment Error Handling
///////////////////////////////////////////////////////////////////////////
@Override
protected boolean onError(Throwable exception) {
if (super.onError(exception)) return true;
protected boolean onError(final Throwable exception) {
if (super.onError(exception)) {
return true;
}
onUnrecoverableError(exception, UserAction.SOMETHING_ELSE,
"none", "Bookmark", R.string.general_error);
@ -243,7 +278,9 @@ public final class BookmarkFragment
@Override
protected void resetFragment() {
super.resetFragment();
if (disposables != null) disposables.clear();
if (disposables != null) {
disposables.clear();
}
}
///////////////////////////////////////////////////////////////////////////
@ -254,28 +291,30 @@ public final class BookmarkFragment
showDeleteDialog(item.getName(), remotePlaylistManager.deletePlaylist(item.getUid()));
}
private void showLocalDialog(PlaylistMetadataEntry selectedItem) {
private void showLocalDialog(final PlaylistMetadataEntry selectedItem) {
View dialogView = View.inflate(getContext(), R.layout.dialog_bookmark, null);
EditText editText = dialogView.findViewById(R.id.playlist_name_edit_text);
editText.setText(selectedItem.name);
Builder builder = new AlertDialog.Builder(activity);
builder.setView(dialogView)
.setPositiveButton(R.string.rename_playlist, (dialog, which) -> {
changeLocalPlaylistName(selectedItem.uid, editText.getText().toString());
})
.setNegativeButton(R.string.cancel, null)
.setNeutralButton(R.string.delete, (dialog, which) -> {
showDeleteDialog(selectedItem.name,
localPlaylistManager.deletePlaylist(selectedItem.uid));
dialog.dismiss();
})
.create()
.show();
.setPositiveButton(R.string.rename_playlist, (dialog, which) -> {
changeLocalPlaylistName(selectedItem.uid, editText.getText().toString());
})
.setNegativeButton(R.string.cancel, null)
.setNeutralButton(R.string.delete, (dialog, which) -> {
showDeleteDialog(selectedItem.name,
localPlaylistManager.deletePlaylist(selectedItem.uid));
dialog.dismiss();
})
.create()
.show();
}
private void showDeleteDialog(final String name, final Single<Integer> deleteReactor) {
if (activity == null || disposables == null) return;
if (activity == null || disposables == null) {
return;
}
new AlertDialog.Builder(activity)
.setTitle(name)
@ -284,40 +323,27 @@ public final class BookmarkFragment
.setPositiveButton(R.string.delete, (dialog, i) ->
disposables.add(deleteReactor
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> {/*Do nothing on success*/}, this::onError))
.subscribe(ignored -> { /*Do nothing on success*/ }, this::onError))
)
.setNegativeButton(R.string.cancel, null)
.show();
}
private void changeLocalPlaylistName(long id, String name) {
private void changeLocalPlaylistName(final long id, final String name) {
if (localPlaylistManager == null) {
return;
}
if (DEBUG) {
Log.d(TAG, "Updating playlist id=[" + id +
"] with new name=[" + name + "] items");
Log.d(TAG, "Updating playlist id=[" + id + "] "
+ "with new name=[" + name + "] items");
}
localPlaylistManager.renamePlaylist(id, name);
final Disposable disposable = localPlaylistManager.renamePlaylist(id, name)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(longs -> {/*Do nothing on success*/}, this::onError);
.observeOn(AndroidSchedulers.mainThread())
.subscribe(longs -> { /*Do nothing on success*/ }, this::onError);
disposables.add(disposable);
}
private static List<PlaylistLocalItem> merge(final List<PlaylistMetadataEntry> localPlaylists,
final List<PlaylistRemoteEntity> remotePlaylists) {
List<PlaylistLocalItem> items = new ArrayList<>(
localPlaylists.size() + remotePlaylists.size());
items.addAll(localPlaylists);
items.addAll(remotePlaylists);
Collections.sort(items, (left, right) ->
left.getOrderingName().compareToIgnoreCase(right.getOrderingName()));
return items;
}
}

View File

@ -69,13 +69,13 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
//////////////////////////////////////////////////////////////////////////*/
@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.dialog_playlists, 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);
final LocalPlaylistManager playlistManager =
@ -84,9 +84,10 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
playlistAdapter = new LocalItemListAdapter(getActivity());
playlistAdapter.setSelectedListener(new OnClickGesture<LocalItem>() {
@Override
public void selected(LocalItem selectedItem) {
if (!(selectedItem instanceof PlaylistMetadataEntry) || getStreams() == null)
public void selected(final LocalItem selectedItem) {
if (!(selectedItem instanceof PlaylistMetadataEntry) || getStreams() == null) {
return;
}
onPlaylistSelected(playlistManager, (PlaylistMetadataEntry) selectedItem,
getStreams());
}
@ -126,7 +127,9 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
//////////////////////////////////////////////////////////////////////////*/
public void openCreatePlaylistDialog() {
if (getStreams() == null || getFragmentManager() == null) return;
if (getStreams() == null || getFragmentManager() == null) {
return;
}
PlaylistCreationDialog.newInstance(getStreams()).show(getFragmentManager(), TAG);
getDialog().dismiss();
@ -145,16 +148,19 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
}
}
private void onPlaylistSelected(@NonNull LocalPlaylistManager manager,
@NonNull PlaylistMetadataEntry playlist,
@NonNull List<StreamEntity> streams) {
if (getStreams() == null) return;
private void onPlaylistSelected(@NonNull final LocalPlaylistManager manager,
@NonNull final PlaylistMetadataEntry playlist,
@NonNull final List<StreamEntity> streams) {
if (getStreams() == null) {
return;
}
final Toast successToast = Toast.makeText(getContext(),
R.string.playlist_add_stream_success, Toast.LENGTH_SHORT);
if (playlist.thumbnailUrl.equals("drawable://" + R.drawable.dummy_thumbnail_playlist)) {
playlistDisposables.add(manager.changePlaylistThumbnail(playlist.uid, streams.get(0).getThumbnailUrl())
playlistDisposables.add(manager
.changePlaylistThumbnail(playlist.uid, streams.get(0).getThumbnailUrl())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> successToast.show()));
}

View File

@ -3,12 +3,13 @@ package org.schabi.newpipe.local.dialog;
import android.app.AlertDialog;
import android.app.Dialog;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.stream.model.StreamEntity;
@ -19,8 +20,6 @@ import java.util.List;
import io.reactivex.android.schedulers.AndroidSchedulers;
public final class PlaylistCreationDialog extends PlaylistDialog {
private static final String TAG = PlaylistCreationDialog.class.getCanonicalName();
public static PlaylistCreationDialog newInstance(final List<StreamEntity> streams) {
PlaylistCreationDialog dialog = new PlaylistCreationDialog();
dialog.setInfo(streams);
@ -33,8 +32,10 @@ public final class PlaylistCreationDialog extends PlaylistDialog {
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
if (getStreams() == null) return super.onCreateDialog(savedInstanceState);
public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
if (getStreams() == null) {
return super.onCreateDialog(savedInstanceState);
}
View dialogView = View.inflate(getContext(), R.layout.dialog_playlist_name, null);
EditText nameInput = dialogView.findViewById(R.id.playlist_name);

View File

@ -2,10 +2,11 @@ package org.schabi.newpipe.local.dialog;
import android.app.Dialog;
import android.os.Bundle;
import android.view.Window;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import android.view.Window;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.util.StateSaver;
@ -14,7 +15,6 @@ import java.util.List;
import java.util.Queue;
public abstract class PlaylistDialog extends DialogFragment implements StateSaver.WriteRead {
private List<StreamEntity> streamEntities;
private StateSaver.SavedState savedState;
@ -32,7 +32,7 @@ public abstract class PlaylistDialog extends DialogFragment implements StateSave
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
savedState = StateSaver.tryToRestore(savedInstanceState, this);
}
@ -45,7 +45,7 @@ public abstract class PlaylistDialog extends DialogFragment implements StateSave
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
public Dialog onCreateDialog(final Bundle savedInstanceState) {
final Dialog dialog = super.onCreateDialog(savedInstanceState);
//remove title
final Window window = dialog.getWindow();
@ -66,18 +66,18 @@ public abstract class PlaylistDialog extends DialogFragment implements StateSave
}
@Override
public void writeTo(Queue<Object> objectsToSave) {
public void writeTo(final Queue<Object> objectsToSave) {
objectsToSave.add(streamEntities);
}
@Override
@SuppressWarnings("unchecked")
public void readFrom(@NonNull Queue<Object> savedObjects) {
public void readFrom(@NonNull final Queue<Object> savedObjects) {
streamEntities = (List<StreamEntity>) savedObjects.poll();
}
@Override
public void onSaveInstanceState(Bundle outState) {
public void onSaveInstanceState(final Bundle outState) {
super.onSaveInstanceState(outState);
if (getActivity() != null) {
savedState = StateSaver.tryToSave(getActivity().isChangingConfigurations(),

View File

@ -41,7 +41,9 @@ import java.util.*
class FeedFragment : BaseListFragment<FeedState, Unit>() {
private lateinit var viewModel: FeedViewModel
@State @JvmField var listState: Parcelable? = null
@State
@JvmField
var listState: Parcelable? = null
private var groupId = FeedGroupEntity.GROUP_ALL_ID
private var groupName = ""
@ -49,13 +51,14 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
init {
setHasOptionsMenu(true)
useDefaultStateSaving(false)
setUseDefaultStateSaving(false)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
groupId = arguments?.getLong(KEY_GROUP_ID, FeedGroupEntity.GROUP_ALL_ID) ?: FeedGroupEntity.GROUP_ALL_ID
groupId = arguments?.getLong(KEY_GROUP_ID, FeedGroupEntity.GROUP_ALL_ID)
?: FeedGroupEntity.GROUP_ALL_ID
groupName = arguments?.getString(KEY_GROUP_NAME) ?: ""
}
@ -107,7 +110,7 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
inflater.inflate(R.menu.menu_feed_fragment, menu)
if (useAsFrontPage) {
menu.findItem(R.id.menu_item_feed_help).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
menu.findItem(R.id.menu_item_feed_help).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
}
}
@ -324,4 +327,4 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
return feedFragment
}
}
}
}

View File

@ -52,6 +52,7 @@ import org.schabi.newpipe.local.feed.FeedDatabaseManager
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.*
import org.schabi.newpipe.local.feed.service.FeedEventManager.postEvent
import org.schabi.newpipe.local.subscription.SubscriptionManager
import org.schabi.newpipe.util.ExceptionUtils
import org.schabi.newpipe.util.ExtractorHelper
import java.io.IOException
import java.util.*
@ -333,11 +334,12 @@ class FeedLoadService : Service() {
val cause = error.cause
when {
error is IOException -> throw error
cause is IOException -> throw cause
error is ReCaptchaException -> throw error
cause is ReCaptchaException -> throw cause
error is IOException -> throw error
cause is IOException -> throw cause
ExceptionUtils.isNetworkRelated(error) -> throw IOException(error)
}
}
}

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