mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2026-01-15 05:58:02 +00:00
Compare commits
162 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f893edeb82 | ||
|
|
64e4adef29 | ||
|
|
c1fe03aab6 | ||
|
|
4b0a071a35 | ||
|
|
4ef69969a7 | ||
|
|
c5885d9ce4 | ||
|
|
4c2d705311 | ||
|
|
939017ada9 | ||
|
|
322c5c05cf | ||
|
|
cd174b8ce9 | ||
|
|
f5999f28e9 | ||
|
|
428a754bfc | ||
|
|
9ba2a9d907 | ||
|
|
71a22348c9 | ||
|
|
4ac0db1869 | ||
|
|
bca9035302 | ||
|
|
33b316d72f | ||
|
|
6068043fd4 | ||
|
|
2bf4032a6f | ||
|
|
5e659e3f46 | ||
|
|
23f70d6b8b | ||
|
|
ddcf2c5206 | ||
|
|
6b88349c77 | ||
|
|
4eb2d3805d | ||
|
|
6cdddc4e9f | ||
|
|
406d844722 | ||
|
|
09ee0357c7 | ||
|
|
f603f63361 | ||
|
|
720bb5d615 | ||
|
|
eebd76661d | ||
|
|
7885ae5d3f | ||
|
|
08257cc5dd | ||
|
|
27f8144bb8 | ||
|
|
146d4a8365 | ||
|
|
bddd9b3409 | ||
|
|
0d4d83f3a6 | ||
|
|
4bb0bb4ff0 | ||
|
|
2d75968532 | ||
|
|
aa0e759168 | ||
|
|
cdae745c00 | ||
|
|
a8b6af1f03 | ||
|
|
c7106073b4 | ||
|
|
d96139798f | ||
|
|
bd560644fe | ||
|
|
f6772c4138 | ||
|
|
dd733640e5 | ||
|
|
75a0c0a527 | ||
|
|
a5925875a7 | ||
|
|
84157f9247 | ||
|
|
fcf3ed7881 | ||
|
|
dac3be69d6 | ||
|
|
5118e5cceb | ||
|
|
7ca0d20dda | ||
|
|
378222e484 | ||
|
|
812bec205c | ||
|
|
2ee8803e33 | ||
|
|
007616ca42 | ||
|
|
f3b235e4a7 | ||
| 074d4be4ba | |||
|
|
03cd48257b | ||
|
|
546db83de3 | ||
|
|
cc11a39f7e | ||
|
|
a83b365afd | ||
|
|
6a6cfac603 | ||
|
|
5270cedc56 | ||
|
|
2ac833d2a6 | ||
|
|
f7be210b12 | ||
|
|
24774d7921 | ||
|
|
2b2e954b84 | ||
|
|
85108be686 | ||
|
|
9fbac943bb | ||
|
|
d901eaa8d2 | ||
|
|
5fb0e8d007 | ||
|
|
cd5c76cb76 | ||
|
|
b2a64f8bf3 | ||
|
|
b91acc9de5 | ||
|
|
07836c7ffa | ||
|
|
8b30a0d7cc | ||
|
|
13a3e2feb2 | ||
|
|
f4dca71497 | ||
|
|
e4c6e87338 | ||
|
|
b2862520fd | ||
|
|
bb29f1ea95 | ||
|
|
69b92757f4 | ||
|
|
62ae2652c7 | ||
| 6f6633050b | |||
|
|
d37bfd6651 | ||
| 55a17a8a83 | |||
|
|
422b06b72d | ||
|
|
873a1d1c3b | ||
|
|
44d06767b5 | ||
|
|
3f4379afbf | ||
|
|
c0515de6b7 | ||
| 09159ec245 | |||
|
|
ab0bf80888 | ||
| 74b8eaed04 | |||
| d64b5ccae2 | |||
| 3883212ca7 | |||
| 1cec41dba0 | |||
| d00c0ff60c | |||
| 1d597c5f5b | |||
| c004ef7d0b | |||
|
|
1bfb977b28 | ||
|
|
04138047f6 | ||
|
|
147e99a915 | ||
|
|
6f7d162edc | ||
|
|
910aee4b73 | ||
|
|
77e376751a | ||
|
|
7e1264ac44 | ||
|
|
8f3a4a5a04 | ||
| 2d2689362d | |||
|
|
913500a6de | ||
|
|
81d7c54a55 | ||
|
|
4501d5a8fc | ||
|
|
becc90409f | ||
|
|
10c4f7b465 | ||
|
|
cb5b0ff764 | ||
|
|
277a817eaf | ||
|
|
fbac6f6a57 | ||
|
|
b1b0d6ceb6 | ||
|
|
07421542df | ||
|
|
74a8139ac9 | ||
|
|
77eb76fefb | ||
|
|
42a450c61f | ||
|
|
9e2ed10144 | ||
|
|
27e5ce7f9b | ||
|
|
f7b322da49 | ||
|
|
16ad13c962 | ||
|
|
a8880bbc0a | ||
|
|
f020b88db3 | ||
|
|
a4f5c9e2a3 | ||
|
|
96d3841dba | ||
|
|
39277d569f | ||
|
|
75b5bc03f7 | ||
|
|
01e17d9c2e | ||
|
|
8047ff96a8 | ||
|
|
0e737a2f3b | ||
|
|
854579d814 | ||
|
|
f3cbd99e45 | ||
|
|
c13ed3f347 | ||
|
|
7409b58546 | ||
|
|
404ebb8810 | ||
|
|
b4a9a300c5 | ||
|
|
aa637e6a2b | ||
|
|
85132e1e6e | ||
|
|
f0aa5a4646 | ||
|
|
b0479d0bd9 | ||
|
|
e475b888aa | ||
|
|
a8a3bbd9f5 | ||
|
|
f3cbd17598 | ||
|
|
3d7e4dc286 | ||
| 2e98729527 | |||
|
|
3ac838a7ec | ||
|
|
a1bd5bfc00 | ||
|
|
b99571a797 | ||
| 43dec8914c | |||
|
|
5b5cb78595 | ||
|
|
cb8c919609 | ||
|
|
be3810dfed | ||
|
|
b961b59ad4 | ||
|
|
d2fbb86d8a | ||
|
|
782c6211d0 |
44
.github/CONTRIBUTING.md
vendored
44
.github/CONTRIBUTING.md
vendored
@@ -1,40 +1,42 @@
|
||||
NewPipe contribution guidelines
|
||||
===============================
|
||||
|
||||
READ THIS GUIDELINES CAREFULLY BEFORE CONTRIBUTING.
|
||||
PLEASE READ THESE GUIDELINES CAREFULLY BEFORE ANY CONTRIBUTION!
|
||||
|
||||
## Crash reporting
|
||||
|
||||
Do not report crashes in the GitHub issue tracker. NewPipe has an automated crash report system that will ask you to send a report if a crash occurs.
|
||||
Do not report crashes in the GitHub issue tracker. NewPipe has an automated crash report system that will ask you to send a report via e-mail when a crash occurs. This contains all the data we need for debugging, and allows you to even add a comment to it. You'll see exactly what is sent, the system is 100% transparent.
|
||||
|
||||
## Issue reporting/feature request
|
||||
## Issue reporting/feature requests
|
||||
|
||||
* Search the [existing issues](https://github.com/theScrabi/NewPipe/issues) first to make sure your issue/feature hasn't been reported/requested before
|
||||
* Check if this issue/feature is already fixed/implemented in the repository
|
||||
* If you are an android/java developer you are always welcome to fix/implement an issue/a feature yourself
|
||||
* Use english
|
||||
* 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
|
||||
* If you are an Android/Java developer, you are always welcome to fix/implement an issue/a feature yourself. PRs welcome!
|
||||
* We use English for development. Issues in other languages will be closed and ignored.
|
||||
|
||||
## Bugfixing
|
||||
* If you want to help NewPipe getting bug free, you can send me a mail to tnp@newpipe.schabi.org to let me know that you intent to help, and than register at our [sentry](https://support.schabi.org) setup.
|
||||
## 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.
|
||||
|
||||
## Translation
|
||||
|
||||
* NewPipe can be translated on [weblate](https://hosted.weblate.org/projects/newpipe/strings/)
|
||||
* NewPipe can be translated via [Weblate](https://hosted.weblate.org/projects/newpipe/strings/). You can log in there with your GitHub account.
|
||||
|
||||
## Code contribution
|
||||
|
||||
* Stick to NewPipe style guidelines (just look the other code and than do it the same way :) )
|
||||
* Do not bring nonfree software/binary blobs into the project (keep it google free)
|
||||
* 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 (Feature-branching)
|
||||
* When submitting changes, you agree that your code will be licensed under GPLv3
|
||||
* Please test (compile and run) your code before you submit changes!!!
|
||||
* Try to figure out you selves why CI fails, or why a merge request collides
|
||||
* Please maintain your code after you contributed it.
|
||||
* Respond yourselves if someone request changes or notifies issues
|
||||
* 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 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
|
||||
* 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 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 sumission, or clearly state that in the description of your PR.
|
||||
* Respond yourselves if someone requests changes or otherwise raises issues about your PRs.
|
||||
|
||||
## Communication
|
||||
|
||||
* WE DO NOW HAVE A MAILING LIST: [newpipe@list.schabi.org](https://list.schabi.org/cgi-bin/mailman/listinfo/newpipe).
|
||||
* If you want to get in contact with me or one of our other contributors you can send me an email at tnp(at)schabi.org
|
||||
* Feel free to post suggestions, changes, ideas etc!
|
||||
* 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 tracker described above!
|
||||
* Feel free to post suggestions, changes, ideas etc. on GitHub, IRC or the mailing list!
|
||||
|
||||
2
.github/PULL_REQUEST_TEAMPLATE.md
vendored
2
.github/PULL_REQUEST_TEAMPLATE.md
vendored
@@ -1 +1 @@
|
||||
[ ] I carefully reed the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md) and agree to them.
|
||||
- [ ] I carefully reed the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md) and agree to them.
|
||||
|
||||
@@ -5,10 +5,10 @@ android:
|
||||
components:
|
||||
# The BuildTools version used by NewPipe
|
||||
- tools
|
||||
- build-tools-25.0.2
|
||||
- build-tools-26.0.1
|
||||
|
||||
# The SDK version used to compile NewPipe
|
||||
- android-25
|
||||
- android-26
|
||||
|
||||
# Additional components
|
||||
- extra-android-m2repository
|
||||
@@ -17,4 +17,3 @@ script: ./gradlew -Dorg.gradle.jvmargs=-Xmx1536m assembleDebug lintDebug testDeb
|
||||
|
||||
licenses:
|
||||
- '.+'
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
WARNING: PUTTING NEWPIPE OR ANY FORK OF IT INTO GOOGLE PLAYSTORE VIOLATES THEIR TERMS OF CONDITIONS.
|
||||
|
||||
# NewPipe
|
||||
NewPipe: A free lightweight Youtube frontend for Android.
|
||||
NewPipe: A free lightweight YouTube frontend for Android.
|
||||
|
||||
[](https://newpipe.schabi.org)
|
||||
[](https://f-droid.org/packages/org.schabi.newpipe/)
|
||||
@@ -70,7 +70,7 @@ NewPipe does not use any Google framework libraries, or the YouTube API. It only
|
||||
Although NewPipe only supports YouTube at the moment, it's designed to support many more streaming services. The plan is, that NewPipe will get such support by the version 2.0.
|
||||
|
||||
## Contribution
|
||||
Whether you have ideas, translation, design changes, code cleaning, or real heavy code changes, help is always welcome.
|
||||
Whether you have ideas, translations, design changes, code cleaning, or real heavy code changes, help is always welcome.
|
||||
The more is done the better it gets!
|
||||
|
||||
If you'd like to get involved, check our [contribution notes](.github/CONTRIBUTING.md).
|
||||
|
||||
@@ -1,23 +1,31 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 25
|
||||
buildToolsVersion '25.0.2'
|
||||
compileSdkVersion 26
|
||||
buildToolsVersion '26.0.1'
|
||||
|
||||
defaultConfig {
|
||||
applicationId "org.schabi.newpipe"
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 25
|
||||
versionCode 37
|
||||
versionName "0.9.10"
|
||||
targetSdkVersion 26
|
||||
versionCode 38
|
||||
versionName "0.10.0"
|
||||
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
debug {
|
||||
multiDexEnabled true
|
||||
|
||||
debuggable true
|
||||
applicationIdSuffix ".debug"
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
@@ -37,25 +45,38 @@ dependencies {
|
||||
exclude module: 'support-annotations'
|
||||
}
|
||||
|
||||
compile 'com.github.TeamNewPipe:NewPipeExtractor:7ae274b'
|
||||
|
||||
testCompile 'junit:junit:4.12'
|
||||
testCompile 'org.mockito:mockito-core:1.10.19'
|
||||
testCompile 'org.json:json:20160810'
|
||||
|
||||
compile 'com.android.support:appcompat-v7:25.3.1'
|
||||
compile 'com.android.support:support-v4:25.3.1'
|
||||
compile 'com.android.support:design:25.3.1'
|
||||
compile 'com.android.support:recyclerview-v7:25.3.1'
|
||||
compile 'com.android.support:appcompat-v7:26.0.1'
|
||||
compile 'com.android.support:support-v4:26.0.1'
|
||||
compile 'com.android.support:design:26.0.1'
|
||||
compile 'com.android.support:recyclerview-v7:26.0.1'
|
||||
compile 'com.android.support:preference-v14:26.0.1'
|
||||
|
||||
compile 'com.google.code.gson:gson:2.7'
|
||||
compile 'org.jsoup:jsoup:1.8.3'
|
||||
compile 'org.mozilla:rhino:1.7.7'
|
||||
compile 'ch.acra:acra:4.9.0'
|
||||
compile 'info.guardianproject.netcipher:netcipher:1.2'
|
||||
|
||||
compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'
|
||||
compile 'de.hdodenhof:circleimageview:2.0.0'
|
||||
compile 'de.hdodenhof:circleimageview:2.1.0'
|
||||
compile 'com.github.nirhart:parallaxscroll:1.0'
|
||||
compile 'com.nononsenseapps:filepicker:3.0.0'
|
||||
compile 'com.google.android.exoplayer:exoplayer:r2.4.2'
|
||||
compile 'com.google.android.exoplayer:exoplayer:r2.5.1'
|
||||
|
||||
debugCompile 'com.facebook.stetho:stetho:1.5.0'
|
||||
debugCompile 'com.facebook.stetho:stetho-urlconnection:1.5.0'
|
||||
debugCompile 'com.android.support:multidex:1.0.1'
|
||||
|
||||
compile 'io.reactivex.rxjava2:rxjava:2.1.2'
|
||||
compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
|
||||
compile 'com.jakewharton.rxbinding2:rxbinding:2.0.0'
|
||||
|
||||
compile 'android.arch.persistence.room:runtime:1.0.0-alpha8'
|
||||
compile 'android.arch.persistence.room:rxjava2:1.0.0-alpha8'
|
||||
annotationProcessor 'android.arch.persistence.room:compiler:1.0.0-alpha8'
|
||||
|
||||
compile 'frankiesardo:icepick:3.2.0'
|
||||
provided 'frankiesardo:icepick-processor:3.2.0'
|
||||
}
|
||||
|
||||
10
app/proguard-rules.pro
vendored
10
app/proguard-rules.pro
vendored
@@ -15,3 +15,13 @@
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
-dontobfuscate
|
||||
-keep class org.mozilla.javascript.** { *; }
|
||||
|
||||
-keep class org.mozilla.classfile.ClassFileWriter
|
||||
-keep class com.google.android.exoplayer2.** { *; }
|
||||
|
||||
-dontwarn org.mozilla.javascript.tools.**
|
||||
-dontwarn android.arch.util.paging.CountedDataSource
|
||||
-dontwarn android.arch.persistence.room.paging.LimitOffsetDataSource
|
||||
17
app/src/debug/AndroidManifest.xml
Normal file
17
app/src/debug/AndroidManifest.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.schabi.newpipe">
|
||||
|
||||
<application
|
||||
android:name=".DebugApp"
|
||||
android:label="NewPipe Debug"
|
||||
tools:replace="android:name, android:label">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="NewPipe Debug"
|
||||
tools:replace="android:label"/>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
45
app/src/debug/java/org/schabi/newpipe/DebugApp.java
Normal file
45
app/src/debug/java/org/schabi/newpipe/DebugApp.java
Normal file
@@ -0,0 +1,45 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.multidex.MultiDex;
|
||||
|
||||
import com.facebook.stetho.Stetho;
|
||||
|
||||
public class DebugApp extends App {
|
||||
private static final String TAG = DebugApp.class.toString();
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context base) {
|
||||
super.attachBaseContext(base);
|
||||
MultiDex.install(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
initStetho();
|
||||
}
|
||||
|
||||
private void initStetho() {
|
||||
// Create an InitializerBuilder
|
||||
Stetho.InitializerBuilder initializerBuilder =
|
||||
Stetho.newInitializerBuilder(this);
|
||||
|
||||
// Enable Chrome DevTools
|
||||
initializerBuilder.enableWebKitInspector(
|
||||
Stetho.defaultInspectorModulesProvider(this)
|
||||
);
|
||||
|
||||
// Enable command line interface
|
||||
initializerBuilder.enableDumpapp(
|
||||
Stetho.defaultDumperPluginsProvider(getApplicationContext())
|
||||
);
|
||||
|
||||
// Use the InitializerBuilder to generate an Initializer
|
||||
Stetho.Initializer initializer = initializerBuilder.build();
|
||||
|
||||
// Initialize Stetho with the Initializer
|
||||
Stetho.initialize(initializer);
|
||||
}
|
||||
}
|
||||
@@ -16,11 +16,12 @@
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:logo="@mipmap/ic_launcher"
|
||||
android:theme="@style/AppTheme"
|
||||
android:theme="@style/DarkTheme"
|
||||
tools:ignore="AllowBackup">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name">
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTask">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
@@ -28,7 +29,7 @@
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".player.PlayVideoActivity"
|
||||
android:name=".player.old.PlayVideoActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:theme="@style/VideoPlayerTheme"
|
||||
tools:ignore="UnusedAttribute"/>
|
||||
@@ -51,6 +52,15 @@
|
||||
<activity
|
||||
android:name=".settings.SettingsActivity"
|
||||
android:label="@string/settings"/>
|
||||
|
||||
<activity
|
||||
android:name=".about.AboutActivity"
|
||||
android:label="@string/title_activity_about"/>
|
||||
|
||||
<activity
|
||||
android:name=".history.HistoryActivity"
|
||||
android:label="@string/title_activity_history"/>
|
||||
|
||||
<activity
|
||||
android:name=".PanicResponderActivity"
|
||||
android:launchMode="singleInstance"
|
||||
@@ -62,6 +72,7 @@
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ExitActivity"
|
||||
android:label="@string/general_error"
|
||||
@@ -72,8 +83,7 @@
|
||||
<activity
|
||||
android:name=".download.DownloadActivity"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/AppTheme"/>
|
||||
android:launchMode="singleTask"/>
|
||||
|
||||
<service android:name="us.shandian.giga.service.DownloadManagerService"/>
|
||||
|
||||
@@ -82,6 +92,7 @@
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/FilePickerTheme"/>
|
||||
|
||||
<activity
|
||||
android:name=".ReCaptchaActivity"
|
||||
android:label="@string/reCaptchaActivity"/>
|
||||
@@ -121,6 +132,8 @@
|
||||
<!-- channel prefix -->
|
||||
<data android:pathPrefix="/channel/"/>
|
||||
<data android:pathPrefix="/user/"/>
|
||||
<!-- playlist prefix -->
|
||||
<data android:pathPrefix="/playlist"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
@@ -154,12 +167,11 @@
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".RouterPopupActivity"
|
||||
android:label="@string/popup_mode_share_menu_title"
|
||||
android:taskAffinity=""
|
||||
android:theme="@android:style/Theme.NoDisplay"
|
||||
android:label="@string/popup_mode_share_menu_title">
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
|
||||
@@ -209,11 +221,5 @@
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".about.AboutActivity"
|
||||
android:label="@string/title_activity_about"
|
||||
android:theme="@style/AppTheme">
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
@@ -1,6 +1,6 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
/**
|
||||
/*
|
||||
* Created by Christian Schabesberger on 24.12.15.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.Application;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
|
||||
@@ -16,12 +20,20 @@ import org.schabi.newpipe.report.AcraReportSenderFactory;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.settings.SettingsActivity;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
|
||||
import info.guardianproject.netcipher.NetCipher;
|
||||
import info.guardianproject.netcipher.proxy.OrbotHelper;
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.net.SocketException;
|
||||
|
||||
/**
|
||||
import io.reactivex.annotations.NonNull;
|
||||
import io.reactivex.exceptions.CompositeException;
|
||||
import io.reactivex.exceptions.UndeliverableException;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import io.reactivex.plugins.RxJavaPlugins;
|
||||
|
||||
/*
|
||||
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
||||
* App.java is part of NewPipe.
|
||||
*
|
||||
@@ -40,73 +52,101 @@ import info.guardianproject.netcipher.proxy.OrbotHelper;
|
||||
*/
|
||||
|
||||
public class App extends Application {
|
||||
private static final String TAG = App.class.toString();
|
||||
protected static final String TAG = App.class.toString();
|
||||
|
||||
private static boolean useTor;
|
||||
@SuppressWarnings("unchecked")
|
||||
private static final Class<? extends ReportSenderFactory>[] reportSenderFactoryClasses = new Class[]{AcraReportSenderFactory.class};
|
||||
|
||||
final Class<? extends ReportSenderFactory>[] reportSenderFactoryClasses
|
||||
= new Class[]{AcraReportSenderFactory.class};
|
||||
@Override
|
||||
protected void attachBaseContext(Context base) {
|
||||
super.attachBaseContext(base);
|
||||
|
||||
initACRA();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
// init crashreport
|
||||
try {
|
||||
final ACRAConfiguration acraConfig = new ConfigurationBuilder(this)
|
||||
.setReportSenderFactoryClasses(reportSenderFactoryClasses)
|
||||
.build();
|
||||
ACRA.init(this, acraConfig);
|
||||
} catch(ACRAConfigurationException ace) {
|
||||
ace.printStackTrace();
|
||||
ErrorActivity.reportError(this, ace, null, null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.SEARCHED,"none",
|
||||
"Could not initialize ACRA crash report", R.string.app_ui_crash));
|
||||
}
|
||||
// Initialize settings first because others inits can use its values
|
||||
SettingsActivity.initSettings(this);
|
||||
|
||||
//init NewPipe
|
||||
NewPipe.init(Downloader.getInstance());
|
||||
NewPipeDatabase.init(this);
|
||||
StateSaver.init(this);
|
||||
initNotificationChannel();
|
||||
|
||||
// Initialize image loader
|
||||
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(this).build();
|
||||
ImageLoader.getInstance().init(config);
|
||||
|
||||
/*
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
if(prefs.getBoolean(getString(R.string.use_tor_key), false)) {
|
||||
OrbotHelper.requestStartTor(this);
|
||||
configureTor(true);
|
||||
} else {
|
||||
configureTor(false);
|
||||
}*/
|
||||
configureTor(false);
|
||||
|
||||
// DO NOT REMOVE THIS FUNCTION!!!
|
||||
// Otherwise downloadPathPreference has invalid value.
|
||||
SettingsActivity.initSettings(this);
|
||||
|
||||
ThemeHelper.setTheme(getApplicationContext());
|
||||
configureRxJavaErrorHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the proxy settings based on whether Tor should be enabled or not.
|
||||
*/
|
||||
public static void configureTor(boolean enabled) {
|
||||
useTor = enabled;
|
||||
if (useTor) {
|
||||
NetCipher.useTor();
|
||||
} else {
|
||||
NetCipher.setProxy(null);
|
||||
private void configureRxJavaErrorHandler() {
|
||||
// https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling
|
||||
RxJavaPlugins.setErrorHandler(new Consumer<Throwable>() {
|
||||
@Override
|
||||
public void accept(@NonNull Throwable throwable) throws Exception {
|
||||
Log.e(TAG, "RxJavaPlugins.ErrorHandler called with -> : throwable = [" + throwable.getClass().getName() + "]");
|
||||
|
||||
if (throwable instanceof UndeliverableException) {
|
||||
// As UndeliverableException is a wrapper, get the cause of it to get the "real" exception
|
||||
throwable = throwable.getCause();
|
||||
}
|
||||
|
||||
if (throwable instanceof CompositeException) {
|
||||
for (Throwable element : ((CompositeException) throwable).getExceptions()) {
|
||||
if (checkThrowable(element)) return;
|
||||
}
|
||||
}
|
||||
|
||||
if (checkThrowable(throwable)) return;
|
||||
|
||||
// Throw uncaught exception that will trigger the report system
|
||||
Thread.currentThread().getUncaughtExceptionHandler()
|
||||
.uncaughtException(Thread.currentThread(), throwable);
|
||||
}
|
||||
|
||||
private boolean checkThrowable(@NonNull Throwable throwable) {
|
||||
// Don't crash the application over a simple network problem
|
||||
return ExtractorHelper.hasAssignableCauseThrowable(throwable,
|
||||
IOException.class, SocketException.class, InterruptedException.class, InterruptedIOException.class);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private void initACRA() {
|
||||
try {
|
||||
final ACRAConfiguration acraConfig = new ConfigurationBuilder(this)
|
||||
.setReportSenderFactoryClasses(reportSenderFactoryClasses)
|
||||
.setBuildConfigClass(BuildConfig.class)
|
||||
.build();
|
||||
ACRA.init(this, acraConfig);
|
||||
} catch (ACRAConfigurationException ace) {
|
||||
ace.printStackTrace();
|
||||
ErrorActivity.reportError(this, ace, null, null, ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
"Could not initialize ACRA crash report", R.string.app_ui_crash));
|
||||
}
|
||||
}
|
||||
|
||||
public static void checkStartTor(Context context) {
|
||||
if (useTor) {
|
||||
OrbotHelper.requestStartTor(context);
|
||||
public void initNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isUsingTor() {
|
||||
return useTor;
|
||||
final String id = getString(R.string.notification_channel_id);
|
||||
final CharSequence name = getString(R.string.notification_channel_name);
|
||||
final String description = getString(R.string.notification_channel_description);
|
||||
|
||||
// Keep this below DEFAULT to avoid making noise on every notification update
|
||||
final int importance = NotificationManager.IMPORTANCE_LOW;
|
||||
|
||||
NotificationChannel mChannel = new NotificationChannel(id, name, importance);
|
||||
mChannel.setDescription(description);
|
||||
|
||||
NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
mNotificationManager.createNotificationChannel(mChannel);
|
||||
}
|
||||
}
|
||||
|
||||
121
app/src/main/java/org/schabi/newpipe/BaseFragment.java
Normal file
121
app/src/main/java/org/schabi/newpipe/BaseFragment.java
Normal file
@@ -0,0 +1,121 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.AttrRes;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer;
|
||||
|
||||
import icepick.Icepick;
|
||||
|
||||
public abstract class BaseFragment extends Fragment {
|
||||
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
|
||||
protected boolean DEBUG = MainActivity.DEBUG;
|
||||
|
||||
protected AppCompatActivity activity;
|
||||
public static final ImageLoader imageLoader = ImageLoader.getInstance();
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Fragment's Lifecycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
activity = (AppCompatActivity) context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
super.onDetach();
|
||||
activity = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
|
||||
super.onCreate(savedInstanceState);
|
||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
||||
if (savedInstanceState != null) onRestoreInstanceState(savedInstanceState);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View rootView, Bundle savedInstanceState) {
|
||||
super.onViewCreated(rootView, savedInstanceState);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onViewCreated() called with: rootView = [" + rootView + "], savedInstanceState = [" + savedInstanceState + "]");
|
||||
}
|
||||
initViews(rootView, savedInstanceState);
|
||||
initListeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
Icepick.saveInstanceState(this, outState);
|
||||
}
|
||||
|
||||
protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
}
|
||||
|
||||
protected void initListeners() {
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected final int resolveResourceIdFromAttr(@AttrRes int attr) {
|
||||
TypedArray a = activity.getTheme().obtainStyledAttributes(new int[]{attr});
|
||||
int attributeResourceId = a.getResourceId(0, 0);
|
||||
a.recycle();
|
||||
return attributeResourceId;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// DisplayImageOptions default configurations
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public static final DisplayImageOptions BASE_OPTIONS =
|
||||
new DisplayImageOptions.Builder().cacheInMemory(true).build();
|
||||
|
||||
public static final DisplayImageOptions DISPLAY_AVATAR_OPTIONS =
|
||||
new DisplayImageOptions.Builder()
|
||||
.cloneFrom(BASE_OPTIONS)
|
||||
.showImageOnLoading(R.drawable.buddy)
|
||||
.showImageForEmptyUri(R.drawable.buddy)
|
||||
.showImageOnFail(R.drawable.buddy)
|
||||
.build();
|
||||
|
||||
public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
|
||||
new DisplayImageOptions.Builder()
|
||||
.cloneFrom(BASE_OPTIONS)
|
||||
.displayer(new FadeInBitmapDisplayer(250))
|
||||
.showImageForEmptyUri(R.drawable.dummy_thumbnail)
|
||||
.showImageOnFail(R.drawable.dummy_thumbnail)
|
||||
.build();
|
||||
|
||||
public static final DisplayImageOptions DISPLAY_BANNER_OPTIONS =
|
||||
new DisplayImageOptions.Builder()
|
||||
.cloneFrom(BASE_OPTIONS)
|
||||
.showImageOnLoading(R.drawable.channel_banner)
|
||||
.showImageForEmptyUri(R.drawable.channel_banner)
|
||||
.showImageOnFail(R.drawable.channel_banner)
|
||||
.build();
|
||||
}
|
||||
@@ -1,20 +1,24 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.net.URL;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
|
||||
|
||||
/**
|
||||
/*
|
||||
* Created by Christian Schabesberger on 28.01.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
@@ -35,16 +39,17 @@ import javax.net.ssl.HttpsURLConnection;
|
||||
*/
|
||||
|
||||
public class Downloader implements org.schabi.newpipe.extractor.Downloader {
|
||||
|
||||
private static final String USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:43.0) Gecko/20100101 Firefox/43.0";
|
||||
|
||||
public static final String USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:43.0) Gecko/20100101 Firefox/43.0";
|
||||
private static String mCookies = "";
|
||||
|
||||
private static Downloader instance = null;
|
||||
|
||||
private Downloader() {}
|
||||
private Downloader() {
|
||||
}
|
||||
|
||||
public static Downloader getInstance() {
|
||||
if(instance == null) {
|
||||
if (instance == null) {
|
||||
synchronized (Downloader.class) {
|
||||
if (instance == null) {
|
||||
instance = new Downloader();
|
||||
@@ -62,41 +67,66 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader {
|
||||
return Downloader.mCookies;
|
||||
}
|
||||
|
||||
/**Download the text file at the supplied URL as in download(String),
|
||||
/**
|
||||
* Download the text file at the supplied URL as in download(String),
|
||||
* but set the HTTP header field "Accept-Language" to the supplied string.
|
||||
* @param siteUrl the URL of the text file to return the contents of
|
||||
*
|
||||
* @param siteUrl the URL of the text file to return the contents of
|
||||
* @param language the language (usually a 2-character code) to set as the preferred language
|
||||
* @return the contents of the specified text file*/
|
||||
* @return the contents of the specified text file
|
||||
*/
|
||||
@Override
|
||||
public String download(String siteUrl, String language) throws IOException, ReCaptchaException {
|
||||
Map<String, String> requestProperties = new HashMap<>();
|
||||
requestProperties.put("Accept-Language", language);
|
||||
return download(siteUrl, requestProperties);
|
||||
}
|
||||
|
||||
|
||||
/**Download the text file at the supplied URL as in download(String),
|
||||
* but set the HTTP header field "Accept-Language" to the supplied string.
|
||||
* @param siteUrl the URL of the text file to return the contents of
|
||||
/**
|
||||
* Download the text file at the supplied URL as in download(String),
|
||||
* but set the HTTP headers included in the customProperties map.
|
||||
*
|
||||
* @param siteUrl the URL of the text file to return the contents of
|
||||
* @param customProperties set request header properties
|
||||
* @return the contents of the specified text file
|
||||
* @throws IOException*/
|
||||
* @throws IOException
|
||||
*/
|
||||
@Override
|
||||
public String download(String siteUrl, Map<String, String> customProperties) throws IOException, ReCaptchaException {
|
||||
URL url = new URL(siteUrl);
|
||||
HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
|
||||
Iterator it = customProperties.entrySet().iterator();
|
||||
while(it.hasNext()) {
|
||||
Map.Entry pair = (Map.Entry)it.next();
|
||||
con.setRequestProperty((String)pair.getKey(), (String)pair.getValue());
|
||||
while (it.hasNext()) {
|
||||
Map.Entry pair = (Map.Entry) it.next();
|
||||
con.setRequestProperty((String) pair.getKey(), (String) pair.getValue());
|
||||
}
|
||||
return dl(con);
|
||||
}
|
||||
|
||||
/**Common functionality between download(String url) and download(String url, String language)*/
|
||||
/**
|
||||
* Download (via HTTP) the text file located at the supplied URL, and return its contents.
|
||||
* Primarily intended for downloading web pages.
|
||||
*
|
||||
* @param siteUrl the URL of the text file to download
|
||||
* @return the contents of the specified text file
|
||||
*/
|
||||
@Override
|
||||
public String download(String siteUrl) throws IOException, ReCaptchaException {
|
||||
URL url = new URL(siteUrl);
|
||||
HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
|
||||
//HttpsURLConnection con = NetCipher.getHttpsURLConnection(url);
|
||||
return dl(con);
|
||||
}
|
||||
|
||||
/**
|
||||
* Common functionality between download(String url) and download(String url, String language)
|
||||
*/
|
||||
private static String dl(HttpsURLConnection con) throws IOException, ReCaptchaException {
|
||||
StringBuilder response = new StringBuilder();
|
||||
BufferedReader in = null;
|
||||
|
||||
try {
|
||||
con.setReadTimeout(30 * 1000);// 30s
|
||||
con.setRequestMethod("GET");
|
||||
con.setRequestProperty("User-Agent", USER_AGENT);
|
||||
|
||||
@@ -104,17 +134,22 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader {
|
||||
con.setRequestProperty("Cookie", getCookies());
|
||||
}
|
||||
|
||||
in = new BufferedReader(
|
||||
new InputStreamReader(con.getInputStream()));
|
||||
in = new BufferedReader(new InputStreamReader(con.getInputStream()));
|
||||
for (Map.Entry<String, List<String>> entry : con.getHeaderFields().entrySet()) {
|
||||
System.err.println(entry.getKey() + ": " + entry.getValue());
|
||||
}
|
||||
String inputLine;
|
||||
|
||||
while((inputLine = in.readLine()) != null) {
|
||||
while ((inputLine = in.readLine()) != null) {
|
||||
response.append(inputLine);
|
||||
}
|
||||
} catch(UnknownHostException uhe) {//thrown when there's no internet connection
|
||||
throw new IOException("unknown host or no network", uhe);
|
||||
//Toast.makeText(getActivity(), uhe.getMessage(), Toast.LENGTH_LONG).show();
|
||||
} catch(Exception e) {
|
||||
} catch (Exception e) {
|
||||
Log.e("Downloader", "dl() ----- Exception thrown → " + e.getClass().getName());
|
||||
|
||||
if (ExtractorHelper.isInterruptedCaused(e)) {
|
||||
throw new InterruptedIOException(e.getMessage());
|
||||
}
|
||||
|
||||
/*
|
||||
* HTTP 429 == Too Many Request
|
||||
* Receive from Youtube.com = ReCaptcha challenge request
|
||||
@@ -123,24 +158,14 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader {
|
||||
if (con.getResponseCode() == 429) {
|
||||
throw new ReCaptchaException("reCaptcha Challenge requested");
|
||||
}
|
||||
throw new IOException(e);
|
||||
|
||||
throw new IOException(con.getResponseCode() + " " + con.getResponseMessage(), e);
|
||||
} finally {
|
||||
if(in != null) {
|
||||
if (in != null) {
|
||||
in.close();
|
||||
}
|
||||
}
|
||||
|
||||
return response.toString();
|
||||
}
|
||||
|
||||
/**Download (via HTTP) the text file located at the supplied URL, and return its contents.
|
||||
* Primarily intended for downloading web pages.
|
||||
* @param siteUrl the URL of the text file to download
|
||||
* @return the contents of the specified text file*/
|
||||
public String download(String siteUrl) throws IOException, ReCaptchaException {
|
||||
URL url = new URL(siteUrl);
|
||||
HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
|
||||
//HttpsURLConnection con = NetCipher.getHttpsURLConnection(url);
|
||||
return dl(con);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
|
||||
/**
|
||||
/*
|
||||
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
||||
* ExitActivity.java is part of NewPipe.
|
||||
*
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.view.View;
|
||||
|
||||
import com.nostra13.universalimageloader.core.assist.FailReason;
|
||||
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
|
||||
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 01.08.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* StreamInfoItemViewCreator.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class ImageErrorLoadingListener implements ImageLoadingListener {
|
||||
|
||||
private int serviceId = -1;
|
||||
private Context context = null;
|
||||
private View rootView = null;
|
||||
|
||||
public ImageErrorLoadingListener(Context context, View rootView, int serviceId) {
|
||||
this.context = context;
|
||||
this.serviceId= serviceId;
|
||||
this.rootView = rootView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadingStarted(String imageUri, View view) {}
|
||||
|
||||
@Override
|
||||
public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
|
||||
ErrorActivity.reportError(context,
|
||||
failReason.getCause(), null, rootView,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.LOAD_IMAGE,
|
||||
NewPipe.getNameOfService(serviceId), imageUri,
|
||||
R.string.could_not_load_image));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadingCancelled(String imageUri, View view) {}
|
||||
}
|
||||
@@ -23,7 +23,10 @@ package org.schabi.newpipe;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
@@ -34,19 +37,37 @@ import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import org.schabi.newpipe.download.DownloadActivity;
|
||||
import org.schabi.newpipe.database.AppDatabase;
|
||||
import org.schabi.newpipe.database.history.dao.HistoryDAO;
|
||||
import org.schabi.newpipe.database.history.dao.SearchHistoryDAO;
|
||||
import org.schabi.newpipe.database.history.dao.WatchHistoryDAO;
|
||||
import org.schabi.newpipe.database.history.model.HistoryEntry;
|
||||
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
||||
import org.schabi.newpipe.database.history.model.WatchHistoryEntry;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.fragments.BackPressable;
|
||||
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
||||
import org.schabi.newpipe.fragments.search.SearchFragment;
|
||||
import org.schabi.newpipe.settings.SettingsActivity;
|
||||
import org.schabi.newpipe.fragments.list.search.SearchFragment;
|
||||
import org.schabi.newpipe.history.HistoryListener;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
import java.util.Date;
|
||||
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.subjects.PublishSubject;
|
||||
|
||||
public class MainActivity extends AppCompatActivity implements HistoryListener {
|
||||
private static final String TAG = "MainActivity";
|
||||
public static final boolean DEBUG = false;
|
||||
private SharedPreferences sharedPreferences;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Activity's LifeCycle
|
||||
@@ -63,8 +84,21 @@ public class MainActivity extends AppCompatActivity {
|
||||
initFragments();
|
||||
}
|
||||
|
||||
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
|
||||
initHistory();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (!isChangingConfigurations()) {
|
||||
StateSaver.clearStateFiles();
|
||||
}
|
||||
|
||||
disposeHistory();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -75,7 +109,14 @@ public class MainActivity extends AppCompatActivity {
|
||||
if (sharedPreferences.getBoolean(Constants.KEY_THEME_CHANGE, false)) {
|
||||
if (DEBUG) Log.d(TAG, "Theme has changed, recreating activity...");
|
||||
sharedPreferences.edit().putBoolean(Constants.KEY_THEME_CHANGE, false).apply();
|
||||
this.recreate();
|
||||
// https://stackoverflow.com/questions/10844112/runtimeexception-performing-pause-of-activity-that-is-not-resumed
|
||||
// Briefly, let the activity resume properly posting the recreate call to end of the message queue
|
||||
new Handler(Looper.getMainLooper()).post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
MainActivity.this.recreate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -100,7 +141,10 @@ public class MainActivity extends AppCompatActivity {
|
||||
if (DEBUG) Log.d(TAG, "onBackPressed() called");
|
||||
|
||||
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
|
||||
if (fragment instanceof VideoDetailFragment) if (((VideoDetailFragment) fragment).onActivityBackPressed()) return;
|
||||
// If current fragment implements BackPressable (i.e. can/wanna handle back press) delegate the back press to it
|
||||
if (fragment instanceof BackPressable) {
|
||||
if (((BackPressable) fragment).onBackPressed()) return;
|
||||
}
|
||||
|
||||
|
||||
if (getSupportFragmentManager().getBackStackEntryCount() == 1) {
|
||||
@@ -143,20 +187,20 @@ public class MainActivity extends AppCompatActivity {
|
||||
int id = item.getItemId();
|
||||
|
||||
switch (id) {
|
||||
case android.R.id.home: {
|
||||
case android.R.id.home:
|
||||
NavigationHelper.gotoMainFragment(getSupportFragmentManager());
|
||||
return true;
|
||||
}
|
||||
case R.id.action_settings: {
|
||||
case R.id.action_settings:
|
||||
NavigationHelper.openSettings(this);
|
||||
return true;
|
||||
}
|
||||
case R.id.action_show_downloads: {
|
||||
case R.id.action_show_downloads:
|
||||
return NavigationHelper.openDownloads(this);
|
||||
}
|
||||
case R.id.action_about:
|
||||
NavigationHelper.openAbout(this);
|
||||
return true;
|
||||
case R.id.action_history:
|
||||
NavigationHelper.openHistory(this);
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
@@ -167,6 +211,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void initFragments() {
|
||||
if (DEBUG) Log.d(TAG, "initFragments() called");
|
||||
StateSaver.clearStateFiles();
|
||||
if (getIntent() != null && getIntent().hasExtra(Constants.KEY_LINK_TYPE)) {
|
||||
handleIntent(getIntent());
|
||||
} else NavigationHelper.gotoMainFragment(getSupportFragmentManager());
|
||||
@@ -191,6 +237,9 @@ public class MainActivity extends AppCompatActivity {
|
||||
case CHANNEL:
|
||||
NavigationHelper.openChannelFragment(getSupportFragmentManager(), serviceId, url, title);
|
||||
break;
|
||||
case PLAYLIST:
|
||||
NavigationHelper.openPlaylistFragment(getSupportFragmentManager(), serviceId, url, title);
|
||||
break;
|
||||
}
|
||||
} else if (intent.hasExtra(Constants.KEY_OPEN_SEARCH)) {
|
||||
String searchQuery = intent.getStringExtra(Constants.KEY_QUERY);
|
||||
@@ -201,4 +250,75 @@ public class MainActivity extends AppCompatActivity {
|
||||
NavigationHelper.gotoMainFragment(getSupportFragmentManager());
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// History
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private WatchHistoryDAO watchHistoryDAO;
|
||||
private SearchHistoryDAO searchHistoryDAO;
|
||||
private PublishSubject<HistoryEntry> historyEntrySubject;
|
||||
private Disposable disposable;
|
||||
|
||||
private void initHistory() {
|
||||
final AppDatabase database = NewPipeDatabase.getInstance();
|
||||
watchHistoryDAO = database.watchHistoryDAO();
|
||||
searchHistoryDAO = database.searchHistoryDAO();
|
||||
historyEntrySubject = PublishSubject.create();
|
||||
disposable = historyEntrySubject
|
||||
.observeOn(Schedulers.io())
|
||||
.subscribe(getHistoryEntryConsumer());
|
||||
}
|
||||
|
||||
private void disposeHistory() {
|
||||
if (disposable != null) disposable.dispose();
|
||||
watchHistoryDAO = null;
|
||||
searchHistoryDAO = null;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Consumer<HistoryEntry> getHistoryEntryConsumer() {
|
||||
return new Consumer<HistoryEntry>() {
|
||||
@Override
|
||||
public void accept(HistoryEntry historyEntry) throws Exception {
|
||||
//noinspection unchecked
|
||||
HistoryDAO<HistoryEntry> historyDAO = (HistoryDAO<HistoryEntry>)
|
||||
(historyEntry instanceof SearchHistoryEntry ? searchHistoryDAO : watchHistoryDAO);
|
||||
|
||||
HistoryEntry latestEntry = historyDAO.getLatestEntry();
|
||||
if (historyEntry.hasEqualValues(latestEntry)) {
|
||||
latestEntry.setCreationDate(historyEntry.getCreationDate());
|
||||
historyDAO.update(latestEntry);
|
||||
} else {
|
||||
historyDAO.insert(historyEntry);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void addWatchHistoryEntry(StreamInfo streamInfo) {
|
||||
if (sharedPreferences.getBoolean(getString(R.string.enable_watch_history_key), true)) {
|
||||
WatchHistoryEntry entry = new WatchHistoryEntry(streamInfo);
|
||||
historyEntrySubject.onNext(entry);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVideoPlayed(StreamInfo streamInfo, VideoStream videoStream) {
|
||||
addWatchHistoryEntry(streamInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioPlayed(StreamInfo streamInfo, AudioStream audioStream) {
|
||||
addWatchHistoryEntry(streamInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSearch(int serviceId, String query) {
|
||||
// Add search history entry
|
||||
if (sharedPreferences.getBoolean(getString(R.string.enable_search_history_key), true)) {
|
||||
SearchHistoryEntry searchHistoryEntry = new SearchHistoryEntry(new Date(), serviceId, query);
|
||||
historyEntrySubject.onNext(searchHistoryEntry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
31
app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java
Normal file
31
app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.arch.persistence.room.Room;
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.database.AppDatabase;
|
||||
|
||||
import static org.schabi.newpipe.database.AppDatabase.DATABASE_NAME;
|
||||
|
||||
public final class NewPipeDatabase {
|
||||
|
||||
private static AppDatabase databaseInstance;
|
||||
|
||||
private NewPipeDatabase() {
|
||||
//no instance
|
||||
}
|
||||
|
||||
public static void init(Context context) {
|
||||
databaseInstance = Room.databaseBuilder(context.getApplicationContext(),
|
||||
AppDatabase.class, DATABASE_NAME
|
||||
).build();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static AppDatabase getInstance() {
|
||||
if (databaseInstance == null) throw new RuntimeException("Database not initialized");
|
||||
|
||||
return databaseInstance;
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,8 @@ import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.media.AudioManager;
|
||||
|
||||
/**
|
||||
/*
|
||||
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
||||
* PanicResponderActivity.java is part of NewPipe.
|
||||
*
|
||||
|
||||
@@ -16,7 +16,7 @@ import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
|
||||
/**
|
||||
/*
|
||||
* Created by beneth <bmauduit@beneth.fr> on 06.12.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
@@ -49,7 +49,7 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
||||
// Set return to Cancel by default
|
||||
setResult(RESULT_CANCELED);
|
||||
|
||||
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
@@ -59,7 +59,7 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
||||
actionBar.setDisplayShowTitleEnabled(true);
|
||||
}
|
||||
|
||||
WebView myWebView = (WebView) findViewById(R.id.reCaptchaWebView);
|
||||
WebView myWebView = findViewById(R.id.reCaptchaWebView);
|
||||
|
||||
// Enable Javascript
|
||||
WebSettings webSettings = myWebView.getSettings();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
@@ -32,7 +32,7 @@ import java.util.HashSet;
|
||||
* This Acitivty is designed to route share/open intents to the specified service, and
|
||||
* to the part of the service which can handle the url.
|
||||
*/
|
||||
public class RouterActivity extends Activity {
|
||||
public class RouterActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@@ -40,8 +40,6 @@ public class RouterActivity extends Activity {
|
||||
|
||||
String videoUrl = getUrl(getIntent());
|
||||
handleUrl(videoUrl);
|
||||
|
||||
finish();
|
||||
}
|
||||
|
||||
protected void handleUrl(String url) {
|
||||
@@ -50,6 +48,8 @@ public class RouterActivity extends Activity {
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
finish();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.player.PopupVideoPlayer;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
@@ -22,8 +23,10 @@ public class RouterPopupActivity extends RouterActivity {
|
||||
Toast.makeText(this, R.string.msg_popup_permission, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
StreamingService service = NewPipe.getServiceByUrl(url);
|
||||
if (service == null) {
|
||||
StreamingService service;
|
||||
try {
|
||||
service = NewPipe.getServiceByUrl(url);
|
||||
} catch (ExtractionException e) {
|
||||
Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
@@ -40,5 +43,7 @@ public class RouterPopupActivity extends RouterActivity {
|
||||
callIntent.putExtra(Constants.KEY_URL, url);
|
||||
callIntent.putExtra(Constants.KEY_SERVICE_ID, service.getServiceId());
|
||||
startService(callIntent);
|
||||
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,11 +36,13 @@ public class AboutActivity extends AppCompatActivity {
|
||||
new SoftwareComponent("Rhino", "2015", "Mozilla", "https://www.mozilla.org/rhino/", StandardLicenses.MPL2),
|
||||
new SoftwareComponent("ACRA", "2013", "Kevin Gaudin", "http://www.acra.ch", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("Universal Image Loader", "2011 - 2015", "Sergey Tarasevich", "https://github.com/nostra13/Android-Universal-Image-Loader", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("Netcipher", "2015", "The Guardian Project", "https://guardianproject.info/code/netcipher/", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("CircleImageView", "2014 - 2017", "Henning Dodenhof", "https://github.com/hdodenhof/CircleImageView", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("ParalaxScrollView", "2014", "Nir Hartmann", "https://github.com/nirhart/ParallaxScroll", StandardLicenses.MIT),
|
||||
new SoftwareComponent("NoNonsense-FilePicker", "2016", "Jonas Kalderstam", "https://github.com/spacecowboy/NoNonsense-FilePicker", StandardLicenses.MPL2),
|
||||
new SoftwareComponent("ExoPlayer", "2014-2017", "Google Inc", "https://github.com/google/ExoPlayer", StandardLicenses.APACHE2)
|
||||
new SoftwareComponent("ExoPlayer", "2014-2017", "Google Inc", "https://github.com/google/ExoPlayer", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("RxAndroid", "2015", "The RxAndroid authors", "https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("RxJava", "2016-present", "RxJava Contributors", "https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("RxBinding", "2015", "Jake Wharton", "https://github.com/JakeWharton/RxBinding", StandardLicenses.APACHE2)
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -65,7 +67,7 @@ public class AboutActivity extends AppCompatActivity {
|
||||
|
||||
setContentView(R.layout.activity_about);
|
||||
|
||||
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
// Create the adapter that will return a fragment for each of the three
|
||||
@@ -73,10 +75,10 @@ public class AboutActivity extends AppCompatActivity {
|
||||
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
|
||||
|
||||
// Set up the ViewPager with the sections adapter.
|
||||
mViewPager = (ViewPager) findViewById(R.id.container);
|
||||
mViewPager = findViewById(R.id.container);
|
||||
mViewPager.setAdapter(mSectionsPagerAdapter);
|
||||
|
||||
TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
|
||||
TabLayout tabLayout = findViewById(R.id.tabs);
|
||||
tabLayout.setupWithViewPager(mViewPager);
|
||||
}
|
||||
|
||||
@@ -127,7 +129,7 @@ public class AboutActivity extends AppCompatActivity {
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View rootView = inflater.inflate(R.layout.fragment_about, container, false);
|
||||
TextView version = (TextView) rootView.findViewById(R.id.app_version);
|
||||
TextView version = rootView.findViewById(R.id.app_version);
|
||||
version.setText(BuildConfig.VERSION_NAME);
|
||||
|
||||
View githubLink = rootView.findViewById(R.id.github_link);
|
||||
|
||||
@@ -3,9 +3,6 @@ package org.schabi.newpipe.about;
|
||||
import android.net.Uri;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
|
||||
/**
|
||||
* A software license
|
||||
|
||||
@@ -87,12 +87,12 @@ public class LicenseFragment extends Fragment {
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View rootView = inflater.inflate(R.layout.fragment_licenses, container, false);
|
||||
ViewGroup softwareComponentsView = (ViewGroup) rootView.findViewById(R.id.software_components);
|
||||
ViewGroup softwareComponentsView = rootView.findViewById(R.id.software_components);
|
||||
|
||||
for (final SoftwareComponent component : softwareComponents) {
|
||||
View componentView = inflater.inflate(R.layout.item_software_component, container, false);
|
||||
TextView softwareName = (TextView) componentView.findViewById(R.id.name);
|
||||
TextView copyright = (TextView) componentView.findViewById(R.id.copyright);
|
||||
TextView softwareName = componentView.findViewById(R.id.name);
|
||||
TextView copyright = componentView.findViewById(R.id.copyright);
|
||||
softwareName.setText(component.getName());
|
||||
copyright.setText(getContext().getString(R.string.copyright,
|
||||
component.getYears(),
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package org.schabi.newpipe.database;
|
||||
|
||||
import android.arch.persistence.room.Database;
|
||||
import android.arch.persistence.room.RoomDatabase;
|
||||
import android.arch.persistence.room.TypeConverters;
|
||||
|
||||
import org.schabi.newpipe.database.history.Converters;
|
||||
import org.schabi.newpipe.database.history.dao.SearchHistoryDAO;
|
||||
import org.schabi.newpipe.database.history.dao.WatchHistoryDAO;
|
||||
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
||||
import org.schabi.newpipe.database.history.model.WatchHistoryEntry;
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionDAO;
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||
|
||||
@TypeConverters({Converters.class})
|
||||
@Database(entities = {SubscriptionEntity.class, WatchHistoryEntry.class, SearchHistoryEntry.class}, version = 1, exportSchema = false)
|
||||
public abstract class AppDatabase extends RoomDatabase {
|
||||
|
||||
public static final String DATABASE_NAME = "newpipe.db";
|
||||
|
||||
public abstract SubscriptionDAO subscriptionDAO();
|
||||
|
||||
public abstract WatchHistoryDAO watchHistoryDAO();
|
||||
|
||||
public abstract SearchHistoryDAO searchHistoryDAO();
|
||||
}
|
||||
49
app/src/main/java/org/schabi/newpipe/database/BasicDAO.java
Normal file
49
app/src/main/java/org/schabi/newpipe/database/BasicDAO.java
Normal file
@@ -0,0 +1,49 @@
|
||||
package org.schabi.newpipe.database;
|
||||
|
||||
import android.arch.persistence.room.Dao;
|
||||
import android.arch.persistence.room.Delete;
|
||||
import android.arch.persistence.room.Insert;
|
||||
import android.arch.persistence.room.OnConflictStrategy;
|
||||
import android.arch.persistence.room.Update;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
|
||||
@Dao
|
||||
public interface BasicDAO<Entity> {
|
||||
/* Inserts */
|
||||
@Insert(onConflict = OnConflictStrategy.FAIL)
|
||||
long insert(final Entity entity);
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.FAIL)
|
||||
List<Long> insertAll(final Entity... entities);
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.FAIL)
|
||||
List<Long> insertAll(final Collection<Entity> entities);
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
long upsert(final Entity entity);
|
||||
|
||||
/* Searches */
|
||||
Flowable<List<Entity>> getAll();
|
||||
|
||||
Flowable<List<Entity>> listByService(int serviceId);
|
||||
|
||||
/* Deletes */
|
||||
@Delete
|
||||
int delete(final Entity entity);
|
||||
|
||||
@Delete
|
||||
int delete(final Collection<Entity> entities);
|
||||
|
||||
int deleteAll();
|
||||
|
||||
/* Updates */
|
||||
@Update
|
||||
int update(final Entity entity);
|
||||
|
||||
@Update
|
||||
int update(final Collection<Entity> entities);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.schabi.newpipe.database.history;
|
||||
|
||||
import android.arch.persistence.room.TypeConverter;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
public class Converters {
|
||||
|
||||
/**
|
||||
* Convert a long value to a date
|
||||
* @param value the long value
|
||||
* @return the date
|
||||
*/
|
||||
@TypeConverter
|
||||
public static Date fromTimestamp(Long value) {
|
||||
return value == null ? null : new Date(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a date to a long value
|
||||
* @param date the date
|
||||
* @return the long value
|
||||
*/
|
||||
@TypeConverter
|
||||
public static Long dateToTimestamp(Date date) {
|
||||
return date == null ? null : date.getTime();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.schabi.newpipe.database.history.dao;
|
||||
|
||||
import org.schabi.newpipe.database.BasicDAO;
|
||||
|
||||
public interface HistoryDAO<T> extends BasicDAO<T> {
|
||||
T getLatestEntry();
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.schabi.newpipe.database.history.dao;
|
||||
|
||||
import android.arch.persistence.room.Dao;
|
||||
import android.arch.persistence.room.Query;
|
||||
|
||||
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.CREATION_DATE;
|
||||
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.ID;
|
||||
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SERVICE_ID;
|
||||
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.TABLE_NAME;
|
||||
|
||||
@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 + ")")
|
||||
@Override
|
||||
SearchHistoryEntry getLatestEntry();
|
||||
|
||||
@Query("DELETE FROM " + TABLE_NAME)
|
||||
@Override
|
||||
int deleteAll();
|
||||
|
||||
@Query("SELECT * FROM " + TABLE_NAME + ORDER_BY_CREATION_DATE)
|
||||
@Override
|
||||
Flowable<List<SearchHistoryEntry>> getAll();
|
||||
|
||||
@Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE)
|
||||
@Override
|
||||
Flowable<List<SearchHistoryEntry>> listByService(int serviceId);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.schabi.newpipe.database.history.dao;
|
||||
|
||||
import android.arch.persistence.room.Dao;
|
||||
import android.arch.persistence.room.Query;
|
||||
|
||||
import org.schabi.newpipe.database.history.model.WatchHistoryEntry;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.history.model.WatchHistoryEntry.CREATION_DATE;
|
||||
import static org.schabi.newpipe.database.history.model.WatchHistoryEntry.ID;
|
||||
import static org.schabi.newpipe.database.history.model.WatchHistoryEntry.SERVICE_ID;
|
||||
import static org.schabi.newpipe.database.history.model.WatchHistoryEntry.TABLE_NAME;
|
||||
|
||||
@Dao
|
||||
public interface WatchHistoryDAO extends HistoryDAO<WatchHistoryEntry> {
|
||||
|
||||
String ORDER_BY_CREATION_DATE = " ORDER BY " + CREATION_DATE + " DESC";
|
||||
|
||||
@Query("SELECT * FROM " + TABLE_NAME + " WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")")
|
||||
@Override
|
||||
WatchHistoryEntry getLatestEntry();
|
||||
|
||||
@Query("DELETE FROM " + TABLE_NAME)
|
||||
@Override
|
||||
int deleteAll();
|
||||
|
||||
@Query("SELECT * FROM " + TABLE_NAME + ORDER_BY_CREATION_DATE)
|
||||
@Override
|
||||
Flowable<List<WatchHistoryEntry>> getAll();
|
||||
|
||||
@Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE)
|
||||
@Override
|
||||
Flowable<List<WatchHistoryEntry>> listByService(int serviceId);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package org.schabi.newpipe.database.history.model;
|
||||
|
||||
import android.arch.persistence.room.ColumnInfo;
|
||||
import android.arch.persistence.room.Entity;
|
||||
import android.arch.persistence.room.Ignore;
|
||||
import android.arch.persistence.room.PrimaryKey;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Entity
|
||||
public abstract class HistoryEntry {
|
||||
|
||||
public static final String ID = "id";
|
||||
public static final String SERVICE_ID = "service_id";
|
||||
public static final String CREATION_DATE = "creation_date";
|
||||
|
||||
@ColumnInfo(name = CREATION_DATE)
|
||||
private Date creationDate;
|
||||
|
||||
@ColumnInfo(name = SERVICE_ID)
|
||||
private int serviceId;
|
||||
|
||||
@ColumnInfo(name = ID)
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
private long id;
|
||||
|
||||
public HistoryEntry(Date creationDate, int serviceId) {
|
||||
this.serviceId = serviceId;
|
||||
this.creationDate = creationDate;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Date getCreationDate() {
|
||||
return creationDate;
|
||||
}
|
||||
|
||||
public void setCreationDate(Date creationDate) {
|
||||
this.creationDate = creationDate;
|
||||
}
|
||||
|
||||
public int getServiceId() {
|
||||
return serviceId;
|
||||
}
|
||||
|
||||
public void setServiceId(int serviceId) {
|
||||
this.serviceId = serviceId;
|
||||
}
|
||||
|
||||
@Ignore
|
||||
public boolean hasEqualValues(HistoryEntry otherEntry) {
|
||||
return otherEntry != null && getServiceId() == otherEntry.getServiceId();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.schabi.newpipe.database.history.model;
|
||||
|
||||
import android.arch.persistence.room.ColumnInfo;
|
||||
import android.arch.persistence.room.Entity;
|
||||
import android.arch.persistence.room.Ignore;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Entity(tableName = SearchHistoryEntry.TABLE_NAME)
|
||||
public class SearchHistoryEntry extends HistoryEntry {
|
||||
|
||||
public static final String TABLE_NAME = "search_history";
|
||||
public static final String SEARCH = "search";
|
||||
|
||||
@ColumnInfo(name = SEARCH)
|
||||
private String search;
|
||||
|
||||
public SearchHistoryEntry(Date creationDate, int serviceId, String search) {
|
||||
super(creationDate, serviceId);
|
||||
this.search = search;
|
||||
}
|
||||
|
||||
public String getSearch() {
|
||||
return search;
|
||||
}
|
||||
|
||||
public void setSearch(String search) {
|
||||
this.search = search;
|
||||
}
|
||||
|
||||
@Ignore
|
||||
@Override
|
||||
public boolean hasEqualValues(HistoryEntry otherEntry) {
|
||||
return otherEntry instanceof SearchHistoryEntry && super.hasEqualValues(otherEntry)
|
||||
&& getSearch().equals(((SearchHistoryEntry) otherEntry).getSearch());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package org.schabi.newpipe.database.history.model;
|
||||
|
||||
import android.arch.persistence.room.ColumnInfo;
|
||||
import android.arch.persistence.room.Entity;
|
||||
import android.arch.persistence.room.Ignore;
|
||||
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Entity(tableName = WatchHistoryEntry.TABLE_NAME)
|
||||
public class WatchHistoryEntry extends HistoryEntry {
|
||||
|
||||
public static final String TABLE_NAME = "watch_history";
|
||||
public static final String TITLE = "title";
|
||||
public static final String URL = "url";
|
||||
public static final String STREAM_ID = "stream_id";
|
||||
public static final String THUMBNAIL_URL = "thumbnail_url";
|
||||
public static final String UPLOADER = "uploader";
|
||||
public static final String DURATION = "duration";
|
||||
|
||||
@ColumnInfo(name = TITLE)
|
||||
private String title;
|
||||
|
||||
@ColumnInfo(name = URL)
|
||||
private String url;
|
||||
|
||||
@ColumnInfo(name = STREAM_ID)
|
||||
private String streamId;
|
||||
|
||||
@ColumnInfo(name = THUMBNAIL_URL)
|
||||
private String thumbnailURL;
|
||||
|
||||
@ColumnInfo(name = UPLOADER)
|
||||
private String uploader;
|
||||
|
||||
@ColumnInfo(name = DURATION)
|
||||
private long duration;
|
||||
|
||||
public WatchHistoryEntry(Date creationDate, int serviceId, String title, String url, String streamId, String thumbnailURL, String uploader, long duration) {
|
||||
super(creationDate, serviceId);
|
||||
this.title = title;
|
||||
this.url = url;
|
||||
this.streamId = streamId;
|
||||
this.thumbnailURL = thumbnailURL;
|
||||
this.uploader = uploader;
|
||||
this.duration = duration;
|
||||
}
|
||||
|
||||
public WatchHistoryEntry(StreamInfo streamInfo) {
|
||||
this(new Date(), streamInfo.service_id, streamInfo.name, streamInfo.url,
|
||||
streamInfo.id, streamInfo.thumbnail_url, streamInfo.uploader_name, streamInfo.duration);
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getStreamId() {
|
||||
return streamId;
|
||||
}
|
||||
|
||||
public void setStreamId(String streamId) {
|
||||
this.streamId = streamId;
|
||||
}
|
||||
|
||||
public String getThumbnailURL() {
|
||||
return thumbnailURL;
|
||||
}
|
||||
|
||||
public void setThumbnailURL(String thumbnailURL) {
|
||||
this.thumbnailURL = thumbnailURL;
|
||||
}
|
||||
|
||||
public String getUploader() {
|
||||
return uploader;
|
||||
}
|
||||
|
||||
public void setUploader(String uploader) {
|
||||
this.uploader = uploader;
|
||||
}
|
||||
|
||||
public long getDuration() {
|
||||
return duration;
|
||||
}
|
||||
|
||||
public void setDuration(int duration) {
|
||||
this.duration = duration;
|
||||
}
|
||||
|
||||
@Ignore
|
||||
@Override
|
||||
public boolean hasEqualValues(HistoryEntry otherEntry) {
|
||||
return otherEntry instanceof WatchHistoryEntry && super.hasEqualValues(otherEntry)
|
||||
&& getUrl().equals(((WatchHistoryEntry) otherEntry).getUrl());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.schabi.newpipe.database.subscription;
|
||||
|
||||
import android.arch.persistence.room.Dao;
|
||||
import android.arch.persistence.room.Query;
|
||||
|
||||
import org.schabi.newpipe.database.BasicDAO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_SERVICE_ID;
|
||||
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_TABLE;
|
||||
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_URL;
|
||||
|
||||
@Dao
|
||||
public interface SubscriptionDAO extends BasicDAO<SubscriptionEntity> {
|
||||
@Override
|
||||
@Query("SELECT * FROM " + SUBSCRIPTION_TABLE)
|
||||
Flowable<List<SubscriptionEntity>> getAll();
|
||||
|
||||
@Override
|
||||
@Query("DELETE FROM " + SUBSCRIPTION_TABLE)
|
||||
int deleteAll();
|
||||
|
||||
@Override
|
||||
@Query("SELECT * FROM " + SUBSCRIPTION_TABLE + " WHERE " + SUBSCRIPTION_SERVICE_ID + " = :serviceId")
|
||||
Flowable<List<SubscriptionEntity>> listByService(int serviceId);
|
||||
|
||||
@Query("SELECT * FROM " + SUBSCRIPTION_TABLE + " WHERE " +
|
||||
SUBSCRIPTION_URL + " LIKE :url AND " +
|
||||
SUBSCRIPTION_SERVICE_ID + " = :serviceId")
|
||||
Flowable<List<SubscriptionEntity>> getSubscription(int serviceId, String url);
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package org.schabi.newpipe.database.subscription;
|
||||
|
||||
import android.arch.persistence.room.ColumnInfo;
|
||||
import android.arch.persistence.room.Entity;
|
||||
import android.arch.persistence.room.Ignore;
|
||||
import android.arch.persistence.room.Index;
|
||||
import android.arch.persistence.room.PrimaryKey;
|
||||
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
|
||||
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_SERVICE_ID;
|
||||
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_TABLE;
|
||||
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_URL;
|
||||
|
||||
@Entity(tableName = SUBSCRIPTION_TABLE,
|
||||
indices = {@Index(value = {SUBSCRIPTION_SERVICE_ID, SUBSCRIPTION_URL}, unique = true)})
|
||||
public class SubscriptionEntity {
|
||||
|
||||
final static String SUBSCRIPTION_TABLE = "subscriptions";
|
||||
final static String SUBSCRIPTION_SERVICE_ID = "service_id";
|
||||
final static String SUBSCRIPTION_URL = "url";
|
||||
final static String SUBSCRIPTION_NAME = "name";
|
||||
final static String SUBSCRIPTION_AVATAR_URL = "avatar_url";
|
||||
final static String SUBSCRIPTION_SUBSCRIBER_COUNT = "subscriber_count";
|
||||
final static String SUBSCRIPTION_DESCRIPTION = "description";
|
||||
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
private long uid = 0;
|
||||
|
||||
@ColumnInfo(name = SUBSCRIPTION_SERVICE_ID)
|
||||
private int serviceId = -1;
|
||||
|
||||
@ColumnInfo(name = SUBSCRIPTION_URL)
|
||||
private String url;
|
||||
|
||||
@ColumnInfo(name = SUBSCRIPTION_NAME)
|
||||
private String name;
|
||||
|
||||
@ColumnInfo(name = SUBSCRIPTION_AVATAR_URL)
|
||||
private String avatarUrl;
|
||||
|
||||
@ColumnInfo(name = SUBSCRIPTION_SUBSCRIBER_COUNT)
|
||||
private Long subscriberCount;
|
||||
|
||||
@ColumnInfo(name = SUBSCRIPTION_DESCRIPTION)
|
||||
private String description;
|
||||
|
||||
public long getUid() {
|
||||
return uid;
|
||||
}
|
||||
|
||||
/* Keep this package-private since UID should always be auto generated by Room impl */
|
||||
void setUid(long uid) {
|
||||
this.uid = uid;
|
||||
}
|
||||
|
||||
public int getServiceId() {
|
||||
return serviceId;
|
||||
}
|
||||
|
||||
public void setServiceId(int serviceId) {
|
||||
this.serviceId = serviceId;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getAvatarUrl() {
|
||||
return avatarUrl;
|
||||
}
|
||||
|
||||
public void setAvatarUrl(String avatarUrl) {
|
||||
this.avatarUrl = avatarUrl;
|
||||
}
|
||||
|
||||
public Long getSubscriberCount() {
|
||||
return subscriberCount;
|
||||
}
|
||||
|
||||
public void setSubscriberCount(Long subscriberCount) {
|
||||
this.subscriberCount = subscriberCount;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(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);
|
||||
}
|
||||
|
||||
@Ignore
|
||||
public ChannelInfoItem toChannelInfoItem() {
|
||||
ChannelInfoItem item = new ChannelInfoItem();
|
||||
item.url = getUrl();
|
||||
item.service_id = getServiceId();
|
||||
item.name = getName();
|
||||
item.thumbnail_url = getAvatarUrl();
|
||||
item.subscriber_count = getSubscriberCount();
|
||||
item.description = getDescription();
|
||||
return item;
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ public class DownloadActivity extends AppCompatActivity {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_downloader);
|
||||
|
||||
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
|
||||
@@ -22,14 +22,15 @@ import android.widget.TextView;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.stream_info.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream_info.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream_info.VideoStream;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.fragments.detail.SpinnerToolbarAdapter;
|
||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
import org.schabi.newpipe.util.FilenameUtils;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.util.Utils;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
@@ -106,19 +107,19 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
@Override
|
||||
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
nameEditText = ((EditText) view.findViewById(R.id.file_name));
|
||||
nameEditText.setText(createFileName(currentInfo.title));
|
||||
selectedAudioIndex = Utils.getPreferredAudioFormat(getContext(), currentInfo.audio_streams);
|
||||
nameEditText = view.findViewById(R.id.file_name);
|
||||
nameEditText.setText(FilenameUtils.createFilename(getContext(), currentInfo.name));
|
||||
selectedAudioIndex = ListHelper.getDefaultAudioFormat(getContext(), currentInfo.audio_streams);
|
||||
|
||||
streamsSpinner = (Spinner) view.findViewById(R.id.quality_spinner);
|
||||
streamsSpinner = view.findViewById(R.id.quality_spinner);
|
||||
streamsSpinner.setOnItemSelectedListener(this);
|
||||
|
||||
threadsCountTextView = (TextView) view.findViewById(R.id.threads_count);
|
||||
threadsSeekBar = (SeekBar) view.findViewById(R.id.threads);
|
||||
radioVideoAudioGroup = (RadioGroup) view.findViewById(R.id.video_audio_group);
|
||||
threadsCountTextView = view.findViewById(R.id.threads_count);
|
||||
threadsSeekBar = view.findViewById(R.id.threads);
|
||||
radioVideoAudioGroup = view.findViewById(R.id.video_audio_group);
|
||||
radioVideoAudioGroup.setOnCheckedChangeListener(this);
|
||||
|
||||
initToolbar((Toolbar) view.findViewById(R.id.toolbar));
|
||||
initToolbar(view.<Toolbar>findViewById(R.id.toolbar));
|
||||
checkDownloadOptions(view);
|
||||
setupVideoSpinner(sortedStreamVideosList, streamsSpinner);
|
||||
|
||||
@@ -134,12 +135,10 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar p1) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar p1) {
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -184,7 +183,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
String[] items = new String[audioStreams.size()];
|
||||
for (int i = 0; i < audioStreams.size(); i++) {
|
||||
AudioStream audioStream = audioStreams.get(i);
|
||||
items[i] = MediaFormat.getNameById(audioStream.format) + " " + audioStream.avgBitrate + "kbps";
|
||||
items[i] = MediaFormat.getNameById(audioStream.format) + " " + audioStream.average_bitrate + "kbps";
|
||||
}
|
||||
|
||||
ArrayAdapter<String> itemAdapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, items);
|
||||
@@ -240,8 +239,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected void checkDownloadOptions(View view) {
|
||||
RadioButton audioButton = (RadioButton) view.findViewById(R.id.audio_button);
|
||||
RadioButton videoButton = (RadioButton) view.findViewById(R.id.video_button);
|
||||
RadioButton audioButton = view.findViewById(R.id.audio_button);
|
||||
RadioButton videoButton = view.findViewById(R.id.video_button);
|
||||
|
||||
if (currentInfo.audio_streams == null || currentInfo.audio_streams.size() == 0) {
|
||||
audioButton.setVisibility(View.GONE);
|
||||
@@ -252,30 +251,12 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* #143 #44 #42 #22: make sure that the filename does not contain illegal chars.
|
||||
* This should fix some of the "cannot download" problems.
|
||||
*/
|
||||
private String createFileName(String fileName) {
|
||||
// from http://eng-przemelek.blogspot.de/2009/07/how-to-create-valid-file-name.html
|
||||
|
||||
List<String> forbiddenCharsPatterns = new ArrayList<>();
|
||||
forbiddenCharsPatterns.add("[:]+"); // Mac OS, but it looks that also Windows XP
|
||||
forbiddenCharsPatterns.add("[\\*\"/\\\\\\[\\]\\:\\;\\|\\=\\,]+"); // Windows
|
||||
forbiddenCharsPatterns.add("[^\\w\\d\\.]+"); // last chance... only latin letters and digits
|
||||
String nameToTest = fileName;
|
||||
for (String pattern : forbiddenCharsPatterns) {
|
||||
nameToTest = nameToTest.replaceAll(pattern, "_");
|
||||
}
|
||||
return nameToTest;
|
||||
}
|
||||
|
||||
|
||||
private void downloadSelected() {
|
||||
String url, location;
|
||||
|
||||
String fileName = nameEditText.getText().toString().trim();
|
||||
if (fileName.isEmpty()) fileName = createFileName(currentInfo.title);
|
||||
if (fileName.isEmpty()) fileName = FilenameUtils.createFilename(getContext(), currentInfo.name);
|
||||
|
||||
boolean isAudio = radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button;
|
||||
url = isAudio ? currentInfo.audio_streams.get(selectedAudioIndex).url : sortedStreamVideosList.get(selectedVideoIndex).url;
|
||||
|
||||
Submodule app/src/main/java/org/schabi/newpipe/extractor deleted from ab530381cf
@@ -0,0 +1,13 @@
|
||||
package org.schabi.newpipe.fragments;
|
||||
|
||||
/**
|
||||
* Indicates that the current fragment can handle back presses
|
||||
*/
|
||||
public interface BackPressable {
|
||||
/**
|
||||
* A back press was delegated to this fragment
|
||||
*
|
||||
* @return if the back press was handled
|
||||
*/
|
||||
boolean onBackPressed();
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
package org.schabi.newpipe.fragments;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.AttrRes;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.util.Log;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer;
|
||||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
public abstract class BaseFragment extends Fragment {
|
||||
protected final String TAG = "BaseFragment@" + Integer.toHexString(hashCode());
|
||||
protected static final boolean DEBUG = MainActivity.DEBUG;
|
||||
|
||||
protected AppCompatActivity activity;
|
||||
|
||||
protected AtomicBoolean isLoading = new AtomicBoolean(false);
|
||||
protected AtomicBoolean wasLoading = new AtomicBoolean(false);
|
||||
|
||||
protected static final ImageLoader imageLoader = ImageLoader.getInstance();
|
||||
protected static final DisplayImageOptions displayImageOptions =
|
||||
new DisplayImageOptions.Builder().displayer(new FadeInBitmapDisplayer(400)).cacheInMemory(false).build();
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected Toolbar toolbar;
|
||||
|
||||
protected View errorPanel;
|
||||
protected Button errorButtonRetry;
|
||||
protected TextView errorTextView;
|
||||
protected ProgressBar loadingProgressBar;
|
||||
//protected SwipeRefreshLayout swipeRefreshLayout;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Fragment's Lifecycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
if (DEBUG) Log.d(TAG, "onAttach() called with: context = [" + context + "]");
|
||||
|
||||
activity = (AppCompatActivity) context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
|
||||
|
||||
isLoading.set(false);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View rootView, Bundle savedInstanceState) {
|
||||
if (DEBUG) Log.d(TAG, "onViewCreated() called with: rootView = [" + rootView + "], savedInstanceState = [" + savedInstanceState + "]");
|
||||
initViews(rootView, savedInstanceState);
|
||||
initListeners();
|
||||
wasLoading.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
if (DEBUG) Log.d(TAG, "onDestroyView() called");
|
||||
toolbar = null;
|
||||
|
||||
errorPanel = null;
|
||||
errorButtonRetry = null;
|
||||
errorTextView = null;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
toolbar = (Toolbar) activity.findViewById(R.id.toolbar);
|
||||
|
||||
loadingProgressBar = (ProgressBar) rootView.findViewById(R.id.loading_progress_bar);
|
||||
//swipeRefreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.swipe_refresh);
|
||||
|
||||
errorPanel = rootView.findViewById(R.id.error_panel);
|
||||
errorButtonRetry = (Button) rootView.findViewById(R.id.error_button_retry);
|
||||
errorTextView = (TextView) rootView.findViewById(R.id.error_message_view);
|
||||
}
|
||||
|
||||
protected void initListeners() {
|
||||
errorButtonRetry.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
onRetryButtonClicked();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected abstract void reloadContent();
|
||||
|
||||
protected void onRetryButtonClicked() {
|
||||
if (DEBUG) Log.d(TAG, "onRetryButtonClicked() called");
|
||||
reloadContent();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected void setErrorMessage(String message, boolean showRetryButton) {
|
||||
if (errorTextView == null || activity == null) return;
|
||||
|
||||
errorTextView.setText(message);
|
||||
if (showRetryButton) animateView(errorButtonRetry, true, 300);
|
||||
else animateView(errorButtonRetry, false, 0);
|
||||
|
||||
animateView(errorPanel, true, 300);
|
||||
isLoading.set(false);
|
||||
|
||||
animateView(loadingProgressBar, false, 200);
|
||||
}
|
||||
|
||||
protected int getResourceIdFromAttr(@AttrRes int attr) {
|
||||
TypedArray a = activity.getTheme().obtainStyledAttributes(new int[]{attr});
|
||||
int attributeResourceId = a.getResourceId(0, 0);
|
||||
a.recycle();
|
||||
return attributeResourceId;
|
||||
}
|
||||
|
||||
public static void showMenuTooltip(View v, String message) {
|
||||
final int[] screenPos = new int[2];
|
||||
final Rect displayFrame = new Rect();
|
||||
v.getLocationOnScreen(screenPos);
|
||||
v.getWindowVisibleDisplayFrame(displayFrame);
|
||||
|
||||
final Context context = v.getContext();
|
||||
final int width = v.getWidth();
|
||||
final int height = v.getHeight();
|
||||
final int midy = screenPos[1] + height / 2;
|
||||
int referenceX = screenPos[0] + width / 2;
|
||||
if (ViewCompat.getLayoutDirection(v) == View.LAYOUT_DIRECTION_LTR) {
|
||||
final int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
|
||||
referenceX = screenWidth - referenceX; // mirror
|
||||
}
|
||||
Toast cheatSheet = Toast.makeText(context, message, Toast.LENGTH_SHORT);
|
||||
if (midy < displayFrame.height()) {
|
||||
// Show along the top; follow action buttons
|
||||
cheatSheet.setGravity(Gravity.TOP | Gravity.END, referenceX,
|
||||
screenPos[1] + height - displayFrame.top);
|
||||
} else {
|
||||
// Show along the bottom center
|
||||
cheatSheet.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, height);
|
||||
}
|
||||
cheatSheet.show();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
package org.schabi.newpipe.fragments;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.StringRes;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.jakewharton.rxbinding2.view.RxView;
|
||||
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.InfoCache;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.functions.Consumer;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
public abstract class BaseStateFragment<I> extends BaseFragment implements ViewContract<I> {
|
||||
|
||||
@State
|
||||
protected AtomicBoolean wasLoading = new AtomicBoolean();
|
||||
protected AtomicBoolean isLoading = new AtomicBoolean();
|
||||
|
||||
@Nullable
|
||||
protected View emptyStateView;
|
||||
@Nullable
|
||||
protected ProgressBar loadingProgressBar;
|
||||
|
||||
protected View errorPanelRoot;
|
||||
protected Button errorButtonRetry;
|
||||
protected TextView errorTextView;
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View rootView, Bundle savedInstanceState) {
|
||||
super.onViewCreated(rootView, savedInstanceState);
|
||||
doInitialLoadLogic();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
wasLoading.set(isLoading.get());
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
|
||||
@Override
|
||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
|
||||
emptyStateView = rootView.findViewById(R.id.empty_state_view);
|
||||
loadingProgressBar = rootView.findViewById(R.id.loading_progress_bar);
|
||||
|
||||
errorPanelRoot = rootView.findViewById(R.id.error_panel);
|
||||
errorButtonRetry = rootView.findViewById(R.id.error_button_retry);
|
||||
errorTextView = rootView.findViewById(R.id.error_message_view);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initListeners() {
|
||||
super.initListeners();
|
||||
RxView.clicks(errorButtonRetry)
|
||||
.debounce(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new Consumer<Object>() {
|
||||
@Override
|
||||
public void accept(Object o) throws Exception {
|
||||
onRetryButtonClicked();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void onRetryButtonClicked() {
|
||||
reloadContent();
|
||||
}
|
||||
|
||||
public void reloadContent() {
|
||||
startLoading(true);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Load
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected void doInitialLoadLogic() {
|
||||
startLoading(true);
|
||||
}
|
||||
|
||||
protected void startLoading(boolean forceLoad) {
|
||||
if (DEBUG) Log.d(TAG, "startLoading() called with: forceLoad = [" + forceLoad + "]");
|
||||
showLoading();
|
||||
isLoading.set(true);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Contract
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void showLoading() {
|
||||
if (emptyStateView != null) animateView(emptyStateView, false, 150);
|
||||
if (loadingProgressBar != null) animateView(loadingProgressBar, true, 400);
|
||||
animateView(errorPanelRoot, false, 150);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hideLoading() {
|
||||
if (emptyStateView != null) animateView(emptyStateView, false, 150);
|
||||
if (loadingProgressBar != null) animateView(loadingProgressBar, false, 0);
|
||||
animateView(errorPanelRoot, false, 150);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showEmptyState() {
|
||||
isLoading.set(false);
|
||||
if (emptyStateView != null) animateView(emptyStateView, true, 200);
|
||||
if (loadingProgressBar != null) animateView(loadingProgressBar, false, 0);
|
||||
animateView(errorPanelRoot, false, 150);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showError(String message, boolean showRetryButton) {
|
||||
if (DEBUG) Log.d(TAG, "showError() called with: message = [" + message + "], showRetryButton = [" + showRetryButton + "]");
|
||||
isLoading.set(false);
|
||||
InfoCache.getInstance().clearCache();
|
||||
hideLoading();
|
||||
|
||||
errorTextView.setText(message);
|
||||
if (showRetryButton) animateView(errorButtonRetry, true, 600);
|
||||
else animateView(errorButtonRetry, false, 0);
|
||||
animateView(errorPanelRoot, true, 300);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResult(I result) {
|
||||
if (DEBUG) Log.d(TAG, "handleResult() called with: result = [" + result + "]");
|
||||
hideLoading();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Error handling
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
/**
|
||||
* Default implementation handles some general exceptions
|
||||
*
|
||||
* @return if the exception was handled
|
||||
*/
|
||||
protected boolean onError(Throwable exception) {
|
||||
if (DEBUG) Log.d(TAG, "onError() called with: exception = [" + exception + "]");
|
||||
isLoading.set(false);
|
||||
|
||||
if (isDetached() || isRemoving()) {
|
||||
if (DEBUG) Log.w(TAG, "onError() is detached or removing = [" + exception + "]");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ExtractorHelper.isInterruptedCaused(exception)) {
|
||||
if (DEBUG) Log.w(TAG, "onError() isInterruptedCaused! = [" + exception + "]");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (exception instanceof ReCaptchaException) {
|
||||
onReCaptchaException();
|
||||
return true;
|
||||
} else if (exception instanceof IOException) {
|
||||
showError(getString(R.string.network_error), true);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void onReCaptchaException() {
|
||||
if (DEBUG) Log.d(TAG, "onReCaptchaException() called");
|
||||
Toast.makeText(activity, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show();
|
||||
// Starting ReCaptcha Challenge Activity
|
||||
startActivityForResult(new Intent(activity, ReCaptchaActivity.class), ReCaptchaActivity.RECAPTCHA_REQUEST);
|
||||
|
||||
showError(getString(R.string.recaptcha_request_toast), false);
|
||||
}
|
||||
|
||||
public void onUnrecoverableError(Throwable exception, UserAction userAction, String serviceName, String request, @StringRes int errorId) {
|
||||
onUnrecoverableError(Collections.singletonList(exception), userAction, serviceName, request, errorId);
|
||||
}
|
||||
|
||||
public void onUnrecoverableError(List<Throwable> exception, UserAction userAction, String serviceName, String request, @StringRes int errorId) {
|
||||
if (DEBUG) Log.d(TAG, "onUnrecoverableError() called with: exception = [" + exception + "]");
|
||||
|
||||
if (serviceName == null) serviceName = "none";
|
||||
if (request == null) request = "none";
|
||||
ErrorActivity.reportError(getContext(), exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(userAction, serviceName, request, errorId));
|
||||
}
|
||||
|
||||
public void showSnackBarError(Throwable exception, UserAction userAction, String serviceName, String request, @StringRes int errorId) {
|
||||
showSnackBarError(Collections.singletonList(exception), userAction, serviceName, request, errorId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a SnackBar and only call ErrorActivity#reportError IF we a find a valid view (otherwise the error screen appears)
|
||||
*/
|
||||
public void showSnackBarError(List<Throwable> exception, UserAction userAction, String serviceName, String request, @StringRes int errorId) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "showSnackBarError() called with: exception = [" + exception + "], userAction = [" + userAction + "], request = [" + request + "], errorId = [" + errorId + "]");
|
||||
}
|
||||
View rootView = activity != null ? activity.findViewById(android.R.id.content) : null;
|
||||
if (rootView == null && getView() != null) rootView = getView();
|
||||
if (rootView == null) return;
|
||||
|
||||
ErrorActivity.reportError(getContext(), exception, MainActivity.class, rootView, ErrorActivity.ErrorInfo.make(userAction, serviceName, request, errorId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.schabi.newpipe.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
public class BlankFragment extends BaseFragment {
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_blank, container, false);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
package org.schabi.newpipe.fragments;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.TabLayout;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentPagerAdapter;
|
||||
import android.support.v4.view.ViewPager;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
@@ -14,40 +16,44 @@ import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.fragments.subscription.SubscriptionFragment;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
|
||||
public class MainFragment extends Fragment {
|
||||
private final String TAG = "MainFragment@" + Integer.toHexString(hashCode());
|
||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||
|
||||
private AppCompatActivity activity;
|
||||
public class MainFragment extends BaseFragment implements TabLayout.OnTabSelectedListener {
|
||||
private ViewPager viewPager;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Fragment's LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
if (DEBUG) Log.d(TAG, "onAttach() called with: context = [" + context + "]");
|
||||
activity = ((AppCompatActivity) context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
if (DEBUG) Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]");
|
||||
return inflater.inflate(R.layout.fragment_main, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
|
||||
TabLayout tabLayout = rootView.findViewById(R.id.main_tab_layout);
|
||||
viewPager = rootView.findViewById(R.id.pager);
|
||||
|
||||
/* Nested fragment, use child fragment here to maintain backstack in view pager. */
|
||||
PagerAdapter adapter = new PagerAdapter(getChildFragmentManager());
|
||||
viewPager.setAdapter(adapter);
|
||||
viewPager.setOffscreenPageLimit(adapter.getCount());
|
||||
|
||||
tabLayout.setupWithViewPager(viewPager);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Menu
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -74,4 +80,53 @@ public class MainFragment extends Fragment {
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Tabs
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onTabSelected(TabLayout.Tab tab) {
|
||||
viewPager.setCurrentItem(tab.getPosition());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabUnselected(TabLayout.Tab tab) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabReselected(TabLayout.Tab tab) {
|
||||
}
|
||||
|
||||
private class PagerAdapter extends FragmentPagerAdapter {
|
||||
|
||||
private int[] tabTitles = new int[]{
|
||||
R.string.tab_main,
|
||||
R.string.tab_subscriptions
|
||||
};
|
||||
|
||||
PagerAdapter(FragmentManager fm) {
|
||||
super(fm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
switch (position) {
|
||||
case 1:
|
||||
return new SubscriptionFragment();
|
||||
default:
|
||||
return new BlankFragment();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getPageTitle(int position) {
|
||||
return getString(this.tabTitles[position]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return this.tabTitles.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package org.schabi.newpipe.fragments;
|
||||
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.StaggeredGridLayoutManager;
|
||||
|
||||
/**
|
||||
* Recycler view scroll listener which calls the method {@link #onScrolledDown(RecyclerView)}
|
||||
* if the view is scrolled below the last item.
|
||||
*/
|
||||
public abstract class OnScrollBelowItemsListener extends RecyclerView.OnScrollListener {
|
||||
|
||||
@Override
|
||||
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
|
||||
super.onScrolled(recyclerView, dx, dy);
|
||||
if (dy > 0) {
|
||||
int pastVisibleItems = 0, visibleItemCount, totalItemCount;
|
||||
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
|
||||
|
||||
visibleItemCount = layoutManager.getChildCount();
|
||||
totalItemCount = layoutManager.getItemCount();
|
||||
|
||||
// Already covers the GridLayoutManager case
|
||||
if (layoutManager instanceof LinearLayoutManager) {
|
||||
pastVisibleItems = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
|
||||
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
|
||||
int[] positions = ((StaggeredGridLayoutManager) layoutManager).findFirstVisibleItemPositions(null);
|
||||
if (positions != null && positions.length > 0) pastVisibleItems = positions[0];
|
||||
}
|
||||
|
||||
if ((visibleItemCount + pastVisibleItems) >= totalItemCount) {
|
||||
onScrolledDown(recyclerView);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the recycler view is scrolled below the last item.
|
||||
*
|
||||
* @param recyclerView the recycler view
|
||||
*/
|
||||
public abstract void onScrolledDown(RecyclerView recyclerView);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.schabi.newpipe.fragments;
|
||||
|
||||
public interface ViewContract<I> {
|
||||
void showLoading();
|
||||
void hideLoading();
|
||||
void showEmptyState();
|
||||
void showError(String message, boolean showRetryButton);
|
||||
|
||||
void handleResult(I result);
|
||||
}
|
||||
@@ -1,403 +0,0 @@
|
||||
package org.schabi.newpipe.fragments.channel;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.ImageErrorLoadingListener;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.fragments.BaseFragment;
|
||||
import org.schabi.newpipe.fragments.search.OnScrollBelowItemsListener;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.info_list.InfoListAdapter;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.workers.ChannelExtractorWorker;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
public class ChannelFragment extends BaseFragment implements ChannelExtractorWorker.OnChannelInfoReceive {
|
||||
private final String TAG = "ChannelFragment@" + Integer.toHexString(hashCode());
|
||||
|
||||
private static final String INFO_LIST_KEY = "info_list_key";
|
||||
private static final String CHANNEL_INFO_KEY = "channel_info_key";
|
||||
private static final String PAGE_NUMBER_KEY = "page_number_key";
|
||||
|
||||
private InfoListAdapter infoListAdapter;
|
||||
|
||||
private ChannelExtractorWorker currentChannelWorker;
|
||||
private ChannelInfo currentChannelInfo;
|
||||
private int serviceId = -1;
|
||||
private String channelName = "";
|
||||
private String channelUrl = "";
|
||||
private int pageNumber = 0;
|
||||
private boolean hasNextPage = true;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private RecyclerView channelVideosList;
|
||||
|
||||
private View headerRootLayout;
|
||||
private ImageView headerChannelBanner;
|
||||
private ImageView headerAvatarView;
|
||||
private TextView headerTitleView;
|
||||
private TextView headerSubscribersTextView;
|
||||
private Button headerRssButton;
|
||||
|
||||
/*////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public ChannelFragment() {
|
||||
}
|
||||
|
||||
public static Fragment getInstance(int serviceId, String channelUrl, String name) {
|
||||
ChannelFragment instance = new ChannelFragment();
|
||||
instance.setChannel(serviceId, channelUrl, name);
|
||||
return instance;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Fragment's LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
if (savedInstanceState != null) {
|
||||
channelUrl = savedInstanceState.getString(Constants.KEY_URL);
|
||||
channelName = savedInstanceState.getString(Constants.KEY_TITLE);
|
||||
serviceId = savedInstanceState.getInt(Constants.KEY_SERVICE_ID, -1);
|
||||
|
||||
pageNumber = savedInstanceState.getInt(PAGE_NUMBER_KEY, 0);
|
||||
Serializable serializable = savedInstanceState.getSerializable(CHANNEL_INFO_KEY);
|
||||
if (serializable instanceof ChannelInfo) currentChannelInfo = (ChannelInfo) serializable;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
if (DEBUG) Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]");
|
||||
return inflater.inflate(R.layout.fragment_channel, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
if (currentChannelInfo == null) loadPage(0);
|
||||
else handleChannelInfo(currentChannelInfo, false, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
if (DEBUG) Log.d(TAG, "onDestroyView() called");
|
||||
headerAvatarView.setImageBitmap(null);
|
||||
headerChannelBanner.setImageBitmap(null);
|
||||
channelVideosList.removeAllViews();
|
||||
|
||||
channelVideosList = null;
|
||||
headerRootLayout = null;
|
||||
headerChannelBanner = null;
|
||||
headerAvatarView = null;
|
||||
headerTitleView = null;
|
||||
headerSubscribersTextView = null;
|
||||
headerRssButton = null;
|
||||
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
if (DEBUG) Log.d(TAG, "onResume() called");
|
||||
super.onResume();
|
||||
if (wasLoading.getAndSet(false) && (currentChannelWorker == null || !currentChannelWorker.isRunning())) {
|
||||
loadPage(pageNumber);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
if (DEBUG) Log.d(TAG, "onStop() called");
|
||||
super.onStop();
|
||||
wasLoading.set(currentChannelWorker != null && currentChannelWorker.isRunning());
|
||||
if (currentChannelWorker != null && currentChannelWorker.isRunning()) currentChannelWorker.cancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
if (DEBUG) Log.d(TAG, "onSaveInstanceState() called with: outState = [" + outState + "]");
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putString(Constants.KEY_URL, channelUrl);
|
||||
outState.putString(Constants.KEY_TITLE, channelName);
|
||||
outState.putInt(Constants.KEY_SERVICE_ID, serviceId);
|
||||
|
||||
outState.putSerializable(INFO_LIST_KEY, ((ArrayList<InfoItem>) infoListAdapter.getItemsList()));
|
||||
outState.putSerializable(CHANNEL_INFO_KEY, currentChannelInfo);
|
||||
outState.putInt(PAGE_NUMBER_KEY, pageNumber);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Menu
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
inflater.inflate(R.menu.menu_channel, menu);
|
||||
|
||||
ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
if (supportActionBar != null) {
|
||||
supportActionBar.setDisplayShowTitleEnabled(true);
|
||||
supportActionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (DEBUG) Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]");
|
||||
super.onOptionsItemSelected(item);
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_item_openInBrowser: {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse(channelUrl));
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.choose_browser)));
|
||||
return true;
|
||||
}
|
||||
case R.id.menu_item_share:
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_SEND);
|
||||
intent.putExtra(Intent.EXTRA_TEXT, channelUrl);
|
||||
intent.setType("text/plain");
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.share_dialog_title)));
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init's
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
|
||||
channelVideosList = (RecyclerView) rootView.findViewById(R.id.channel_streams_view);
|
||||
|
||||
channelVideosList.setLayoutManager(new LinearLayoutManager(activity));
|
||||
if (infoListAdapter == null) {
|
||||
infoListAdapter = new InfoListAdapter(activity);
|
||||
if (savedInstanceState != null) {
|
||||
//noinspection unchecked
|
||||
ArrayList<InfoItem> serializable = (ArrayList<InfoItem>) savedInstanceState.getSerializable(INFO_LIST_KEY);
|
||||
infoListAdapter.addInfoItemList(serializable);
|
||||
}
|
||||
}
|
||||
|
||||
channelVideosList.setAdapter(infoListAdapter);
|
||||
headerRootLayout = activity.getLayoutInflater().inflate(R.layout.channel_header, channelVideosList, false);
|
||||
infoListAdapter.setHeader(headerRootLayout);
|
||||
infoListAdapter.setFooter(activity.getLayoutInflater().inflate(R.layout.pignate_footer, channelVideosList, false));
|
||||
|
||||
headerChannelBanner = (ImageView) headerRootLayout.findViewById(R.id.channel_banner_image);
|
||||
headerAvatarView = (ImageView) headerRootLayout.findViewById(R.id.channel_avatar_view);
|
||||
headerTitleView = (TextView) headerRootLayout.findViewById(R.id.channel_title_view);
|
||||
headerSubscribersTextView = (TextView) headerRootLayout.findViewById(R.id.channel_subscriber_view);
|
||||
headerRssButton = (Button) headerRootLayout.findViewById(R.id.channel_rss_button);
|
||||
}
|
||||
|
||||
protected void initListeners() {
|
||||
super.initListeners();
|
||||
|
||||
infoListAdapter.setOnStreamInfoItemSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() {
|
||||
@Override
|
||||
public void selected(int serviceId, String url, String title) {
|
||||
if (DEBUG) Log.d(TAG, "selected() called with: serviceId = [" + serviceId + "], url = [" + url + "], title = [" + title + "]");
|
||||
NavigationHelper.openVideoDetailFragment(getFragmentManager(), serviceId, url, title);
|
||||
}
|
||||
});
|
||||
|
||||
channelVideosList.clearOnScrollListeners();
|
||||
channelVideosList.addOnScrollListener(new OnScrollBelowItemsListener() {
|
||||
@Override
|
||||
public void onScrolledDown(RecyclerView recyclerView) {
|
||||
if ((currentChannelWorker == null || !currentChannelWorker.isRunning()) && hasNextPage && !isLoading.get()) {
|
||||
pageNumber++;
|
||||
loadMoreVideos();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
headerRssButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (DEBUG) Log.d(TAG, "onClick() called with: view = [" + view + "] feed url > " + currentChannelInfo.feed_url);
|
||||
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(currentChannelInfo.feed_url));
|
||||
startActivity(i);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void reloadContent() {
|
||||
if (DEBUG) Log.d(TAG, "reloadContent() called");
|
||||
currentChannelInfo = null;
|
||||
infoListAdapter.clearStreamItemList();
|
||||
loadPage(0);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private String buildSubscriberString(long count) {
|
||||
String out = NumberFormat.getNumberInstance().format(count);
|
||||
out += " " + getString(count > 1 ? R.string.subscriber_plural : R.string.subscriber);
|
||||
return out;
|
||||
}
|
||||
|
||||
private void loadPage(int page) {
|
||||
if (DEBUG) Log.d(TAG, "loadPage() called with: page = [" + page + "]");
|
||||
if (currentChannelWorker != null && currentChannelWorker.isRunning()) currentChannelWorker.cancel();
|
||||
isLoading.set(true);
|
||||
pageNumber = page;
|
||||
infoListAdapter.showFooter(false);
|
||||
|
||||
animateView(loadingProgressBar, true, 200);
|
||||
animateView(errorPanel, false, 200);
|
||||
|
||||
imageLoader.cancelDisplayTask(headerChannelBanner);
|
||||
imageLoader.cancelDisplayTask(headerAvatarView);
|
||||
|
||||
headerRssButton.setVisibility(View.GONE);
|
||||
headerSubscribersTextView.setVisibility(View.GONE);
|
||||
|
||||
headerTitleView.setText(channelName != null ? channelName : "");
|
||||
headerChannelBanner.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.channel_banner));
|
||||
headerAvatarView.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.buddy));
|
||||
if (activity.getSupportActionBar() != null) activity.getSupportActionBar().setTitle(channelName != null ? channelName : "");
|
||||
|
||||
currentChannelWorker = new ChannelExtractorWorker(activity, serviceId, channelUrl, page, false, this);
|
||||
currentChannelWorker.start();
|
||||
}
|
||||
|
||||
private void loadMoreVideos() {
|
||||
if (DEBUG) Log.d(TAG, "loadMoreVideos() called");
|
||||
if (currentChannelWorker != null && currentChannelWorker.isRunning()) currentChannelWorker.cancel();
|
||||
isLoading.set(true);
|
||||
currentChannelWorker = new ChannelExtractorWorker(activity, serviceId, channelUrl, pageNumber, true, this);
|
||||
currentChannelWorker.start();
|
||||
}
|
||||
|
||||
private void setChannel(int serviceId, String channelUrl, String name) {
|
||||
this.serviceId = serviceId;
|
||||
this.channelUrl = channelUrl;
|
||||
this.channelName = name;
|
||||
}
|
||||
|
||||
private void handleChannelInfo(ChannelInfo info, boolean onlyVideos, boolean addVideos) {
|
||||
currentChannelInfo = info;
|
||||
|
||||
animateView(errorPanel, false, 300);
|
||||
animateView(channelVideosList, true, 200);
|
||||
animateView(loadingProgressBar, false, 200);
|
||||
|
||||
if (!onlyVideos) {
|
||||
headerRootLayout.setVisibility(View.VISIBLE);
|
||||
//animateView(loadingProgressBar, false, 200, null);
|
||||
|
||||
if (!TextUtils.isEmpty(info.channel_name)) {
|
||||
if (activity.getSupportActionBar() != null) activity.getSupportActionBar().setTitle(info.channel_name);
|
||||
headerTitleView.setText(info.channel_name);
|
||||
channelName = info.channel_name;
|
||||
} else channelName = "";
|
||||
|
||||
if (!TextUtils.isEmpty(info.banner_url)) {
|
||||
imageLoader.displayImage(info.banner_url, headerChannelBanner, displayImageOptions, new ImageErrorLoadingListener(activity, getView(), info.service_id));
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(info.avatar_url)) {
|
||||
headerAvatarView.setVisibility(View.VISIBLE);
|
||||
imageLoader.displayImage(info.avatar_url, headerAvatarView, displayImageOptions, new ImageErrorLoadingListener(activity, getView(), info.service_id));
|
||||
}
|
||||
|
||||
if (info.subscriberCount != -1) {
|
||||
headerSubscribersTextView.setText(buildSubscriberString(info.subscriberCount));
|
||||
headerSubscribersTextView.setVisibility(View.VISIBLE);
|
||||
} else headerSubscribersTextView.setVisibility(View.GONE);
|
||||
|
||||
if (!TextUtils.isEmpty(info.feed_url)) headerRssButton.setVisibility(View.VISIBLE);
|
||||
else headerRssButton.setVisibility(View.INVISIBLE);
|
||||
|
||||
infoListAdapter.showFooter(true);
|
||||
}
|
||||
|
||||
hasNextPage = info.hasNextPage;
|
||||
if (!hasNextPage) infoListAdapter.showFooter(false);
|
||||
|
||||
//if (!listRestored) {
|
||||
if (addVideos) infoListAdapter.addInfoItemList(info.related_streams);
|
||||
//}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setErrorMessage(String message, boolean showRetryButton) {
|
||||
super.setErrorMessage(message, showRetryButton);
|
||||
|
||||
animateView(channelVideosList, false, 200);
|
||||
currentChannelInfo = null;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// OnChannelInfoReceiveListener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onReceive(ChannelInfo info, boolean onlyVideos) {
|
||||
if (DEBUG) Log.d(TAG, "onReceive() called with: info = [" + info + "]");
|
||||
if (info == null || isRemoving() || !isVisible()) return;
|
||||
|
||||
handleChannelInfo(info, onlyVideos, true);
|
||||
isLoading.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(int messageId) {
|
||||
if (DEBUG) Log.d(TAG, "onError() called with: messageId = [" + messageId + "]");
|
||||
setErrorMessage(getString(messageId), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnrecoverableError(Exception exception) {
|
||||
if (DEBUG) Log.d(TAG, "onUnrecoverableError() called with: exception = [" + exception + "]");
|
||||
activity.finish();
|
||||
}
|
||||
}
|
||||
@@ -12,12 +12,12 @@ import android.widget.AdapterView;
|
||||
import android.widget.Spinner;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.stream_info.VideoStream;
|
||||
import org.schabi.newpipe.util.Utils;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
/*
|
||||
* Created by Christian Schabesberger on 18.08.15.
|
||||
* <p>
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
@@ -68,7 +68,7 @@ class ActionBarHandler {
|
||||
public void setupStreamList(final List<VideoStream> videoStreams, Spinner toolbarSpinner) {
|
||||
if (activity == null) return;
|
||||
|
||||
selectedVideoStream = Utils.getDefaultResolution(activity, videoStreams);
|
||||
selectedVideoStream = ListHelper.getDefaultResolutionIndex(activity, videoStreams);
|
||||
|
||||
boolean isExternalPlayerEnabled = PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(activity.getString(R.string.use_external_video_player_key), false);
|
||||
toolbarSpinner.setAdapter(new SpinnerToolbarAdapter(activity, videoStreams, isExternalPlayerEnabled));
|
||||
|
||||
@@ -11,7 +11,7 @@ import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.stream_info.VideoStream;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -57,8 +57,8 @@ public class SpinnerToolbarAdapter extends BaseAdapter {
|
||||
convertView = LayoutInflater.from(context).inflate(R.layout.resolutions_spinner_item, parent, false);
|
||||
}
|
||||
|
||||
ImageView woSoundIcon = (ImageView) convertView.findViewById(R.id.wo_sound_icon);
|
||||
TextView text = (TextView) convertView.findViewById(android.R.id.text1);
|
||||
ImageView woSoundIcon = convertView.findViewById(R.id.wo_sound_icon);
|
||||
TextView text = convertView.findViewById(android.R.id.text1);
|
||||
VideoStream item = (VideoStream) getItem(position);
|
||||
text.setText(MediaFormat.getNameById(item.format) + " " + item.resolution);
|
||||
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
package org.schabi.newpipe.fragments.detail;
|
||||
|
||||
import org.schabi.newpipe.extractor.stream_info.StreamInfo;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class StackItem implements Serializable {
|
||||
class StackItem implements Serializable {
|
||||
private int serviceId;
|
||||
private String title, url;
|
||||
private StreamInfo info;
|
||||
|
||||
public StackItem(String url, String title) {
|
||||
this.title = title;
|
||||
StackItem(int serviceId, String url, String title) {
|
||||
this.serviceId = serviceId;
|
||||
this.url = url;
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public int getServiceId() {
|
||||
return serviceId;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
@@ -27,16 +28,8 @@ public class StackItem implements Serializable {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setInfo(StreamInfo info) {
|
||||
this.info = info;
|
||||
}
|
||||
|
||||
public StreamInfo getInfo() {
|
||||
return info;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getUrl() + " > " + getTitle();
|
||||
return getServiceId() + ":" + getUrl() + " > " + getTitle();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
package org.schabi.newpipe.fragments.detail;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.extractor.stream_info.StreamInfo;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class StreamInfoCache {
|
||||
private static String TAG = "StreamInfoCache@";
|
||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||
private static final StreamInfoCache instance = new StreamInfoCache();
|
||||
private static final int MAX_ITEMS_ON_CACHE = 20;
|
||||
|
||||
private final LinkedHashMap<String, StreamInfo> myCache = new LinkedHashMap<>();
|
||||
|
||||
private StreamInfoCache() {
|
||||
TAG += "" + Integer.toHexString(hashCode());
|
||||
}
|
||||
|
||||
public static StreamInfoCache getInstance() {
|
||||
if (DEBUG) Log.d(TAG, "getInstance() called");
|
||||
return instance;
|
||||
}
|
||||
|
||||
public boolean hasKey(@NonNull String url) {
|
||||
if (DEBUG) Log.d(TAG, "hasKey() called with: url = [" + url + "]");
|
||||
return !TextUtils.isEmpty(url) && myCache.containsKey(url) && myCache.get(url) != null;
|
||||
}
|
||||
|
||||
public StreamInfo getFromKey(@NonNull String url) {
|
||||
if (DEBUG) Log.d(TAG, "getFromKey() called with: url = [" + url + "]");
|
||||
return myCache.get(url);
|
||||
}
|
||||
|
||||
public void putInfo(@NonNull StreamInfo info) {
|
||||
if (DEBUG) Log.d(TAG, "putInfo() called with: info = [" + info + "]");
|
||||
putInfo(info.webpage_url, info);
|
||||
}
|
||||
|
||||
public void putInfo(@NonNull String url, @NonNull StreamInfo info) {
|
||||
if (DEBUG) Log.d(TAG, "putInfo() called with: url = [" + url + "], info = [" + info + "]");
|
||||
myCache.put(url, info);
|
||||
}
|
||||
|
||||
public void removeInfo(@NonNull StreamInfo info) {
|
||||
if (DEBUG) Log.d(TAG, "removeInfo() called with: info = [" + info + "]");
|
||||
myCache.remove(info.webpage_url);
|
||||
}
|
||||
|
||||
public void removeInfo(@NonNull String url) {
|
||||
if (DEBUG) Log.d(TAG, "removeInfo() called with: url = [" + url + "]");
|
||||
myCache.remove(url);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public void clearCache() {
|
||||
if (DEBUG) Log.d(TAG, "clearCache() called");
|
||||
myCache.clear();
|
||||
}
|
||||
|
||||
public void removeOldEntries() {
|
||||
if (DEBUG) Log.d(TAG, "removeOldEntries() called , size = " + getSize());
|
||||
if (getSize() > MAX_ITEMS_ON_CACHE) {
|
||||
Iterator<String> iterator = myCache.keySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
iterator.next();
|
||||
iterator.remove();
|
||||
if (DEBUG) Log.d(TAG, "getSize() = " + getSize());
|
||||
if (getSize() <= MAX_ITEMS_ON_CACHE) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int getSize() {
|
||||
return myCache.size();
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,239 @@
|
||||
package org.schabi.newpipe.fragments.list;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.info_list.InfoListAdapter;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implements ListViewContract<I, N>, StateSaver.WriteRead {
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected InfoListAdapter infoListAdapter;
|
||||
protected RecyclerView itemsList;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
infoListAdapter = new InfoListAdapter(activity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
StateSaver.onDestroy(savedState);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// State Saving
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected StateSaver.SavedState savedState;
|
||||
|
||||
@Override
|
||||
public String generateSuffix() {
|
||||
// Naive solution, but it's good for now (the items don't change)
|
||||
return "." + infoListAdapter.getItemsList().size() + ".list";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTo(Queue<Object> objectsToSave) {
|
||||
objectsToSave.add(infoListAdapter.getItemsList());
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception {
|
||||
infoListAdapter.getItemsList().clear();
|
||||
infoListAdapter.getItemsList().addAll((List<InfoItem>) savedObjects.poll());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle bundle) {
|
||||
super.onSaveInstanceState(bundle);
|
||||
savedState = StateSaver.tryToSave(activity.isChangingConfigurations(), savedState, bundle, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(@NonNull Bundle bundle) {
|
||||
super.onRestoreInstanceState(bundle);
|
||||
savedState = StateSaver.tryToRestore(bundle, this);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected View getListHeader() {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected View getListFooter() {
|
||||
return activity.getLayoutInflater().inflate(R.layout.pignate_footer, itemsList, false);
|
||||
}
|
||||
|
||||
protected RecyclerView.LayoutManager getListLayoutManager() {
|
||||
return new LinearLayoutManager(activity);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
|
||||
itemsList = rootView.findViewById(R.id.items_list);
|
||||
itemsList.setLayoutManager(getListLayoutManager());
|
||||
|
||||
infoListAdapter.setFooter(getListFooter());
|
||||
infoListAdapter.setHeader(getListHeader());
|
||||
|
||||
itemsList.setAdapter(infoListAdapter);
|
||||
}
|
||||
|
||||
protected void onItemSelected(InfoItem selectedItem) {
|
||||
if (DEBUG) Log.d(TAG, "onItemSelected() called with: selectedItem = [" + selectedItem + "]");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initListeners() {
|
||||
super.initListeners();
|
||||
infoListAdapter.setOnStreamSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener<StreamInfoItem>() {
|
||||
@Override
|
||||
public void selected(StreamInfoItem selectedItem) {
|
||||
onItemSelected(selectedItem);
|
||||
NavigationHelper.openVideoDetailFragment(getFragmentManager(), selectedItem.service_id, selectedItem.url, selectedItem.name);
|
||||
}
|
||||
});
|
||||
|
||||
infoListAdapter.setOnChannelSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener<ChannelInfoItem>() {
|
||||
@Override
|
||||
public void selected(ChannelInfoItem selectedItem) {
|
||||
onItemSelected(selectedItem);
|
||||
NavigationHelper.openChannelFragment(getFragmentManager(), selectedItem.service_id, selectedItem.url, selectedItem.name);
|
||||
}
|
||||
});
|
||||
|
||||
infoListAdapter.setOnPlaylistSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener<PlaylistInfoItem>() {
|
||||
@Override
|
||||
public void selected(PlaylistInfoItem selectedItem) {
|
||||
onItemSelected(selectedItem);
|
||||
NavigationHelper.openPlaylistFragment(getFragmentManager(), selectedItem.service_id, selectedItem.url, selectedItem.name);
|
||||
}
|
||||
});
|
||||
|
||||
itemsList.clearOnScrollListeners();
|
||||
itemsList.addOnScrollListener(new OnScrollBelowItemsListener() {
|
||||
@Override
|
||||
public void onScrolledDown(RecyclerView recyclerView) {
|
||||
onScrollToBottom();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void onScrollToBottom() {
|
||||
if (hasMoreItems() && !isLoading.get()) {
|
||||
loadMoreItems();
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Menu
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
if (supportActionBar != null) {
|
||||
supportActionBar.setDisplayShowTitleEnabled(true);
|
||||
supportActionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Load and handle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected abstract void loadMoreItems();
|
||||
|
||||
protected abstract boolean hasMoreItems();
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Contract
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void showLoading() {
|
||||
super.showLoading();
|
||||
// animateView(itemsList, false, 400);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hideLoading() {
|
||||
super.hideLoading();
|
||||
animateView(itemsList, true, 300);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showError(String message, boolean showRetryButton) {
|
||||
super.showError(message, showRetryButton);
|
||||
showListFooter(false);
|
||||
animateView(itemsList, false, 200);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showEmptyState() {
|
||||
super.showEmptyState();
|
||||
showListFooter(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showListFooter(final boolean show) {
|
||||
itemsList.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
infoListAdapter.showFooter(show);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleNextItems(N result) {
|
||||
isLoading.set(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
package org.schabi.newpipe.fragments.list;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.ListInfo;
|
||||
|
||||
import java.util.Queue;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
public abstract class BaseListInfoFragment<I extends ListInfo> extends BaseListFragment<I, ListExtractor.NextItemsResult> {
|
||||
|
||||
@State
|
||||
protected int serviceId = -1;
|
||||
@State
|
||||
protected String name;
|
||||
@State
|
||||
protected String url;
|
||||
|
||||
protected I currentInfo;
|
||||
protected String currentNextItemsUrl;
|
||||
protected Disposable currentWorker;
|
||||
|
||||
@Override
|
||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
setTitle(name);
|
||||
showListFooter(hasMoreItems());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
if (currentWorker != null) currentWorker.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
// Check if it was loading when the fragment was stopped/paused,
|
||||
if (wasLoading.getAndSet(false)) {
|
||||
if (hasMoreItems() && infoListAdapter.getItemsList().size() > 0) {
|
||||
loadMoreItems();
|
||||
} else {
|
||||
doInitialLoadLogic();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (currentWorker != null) currentWorker.dispose();
|
||||
currentWorker = null;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// State Saving
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void writeTo(Queue<Object> objectsToSave) {
|
||||
super.writeTo(objectsToSave);
|
||||
objectsToSave.add(currentInfo);
|
||||
objectsToSave.add(currentNextItemsUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception {
|
||||
super.readFrom(savedObjects);
|
||||
currentInfo = (I) savedObjects.poll();
|
||||
currentNextItemsUrl = (String) savedObjects.poll();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public void setTitle(String title) {
|
||||
Log.d(TAG, "setTitle() called with: title = [" + title + "]");
|
||||
if (activity.getSupportActionBar() != null) {
|
||||
activity.getSupportActionBar().setTitle(title);
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Load and handle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected void doInitialLoadLogic() {
|
||||
if (DEBUG) Log.d(TAG, "doInitialLoadLogic() called");
|
||||
if (currentInfo == null) {
|
||||
startLoading(false);
|
||||
} else handleResult(currentInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement the logic to load the info from the network.<br/>
|
||||
* You can use the default implementations from {@link org.schabi.newpipe.util.ExtractorHelper}.
|
||||
*
|
||||
* @param forceLoad allow or disallow the result to come from the cache
|
||||
*/
|
||||
protected abstract Single<I> loadResult(boolean forceLoad);
|
||||
|
||||
@Override
|
||||
public void startLoading(boolean forceLoad) {
|
||||
super.startLoading(forceLoad);
|
||||
|
||||
showListFooter(false);
|
||||
currentInfo = null;
|
||||
if (currentWorker != null) currentWorker.dispose();
|
||||
currentWorker = loadResult(forceLoad)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new Consumer<I>() {
|
||||
@Override
|
||||
public void accept(@NonNull I result) throws Exception {
|
||||
isLoading.set(false);
|
||||
currentInfo = result;
|
||||
currentNextItemsUrl = result.next_streams_url;
|
||||
handleResult(result);
|
||||
}
|
||||
}, new Consumer<Throwable>() {
|
||||
@Override
|
||||
public void accept(@NonNull Throwable throwable) throws Exception {
|
||||
onError(throwable);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement the logic to load more items<br/>
|
||||
* You can use the default implementations from {@link org.schabi.newpipe.util.ExtractorHelper}
|
||||
*/
|
||||
protected abstract Single<ListExtractor.NextItemsResult> loadMoreItemsLogic();
|
||||
|
||||
protected void loadMoreItems() {
|
||||
isLoading.set(true);
|
||||
|
||||
if (currentWorker != null) currentWorker.dispose();
|
||||
currentWorker = loadMoreItemsLogic()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new Consumer<ListExtractor.NextItemsResult>() {
|
||||
@Override
|
||||
public void accept(@io.reactivex.annotations.NonNull ListExtractor.NextItemsResult nextItemsResult) throws Exception {
|
||||
isLoading.set(false);
|
||||
handleNextItems(nextItemsResult);
|
||||
}
|
||||
}, new Consumer<Throwable>() {
|
||||
@Override
|
||||
public void accept(@io.reactivex.annotations.NonNull Throwable throwable) throws Exception {
|
||||
isLoading.set(false);
|
||||
onError(throwable);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleNextItems(ListExtractor.NextItemsResult result) {
|
||||
super.handleNextItems(result);
|
||||
currentNextItemsUrl = result.nextItemsUrl;
|
||||
infoListAdapter.addInfoItemList(result.nextItemsList);
|
||||
|
||||
showListFooter(hasMoreItems());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean hasMoreItems() {
|
||||
return !TextUtils.isEmpty(currentNextItemsUrl);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Contract
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void handleResult(@NonNull I result) {
|
||||
super.handleResult(result);
|
||||
|
||||
url = result.url;
|
||||
name = result.name;
|
||||
setTitle(name);
|
||||
|
||||
if (infoListAdapter.getItemsList().size() == 0) {
|
||||
if (result.related_streams.size() > 0) {
|
||||
infoListAdapter.addInfoItemList(result.related_streams);
|
||||
showListFooter(hasMoreItems());
|
||||
} else {
|
||||
infoListAdapter.clearStreamItemList();
|
||||
showEmptyState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected void setInitialData(int serviceId, String url, String name) {
|
||||
this.serviceId = serviceId;
|
||||
this.url = url;
|
||||
this.name = !TextUtils.isEmpty(name) ? name : "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.schabi.newpipe.fragments.list;
|
||||
|
||||
import org.schabi.newpipe.fragments.ViewContract;
|
||||
|
||||
public interface ListViewContract<I, N> extends ViewContract<I> {
|
||||
void showListFooter(boolean show);
|
||||
|
||||
void handleNextItems(N result);
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
package org.schabi.newpipe.fragments.list.channel;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.jakewharton.rxbinding2.view.RxView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.fragments.subscription.SubscriptionService;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.functions.Action;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import io.reactivex.functions.Function;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateBackgroundColor;
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateTextColor;
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
|
||||
private CompositeDisposable disposables = new CompositeDisposable();
|
||||
private Disposable subscribeButtonMonitor;
|
||||
private SubscriptionService subscriptionService;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private View headerRootLayout;
|
||||
private ImageView headerChannelBanner;
|
||||
private ImageView headerAvatarView;
|
||||
private TextView headerTitleView;
|
||||
private TextView headerSubscribersTextView;
|
||||
private Button headerSubscribeButton;
|
||||
|
||||
private MenuItem menuRssButton;
|
||||
|
||||
public static ChannelFragment getInstance(int serviceId, String url, String name) {
|
||||
ChannelFragment instance = new ChannelFragment();
|
||||
instance.setInitialData(serviceId, url, name);
|
||||
return instance;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
subscriptionService = SubscriptionService.getInstance();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_channel, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (disposables != null) disposables.clear();
|
||||
if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected View getListHeader() {
|
||||
headerRootLayout = activity.getLayoutInflater().inflate(R.layout.channel_header, itemsList, false);
|
||||
headerChannelBanner = headerRootLayout.findViewById(R.id.channel_banner_image);
|
||||
headerAvatarView = headerRootLayout.findViewById(R.id.channel_avatar_view);
|
||||
headerTitleView = headerRootLayout.findViewById(R.id.channel_title_view);
|
||||
headerSubscribersTextView = headerRootLayout.findViewById(R.id.channel_subscriber_view);
|
||||
headerSubscribeButton = headerRootLayout.findViewById(R.id.channel_subscribe_button);
|
||||
return headerRootLayout;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Menu
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
inflater.inflate(R.menu.menu_channel, menu);
|
||||
|
||||
menuRssButton = menu.findItem(R.id.menu_item_rss);
|
||||
if (currentInfo != null) {
|
||||
menuRssButton.setVisible(!TextUtils.isEmpty(currentInfo.feed_url));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_item_rss: {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse(currentInfo.feed_url));
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Channel Subscription
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
|
||||
|
||||
private void monitorSubscription(final ChannelInfo info) {
|
||||
final Consumer<Throwable> onError = new Consumer<Throwable>() {
|
||||
@Override
|
||||
public void accept(Throwable throwable) throws Exception {
|
||||
animateView(headerSubscribeButton, false, 100);
|
||||
showSnackBarError(throwable, UserAction.SUBSCRIPTION, NewPipe.getNameOfService(currentInfo.service_id), "Get subscription status", 0);
|
||||
}
|
||||
};
|
||||
|
||||
final Observable<List<SubscriptionEntity>> observable = subscriptionService.subscriptionTable()
|
||||
.getSubscription(info.service_id, info.url)
|
||||
.toObservable();
|
||||
|
||||
disposables.add(observable
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(getSubscribeUpdateMonitor(info), onError));
|
||||
|
||||
disposables.add(observable
|
||||
// Some updates are very rapid (when calling the updateSubscription(info), for example)
|
||||
// so only update the UI for the latest emission ("sync" the subscribe button's state)
|
||||
.debounce(100, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new Consumer<List<SubscriptionEntity>>() {
|
||||
@Override
|
||||
public void accept(List<SubscriptionEntity> subscriptionEntities) throws Exception {
|
||||
updateSubscribeButton(!subscriptionEntities.isEmpty());
|
||||
}
|
||||
}, onError));
|
||||
|
||||
}
|
||||
|
||||
private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription) {
|
||||
return new Function<Object, Object>() {
|
||||
@Override
|
||||
public Object apply(@NonNull Object o) throws Exception {
|
||||
subscriptionService.subscriptionTable().insert(subscription);
|
||||
return o;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Function<Object, Object> mapOnUnsubscribe(final SubscriptionEntity subscription) {
|
||||
return new Function<Object, Object>() {
|
||||
@Override
|
||||
public Object apply(@NonNull Object o) throws Exception {
|
||||
subscriptionService.subscriptionTable().delete(subscription);
|
||||
return o;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void updateSubscription(final ChannelInfo info) {
|
||||
if (DEBUG) Log.d(TAG, "updateSubscription() called with: info = [" + info + "]");
|
||||
final Action onComplete = new Action() {
|
||||
@Override
|
||||
public void run() throws Exception {
|
||||
if (DEBUG) Log.d(TAG, "Updated subscription: " + info.url);
|
||||
}
|
||||
};
|
||||
|
||||
final Consumer<Throwable> onError = new Consumer<Throwable>() {
|
||||
@Override
|
||||
public void accept(@NonNull Throwable throwable) throws Exception {
|
||||
onUnrecoverableError(throwable, UserAction.SUBSCRIPTION, NewPipe.getNameOfService(info.service_id), "Updating Subscription for " + info.url, R.string.subscription_update_failed);
|
||||
}
|
||||
};
|
||||
|
||||
disposables.add(subscriptionService.updateChannelInfo(info)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(onComplete, onError));
|
||||
}
|
||||
|
||||
private Disposable monitorSubscribeButton(final Button subscribeButton, final Function<Object, Object> action) {
|
||||
final Consumer<Object> onNext = new Consumer<Object>() {
|
||||
@Override
|
||||
public void accept(@NonNull Object o) throws Exception {
|
||||
if (DEBUG) Log.d(TAG, "Changed subscription status to this channel!");
|
||||
}
|
||||
};
|
||||
|
||||
final Consumer<Throwable> onError = new Consumer<Throwable>() {
|
||||
@Override
|
||||
public void accept(@NonNull Throwable throwable) throws Exception {
|
||||
onUnrecoverableError(throwable, UserAction.SUBSCRIPTION, NewPipe.getNameOfService(currentInfo.service_id), "Subscription Change", R.string.subscription_change_failed);
|
||||
}
|
||||
};
|
||||
|
||||
/* Emit clicks from main thread unto io thread */
|
||||
return RxView.clicks(subscribeButton)
|
||||
.subscribeOn(AndroidSchedulers.mainThread())
|
||||
.observeOn(Schedulers.io())
|
||||
.debounce(BUTTON_DEBOUNCE_INTERVAL, TimeUnit.MILLISECONDS) // Ignore rapid clicks
|
||||
.map(action)
|
||||
.subscribe(onNext, onError);
|
||||
}
|
||||
|
||||
private Consumer<List<SubscriptionEntity>> getSubscribeUpdateMonitor(final ChannelInfo info) {
|
||||
return new Consumer<List<SubscriptionEntity>>() {
|
||||
@Override
|
||||
public void accept(List<SubscriptionEntity> subscriptionEntities) throws Exception {
|
||||
if (DEBUG)
|
||||
Log.d(TAG, "subscriptionService.subscriptionTable.doOnNext() called with: subscriptionEntities = [" + subscriptionEntities + "]");
|
||||
if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose();
|
||||
|
||||
if (subscriptionEntities.isEmpty()) {
|
||||
if (DEBUG) Log.d(TAG, "No subscription to this channel!");
|
||||
SubscriptionEntity channel = new SubscriptionEntity();
|
||||
channel.setServiceId(info.service_id);
|
||||
channel.setUrl(info.url);
|
||||
channel.setData(info.name, info.avatar_url, info.description, info.subscriber_count);
|
||||
subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnSubscribe(channel));
|
||||
} else {
|
||||
if (DEBUG) Log.d(TAG, "Found subscription to this channel!");
|
||||
final SubscriptionEntity subscription = subscriptionEntities.get(0);
|
||||
subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnUnsubscribe(subscription));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void updateSubscribeButton(boolean isSubscribed) {
|
||||
if (DEBUG) Log.d(TAG, "updateSubscribeButton() called with: isSubscribed = [" + isSubscribed + "]");
|
||||
|
||||
boolean isButtonVisible = headerSubscribeButton.getVisibility() == View.VISIBLE;
|
||||
int backgroundDuration = isButtonVisible ? 300 : 0;
|
||||
int textDuration = isButtonVisible ? 200 : 0;
|
||||
|
||||
int subscribeBackground = ContextCompat.getColor(activity, R.color.subscribe_background_color);
|
||||
int subscribeText = ContextCompat.getColor(activity, R.color.subscribe_text_color);
|
||||
int subscribedBackground = ContextCompat.getColor(activity, R.color.subscribed_background_color);
|
||||
int subscribedText = ContextCompat.getColor(activity, R.color.subscribed_text_color);
|
||||
|
||||
if (!isSubscribed) {
|
||||
headerSubscribeButton.setText(R.string.subscribe_button_title);
|
||||
animateBackgroundColor(headerSubscribeButton, backgroundDuration, subscribedBackground, subscribeBackground);
|
||||
animateTextColor(headerSubscribeButton, textDuration, subscribedText, subscribeText);
|
||||
} else {
|
||||
headerSubscribeButton.setText(R.string.subscribed_button_title);
|
||||
animateBackgroundColor(headerSubscribeButton, backgroundDuration, subscribeBackground, subscribedBackground);
|
||||
animateTextColor(headerSubscribeButton, textDuration, subscribeText, subscribedText);
|
||||
}
|
||||
|
||||
animateView(headerSubscribeButton, AnimationUtils.Type.LIGHT_SCALE_AND_ALPHA, true, 100);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Load and handle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected Single<ListExtractor.NextItemsResult> loadMoreItemsLogic() {
|
||||
return ExtractorHelper.getMoreChannelItems(serviceId, url, currentNextItemsUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Single<ChannelInfo> loadResult(boolean forceLoad) {
|
||||
return ExtractorHelper.getChannelInfo(serviceId, url, forceLoad);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Contract
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void showLoading() {
|
||||
super.showLoading();
|
||||
|
||||
imageLoader.cancelDisplayTask(headerChannelBanner);
|
||||
imageLoader.cancelDisplayTask(headerAvatarView);
|
||||
animateView(headerSubscribeButton, false, 100);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResult(@NonNull ChannelInfo result) {
|
||||
super.handleResult(result);
|
||||
|
||||
headerRootLayout.setVisibility(View.VISIBLE);
|
||||
imageLoader.displayImage(result.banner_url, headerChannelBanner, DISPLAY_BANNER_OPTIONS);
|
||||
imageLoader.displayImage(result.avatar_url, headerAvatarView, DISPLAY_AVATAR_OPTIONS);
|
||||
|
||||
if (result.subscriber_count != -1) {
|
||||
headerSubscribersTextView.setText(Localization.localizeSubscribersCount(activity, result.subscriber_count));
|
||||
headerSubscribersTextView.setVisibility(View.VISIBLE);
|
||||
} else headerSubscribersTextView.setVisibility(View.GONE);
|
||||
|
||||
if (menuRssButton != null) menuRssButton.setVisible(!TextUtils.isEmpty(result.feed_url));
|
||||
|
||||
if (!result.errors.isEmpty()) {
|
||||
showSnackBarError(result.errors, UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(result.service_id), result.url, 0);
|
||||
}
|
||||
|
||||
if (disposables != null) disposables.clear();
|
||||
if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose();
|
||||
updateSubscription(result);
|
||||
monitorSubscription(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleNextItems(ListExtractor.NextItemsResult result) {
|
||||
super.handleNextItems(result);
|
||||
|
||||
if (!result.errors.isEmpty()) {
|
||||
showSnackBarError(result.errors, UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(serviceId),
|
||||
"Get next page of: " + url, R.string.general_error);
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// OnError
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected boolean onError(Throwable exception) {
|
||||
if (super.onError(exception)) return true;
|
||||
|
||||
int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error;
|
||||
onUnrecoverableError(exception, UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(serviceId), url, errorId);
|
||||
return true;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void setTitle(String title) {
|
||||
super.setTitle(title);
|
||||
headerTitleView.setText(title);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
package org.schabi.newpipe.fragments.list.feed;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.reactivestreams.Subscriber;
|
||||
import org.reactivestreams.Subscription;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.fragments.list.BaseListFragment;
|
||||
import org.schabi.newpipe.fragments.subscription.SubscriptionService;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.MaybeObserver;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.functions.Action;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import io.reactivex.functions.Predicate;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Void> {
|
||||
|
||||
private static final int OFF_SCREEN_ITEMS_COUNT = 3;
|
||||
private static final int MIN_ITEMS_INITIAL_LOAD = 8;
|
||||
private int FEED_LOAD_COUNT = MIN_ITEMS_INITIAL_LOAD;
|
||||
|
||||
private int subscriptionPoolSize;
|
||||
|
||||
private SubscriptionService subscriptionService;
|
||||
|
||||
private AtomicBoolean allItemsLoaded = new AtomicBoolean(false);
|
||||
private HashSet<String> itemsLoaded = new HashSet<>();
|
||||
private final AtomicInteger requestLoadedAtomic = new AtomicInteger();
|
||||
|
||||
private CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||
private Disposable subscriptionObserver;
|
||||
private Subscription feedSubscriber;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Fragment LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
subscriptionService = SubscriptionService.getInstance();
|
||||
|
||||
FEED_LOAD_COUNT = howManyItemsToLoad();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_feed, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
disposeEverything();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (wasLoading.get()) doInitialLoadLogic();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
disposeEverything();
|
||||
subscriptionService = null;
|
||||
compositeDisposable = null;
|
||||
subscriptionObserver = null;
|
||||
feedSubscriber = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
// Do not monitor for updates when user is not viewing the feed fragment.
|
||||
// This is a waste of bandwidth.
|
||||
disposeEverything();
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
/*@Override
|
||||
protected RecyclerView.LayoutManager getListLayoutManager() {
|
||||
boolean isPortrait = getResources().getDisplayMetrics().heightPixels > getResources().getDisplayMetrics().widthPixels;
|
||||
return new GridLayoutManager(activity, isPortrait ? 1 : 2);
|
||||
}*/
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
|
||||
ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
if (supportActionBar != null) {
|
||||
supportActionBar.setTitle(R.string.fragment_whats_new);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reloadContent() {
|
||||
resetFragment();
|
||||
super.reloadContent();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// StateSaving
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void writeTo(Queue<Object> objectsToSave) {
|
||||
super.writeTo(objectsToSave);
|
||||
objectsToSave.add(allItemsLoaded);
|
||||
objectsToSave.add(itemsLoaded);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception {
|
||||
super.readFrom(savedObjects);
|
||||
allItemsLoaded = (AtomicBoolean) savedObjects.poll();
|
||||
itemsLoaded = (HashSet<String>) savedObjects.poll();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Feed Loader
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void startLoading(boolean forceLoad) {
|
||||
if (DEBUG) Log.d(TAG, "startLoading() called with: forceLoad = [" + forceLoad + "]");
|
||||
if (subscriptionObserver != null) subscriptionObserver.dispose();
|
||||
|
||||
if (allItemsLoaded.get()) {
|
||||
if (infoListAdapter.getItemsList().size() == 0) {
|
||||
showEmptyState();
|
||||
} else {
|
||||
showListFooter(false);
|
||||
hideLoading();
|
||||
}
|
||||
|
||||
isLoading.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.set(true);
|
||||
showLoading();
|
||||
showListFooter(true);
|
||||
subscriptionObserver = subscriptionService.getSubscription()
|
||||
.onErrorReturnItem(Collections.<SubscriptionEntity>emptyList())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new Consumer<List<SubscriptionEntity>>() {
|
||||
@Override
|
||||
public void accept(List<SubscriptionEntity> subscriptionEntities) throws Exception {
|
||||
handleResult(subscriptionEntities);
|
||||
}
|
||||
}, new Consumer<Throwable>() {
|
||||
@Override
|
||||
public void accept(Throwable throwable) throws Exception {
|
||||
onError(throwable);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResult(@android.support.annotation.NonNull List<SubscriptionEntity> result) {
|
||||
super.handleResult(result);
|
||||
|
||||
if (result.isEmpty()) {
|
||||
infoListAdapter.clearStreamItemList();
|
||||
showEmptyState();
|
||||
return;
|
||||
}
|
||||
|
||||
subscriptionPoolSize = result.size();
|
||||
Flowable.fromIterable(result)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(getSubscriptionObserver());
|
||||
}
|
||||
|
||||
/**
|
||||
* Responsible for reacting to user pulling request and starting a request for new feed stream.
|
||||
* <p>
|
||||
* On initialization, it automatically requests the amount of feed needed to display
|
||||
* a minimum amount required (FEED_LOAD_SIZE).
|
||||
* <p>
|
||||
* Upon receiving a user pull, it creates a Single Observer to fetch the ChannelInfo
|
||||
* containing the feed streams.
|
||||
**/
|
||||
private Subscriber<SubscriptionEntity> getSubscriptionObserver() {
|
||||
return new Subscriber<SubscriptionEntity>() {
|
||||
@Override
|
||||
public void onSubscribe(Subscription s) {
|
||||
if (feedSubscriber != null) feedSubscriber.cancel();
|
||||
feedSubscriber = s;
|
||||
|
||||
int requestSize = FEED_LOAD_COUNT - infoListAdapter.getItemsList().size();
|
||||
if (wasLoading.getAndSet(false)) requestSize = FEED_LOAD_COUNT;
|
||||
|
||||
boolean hasToLoad = requestSize > 0;
|
||||
if (hasToLoad) {
|
||||
requestLoadedAtomic.set(infoListAdapter.getItemsList().size());
|
||||
requestFeed(requestSize);
|
||||
}
|
||||
isLoading.set(hasToLoad);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(SubscriptionEntity subscriptionEntity) {
|
||||
if (!itemsLoaded.contains(subscriptionEntity.getServiceId() + subscriptionEntity.getUrl())) {
|
||||
subscriptionService.getChannelInfo(subscriptionEntity)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.onErrorComplete(new Predicate<Throwable>() {
|
||||
@Override
|
||||
public boolean test(@io.reactivex.annotations.NonNull Throwable throwable) throws Exception {
|
||||
return FeedFragment.super.onError(throwable);
|
||||
}
|
||||
})
|
||||
.subscribe(getChannelInfoObserver(subscriptionEntity.getServiceId(), subscriptionEntity.getUrl()));
|
||||
} else {
|
||||
requestFeed(1);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable exception) {
|
||||
FeedFragment.this.onError(exception);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
if (DEBUG) Log.d(TAG, "getSubscriptionObserver > onComplete() called");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* On each request, a subscription item from the updated table is transformed
|
||||
* into a ChannelInfo, containing the latest streams from the channel.
|
||||
* <p>
|
||||
* Currently, the feed uses the first into from the list of streams.
|
||||
* <p>
|
||||
* If chosen feed already displayed, then we request another feed from another
|
||||
* subscription, until the subscription table runs out of new items.
|
||||
* <p>
|
||||
* This Observer is self-contained and will dispose itself when complete. However, this
|
||||
* does not obey the fragment lifecycle and may continue running in the background
|
||||
* until it is complete. This is done due to RxJava2 no longer propagate errors once
|
||||
* an observer is unsubscribed while the thread process is still running.
|
||||
* <p>
|
||||
* To solve the above issue, we can either set a global RxJava Error Handler, or
|
||||
* manage exceptions case by case. This should be done if the current implementation is
|
||||
* too costly when dealing with larger subscription sets.
|
||||
*
|
||||
* @param url + serviceId to put in {@link #allItemsLoaded} to signal that this specific entity has been loaded.
|
||||
*/
|
||||
private MaybeObserver<ChannelInfo> getChannelInfoObserver(final int serviceId, final String url) {
|
||||
return new MaybeObserver<ChannelInfo>() {
|
||||
private Disposable observer;
|
||||
|
||||
@Override
|
||||
public void onSubscribe(Disposable d) {
|
||||
observer = d;
|
||||
compositeDisposable.add(d);
|
||||
isLoading.set(true);
|
||||
}
|
||||
|
||||
// Called only when response is non-empty
|
||||
@Override
|
||||
public void onSuccess(final ChannelInfo channelInfo) {
|
||||
if (infoListAdapter == null || channelInfo.related_streams.isEmpty()) {
|
||||
onDone();
|
||||
return;
|
||||
}
|
||||
|
||||
final InfoItem item = channelInfo.related_streams.get(0);
|
||||
// Keep requesting new items if the current one already exists
|
||||
boolean itemExists = doesItemExist(infoListAdapter.getItemsList(), item);
|
||||
if (!itemExists) {
|
||||
infoListAdapter.addInfoItem(item);
|
||||
//updateSubscription(channelInfo);
|
||||
} else {
|
||||
requestFeed(1);
|
||||
}
|
||||
onDone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable exception) {
|
||||
showSnackBarError(exception, UserAction.SUBSCRIPTION, NewPipe.getNameOfService(serviceId), url, 0);
|
||||
requestFeed(1);
|
||||
onDone();
|
||||
}
|
||||
|
||||
// Called only when response is empty
|
||||
@Override
|
||||
public void onComplete() {
|
||||
onDone();
|
||||
}
|
||||
|
||||
private void onDone() {
|
||||
if (observer.isDisposed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
itemsLoaded.add(serviceId + url);
|
||||
compositeDisposable.remove(observer);
|
||||
|
||||
int loaded = requestLoadedAtomic.incrementAndGet();
|
||||
if (loaded >= Math.min(FEED_LOAD_COUNT, subscriptionPoolSize)) {
|
||||
requestLoadedAtomic.set(0);
|
||||
isLoading.set(false);
|
||||
}
|
||||
|
||||
if (itemsLoaded.size() == subscriptionPoolSize) {
|
||||
if (DEBUG) Log.d(TAG, "getChannelInfoObserver > All Items Loaded");
|
||||
allItemsLoaded.set(true);
|
||||
showListFooter(false);
|
||||
isLoading.set(false);
|
||||
hideLoading();
|
||||
if (infoListAdapter.getItemsList().size() == 0) {
|
||||
showEmptyState();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void loadMoreItems() {
|
||||
isLoading.set(true);
|
||||
delayHandler.removeCallbacksAndMessages(null);
|
||||
// Add a little of a delay when requesting more items because the cache is so fast,
|
||||
// that the view seems stuck to the user when he scroll to the bottom
|
||||
delayHandler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
requestFeed(FEED_LOAD_COUNT);
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean hasMoreItems() {
|
||||
return !allItemsLoaded.get();
|
||||
}
|
||||
|
||||
private final Handler delayHandler = new Handler();
|
||||
|
||||
private void requestFeed(final int count) {
|
||||
if (DEBUG) Log.d(TAG, "requestFeed() called with: count = [" + count + "], feedSubscriber = [" + feedSubscriber + "]");
|
||||
if (feedSubscriber == null) return;
|
||||
|
||||
isLoading.set(true);
|
||||
delayHandler.removeCallbacksAndMessages(null);
|
||||
feedSubscriber.request(count);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void resetFragment() {
|
||||
if (DEBUG) Log.d(TAG, "resetFragment() called");
|
||||
if (subscriptionObserver != null) subscriptionObserver.dispose();
|
||||
if (compositeDisposable != null) compositeDisposable.clear();
|
||||
if (infoListAdapter != null) infoListAdapter.clearStreamItemList();
|
||||
|
||||
delayHandler.removeCallbacksAndMessages(null);
|
||||
requestLoadedAtomic.set(0);
|
||||
allItemsLoaded.set(false);
|
||||
showListFooter(false);
|
||||
itemsLoaded.clear();
|
||||
}
|
||||
|
||||
private void disposeEverything() {
|
||||
if (subscriptionObserver != null) subscriptionObserver.dispose();
|
||||
if (compositeDisposable != null) compositeDisposable.clear();
|
||||
if (feedSubscriber != null) feedSubscriber.cancel();
|
||||
delayHandler.removeCallbacksAndMessages(null);
|
||||
}
|
||||
|
||||
private boolean doesItemExist(final List<InfoItem> items, final InfoItem item) {
|
||||
for (final InfoItem existingItem : items) {
|
||||
if (existingItem.info_type == item.info_type &&
|
||||
existingItem.service_id == item.service_id &&
|
||||
existingItem.name.equals(item.name) &&
|
||||
existingItem.url.equals(item.url)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private int howManyItemsToLoad() {
|
||||
int heightPixels = getResources().getDisplayMetrics().heightPixels;
|
||||
int itemHeightPixels = activity.getResources().getDimensionPixelSize(R.dimen.video_item_search_height);
|
||||
|
||||
int items = itemHeightPixels > 0 ? heightPixels / itemHeightPixels + OFF_SCREEN_ITEMS_COUNT : MIN_ITEMS_INITIAL_LOAD;
|
||||
return Math.max(MIN_ITEMS_INITIAL_LOAD, items);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Fragment Error Handling
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void showError(String message, boolean showRetryButton) {
|
||||
resetFragment();
|
||||
super.showError(message, showRetryButton);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onError(Throwable exception) {
|
||||
if (super.onError(exception)) return true;
|
||||
|
||||
int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error;
|
||||
onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, "none", "Requesting feed", errorId);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package org.schabi.newpipe.fragments.list.playlist;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
|
||||
import io.reactivex.Single;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private View headerRootLayout;
|
||||
private TextView headerTitleView;
|
||||
private View headerUploaderLayout;
|
||||
private TextView headerUploaderName;
|
||||
private ImageView headerUploaderAvatar;
|
||||
private TextView headerStreamCount;
|
||||
|
||||
public static PlaylistFragment getInstance(int serviceId, String url, String name) {
|
||||
PlaylistFragment instance = new PlaylistFragment();
|
||||
instance.setInitialData(serviceId, url, name);
|
||||
return instance;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_playlist, container, false);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected View getListHeader() {
|
||||
headerRootLayout = activity.getLayoutInflater().inflate(R.layout.playlist_header, itemsList, false);
|
||||
headerTitleView = headerRootLayout.findViewById(R.id.playlist_title_view);
|
||||
headerUploaderLayout = headerRootLayout.findViewById(R.id.uploader_layout);
|
||||
headerUploaderName = headerRootLayout.findViewById(R.id.uploader_name);
|
||||
headerUploaderAvatar = headerRootLayout.findViewById(R.id.uploader_avatar_view);
|
||||
headerStreamCount = headerRootLayout.findViewById(R.id.playlist_stream_count);
|
||||
|
||||
return headerRootLayout;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
|
||||
infoListAdapter.useMiniItemVariants(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
inflater.inflate(R.menu.menu_playlist, menu);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Load and handle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected Single<ListExtractor.NextItemsResult> loadMoreItemsLogic() {
|
||||
return ExtractorHelper.getMorePlaylistItems(serviceId, url, currentNextItemsUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Single<PlaylistInfo> loadResult(boolean forceLoad) {
|
||||
return ExtractorHelper.getPlaylistInfo(serviceId, url, forceLoad);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Contract
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void showLoading() {
|
||||
super.showLoading();
|
||||
animateView(headerRootLayout, false, 200);
|
||||
animateView(itemsList, false, 100);
|
||||
|
||||
imageLoader.cancelDisplayTask(headerUploaderAvatar);
|
||||
animateView(headerUploaderLayout, false, 200);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResult(@NonNull final PlaylistInfo result) {
|
||||
super.handleResult(result);
|
||||
|
||||
animateView(headerRootLayout, true, 100);
|
||||
animateView(headerUploaderLayout, true, 300);
|
||||
headerUploaderLayout.setOnClickListener(null);
|
||||
if (!TextUtils.isEmpty(result.uploader_name)) {
|
||||
headerUploaderName.setText(result.uploader_name);
|
||||
if (!TextUtils.isEmpty(result.uploader_url)) {
|
||||
headerUploaderLayout.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
NavigationHelper.openChannelFragment(getFragmentManager(), result.service_id, result.uploader_url, result.uploader_name);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
imageLoader.displayImage(result.uploader_avatar_url, headerUploaderAvatar, DISPLAY_AVATAR_OPTIONS);
|
||||
headerStreamCount.setText(result.stream_count + " videos");
|
||||
|
||||
if (!result.errors.isEmpty()) {
|
||||
showSnackBarError(result.errors, UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(result.service_id), result.url, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleNextItems(ListExtractor.NextItemsResult result) {
|
||||
super.handleNextItems(result);
|
||||
|
||||
if (!result.errors.isEmpty()) {
|
||||
showSnackBarError(result.errors, UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(serviceId)
|
||||
, "Get next page of: " + url, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// OnError
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected boolean onError(Throwable exception) {
|
||||
if (super.onError(exception)) return true;
|
||||
|
||||
int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error;
|
||||
onUnrecoverableError(exception, UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(serviceId), url, errorId);
|
||||
return true;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void setTitle(String title) {
|
||||
super.setTitle(title);
|
||||
headerTitleView.setText(title);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,695 @@
|
||||
package org.schabi.newpipe.fragments.list.search;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.widget.TooltipCompat;
|
||||
import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AutoCompleteTextView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.search.SearchEngine;
|
||||
import org.schabi.newpipe.extractor.search.SearchResult;
|
||||
import org.schabi.newpipe.fragments.list.BaseListFragment;
|
||||
import org.schabi.newpipe.history.HistoryListener;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.Notification;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import io.reactivex.functions.Function;
|
||||
import io.reactivex.functions.Predicate;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.subjects.PublishSubject;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor.NextItemsResult> {
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Search
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
/**
|
||||
* The suggestions will appear only if the query meet this threshold (>=).
|
||||
*/
|
||||
private static final int THRESHOLD_SUGGESTION = 3;
|
||||
|
||||
/**
|
||||
* How much time have to pass without emitting a item (i.e. the user stop typing) to fetch/show the suggestions, in milliseconds.
|
||||
*/
|
||||
private static final int SUGGESTIONS_DEBOUNCE = 150; //ms
|
||||
|
||||
@State
|
||||
protected int filterItemCheckedId = -1;
|
||||
private SearchEngine.Filter filter = SearchEngine.Filter.ANY;
|
||||
|
||||
@State
|
||||
protected int serviceId = -1;
|
||||
@State
|
||||
protected String searchQuery = "";
|
||||
@State
|
||||
protected boolean wasSearchFocused = false;
|
||||
|
||||
private int currentPage = 0;
|
||||
private int currentNextPage = 0;
|
||||
private String searchLanguage;
|
||||
private boolean showSuggestions = true;
|
||||
|
||||
private PublishSubject<String> suggestionPublisher = PublishSubject.create();
|
||||
private Disposable searchDisposable;
|
||||
private Disposable suggestionWorkerDisposable;
|
||||
|
||||
private SuggestionListAdapter suggestionListAdapter;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private View searchToolbarContainer;
|
||||
private AutoCompleteTextView searchEditText;
|
||||
private View searchClear;
|
||||
|
||||
/*////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public static SearchFragment getInstance(int serviceId, String query) {
|
||||
SearchFragment searchFragment = new SearchFragment();
|
||||
searchFragment.setQuery(serviceId, query);
|
||||
searchFragment.searchOnResume();
|
||||
return searchFragment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set wasLoading to true so when the fragment onResume is called, the initial search is done.
|
||||
* (it will only start searching if the query is not null or empty)
|
||||
*/
|
||||
private void searchOnResume() {
|
||||
if (!TextUtils.isEmpty(searchQuery)) {
|
||||
wasLoading.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Fragment's LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
suggestionListAdapter = new SuggestionListAdapter(activity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_search, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
wasSearchFocused = searchEditText.hasFocus();
|
||||
|
||||
if (searchDisposable != null) searchDisposable.dispose();
|
||||
if (suggestionWorkerDisposable != null) suggestionWorkerDisposable.dispose();
|
||||
hideSoftKeyboard(searchEditText);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
if (DEBUG) Log.d(TAG, "onResume() called");
|
||||
super.onResume();
|
||||
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
|
||||
showSuggestions = preferences.getBoolean(getString(R.string.show_search_suggestions_key), true);
|
||||
searchLanguage = preferences.getString(getString(R.string.search_language_key), getString(R.string.default_language_value));
|
||||
|
||||
if (!TextUtils.isEmpty(searchQuery)) {
|
||||
if (wasLoading.getAndSet(false)) {
|
||||
if (currentNextPage > currentPage) loadMoreItems();
|
||||
else search(searchQuery);
|
||||
} else if (infoListAdapter.getItemsList().size() == 0) {
|
||||
if (savedState == null) {
|
||||
search(searchQuery);
|
||||
} else if (!isLoading.get() && !wasSearchFocused) {
|
||||
infoListAdapter.clearStreamItemList();
|
||||
showEmptyState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (suggestionWorkerDisposable == null || suggestionWorkerDisposable.isDisposed()) initSuggestionObserver();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
if (DEBUG) Log.d(TAG, "onDestroyView() called");
|
||||
unsetSearchListeners();
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (!activity.isChangingConfigurations()) StateSaver.onDestroy(savedState);
|
||||
|
||||
if (searchDisposable != null) searchDisposable.dispose();
|
||||
if (suggestionWorkerDisposable != null) suggestionWorkerDisposable.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
switch (requestCode) {
|
||||
case ReCaptchaActivity.RECAPTCHA_REQUEST:
|
||||
if (resultCode == Activity.RESULT_OK && searchQuery.length() != 0) {
|
||||
search(searchQuery);
|
||||
} else Log.e(TAG, "ReCaptcha failed");
|
||||
break;
|
||||
|
||||
default:
|
||||
Log.e(TAG, "Request code from activity not supported [" + requestCode + "]");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// State Saving
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void writeTo(Queue<Object> objectsToSave) {
|
||||
super.writeTo(objectsToSave);
|
||||
objectsToSave.add(currentPage);
|
||||
objectsToSave.add(currentNextPage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception {
|
||||
super.readFrom(savedObjects);
|
||||
currentPage = (int) savedObjects.poll();
|
||||
currentNextPage = (int) savedObjects.poll();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle bundle) {
|
||||
searchQuery = searchEditText != null && !TextUtils.isEmpty(searchEditText.getText().toString())
|
||||
? searchEditText.getText().toString() : searchQuery;
|
||||
super.onSaveInstanceState(bundle);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init's
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void reloadContent() {
|
||||
if (!TextUtils.isEmpty(searchQuery) || (searchEditText != null && !TextUtils.isEmpty(searchEditText.getText()))) {
|
||||
search(!TextUtils.isEmpty(searchQuery) ? searchQuery : searchEditText.getText().toString());
|
||||
} else {
|
||||
if (searchEditText != null) {
|
||||
searchEditText.setText("");
|
||||
showSoftKeyboard(searchEditText);
|
||||
}
|
||||
animateView(errorPanelRoot, false, 200);
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Menu
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
|
||||
ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
if (supportActionBar != null) {
|
||||
supportActionBar.setDisplayShowTitleEnabled(false);
|
||||
supportActionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
|
||||
inflater.inflate(R.menu.menu_search, menu);
|
||||
|
||||
searchToolbarContainer = activity.findViewById(R.id.toolbar_search_container);
|
||||
searchEditText = searchToolbarContainer.findViewById(R.id.toolbar_search_edit_text);
|
||||
searchClear = searchToolbarContainer.findViewById(R.id.toolbar_search_clear);
|
||||
setupSearchView();
|
||||
|
||||
restoreFilterChecked(menu, filterItemCheckedId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_filter_all:
|
||||
case R.id.menu_filter_video:
|
||||
case R.id.menu_filter_channel:
|
||||
case R.id.menu_filter_playlist:
|
||||
changeFilter(item, getFilterFromMenuId(item.getItemId()));
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void restoreFilterChecked(Menu menu, int itemId) {
|
||||
if (itemId != -1) {
|
||||
MenuItem item = menu.findItem(itemId);
|
||||
if (item == null) return;
|
||||
|
||||
item.setChecked(true);
|
||||
filter = getFilterFromMenuId(itemId);
|
||||
}
|
||||
}
|
||||
|
||||
private SearchEngine.Filter getFilterFromMenuId(int itemId) {
|
||||
switch (itemId) {
|
||||
case R.id.menu_filter_all:
|
||||
return SearchEngine.Filter.ANY;
|
||||
case R.id.menu_filter_video:
|
||||
return SearchEngine.Filter.STREAM;
|
||||
case R.id.menu_filter_channel:
|
||||
return SearchEngine.Filter.CHANNEL;
|
||||
case R.id.menu_filter_playlist:
|
||||
return SearchEngine.Filter.PLAYLIST;
|
||||
default:
|
||||
return SearchEngine.Filter.ANY;
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Search
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private TextWatcher textWatcher;
|
||||
|
||||
private void setupSearchView() {
|
||||
searchEditText.setText(searchQuery != null ? searchQuery : "");
|
||||
searchEditText.setAdapter(suggestionListAdapter);
|
||||
|
||||
if (TextUtils.isEmpty(searchQuery) || TextUtils.isEmpty(searchEditText.getText())) {
|
||||
searchToolbarContainer.setTranslationX(100);
|
||||
searchToolbarContainer.setAlpha(0f);
|
||||
searchToolbarContainer.setVisibility(View.VISIBLE);
|
||||
searchToolbarContainer.animate().translationX(0).alpha(1f).setDuration(200).setInterpolator(new DecelerateInterpolator()).start();
|
||||
} else {
|
||||
searchToolbarContainer.setTranslationX(0);
|
||||
searchToolbarContainer.setAlpha(1f);
|
||||
searchToolbarContainer.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
initSearchListeners();
|
||||
|
||||
if (TextUtils.isEmpty(searchQuery) || wasSearchFocused) showSoftKeyboard(searchEditText);
|
||||
else hideSoftKeyboard(searchEditText);
|
||||
wasSearchFocused = false;
|
||||
}
|
||||
|
||||
private void initSearchListeners() {
|
||||
searchClear.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]");
|
||||
if (TextUtils.isEmpty(searchEditText.getText())) {
|
||||
NavigationHelper.gotoMainFragment(getFragmentManager());
|
||||
return;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
searchEditText.setText("", false);
|
||||
} else searchEditText.setText("");
|
||||
suggestionListAdapter.updateAdapter(new ArrayList<String>());
|
||||
showSoftKeyboard(searchEditText);
|
||||
}
|
||||
});
|
||||
|
||||
TooltipCompat.setTooltipText(searchClear, getString(R.string.clear));
|
||||
|
||||
searchEditText.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]");
|
||||
searchEditText.showDropDown();
|
||||
}
|
||||
});
|
||||
|
||||
searchEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
|
||||
@Override
|
||||
public void onFocusChange(View v, boolean hasFocus) {
|
||||
if (DEBUG) Log.d(TAG, "onFocusChange() called with: v = [" + v + "], hasFocus = [" + hasFocus + "]");
|
||||
if (hasFocus) searchEditText.showDropDown();
|
||||
}
|
||||
});
|
||||
|
||||
searchEditText.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onItemClick() called with: parent = [" + parent + "], view = [" + view + "], position = [" + position + "], id = [" + id + "]");
|
||||
}
|
||||
String s = suggestionListAdapter.getSuggestion(position);
|
||||
if (DEBUG) Log.d(TAG, "onItemClick text = " + s);
|
||||
submitQuery(s);
|
||||
}
|
||||
});
|
||||
searchEditText.setThreshold(THRESHOLD_SUGGESTION);
|
||||
|
||||
if (textWatcher != null) searchEditText.removeTextChangedListener(textWatcher);
|
||||
textWatcher = new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
String newText = searchEditText.getText().toString();
|
||||
if (!TextUtils.isEmpty(newText)) suggestionPublisher.onNext(newText);
|
||||
}
|
||||
};
|
||||
searchEditText.addTextChangedListener(textWatcher);
|
||||
|
||||
searchEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
||||
@Override
|
||||
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
|
||||
if (DEBUG)
|
||||
Log.d(TAG, "onEditorAction() called with: v = [" + v + "], actionId = [" + actionId + "], event = [" + event + "]");
|
||||
if (event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER || event.getAction() == EditorInfo.IME_ACTION_SEARCH)) {
|
||||
submitQuery(searchEditText.getText().toString());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (suggestionWorkerDisposable == null || suggestionWorkerDisposable.isDisposed()) initSuggestionObserver();
|
||||
}
|
||||
|
||||
private void unsetSearchListeners() {
|
||||
searchClear.setOnClickListener(null);
|
||||
searchClear.setOnLongClickListener(null);
|
||||
searchEditText.setOnClickListener(null);
|
||||
searchEditText.setOnItemClickListener(null);
|
||||
searchEditText.setOnFocusChangeListener(null);
|
||||
searchEditText.setOnEditorActionListener(null);
|
||||
|
||||
if (textWatcher != null) searchEditText.removeTextChangedListener(textWatcher);
|
||||
textWatcher = null;
|
||||
}
|
||||
|
||||
private void showSoftKeyboard(View view) {
|
||||
if (DEBUG) Log.d(TAG, "showSoftKeyboard() called with: view = [" + view + "]");
|
||||
if (view == null) return;
|
||||
|
||||
if (view.requestFocus()) {
|
||||
InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT);
|
||||
}
|
||||
}
|
||||
|
||||
private void hideSoftKeyboard(View view) {
|
||||
if (DEBUG) Log.d(TAG, "hideSoftKeyboard() called with: view = [" + view + "]");
|
||||
if (view == null) return;
|
||||
|
||||
InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
|
||||
|
||||
view.clearFocus();
|
||||
}
|
||||
|
||||
public void giveSearchEditTextFocus() {
|
||||
showSoftKeyboard(searchEditText);
|
||||
}
|
||||
|
||||
private void initSuggestionObserver() {
|
||||
if (suggestionWorkerDisposable != null) suggestionWorkerDisposable.dispose();
|
||||
final Predicate<String> checkEnabledAndLength = new Predicate<String>() {
|
||||
@Override
|
||||
public boolean test(@io.reactivex.annotations.NonNull String s) throws Exception {
|
||||
boolean lengthCheck = s.length() >= THRESHOLD_SUGGESTION;
|
||||
// Clear the suggestions adapter if the length check fails
|
||||
if (!lengthCheck && !suggestionListAdapter.isEmpty()) {
|
||||
suggestionListAdapter.updateAdapter(new ArrayList<String>());
|
||||
}
|
||||
// Only pass through if suggestions is enabled and the query length is equal or greater than THRESHOLD_SUGGESTION
|
||||
return showSuggestions && lengthCheck;
|
||||
}
|
||||
};
|
||||
|
||||
suggestionWorkerDisposable = suggestionPublisher
|
||||
.debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS)
|
||||
.startWith(!TextUtils.isEmpty(searchQuery) ? searchQuery : "")
|
||||
.filter(checkEnabledAndLength)
|
||||
.switchMap(new Function<String, Observable<Notification<List<String>>>>() {
|
||||
@Override
|
||||
public Observable<Notification<List<String>>> apply(@io.reactivex.annotations.NonNull String query) throws Exception {
|
||||
return ExtractorHelper.suggestionsFor(serviceId, query, searchLanguage).toObservable().materialize();
|
||||
}
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new Consumer<Notification<List<String>>>() {
|
||||
@Override
|
||||
public void accept(@io.reactivex.annotations.NonNull Notification<List<String>> listNotification) throws Exception {
|
||||
if (listNotification.isOnNext()) {
|
||||
handleSuggestions(listNotification.getValue());
|
||||
if (errorPanelRoot.getVisibility() == View.VISIBLE) {
|
||||
hideLoading();
|
||||
}
|
||||
} else if (listNotification.isOnError()) {
|
||||
Throwable error = listNotification.getError();
|
||||
if (!ExtractorHelper.isInterruptedCaused(error)) {
|
||||
onSuggestionError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doInitialLoadLogic() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
private void search(final String query) {
|
||||
if (DEBUG) Log.d(TAG, "search() called with: query = [" + query + "]");
|
||||
|
||||
hideSoftKeyboard(searchEditText);
|
||||
this.searchQuery = query;
|
||||
this.currentPage = 0;
|
||||
infoListAdapter.clearStreamItemList();
|
||||
|
||||
if (activity instanceof HistoryListener) {
|
||||
((HistoryListener) activity).onSearch(serviceId, query);
|
||||
}
|
||||
|
||||
final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
final String searchLanguageKey = getContext().getString(R.string.search_language_key);
|
||||
searchLanguage = sharedPreferences.getString(searchLanguageKey, getContext().getString(R.string.default_language_value));
|
||||
startLoading(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startLoading(boolean forceLoad) {
|
||||
super.startLoading(forceLoad);
|
||||
if (searchDisposable != null) searchDisposable.dispose();
|
||||
searchDisposable = ExtractorHelper.searchFor(serviceId, searchQuery, currentPage, searchLanguage, filter)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new Consumer<SearchResult>() {
|
||||
@Override
|
||||
public void accept(@NonNull SearchResult result) throws Exception {
|
||||
isLoading.set(false);
|
||||
handleResult(result);
|
||||
}
|
||||
}, new Consumer<Throwable>() {
|
||||
@Override
|
||||
public void accept(@NonNull Throwable throwable) throws Exception {
|
||||
isLoading.set(false);
|
||||
onError(throwable);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void loadMoreItems() {
|
||||
isLoading.set(true);
|
||||
showListFooter(true);
|
||||
if (searchDisposable != null) searchDisposable.dispose();
|
||||
currentNextPage = currentPage + 1;
|
||||
searchDisposable = ExtractorHelper.getMoreSearchItems(serviceId, searchQuery, currentNextPage, searchLanguage, filter)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new Consumer<ListExtractor.NextItemsResult>() {
|
||||
@Override
|
||||
public void accept(@NonNull ListExtractor.NextItemsResult result) throws Exception {
|
||||
isLoading.set(false);
|
||||
handleNextItems(result);
|
||||
}
|
||||
}, new Consumer<Throwable>() {
|
||||
@Override
|
||||
public void accept(@NonNull Throwable throwable) throws Exception {
|
||||
isLoading.set(false);
|
||||
onError(throwable);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean hasMoreItems() {
|
||||
// TODO: No way to tell if search has more items in the moment
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onItemSelected(InfoItem selectedItem) {
|
||||
super.onItemSelected(selectedItem);
|
||||
hideSoftKeyboard(searchEditText);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void changeFilter(MenuItem item, SearchEngine.Filter filter) {
|
||||
this.filter = filter;
|
||||
this.filterItemCheckedId = item.getItemId();
|
||||
item.setChecked(true);
|
||||
if (searchQuery != null && !searchQuery.isEmpty()) search(searchQuery);
|
||||
}
|
||||
|
||||
private void submitQuery(String query) {
|
||||
if (DEBUG) Log.d(TAG, "submitQuery() called with: query = [" + query + "]");
|
||||
if (query.isEmpty()) return;
|
||||
search(query);
|
||||
}
|
||||
|
||||
private void setQuery(int serviceId, String searchQuery) {
|
||||
this.serviceId = serviceId;
|
||||
this.searchQuery = searchQuery;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showError(String message, boolean showRetryButton) {
|
||||
super.showError(message, showRetryButton);
|
||||
hideSoftKeyboard(searchEditText);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Suggestion Results
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public void handleSuggestions(@NonNull List<String> suggestions) {
|
||||
if (DEBUG) Log.d(TAG, "handleSuggestions() called with: suggestions = [" + suggestions + "]");
|
||||
suggestionListAdapter.updateAdapter(suggestions);
|
||||
}
|
||||
|
||||
public void onSuggestionError(Throwable exception) {
|
||||
if (DEBUG) Log.d(TAG, "onSuggestionError() called with: exception = [" + exception + "]");
|
||||
if (super.onError(exception)) return;
|
||||
|
||||
int errorId = exception instanceof ParsingException ? R.string.parsing_error : R.string.general_error;
|
||||
onUnrecoverableError(exception, UserAction.GET_SUGGESTIONS, NewPipe.getNameOfService(serviceId), searchQuery, errorId);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Contract
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void hideLoading() {
|
||||
super.hideLoading();
|
||||
showListFooter(false);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Search Results
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void handleResult(@NonNull SearchResult result) {
|
||||
if (!result.errors.isEmpty()) {
|
||||
showSnackBarError(result.errors, UserAction.SEARCHED, NewPipe.getNameOfService(serviceId), searchQuery, 0);
|
||||
}
|
||||
|
||||
if (infoListAdapter.getItemsList().size() == 0) {
|
||||
if (result.resultList.size() > 0) {
|
||||
infoListAdapter.addInfoItemList(result.resultList);
|
||||
} else {
|
||||
infoListAdapter.clearStreamItemList();
|
||||
showEmptyState();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
super.handleResult(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleNextItems(ListExtractor.NextItemsResult result) {
|
||||
showListFooter(false);
|
||||
currentPage = Integer.parseInt(result.nextItemsUrl);
|
||||
infoListAdapter.addInfoItemList(result.nextItemsList);
|
||||
|
||||
if (!result.errors.isEmpty()) {
|
||||
showSnackBarError(result.errors, UserAction.SEARCHED, NewPipe.getNameOfService(serviceId)
|
||||
, "\"" + searchQuery + "\" → page " + currentPage, 0);
|
||||
}
|
||||
super.handleNextItems(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onError(Throwable exception) {
|
||||
if (super.onError(exception)) return true;
|
||||
|
||||
if (exception instanceof SearchEngine.NothingFoundException) {
|
||||
infoListAdapter.clearStreamItemList();
|
||||
showEmptyState();
|
||||
} else {
|
||||
int errorId = exception instanceof ParsingException ? R.string.parsing_error : R.string.general_error;
|
||||
onUnrecoverableError(exception, UserAction.SEARCHED, NewPipe.getNameOfService(serviceId), searchQuery, errorId);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.fragments.search;
|
||||
package org.schabi.newpipe.fragments.list.search;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
@@ -9,7 +9,7 @@ import android.widget.TextView;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
/*
|
||||
* Created by Christian Schabesberger on 02.08.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
@@ -83,7 +83,7 @@ public class SuggestionListAdapter extends ResourceCursorAdapter {
|
||||
private class ViewHolder {
|
||||
private final TextView suggestionTitle;
|
||||
private ViewHolder(View view) {
|
||||
this.suggestionTitle = (TextView) view.findViewById(android.R.id.text1);
|
||||
this.suggestionTitle = view.findViewById(android.R.id.text1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package org.schabi.newpipe.fragments.search;
|
||||
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
|
||||
/**
|
||||
* Recycler view scroll listener which calls the method {@link #onScrolledDown(RecyclerView)}
|
||||
* if the view is scrolled below the last item.
|
||||
*/
|
||||
public abstract class OnScrollBelowItemsListener extends RecyclerView.OnScrollListener {
|
||||
|
||||
@Override
|
||||
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
|
||||
super.onScrolled(recyclerView, dx, dy);
|
||||
//check for scroll down
|
||||
if (dy > 0) {
|
||||
int pastVisibleItems, visibleItemCount, totalItemCount;
|
||||
LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
|
||||
visibleItemCount = recyclerView.getLayoutManager().getChildCount();
|
||||
totalItemCount = recyclerView.getLayoutManager().getItemCount();
|
||||
pastVisibleItems = layoutManager.findFirstVisibleItemPosition();
|
||||
if ((visibleItemCount + pastVisibleItems) >= totalItemCount) {
|
||||
onScrolledDown(recyclerView);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the recycler view is scrolled below the last item.
|
||||
* @param recyclerView the recycler view
|
||||
*/
|
||||
public abstract void onScrolledDown(RecyclerView recyclerView);
|
||||
}
|
||||
@@ -1,615 +0,0 @@
|
||||
package org.schabi.newpipe.fragments.search;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AutoCompleteTextView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.search.SearchEngine;
|
||||
import org.schabi.newpipe.extractor.search.SearchResult;
|
||||
import org.schabi.newpipe.fragments.BaseFragment;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.info_list.InfoListAdapter;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.workers.SearchWorker;
|
||||
import org.schabi.newpipe.workers.SuggestionWorker;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
public class SearchFragment extends BaseFragment implements SuggestionWorker.OnSuggestionResult, SearchWorker.OnSearchResult {
|
||||
private final String TAG = "SearchFragment@" + Integer.toHexString(hashCode());
|
||||
// savedInstanceBundle arguments
|
||||
private static final String QUERY_KEY = "query_key";
|
||||
private static final String PAGE_NUMBER_KEY = "page_number_key";
|
||||
private static final String INFO_LIST_KEY = "info_list_key";
|
||||
private static final String WAS_LOADING_KEY = "was_loading_key";
|
||||
private static final String ERROR_KEY = "error_key";
|
||||
private static final String FILTER_CHECKED_ID_KEY = "filter_checked_id_key";
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Search
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private int filterItemCheckedId = -1;
|
||||
private EnumSet<SearchEngine.Filter> filter = EnumSet.of(SearchEngine.Filter.CHANNEL, SearchEngine.Filter.STREAM);
|
||||
|
||||
private int serviceId = -1;
|
||||
private String searchQuery = "";
|
||||
private int pageNumber = 0;
|
||||
private boolean showSuggestions = true;
|
||||
|
||||
private SearchWorker curSearchWorker;
|
||||
private SuggestionWorker curSuggestionWorker;
|
||||
private SuggestionListAdapter suggestionListAdapter;
|
||||
private InfoListAdapter infoListAdapter;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private View searchToolbarContainer;
|
||||
private AutoCompleteTextView searchEditText;
|
||||
private View searchClear;
|
||||
|
||||
private RecyclerView resultRecyclerView;
|
||||
|
||||
/*////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public static SearchFragment getInstance(int serviceId, String query) {
|
||||
SearchFragment searchFragment = new SearchFragment();
|
||||
searchFragment.setQuery(serviceId, query);
|
||||
return searchFragment;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Fragment's LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
|
||||
setHasOptionsMenu(true);
|
||||
if (savedInstanceState != null) {
|
||||
searchQuery = savedInstanceState.getString(QUERY_KEY);
|
||||
serviceId = savedInstanceState.getInt(Constants.KEY_SERVICE_ID, 0);
|
||||
pageNumber = savedInstanceState.getInt(PAGE_NUMBER_KEY, 0);
|
||||
wasLoading.set(savedInstanceState.getBoolean(WAS_LOADING_KEY, false));
|
||||
filterItemCheckedId = savedInstanceState.getInt(FILTER_CHECKED_ID_KEY, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
if (DEBUG) Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]");
|
||||
return inflater.inflate(R.layout.fragment_search, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View rootView, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(rootView, savedInstanceState);
|
||||
if (DEBUG) Log.d(TAG, "onViewCreated() called with: rootView = [" + rootView + "], savedInstanceState = [" + savedInstanceState + "]");
|
||||
|
||||
if (savedInstanceState != null && savedInstanceState.getBoolean(ERROR_KEY, false)) {
|
||||
search(searchQuery, 0, true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (DEBUG) Log.d(TAG, "onResume() called");
|
||||
if (wasLoading.getAndSet(false) && !TextUtils.isEmpty(searchQuery)) {
|
||||
if (pageNumber > 0) search(searchQuery, pageNumber);
|
||||
else search(searchQuery, 0, true);
|
||||
}
|
||||
|
||||
showSuggestions = PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(getString(R.string.show_search_suggestions_key), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
if (DEBUG) Log.d(TAG, "onStop() called");
|
||||
|
||||
hideSoftKeyboard(searchEditText);
|
||||
|
||||
wasLoading.set(curSearchWorker != null && curSearchWorker.isRunning());
|
||||
if (curSearchWorker != null && curSearchWorker.isRunning()) curSearchWorker.cancel();
|
||||
if (curSuggestionWorker != null && curSuggestionWorker.isRunning()) curSuggestionWorker.cancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
if (DEBUG) Log.d(TAG, "onDestroyView() called");
|
||||
unsetSearchListeners();
|
||||
|
||||
resultRecyclerView.removeAllViews();
|
||||
|
||||
searchToolbarContainer = null;
|
||||
searchEditText = null;
|
||||
searchClear = null;
|
||||
|
||||
resultRecyclerView = null;
|
||||
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
if (DEBUG) Log.d(TAG, "onSaveInstanceState() called with: outState = [" + outState + "]");
|
||||
|
||||
String query = searchEditText != null && !TextUtils.isEmpty(searchEditText.getText().toString())
|
||||
? searchEditText.getText().toString() : searchQuery;
|
||||
outState.putString(QUERY_KEY, query);
|
||||
outState.putInt(Constants.KEY_SERVICE_ID, serviceId);
|
||||
outState.putInt(PAGE_NUMBER_KEY, pageNumber);
|
||||
outState.putSerializable(INFO_LIST_KEY, ((ArrayList<InfoItem>) infoListAdapter.getItemsList()));
|
||||
outState.putBoolean(WAS_LOADING_KEY, curSearchWorker != null && curSearchWorker.isRunning());
|
||||
|
||||
if (errorPanel != null && errorPanel.getVisibility() == View.VISIBLE) outState.putBoolean(ERROR_KEY, true);
|
||||
if (filterItemCheckedId != -1) outState.putInt(FILTER_CHECKED_ID_KEY, filterItemCheckedId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
switch (requestCode) {
|
||||
case ReCaptchaActivity.RECAPTCHA_REQUEST:
|
||||
if (resultCode == Activity.RESULT_OK && searchQuery.length() != 0) {
|
||||
search(searchQuery, pageNumber, true);
|
||||
} else Log.e(TAG, "ReCaptcha failed");
|
||||
break;
|
||||
|
||||
default:
|
||||
Log.e(TAG, "Request code from activity not supported [" + requestCode + "]");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init's
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
resultRecyclerView = ((RecyclerView) rootView.findViewById(R.id.result_list_view));
|
||||
resultRecyclerView.setLayoutManager(new LinearLayoutManager(activity));
|
||||
|
||||
if (infoListAdapter == null) {
|
||||
infoListAdapter = new InfoListAdapter(getActivity());
|
||||
if (savedInstanceState != null) {
|
||||
//noinspection unchecked
|
||||
ArrayList<InfoItem> serializable = (ArrayList<InfoItem>) savedInstanceState.getSerializable(INFO_LIST_KEY);
|
||||
infoListAdapter.addInfoItemList(serializable);
|
||||
}
|
||||
|
||||
infoListAdapter.setFooter(activity.getLayoutInflater().inflate(R.layout.pignate_footer, resultRecyclerView, false));
|
||||
infoListAdapter.showFooter(false);
|
||||
infoListAdapter.setOnStreamInfoItemSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() {
|
||||
@Override
|
||||
public void selected(int serviceId, String url, String title) {
|
||||
NavigationHelper.openVideoDetailFragment(getFragmentManager(), serviceId, url, title);
|
||||
}
|
||||
});
|
||||
infoListAdapter.setOnChannelInfoItemSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() {
|
||||
@Override
|
||||
public void selected(int serviceId, String url, String title) {
|
||||
NavigationHelper.openChannelFragment(getFragmentManager(), serviceId, url, title);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
resultRecyclerView.setAdapter(infoListAdapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initListeners() {
|
||||
super.initListeners();
|
||||
resultRecyclerView.clearOnScrollListeners();
|
||||
resultRecyclerView.addOnScrollListener(new OnScrollBelowItemsListener() {
|
||||
@Override
|
||||
public void onScrolledDown(RecyclerView recyclerView) {
|
||||
if(!isLoading.get()) {
|
||||
pageNumber++;
|
||||
recyclerView.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
infoListAdapter.showFooter(true);
|
||||
}
|
||||
});
|
||||
search(searchQuery, pageNumber);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void reloadContent() {
|
||||
if (DEBUG) Log.d(TAG, "reloadContent() called");
|
||||
if (!TextUtils.isEmpty(searchQuery) || (searchEditText != null && !TextUtils.isEmpty(searchEditText.getText()))) {
|
||||
search(!TextUtils.isEmpty(searchQuery) ? searchQuery : searchEditText.getText().toString(), 0, true);
|
||||
} else {
|
||||
if (searchEditText != null) {
|
||||
searchEditText.setText("");
|
||||
showSoftKeyboard(searchEditText);
|
||||
}
|
||||
animateView(errorPanel, false, 200);
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Menu
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||
inflater.inflate(R.menu.search_menu, menu);
|
||||
|
||||
ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
if (supportActionBar != null) {
|
||||
supportActionBar.setDisplayShowTitleEnabled(false);
|
||||
supportActionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
|
||||
searchToolbarContainer = activity.findViewById(R.id.toolbar_search_container);
|
||||
searchEditText = (AutoCompleteTextView) searchToolbarContainer.findViewById(R.id.toolbar_search_edit_text);
|
||||
searchClear = searchToolbarContainer.findViewById(R.id.toolbar_search_clear);
|
||||
setupSearchView();
|
||||
|
||||
restoreFilterChecked(menu, filterItemCheckedId);
|
||||
}
|
||||
|
||||
private void restoreFilterChecked(Menu menu, int itemId) {
|
||||
if (itemId != -1) {
|
||||
MenuItem item = menu.findItem(itemId);
|
||||
if (item == null) return;
|
||||
|
||||
item.setChecked(true);
|
||||
switch (itemId) {
|
||||
case R.id.menu_filter_all:
|
||||
filter = EnumSet.of(SearchEngine.Filter.STREAM, SearchEngine.Filter.CHANNEL);
|
||||
break;
|
||||
case R.id.menu_filter_video:
|
||||
filter = EnumSet.of(SearchEngine.Filter.STREAM);
|
||||
break;
|
||||
case R.id.menu_filter_channel:
|
||||
filter = EnumSet.of(SearchEngine.Filter.CHANNEL);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (DEBUG) Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]");
|
||||
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_filter_all:
|
||||
changeFilter(item, EnumSet.of(SearchEngine.Filter.STREAM, SearchEngine.Filter.CHANNEL));
|
||||
return true;
|
||||
case R.id.menu_filter_video:
|
||||
changeFilter(item, EnumSet.of(SearchEngine.Filter.STREAM));
|
||||
return true;
|
||||
case R.id.menu_filter_channel:
|
||||
changeFilter(item, EnumSet.of(SearchEngine.Filter.CHANNEL));
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Search
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private TextWatcher textWatcher;
|
||||
|
||||
private void setupSearchView() {
|
||||
searchEditText.setText(searchQuery != null ? searchQuery : "");
|
||||
searchEditText.setHint(getString(R.string.search) + "...");
|
||||
////searchEditText.setCursorVisible(true);
|
||||
|
||||
suggestionListAdapter = new SuggestionListAdapter(activity);
|
||||
searchEditText.setAdapter(suggestionListAdapter);
|
||||
|
||||
|
||||
if (TextUtils.isEmpty(searchQuery) || TextUtils.isEmpty(searchEditText.getText())) {
|
||||
searchToolbarContainer.setTranslationX(100);
|
||||
searchToolbarContainer.setAlpha(0f);
|
||||
searchToolbarContainer.setVisibility(View.VISIBLE);
|
||||
searchToolbarContainer.animate().translationX(0).alpha(1f).setDuration(400).setInterpolator(new DecelerateInterpolator()).start();
|
||||
} else {
|
||||
searchToolbarContainer.setTranslationX(0);
|
||||
searchToolbarContainer.setAlpha(1f);
|
||||
searchToolbarContainer.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
//
|
||||
initSearchListeners();
|
||||
|
||||
if (TextUtils.isEmpty(searchQuery)) showSoftKeyboard(searchEditText);
|
||||
else hideSoftKeyboard(searchEditText);
|
||||
|
||||
if (!TextUtils.isEmpty(searchQuery) && searchQuery.length() > 2 && suggestionListAdapter != null && suggestionListAdapter.isEmpty()) {
|
||||
searchSuggestions(searchQuery);
|
||||
}
|
||||
}
|
||||
|
||||
private void initSearchListeners() {
|
||||
searchClear.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]");
|
||||
if (TextUtils.isEmpty(searchEditText.getText())) {
|
||||
NavigationHelper.gotoMainFragment(getFragmentManager());
|
||||
return;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
searchEditText.setText("", false);
|
||||
} else searchEditText.setText("");
|
||||
suggestionListAdapter.updateAdapter(new ArrayList<String>());
|
||||
showSoftKeyboard(searchEditText);
|
||||
}
|
||||
});
|
||||
|
||||
searchClear.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
if (DEBUG) Log.d(TAG, "onLongClick() called with: v = [" + v + "]");
|
||||
showMenuTooltip(v, getString(R.string.clear));
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
searchEditText.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
searchEditText.showDropDown();
|
||||
}
|
||||
});
|
||||
|
||||
searchEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
|
||||
@Override
|
||||
public void onFocusChange(View v, boolean hasFocus) {
|
||||
if (hasFocus) searchEditText.showDropDown();
|
||||
}
|
||||
});
|
||||
|
||||
searchEditText.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
if (DEBUG) Log.d(TAG, "onItemClick() called with: parent = [" + parent + "], view = [" + view + "], position = [" + position + "], id = [" + id + "]");
|
||||
String s = suggestionListAdapter.getSuggestion(position);
|
||||
if (DEBUG) Log.d(TAG, "onItemClick text = " + s);
|
||||
submitQuery(s);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (textWatcher != null) searchEditText.removeTextChangedListener(textWatcher);
|
||||
textWatcher = new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
String newText = searchEditText.getText().toString();
|
||||
if (!TextUtils.isEmpty(newText) && newText.length() > 1) onQueryTextChange(newText);
|
||||
}
|
||||
};
|
||||
searchEditText.addTextChangedListener(textWatcher);
|
||||
|
||||
searchEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
||||
@Override
|
||||
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
|
||||
if (DEBUG) Log.d(TAG, "onEditorAction() called with: v = [" + v + "], actionId = [" + actionId + "], event = [" + event + "]");
|
||||
if (event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER || event.getAction() == EditorInfo.IME_ACTION_SEARCH)) {
|
||||
submitQuery(searchEditText.getText().toString());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void unsetSearchListeners() {
|
||||
searchClear.setOnClickListener(null);
|
||||
searchClear.setOnLongClickListener(null);
|
||||
if (textWatcher != null) searchEditText.removeTextChangedListener(textWatcher);
|
||||
searchEditText.setOnClickListener(null);
|
||||
searchEditText.setOnItemClickListener(null);
|
||||
searchEditText.setOnFocusChangeListener(null);
|
||||
searchEditText.setOnEditorActionListener(null);
|
||||
|
||||
textWatcher = null;
|
||||
}
|
||||
|
||||
public void showSoftKeyboard(View view) {
|
||||
if (DEBUG) Log.d(TAG, "showSoftKeyboard() called with: view = [" + view + "]");
|
||||
if (view == null) return;
|
||||
|
||||
if (view.requestFocus()) {
|
||||
InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT);
|
||||
}
|
||||
}
|
||||
|
||||
public void hideSoftKeyboard(View view) {
|
||||
if (DEBUG) Log.d(TAG, "hideSoftKeyboard() called with: view = [" + view + "]");
|
||||
if (view == null) return;
|
||||
|
||||
InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
|
||||
|
||||
view.clearFocus();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void changeFilter(MenuItem item, EnumSet<SearchEngine.Filter> filter) {
|
||||
this.filter = filter;
|
||||
this.filterItemCheckedId = item.getItemId();
|
||||
item.setChecked(true);
|
||||
if (searchQuery != null && !searchQuery.isEmpty()) search(searchQuery, 0, true);
|
||||
}
|
||||
|
||||
public void submitQuery(String query) {
|
||||
if (DEBUG) Log.d(TAG, "submitQuery() called with: query = [" + query + "]");
|
||||
if (query.isEmpty()) return;
|
||||
search(query, 0, true);
|
||||
searchQuery = query;
|
||||
}
|
||||
|
||||
public void onQueryTextChange(String newText) {
|
||||
if (DEBUG) Log.d(TAG, "onQueryTextChange() called with: newText = [" + newText + "]");
|
||||
if (!newText.isEmpty()) searchSuggestions(newText);
|
||||
}
|
||||
|
||||
private void setQuery(int serviceId, String searchQuery) {
|
||||
this.serviceId = serviceId;
|
||||
this.searchQuery = searchQuery;
|
||||
}
|
||||
|
||||
private void searchSuggestions(String query) {
|
||||
if (!showSuggestions) {
|
||||
if (DEBUG) Log.d(TAG, "searchSuggestions() showSuggestions is disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
if (DEBUG) Log.d(TAG, "searchSuggestions() called with: query = [" + query + "]");
|
||||
if (curSuggestionWorker != null && curSuggestionWorker.isRunning()) curSuggestionWorker.cancel();
|
||||
curSuggestionWorker = SuggestionWorker.startForQuery(activity, serviceId, query, this);
|
||||
}
|
||||
|
||||
private void search(String query, int pageNumber) {
|
||||
if (DEBUG) Log.d(TAG, "search() called with: query = [" + query + "], pageNumber = [" + pageNumber + "]");
|
||||
search(query, pageNumber, false);
|
||||
}
|
||||
|
||||
private void search(String query, int pageNumber, boolean clearList) {
|
||||
if (DEBUG) Log.d(TAG, "search() called with: query = [" + query + "], pageNumber = [" + pageNumber + "], clearList = [" + clearList + "]");
|
||||
isLoading.set(true);
|
||||
hideSoftKeyboard(searchEditText);
|
||||
|
||||
searchQuery = query;
|
||||
this.pageNumber = pageNumber;
|
||||
|
||||
if (clearList) {
|
||||
animateView(resultRecyclerView, false, 50);
|
||||
infoListAdapter.clearStreamItemList();
|
||||
infoListAdapter.showFooter(false);
|
||||
animateView(loadingProgressBar, true, 200);
|
||||
}
|
||||
animateView(errorPanel, false, 200);
|
||||
|
||||
if (curSearchWorker != null && curSearchWorker.isRunning()) curSearchWorker.cancel();
|
||||
curSearchWorker = SearchWorker.startForQuery(activity, serviceId, query, pageNumber, filter, this);
|
||||
}
|
||||
|
||||
protected void setErrorMessage(String message, boolean showRetryButton) {
|
||||
super.setErrorMessage(message, showRetryButton);
|
||||
|
||||
animateView(resultRecyclerView, false, 400);
|
||||
hideSoftKeyboard(searchEditText);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// OnSuggestionResult
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onSuggestionResult(@NonNull List<String> suggestions) {
|
||||
if (DEBUG) Log.d(TAG, "onSuggestionResult() called with: suggestions = [" + suggestions + "]");
|
||||
suggestionListAdapter.updateAdapter(suggestions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuggestionError(int messageId) {
|
||||
if (DEBUG) Log.d(TAG, "onSuggestionError() called with: messageId = [" + messageId + "]");
|
||||
setErrorMessage(getString(messageId), true);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// SearchWorkerResultListener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onSearchResult(SearchResult result) {
|
||||
if (DEBUG) Log.d(TAG, "onSearchResult() called with: result = [" + result + "]");
|
||||
infoListAdapter.addInfoItemList(result.resultList);
|
||||
animateView(resultRecyclerView, true, 400);
|
||||
animateView(loadingProgressBar, false, 200);
|
||||
isLoading.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingFound(String message) {
|
||||
if (DEBUG) Log.d(TAG, "onNothingFound() called with: messageId = [" + message + "]");
|
||||
setErrorMessage(message, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSearchError(int messageId) {
|
||||
if (DEBUG) Log.d(TAG, "onSearchError() called with: messageId = [" + messageId + "]");
|
||||
//Toast.makeText(getActivity(), messageId, Toast.LENGTH_LONG).show();
|
||||
setErrorMessage(getString(messageId), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReCaptchaChallenge() {
|
||||
if (DEBUG) Log.d(TAG, "onReCaptchaChallenge() called");
|
||||
Toast.makeText(getActivity(), R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show();
|
||||
setErrorMessage(getString(R.string.recaptcha_request_toast), false);
|
||||
|
||||
// Starting ReCaptcha Challenge Activity
|
||||
startActivityForResult(new Intent(getActivity(), ReCaptchaActivity.class), ReCaptchaActivity.RECAPTCHA_REQUEST);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
package org.schabi.newpipe.fragments.subscription;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.info_list.InfoListAdapter;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.Observer;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEntity>> {
|
||||
private View headerRootLayout;
|
||||
|
||||
private InfoListAdapter infoListAdapter;
|
||||
private RecyclerView itemsList;
|
||||
|
||||
@State
|
||||
protected Parcelable itemsListState;
|
||||
|
||||
/* Used for independent events */
|
||||
private CompositeDisposable disposables = new CompositeDisposable();
|
||||
private SubscriptionService subscriptionService;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Fragment LifeCycle
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
infoListAdapter = new InfoListAdapter(activity);
|
||||
subscriptionService = SubscriptionService.getInstance();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_subscription, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
itemsListState = itemsList.getLayoutManager().onSaveInstanceState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
if (disposables != null) disposables.clear();
|
||||
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
if (disposables != null) disposables.dispose();
|
||||
disposables = null;
|
||||
subscriptionService = null;
|
||||
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Fragment Views
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
|
||||
infoListAdapter = new InfoListAdapter(getActivity());
|
||||
itemsList = rootView.findViewById(R.id.items_list);
|
||||
itemsList.setLayoutManager(new LinearLayoutManager(activity));
|
||||
|
||||
infoListAdapter.setHeader(headerRootLayout = activity.getLayoutInflater().inflate(R.layout.subscription_header, itemsList, false));
|
||||
infoListAdapter.useMiniItemVariants(true);
|
||||
|
||||
itemsList.setAdapter(infoListAdapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initListeners() {
|
||||
super.initListeners();
|
||||
|
||||
infoListAdapter.setOnChannelSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener<ChannelInfoItem>() {
|
||||
@Override
|
||||
public void selected(ChannelInfoItem selectedItem) {
|
||||
// Requires the parent fragment to find holder for fragment replacement
|
||||
NavigationHelper.openChannelFragment(getParentFragment().getFragmentManager(), selectedItem.service_id, selectedItem.url, selectedItem.name);
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
headerRootLayout.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
NavigationHelper.openWhatsNewFragment(getParentFragment().getFragmentManager());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void resetFragment() {
|
||||
if (disposables != null) disposables.clear();
|
||||
if (infoListAdapter != null) infoListAdapter.clearStreamItemList();
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Subscriptions Loader
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void startLoading(boolean forceLoad) {
|
||||
super.startLoading(forceLoad);
|
||||
resetFragment();
|
||||
|
||||
subscriptionService.getSubscription().toObservable()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(getSubscriptionObserver());
|
||||
}
|
||||
|
||||
private Observer<List<SubscriptionEntity>> getSubscriptionObserver() {
|
||||
return new Observer<List<SubscriptionEntity>>() {
|
||||
@Override
|
||||
public void onSubscribe(Disposable d) {
|
||||
showLoading();
|
||||
disposables.add(d);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(List<SubscriptionEntity> subscriptions) {
|
||||
handleResult(subscriptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable exception) {
|
||||
SubscriptionFragment.this.onError(exception);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResult(@NonNull List<SubscriptionEntity> result) {
|
||||
super.handleResult(result);
|
||||
|
||||
infoListAdapter.clearStreamItemList();
|
||||
|
||||
if (result.isEmpty()) {
|
||||
showEmptyState();
|
||||
} else {
|
||||
infoListAdapter.addInfoItemList(getSubscriptionItems(result));
|
||||
if (itemsListState != null) {
|
||||
itemsList.getLayoutManager().onRestoreInstanceState(itemsListState);
|
||||
itemsListState = null;
|
||||
}
|
||||
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private List<InfoItem> getSubscriptionItems(List<SubscriptionEntity> subscriptions) {
|
||||
List<InfoItem> items = new ArrayList<>();
|
||||
for (final SubscriptionEntity subscription : subscriptions) items.add(subscription.toChannelInfoItem());
|
||||
|
||||
Collections.sort(items, new Comparator<InfoItem>() {
|
||||
@Override
|
||||
public int compare(InfoItem o1, InfoItem o2) {
|
||||
return o1.name.compareToIgnoreCase(o2.name);
|
||||
}
|
||||
});
|
||||
return items;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Contract
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void showLoading() {
|
||||
super.showLoading();
|
||||
animateView(itemsList, false, 100);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hideLoading() {
|
||||
super.hideLoading();
|
||||
animateView(itemsList, true, 200);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showEmptyState() {
|
||||
super.showEmptyState();
|
||||
animateView(itemsList, false, 200);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Fragment Error Handling
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
protected boolean onError(Throwable exception) {
|
||||
resetFragment();
|
||||
if (super.onError(exception)) return true;
|
||||
|
||||
onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, "none", "Subscriptions", R.string.general_error);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package org.schabi.newpipe.fragments.subscription;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
import org.schabi.newpipe.database.AppDatabase;
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionDAO;
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.CompletableSource;
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.Maybe;
|
||||
import io.reactivex.Scheduler;
|
||||
import io.reactivex.annotations.NonNull;
|
||||
import io.reactivex.functions.Function;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
/**
|
||||
* Subscription Service singleton:
|
||||
* Provides a basis for channel Subscriptions.
|
||||
* Provides access to subscription table in database as well as
|
||||
* up-to-date observations on the subscribed channels
|
||||
*/
|
||||
public class SubscriptionService {
|
||||
|
||||
private static final SubscriptionService sInstance = new SubscriptionService();
|
||||
|
||||
public static SubscriptionService getInstance() {
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
protected final String TAG = "SubscriptionService@" + Integer.toHexString(hashCode());
|
||||
protected static final boolean DEBUG = MainActivity.DEBUG;
|
||||
private static final int SUBSCRIPTION_DEBOUNCE_INTERVAL = 500;
|
||||
private static final int SUBSCRIPTION_THREAD_POOL_SIZE = 4;
|
||||
|
||||
private AppDatabase db;
|
||||
private Flowable<List<SubscriptionEntity>> subscription;
|
||||
|
||||
private Scheduler subscriptionScheduler;
|
||||
|
||||
private SubscriptionService() {
|
||||
db = NewPipeDatabase.getInstance();
|
||||
subscription = getSubscriptionInfos();
|
||||
|
||||
final Executor subscriptionExecutor = Executors.newFixedThreadPool(SUBSCRIPTION_THREAD_POOL_SIZE);
|
||||
subscriptionScheduler = Schedulers.from(subscriptionExecutor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Part of subscription observation pipeline
|
||||
*
|
||||
* @see SubscriptionService#getSubscription()
|
||||
*/
|
||||
private Flowable<List<SubscriptionEntity>> getSubscriptionInfos() {
|
||||
return subscriptionTable().getAll()
|
||||
// Wait for a period of infrequent updates and return the latest update
|
||||
.debounce(SUBSCRIPTION_DEBOUNCE_INTERVAL, TimeUnit.MILLISECONDS)
|
||||
.share() // Share allows multiple subscribers on the same observable
|
||||
.replay(1) // Replay synchronizes subscribers to the last emitted result
|
||||
.autoConnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides an observer to the latest update to the subscription table.
|
||||
* <p>
|
||||
* This observer may be subscribed multiple times, where each subscriber obtains
|
||||
* the latest synchronized changes available, effectively share the same data
|
||||
* across all subscribers.
|
||||
* <p>
|
||||
* This observer has a debounce cooldown, meaning if multiple updates are observed
|
||||
* in the cooldown interval, only the latest changes are emitted to the subscribers.
|
||||
* This reduces the amount of observations caused by frequent updates to the database.
|
||||
*/
|
||||
@android.support.annotation.NonNull
|
||||
public Flowable<List<SubscriptionEntity>> getSubscription() {
|
||||
return subscription;
|
||||
}
|
||||
|
||||
public Maybe<ChannelInfo> getChannelInfo(final SubscriptionEntity subscriptionEntity) {
|
||||
if (DEBUG) Log.d(TAG, "getChannelInfo() called with: subscriptionEntity = [" + subscriptionEntity + "]");
|
||||
|
||||
return Maybe.fromSingle(ExtractorHelper
|
||||
.getChannelInfo(subscriptionEntity.getServiceId(), subscriptionEntity.getUrl(), false))
|
||||
.subscribeOn(subscriptionScheduler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the database access interface for subscription table.
|
||||
*/
|
||||
public SubscriptionDAO subscriptionTable() {
|
||||
return db.subscriptionDAO();
|
||||
}
|
||||
|
||||
public Completable updateChannelInfo(final ChannelInfo info) {
|
||||
final Function<List<SubscriptionEntity>, CompletableSource> update = new Function<List<SubscriptionEntity>, CompletableSource>() {
|
||||
@Override
|
||||
public CompletableSource apply(@NonNull List<SubscriptionEntity> subscriptionEntities) throws Exception {
|
||||
if (DEBUG) Log.d(TAG, "updateChannelInfo() called with: subscriptionEntities = [" + subscriptionEntities + "]");
|
||||
if (subscriptionEntities.size() == 1) {
|
||||
SubscriptionEntity subscription = subscriptionEntities.get(0);
|
||||
|
||||
// Subscriber count changes very often, making this check almost unnecessary.
|
||||
// Consider removing it later.
|
||||
if (!isSubscriptionUpToDate(info, subscription)) {
|
||||
subscription.setData(info.name, info.avatar_url, info.description, info.subscriber_count);
|
||||
|
||||
return update(subscription);
|
||||
}
|
||||
}
|
||||
|
||||
return Completable.complete();
|
||||
}
|
||||
};
|
||||
|
||||
return subscriptionTable().getSubscription(info.service_id, info.url)
|
||||
.firstOrError()
|
||||
.flatMapCompletable(update);
|
||||
}
|
||||
|
||||
private Completable update(final SubscriptionEntity updatedSubscription) {
|
||||
return Completable.fromRunnable(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
subscriptionTable().update(updatedSubscription);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boolean isSubscriptionUpToDate(final ChannelInfo info, final SubscriptionEntity entity) {
|
||||
return info.url.equals(entity.getUrl()) &&
|
||||
info.service_id == entity.getServiceId() &&
|
||||
info.name.equals(entity.getName()) &&
|
||||
info.avatar_url.equals(entity.getAvatarUrl()) &&
|
||||
info.description.equals(entity.getDescription()) &&
|
||||
info.subscriber_count == entity.getSubscriberCount();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package org.schabi.newpipe.history;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.design.widget.FloatingActionButton;
|
||||
import android.support.design.widget.TabLayout;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentPagerAdapter;
|
||||
import android.support.v4.app.FragmentStatePagerAdapter;
|
||||
import android.support.v4.view.ViewPager;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import com.jakewharton.rxbinding2.view.RxView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.settings.SettingsActivity;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.functions.Consumer;
|
||||
|
||||
public class HistoryActivity extends AppCompatActivity {
|
||||
|
||||
private static final String TAG = "HistoryActivity";
|
||||
/**
|
||||
* The {@link android.support.v4.view.PagerAdapter} that will provide
|
||||
* fragments for each of the sections. We use a
|
||||
* {@link FragmentPagerAdapter} derivative, which will keep every
|
||||
* loaded fragment in memory. If this becomes too memory intensive, it
|
||||
* may be best to switch to a
|
||||
* {@link android.support.v4.app.FragmentStatePagerAdapter}.
|
||||
*/
|
||||
private SectionsPagerAdapter mSectionsPagerAdapter;
|
||||
|
||||
/**
|
||||
* The {@link ViewPager} that will host the section contents.
|
||||
*/
|
||||
private ViewPager mViewPager;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
ThemeHelper.setTheme(this);
|
||||
setContentView(R.layout.activity_history);
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setTitle(R.string.title_activity_history);
|
||||
// Create the adapter that will return a fragment for each of the three
|
||||
// primary sections of the activity.
|
||||
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
|
||||
|
||||
// Set up the ViewPager with the sections adapter.
|
||||
mViewPager = findViewById(R.id.container);
|
||||
mViewPager.setAdapter(mSectionsPagerAdapter);
|
||||
|
||||
TabLayout tabLayout = findViewById(R.id.tabs);
|
||||
tabLayout.setupWithViewPager(mViewPager);
|
||||
|
||||
final FloatingActionButton fab = findViewById(R.id.fab);
|
||||
RxView.clicks(fab)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new Consumer<Object>() {
|
||||
@Override
|
||||
public void accept(Object o) {
|
||||
int currentItem = mViewPager.getCurrentItem();
|
||||
HistoryFragment fragment = (HistoryFragment) mSectionsPagerAdapter.instantiateItem(mViewPager, currentItem);
|
||||
if(fragment != null) {
|
||||
fragment.onHistoryCleared();
|
||||
} else {
|
||||
Log.w(TAG, "Couldn't find current fragment");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
// Inflate the menu; this adds items to the action bar if it is present.
|
||||
getMenuInflater().inflate(R.menu.menu_history, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
finish();
|
||||
return true;
|
||||
case R.id.action_settings:
|
||||
Intent intent = new Intent(this, SettingsActivity.class);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link FragmentPagerAdapter} that returns a fragment corresponding to
|
||||
* one of the sections/tabs/pages.
|
||||
*/
|
||||
public class SectionsPagerAdapter extends FragmentStatePagerAdapter {
|
||||
|
||||
public SectionsPagerAdapter(FragmentManager fm) {
|
||||
super(fm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
Fragment fragment;
|
||||
switch (position) {
|
||||
case 0:
|
||||
fragment = SearchHistoryFragment.newInstance();
|
||||
break;
|
||||
case 1:
|
||||
fragment = WatchedHistoryFragment.newInstance();
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("position: " + position);
|
||||
}
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getPageTitle(int position) {
|
||||
switch (position) {
|
||||
case 0:
|
||||
return getString(R.string.title_history_search);
|
||||
case 1:
|
||||
return getString(R.string.title_history_view);
|
||||
}
|
||||
throw new IllegalArgumentException("position: " + position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
// Show 3 total pages.
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package org.schabi.newpipe.history;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.View;
|
||||
|
||||
import org.schabi.newpipe.database.history.model.HistoryEntry;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
|
||||
|
||||
/**
|
||||
* Adapter for history entries
|
||||
* @param <E> the type of the entries
|
||||
* @param <VH> the type of the view holder
|
||||
*/
|
||||
public abstract class HistoryEntryAdapter<E extends HistoryEntry, VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> {
|
||||
|
||||
private final ArrayList<E> mEntries;
|
||||
private final DateFormat mDateFormat;
|
||||
private OnHistoryItemClickListener<E> onHistoryItemClickListener = null;
|
||||
|
||||
|
||||
public HistoryEntryAdapter(Context context) {
|
||||
super();
|
||||
mEntries = new ArrayList<>();
|
||||
mDateFormat = android.text.format.DateFormat.getDateFormat(context.getApplicationContext());
|
||||
|
||||
setHasStableIds(true);
|
||||
}
|
||||
|
||||
public void setEntries(@NonNull Collection<E> historyEntries) {
|
||||
mEntries.clear();
|
||||
mEntries.addAll(historyEntries);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public Collection<E> getItems() {
|
||||
return mEntries;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
mEntries.clear();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
protected String getFormattedDate(Date date) {
|
||||
return mDateFormat.format(date);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return mEntries.get(position).getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return mEntries.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(VH holder, int position) {
|
||||
final E entry = mEntries.get(position);
|
||||
holder.itemView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
final OnHistoryItemClickListener<E> historyItemClickListener = onHistoryItemClickListener;
|
||||
if(historyItemClickListener != null) {
|
||||
historyItemClickListener.onHistoryItemClick(entry);
|
||||
}
|
||||
}
|
||||
});
|
||||
onBindViewHolder(holder, entry, position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewRecycled(VH holder) {
|
||||
super.onViewRecycled(holder);
|
||||
holder.itemView.setOnClickListener(null);
|
||||
}
|
||||
|
||||
abstract void onBindViewHolder(VH holder, E entry, int position);
|
||||
|
||||
public void setOnHistoryItemClickListener(@Nullable OnHistoryItemClickListener<E> onHistoryItemClickListener) {
|
||||
this.onHistoryItemClickListener = onHistoryItemClickListener;
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return mEntries.isEmpty();
|
||||
}
|
||||
|
||||
public E removeItemAt(int position) {
|
||||
E entry = mEntries.remove(position);
|
||||
notifyItemRemoved(position);
|
||||
return entry;
|
||||
}
|
||||
|
||||
public interface OnHistoryItemClickListener<E extends HistoryEntry> {
|
||||
void onHistoryItemClick(E historyItem);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
package org.schabi.newpipe.history;
|
||||
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.CallSuper;
|
||||
import android.support.annotation.MainThread;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.StringRes;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.helper.ItemTouchHelper;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.history.dao.HistoryDAO;
|
||||
import org.schabi.newpipe.database.history.model.HistoryEntry;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.Observer;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.subjects.PublishSubject;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
public abstract class HistoryFragment<E extends HistoryEntry> extends BaseFragment
|
||||
implements HistoryEntryAdapter.OnHistoryItemClickListener<E> {
|
||||
|
||||
private SharedPreferences mSharedPreferences;
|
||||
private String mHistoryIsEnabledKey;
|
||||
private boolean mHistoryIsEnabled;
|
||||
private HistoryIsEnabledChangeListener mHistoryIsEnabledChangeListener;
|
||||
|
||||
private View mDisabledView;
|
||||
private View mEmptyHistoryView;
|
||||
|
||||
@State
|
||||
Parcelable mRecyclerViewState;
|
||||
private RecyclerView mRecyclerView;
|
||||
private HistoryEntryAdapter<E, ? extends RecyclerView.ViewHolder> mHistoryAdapter;
|
||||
private ItemTouchHelper.SimpleCallback mHistoryItemSwipeCallback;
|
||||
private int allowedSwipeToDeleteDirections = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
|
||||
|
||||
private HistoryDAO<E> mHistoryDataSource;
|
||||
private PublishSubject<Collection<E>> mHistoryEntryDeleteSubject;
|
||||
private PublishSubject<Collection<E>> mHistoryEntryInsertSubject;
|
||||
|
||||
@StringRes
|
||||
abstract int getEnabledConfigKey();
|
||||
|
||||
@CallSuper
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
mHistoryIsEnabledKey = getString(getEnabledConfigKey());
|
||||
|
||||
mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
// Read history enabled from preferences
|
||||
mHistoryIsEnabled = isHistoryEnabled();
|
||||
// Register history enabled listener
|
||||
mSharedPreferences.registerOnSharedPreferenceChangeListener(mHistoryIsEnabledChangeListener);
|
||||
|
||||
mHistoryDataSource = createHistoryDAO();
|
||||
|
||||
mHistoryEntryDeleteSubject = PublishSubject.create();
|
||||
mHistoryEntryDeleteSubject
|
||||
.observeOn(Schedulers.io())
|
||||
.subscribe(new Consumer<Collection<E>>() {
|
||||
@Override
|
||||
public void accept(Collection<E> historyEntries) throws Exception {
|
||||
mHistoryDataSource.delete(historyEntries);
|
||||
}
|
||||
});
|
||||
|
||||
mHistoryEntryInsertSubject = PublishSubject.create();
|
||||
mHistoryEntryInsertSubject
|
||||
.observeOn(Schedulers.io())
|
||||
.subscribe(new Consumer<Collection<E>>() {
|
||||
@Override
|
||||
public void accept(Collection<E> historyEntries) throws Exception {
|
||||
mHistoryDataSource.insertAll(historyEntries);
|
||||
}
|
||||
});
|
||||
|
||||
mHistoryItemSwipeCallback = new ItemTouchHelper.SimpleCallback(0, allowedSwipeToDeleteDirections) {
|
||||
@Override
|
||||
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {
|
||||
if (mHistoryAdapter != null) {
|
||||
final E historyEntry = mHistoryAdapter.removeItemAt(viewHolder.getAdapterPosition());
|
||||
mHistoryEntryDeleteSubject.onNext(Collections.singletonList(historyEntry));
|
||||
|
||||
View view = getActivity().findViewById(R.id.main_content);
|
||||
if (view == null) view = mRecyclerView.getRootView();
|
||||
|
||||
Snackbar.make(view, R.string.item_deleted, 5 * 1000)
|
||||
.setActionTextColor(Color.WHITE)
|
||||
.setAction(R.string.undo, new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
mHistoryEntryInsertSubject.onNext(Collections.singletonList(historyEntry));
|
||||
}
|
||||
}).show();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@NonNull
|
||||
protected abstract HistoryEntryAdapter<E, ? extends RecyclerView.ViewHolder> createAdapter();
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
mHistoryDataSource.getAll()
|
||||
.toObservable()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(getHistoryListConsumer());
|
||||
boolean newEnabled = isHistoryEnabled();
|
||||
if (newEnabled != mHistoryIsEnabled) {
|
||||
onHistoryIsEnabledChanged(newEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Observer<List<E>> getHistoryListConsumer() {
|
||||
return new Observer<List<E>>() {
|
||||
@Override
|
||||
public void onSubscribe(@NonNull Disposable d) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(@NonNull List<E> historyEntries) {
|
||||
if (!historyEntries.isEmpty()) {
|
||||
mHistoryAdapter.setEntries(historyEntries);
|
||||
animateView(mEmptyHistoryView, false, 200);
|
||||
|
||||
if (mRecyclerViewState != null) {
|
||||
mRecyclerView.getLayoutManager().onRestoreInstanceState(mRecyclerViewState);
|
||||
mRecyclerViewState = null;
|
||||
}
|
||||
} else {
|
||||
mHistoryAdapter.clear();
|
||||
showEmptyHistory();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(@NonNull Throwable e) {
|
||||
// TODO: error handling like in (see e.g. subscription fragment)
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private boolean isHistoryEnabled() {
|
||||
return mSharedPreferences.getBoolean(mHistoryIsEnabledKey, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the history is cleared to update the views
|
||||
*/
|
||||
@MainThread
|
||||
public void onHistoryCleared() {
|
||||
final Parcelable stateBeforeClear = mRecyclerView.getLayoutManager().onSaveInstanceState();
|
||||
final Collection<E> itemsToDelete = new ArrayList<>(mHistoryAdapter.getItems());
|
||||
mHistoryEntryDeleteSubject.onNext(itemsToDelete);
|
||||
|
||||
View view = getActivity().findViewById(R.id.main_content);
|
||||
if (view == null) view = mRecyclerView.getRootView();
|
||||
|
||||
if (!itemsToDelete.isEmpty()) {
|
||||
Snackbar.make(view, R.string.history_cleared, 5 * 1000)
|
||||
.setActionTextColor(Color.WHITE)
|
||||
.setAction(R.string.undo, new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
mRecyclerViewState = stateBeforeClear;
|
||||
mHistoryEntryInsertSubject.onNext(itemsToDelete);
|
||||
}
|
||||
}).show();
|
||||
} else {
|
||||
Snackbar.make(view, R.string.history_cleared, Snackbar.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
mHistoryAdapter.clear();
|
||||
showEmptyHistory();
|
||||
|
||||
}
|
||||
|
||||
private void showEmptyHistory() {
|
||||
if (mHistoryIsEnabled) {
|
||||
animateView(mEmptyHistoryView, true, 200);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@CallSuper
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View rootView = inflater.inflate(R.layout.fragment_history, container, false);
|
||||
mRecyclerView = rootView.findViewById(R.id.history_view);
|
||||
|
||||
RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false);
|
||||
mRecyclerView.setLayoutManager(layoutManager);
|
||||
|
||||
mHistoryAdapter = createAdapter();
|
||||
mHistoryAdapter.setOnHistoryItemClickListener(this);
|
||||
mRecyclerView.setAdapter(mHistoryAdapter);
|
||||
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(mHistoryItemSwipeCallback);
|
||||
itemTouchHelper.attachToRecyclerView(mRecyclerView);
|
||||
mDisabledView = rootView.findViewById(R.id.history_disabled_view);
|
||||
mEmptyHistoryView = rootView.findViewById(R.id.history_empty);
|
||||
|
||||
if (mHistoryIsEnabled) {
|
||||
mRecyclerView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mDisabledView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
return rootView;
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
mSharedPreferences.unregisterOnSharedPreferenceChangeListener(mHistoryIsEnabledChangeListener);
|
||||
mSharedPreferences = null;
|
||||
mHistoryIsEnabledChangeListener = null;
|
||||
mHistoryIsEnabledKey = null;
|
||||
mHistoryDataSource = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
mRecyclerViewState = mRecyclerView.getLayoutManager().onSaveInstanceState();
|
||||
}
|
||||
|
||||
public void setAllowedSwipeToDeleteDirections(int allowedSwipeToDeleteDirections) {
|
||||
this.allowedSwipeToDeleteDirections = allowedSwipeToDeleteDirections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when history enabled flag is changed.
|
||||
*
|
||||
* @param historyIsEnabled the new value
|
||||
*/
|
||||
@CallSuper
|
||||
public void onHistoryIsEnabledChanged(boolean historyIsEnabled) {
|
||||
mHistoryIsEnabled = historyIsEnabled;
|
||||
if (historyIsEnabled) {
|
||||
animateView(mRecyclerView, true, 300);
|
||||
animateView(mDisabledView, false, 300);
|
||||
if (mHistoryAdapter.isEmpty()) {
|
||||
animateView(mEmptyHistoryView, true, 300);
|
||||
}
|
||||
} else {
|
||||
animateView(mRecyclerView, false, 300);
|
||||
animateView(mDisabledView, true, 300);
|
||||
animateView(mEmptyHistoryView, false, 300);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new history DAO
|
||||
*
|
||||
* @return the history DAO
|
||||
*/
|
||||
@NonNull
|
||||
protected abstract HistoryDAO<E> createHistoryDAO();
|
||||
|
||||
private class HistoryIsEnabledChangeListener implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
if (key.equals(mHistoryIsEnabledKey)) {
|
||||
boolean enabled = sharedPreferences.getBoolean(key, false);
|
||||
if (mHistoryIsEnabled != enabled) {
|
||||
onHistoryIsEnabledChanged(enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.schabi.newpipe.history;
|
||||
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
|
||||
public interface HistoryListener {
|
||||
/**
|
||||
* Called when a video is played
|
||||
*
|
||||
* @param streamInfo the stream info
|
||||
* @param videoStream the video stream that is played
|
||||
*/
|
||||
void onVideoPlayed(StreamInfo streamInfo, VideoStream videoStream);
|
||||
|
||||
/**
|
||||
* Called when the audio is played in the background
|
||||
*
|
||||
* @param streamInfo the stream info
|
||||
* @param audioStream the audio stream that is played
|
||||
*/
|
||||
void onAudioPlayed(StreamInfo streamInfo, AudioStream audioStream);
|
||||
|
||||
/**
|
||||
* Called when the user searched for something
|
||||
*
|
||||
* @param serviceId which service the search was done
|
||||
* @param query what the user searched for
|
||||
*/
|
||||
void onSearch(int serviceId, String query);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package org.schabi.newpipe.history;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.StringRes;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.history.dao.HistoryDAO;
|
||||
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
|
||||
public class SearchHistoryFragment extends HistoryFragment<SearchHistoryEntry> {
|
||||
|
||||
@NonNull
|
||||
public static SearchHistoryFragment newInstance() {
|
||||
return new SearchHistoryFragment();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected SearchHistoryAdapter createAdapter() {
|
||||
return new SearchHistoryAdapter(getContext());
|
||||
}
|
||||
|
||||
@StringRes
|
||||
@Override
|
||||
int getEnabledConfigKey() {
|
||||
return R.string.enable_search_history_key;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected HistoryDAO<SearchHistoryEntry> createHistoryDAO() {
|
||||
return NewPipeDatabase.getInstance().searchHistoryDAO();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHistoryItemClick(SearchHistoryEntry historyItem) {
|
||||
NavigationHelper.openSearch(getContext(), historyItem.getServiceId(), historyItem.getSearch());
|
||||
}
|
||||
|
||||
private static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
private final TextView search;
|
||||
private final TextView time;
|
||||
|
||||
public ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
search = itemView.findViewById(R.id.search);
|
||||
time = itemView.findViewById(R.id.time);
|
||||
}
|
||||
}
|
||||
|
||||
protected class SearchHistoryAdapter extends HistoryEntryAdapter<SearchHistoryEntry, ViewHolder> {
|
||||
|
||||
|
||||
public SearchHistoryAdapter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
|
||||
View rootView = inflater.inflate(R.layout.item_search_history, parent, false);
|
||||
return new ViewHolder(rootView);
|
||||
}
|
||||
|
||||
@Override
|
||||
void onBindViewHolder(ViewHolder holder, SearchHistoryEntry entry, int position) {
|
||||
holder.search.setText(entry.getSearch());
|
||||
holder.time.setText(getFormattedDate(entry.getCreationDate()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package org.schabi.newpipe.history;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.StringRes;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.history.dao.HistoryDAO;
|
||||
import org.schabi.newpipe.database.history.model.WatchHistoryEntry;
|
||||
import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
|
||||
|
||||
public class WatchedHistoryFragment extends HistoryFragment<WatchHistoryEntry> {
|
||||
|
||||
@NonNull
|
||||
public static WatchedHistoryFragment newInstance() {
|
||||
return new WatchedHistoryFragment();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
}
|
||||
|
||||
@StringRes
|
||||
@Override
|
||||
int getEnabledConfigKey() {
|
||||
return R.string.enable_watch_history_key;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected WatchedHistoryAdapter createAdapter() {
|
||||
return new WatchedHistoryAdapter(getContext());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected HistoryDAO<WatchHistoryEntry> createHistoryDAO() {
|
||||
return NewPipeDatabase.getInstance().watchHistoryDAO();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHistoryItemClick(WatchHistoryEntry historyItem) {
|
||||
NavigationHelper.openVideoDetail(getContext(),
|
||||
historyItem.getServiceId(),
|
||||
historyItem.getUrl(),
|
||||
historyItem.getTitle());
|
||||
}
|
||||
|
||||
private static class WatchedHistoryAdapter extends HistoryEntryAdapter<WatchHistoryEntry, ViewHolder> {
|
||||
|
||||
public WatchedHistoryAdapter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
|
||||
View itemView = inflater.inflate(R.layout.list_stream_item, parent, false);
|
||||
return new ViewHolder(itemView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewRecycled(ViewHolder holder) {
|
||||
holder.itemView.setOnClickListener(null);
|
||||
ImageLoader.getInstance()
|
||||
.cancelDisplayTask(holder.thumbnailView);
|
||||
}
|
||||
|
||||
@Override
|
||||
void onBindViewHolder(ViewHolder holder, WatchHistoryEntry entry, int position) {
|
||||
holder.date.setText(getFormattedDate(entry.getCreationDate()));
|
||||
holder.streamTitle.setText(entry.getTitle());
|
||||
holder.uploader.setText(entry.getUploader());
|
||||
holder.duration.setText(Localization.getDurationString(entry.getDuration()));
|
||||
ImageLoader.getInstance()
|
||||
.displayImage(entry.getThumbnailURL(), holder.thumbnailView, StreamInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
private final TextView date;
|
||||
private final TextView streamTitle;
|
||||
private final ImageView thumbnailView;
|
||||
private final TextView uploader;
|
||||
private final TextView duration;
|
||||
|
||||
public ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
thumbnailView = itemView.findViewById(R.id.itemThumbnailView);
|
||||
date = itemView.findViewById(R.id.itemAdditionalDetails);
|
||||
streamTitle = itemView.findViewById(R.id.itemVideoTitleView);
|
||||
uploader = itemView.findViewById(R.id.itemUploaderView);
|
||||
duration = itemView.findViewById(R.id.itemDurationView);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package org.schabi.newpipe.info_list;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
|
||||
import de.hdodenhof.circleimageview.CircleImageView;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 12.02.17.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* ChannelInfoItemHolder .java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class ChannelInfoItemHolder extends InfoItemHolder {
|
||||
public final CircleImageView itemThumbnailView;
|
||||
public final TextView itemChannelTitleView;
|
||||
public final TextView itemAdditionalDetailView;
|
||||
public final TextView itemChannelDescriptionView;
|
||||
|
||||
public final View itemRoot;
|
||||
|
||||
ChannelInfoItemHolder(View v) {
|
||||
super(v);
|
||||
itemRoot = v.findViewById(R.id.itemRoot);
|
||||
itemThumbnailView = (CircleImageView) v.findViewById(R.id.itemThumbnailView);
|
||||
itemChannelTitleView = (TextView) v.findViewById(R.id.itemChannelTitleView);
|
||||
itemAdditionalDetailView = (TextView) v.findViewById(R.id.itemAdditionalDetails);
|
||||
itemChannelDescriptionView = (TextView) v.findViewById(R.id.itemChannelDescriptionView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InfoItem.InfoType infoType() {
|
||||
return InfoItem.InfoType.CHANNEL;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,25 @@
|
||||
package org.schabi.newpipe.info_list;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
|
||||
import org.schabi.newpipe.ImageErrorLoadingListener;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.AbstractStreamInfo;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream_info.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
/*
|
||||
* Created by Christian Schabesberger on 26.09.16.
|
||||
* <p>
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
@@ -40,251 +40,77 @@ import java.util.Locale;
|
||||
*/
|
||||
|
||||
public class InfoItemBuilder {
|
||||
|
||||
private final String viewsS;
|
||||
private final String videosS;
|
||||
private final String subsS;
|
||||
private final String subsPluralS;
|
||||
|
||||
private final String thousand;
|
||||
private final String million;
|
||||
private final String billion;
|
||||
|
||||
private static final String TAG = InfoItemBuilder.class.toString();
|
||||
|
||||
public interface OnInfoItemSelectedListener {
|
||||
void selected(int serviceId, String url, String title);
|
||||
public interface OnInfoItemSelectedListener<T extends InfoItem> {
|
||||
void selected(T selectedItem);
|
||||
}
|
||||
|
||||
private final Context context;
|
||||
private ImageLoader imageLoader = ImageLoader.getInstance();
|
||||
|
||||
/** Base display options */
|
||||
private static final DisplayImageOptions DISPLAY_IMAGE_OPTIONS =
|
||||
new DisplayImageOptions.Builder()
|
||||
.cacheInMemory(true)
|
||||
.build();
|
||||
|
||||
/** Display options for stream thumbnails */
|
||||
private static final DisplayImageOptions DISPLAY_STREAM_THUMBNAIL_OPTIONS =
|
||||
new DisplayImageOptions.Builder()
|
||||
.cloneFrom(DISPLAY_IMAGE_OPTIONS)
|
||||
.showImageOnFail(R.drawable.dummy_thumbnail)
|
||||
.showImageForEmptyUri(R.drawable.dummy_thumbnail)
|
||||
.showImageOnLoading(R.drawable.dummy_thumbnail)
|
||||
.build();
|
||||
|
||||
/** Display options for channel thumbnails */
|
||||
private static final DisplayImageOptions DISPLAY_CHANNEL_THUMBNAIL_OPTIONS =
|
||||
new DisplayImageOptions.Builder()
|
||||
.cloneFrom(DISPLAY_IMAGE_OPTIONS)
|
||||
.showImageOnLoading(R.drawable.buddy_channel_item)
|
||||
.showImageForEmptyUri(R.drawable.buddy_channel_item)
|
||||
.showImageOnFail(R.drawable.buddy_channel_item)
|
||||
.build();
|
||||
private OnInfoItemSelectedListener onStreamInfoItemSelectedListener;
|
||||
private OnInfoItemSelectedListener onChannelInfoItemSelectedListener;
|
||||
private OnInfoItemSelectedListener<StreamInfoItem> onStreamSelectedListener;
|
||||
private OnInfoItemSelectedListener<ChannelInfoItem> onChannelSelectedListener;
|
||||
private OnInfoItemSelectedListener<PlaylistInfoItem> onPlaylistSelectedListener;
|
||||
|
||||
public InfoItemBuilder(Context context) {
|
||||
viewsS = context.getString(R.string.views);
|
||||
videosS = context.getString(R.string.videos);
|
||||
subsS = context.getString(R.string.subscriber);
|
||||
subsPluralS = context.getString(R.string.subscriber_plural);
|
||||
thousand = context.getString(R.string.short_thousand);
|
||||
million = context.getString(R.string.short_million);
|
||||
billion = context.getString(R.string.short_billion);
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public void setOnStreamInfoItemSelectedListener(
|
||||
OnInfoItemSelectedListener listener) {
|
||||
this.onStreamInfoItemSelectedListener = listener;
|
||||
public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem) {
|
||||
return buildView(parent, infoItem, false);
|
||||
}
|
||||
|
||||
public void setOnChannelInfoItemSelectedListener(
|
||||
OnInfoItemSelectedListener listener) {
|
||||
this.onChannelInfoItemSelectedListener = listener;
|
||||
public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem, boolean useMiniVariant) {
|
||||
InfoItemHolder holder = holderFromInfoType(parent, infoItem.info_type, useMiniVariant);
|
||||
holder.updateFromItem(infoItem);
|
||||
return holder.itemView;
|
||||
}
|
||||
|
||||
public void buildByHolder(InfoItemHolder holder, final InfoItem i) {
|
||||
if (i.infoType() != holder.infoType())
|
||||
return;
|
||||
switch (i.infoType()) {
|
||||
private InfoItemHolder holderFromInfoType(@NonNull ViewGroup parent, @NonNull InfoItem.InfoType infoType, boolean useMiniVariant) {
|
||||
switch (infoType) {
|
||||
case STREAM:
|
||||
buildStreamInfoItem((StreamInfoItemHolder) holder, (StreamInfoItem) i);
|
||||
break;
|
||||
return useMiniVariant ? new StreamMiniInfoItemHolder(this, parent) : new StreamInfoItemHolder(this, parent);
|
||||
case CHANNEL:
|
||||
buildChannelInfoItem((ChannelInfoItemHolder) holder, (ChannelInfoItem) i);
|
||||
break;
|
||||
return useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent) : new ChannelInfoItemHolder(this, parent);
|
||||
case PLAYLIST:
|
||||
Log.e(TAG, "Not yet implemented");
|
||||
break;
|
||||
return new PlaylistInfoItemHolder(this, parent);
|
||||
default:
|
||||
Log.e(TAG, "Trollolo");
|
||||
throw new RuntimeException("InfoType not expected = " + infoType.name());
|
||||
}
|
||||
}
|
||||
|
||||
public View buildView(ViewGroup parent, final InfoItem info) {
|
||||
View itemView = null;
|
||||
InfoItemHolder holder = null;
|
||||
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
|
||||
switch (info.infoType()) {
|
||||
case STREAM:
|
||||
//long start = System.nanoTime();
|
||||
itemView = inflater.inflate(R.layout.stream_item, parent, false);
|
||||
//Log.d(TAG, "time to inflate: " + ((System.nanoTime() - start) / 1000000L) + "ms");
|
||||
holder = new StreamInfoItemHolder(itemView);
|
||||
break;
|
||||
case CHANNEL:
|
||||
itemView = inflater.inflate(R.layout.channel_item, parent, false);
|
||||
holder = new ChannelInfoItemHolder(itemView);
|
||||
break;
|
||||
case PLAYLIST:
|
||||
Log.e(TAG, "Not yet implemented");
|
||||
default:
|
||||
Log.e(TAG, "Trollolo");
|
||||
}
|
||||
buildByHolder(holder, info);
|
||||
return itemView;
|
||||
public Context getContext() {
|
||||
return context;
|
||||
}
|
||||
|
||||
|
||||
private String getStreamInfoDetailLine(final StreamInfoItem info) {
|
||||
String viewsAndDate = "";
|
||||
if(info.view_count >= 0) {
|
||||
viewsAndDate = shortViewCount(info.view_count);
|
||||
}
|
||||
if(!TextUtils.isEmpty(info.upload_date)) {
|
||||
if(viewsAndDate.isEmpty()) {
|
||||
viewsAndDate = info.upload_date;
|
||||
} else {
|
||||
viewsAndDate += " • " + info.upload_date;
|
||||
}
|
||||
}
|
||||
return viewsAndDate;
|
||||
public ImageLoader getImageLoader() {
|
||||
return imageLoader;
|
||||
}
|
||||
|
||||
private void buildStreamInfoItem(StreamInfoItemHolder holder, final StreamInfoItem info) {
|
||||
if (info.infoType() != InfoItem.InfoType.STREAM) {
|
||||
Log.e("InfoItemBuilder", "Info type not yet supported");
|
||||
}
|
||||
// fill holder with information
|
||||
if (!TextUtils.isEmpty(info.title)) holder.itemVideoTitleView.setText(info.title);
|
||||
|
||||
if (!TextUtils.isEmpty(info.uploader)) holder.itemUploaderView.setText(info.uploader);
|
||||
else holder.itemUploaderView.setVisibility(View.INVISIBLE);
|
||||
|
||||
if (info.duration > 0) {
|
||||
holder.itemDurationView.setText(getDurationString(info.duration));
|
||||
} else {
|
||||
if (info.stream_type == AbstractStreamInfo.StreamType.LIVE_STREAM) {
|
||||
holder.itemDurationView.setText(R.string.duration_live);
|
||||
} else {
|
||||
holder.itemDurationView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
holder.itemAdditionalDetails.setText(getStreamInfoDetailLine(info));
|
||||
|
||||
// Default thumbnail is shown on error, while loading and if the url is empty
|
||||
imageLoader.displayImage(info.thumbnail_url,
|
||||
holder.itemThumbnailView,
|
||||
DISPLAY_STREAM_THUMBNAIL_OPTIONS,
|
||||
new ImageErrorLoadingListener(holder.itemRoot.getContext(), holder.itemRoot.getRootView(), info.service_id));
|
||||
|
||||
|
||||
holder.itemRoot.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if(onStreamInfoItemSelectedListener != null) {
|
||||
onStreamInfoItemSelectedListener.selected(info.service_id, info.webpage_url, info.getTitle());
|
||||
}
|
||||
}
|
||||
});
|
||||
public OnInfoItemSelectedListener<StreamInfoItem> getOnStreamSelectedListener() {
|
||||
return onStreamSelectedListener;
|
||||
}
|
||||
|
||||
private String getChannelInfoDetailLine(final ChannelInfoItem info) {
|
||||
String details = "";
|
||||
if(info.subscriberCount >= 0) {
|
||||
details = shortSubscriber(info.subscriberCount);
|
||||
}
|
||||
if(info.videoAmount >= 0) {
|
||||
String formattedVideoAmount = info.videoAmount + " " + videosS;
|
||||
if(!details.isEmpty()) {
|
||||
details += " • " + formattedVideoAmount;
|
||||
} else {
|
||||
details = formattedVideoAmount;
|
||||
}
|
||||
}
|
||||
return details;
|
||||
public void setOnStreamSelectedListener(OnInfoItemSelectedListener<StreamInfoItem> listener) {
|
||||
this.onStreamSelectedListener = listener;
|
||||
}
|
||||
|
||||
private void buildChannelInfoItem(ChannelInfoItemHolder holder, final ChannelInfoItem info) {
|
||||
if (!TextUtils.isEmpty(info.getTitle())) holder.itemChannelTitleView.setText(info.getTitle());
|
||||
holder.itemAdditionalDetailView.setText(getChannelInfoDetailLine(info));
|
||||
if (!TextUtils.isEmpty(info.description)) holder.itemChannelDescriptionView.setText(info.description);
|
||||
|
||||
imageLoader.displayImage(info.thumbnailUrl,
|
||||
holder.itemThumbnailView,
|
||||
DISPLAY_CHANNEL_THUMBNAIL_OPTIONS,
|
||||
new ImageErrorLoadingListener(holder.itemRoot.getContext(), holder.itemRoot.getRootView(), info.serviceId));
|
||||
|
||||
|
||||
holder.itemRoot.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if(onStreamInfoItemSelectedListener != null) {
|
||||
onChannelInfoItemSelectedListener.selected(info.serviceId, info.getLink(), info.channelName);
|
||||
}
|
||||
}
|
||||
});
|
||||
public OnInfoItemSelectedListener<ChannelInfoItem> getOnChannelSelectedListener() {
|
||||
return onChannelSelectedListener;
|
||||
}
|
||||
|
||||
|
||||
public String shortViewCount(Long viewCount) {
|
||||
if (viewCount >= 1000000000) {
|
||||
return Long.toString(viewCount / 1000000000) + billion + " " + viewsS;
|
||||
} else if (viewCount >= 1000000) {
|
||||
return Long.toString(viewCount / 1000000) + million + " " + viewsS;
|
||||
} else if (viewCount >= 1000) {
|
||||
return Long.toString(viewCount / 1000) + thousand + " " + viewsS;
|
||||
} else {
|
||||
return Long.toString(viewCount) + " " + viewsS;
|
||||
}
|
||||
public void setOnChannelSelectedListener(OnInfoItemSelectedListener<ChannelInfoItem> listener) {
|
||||
this.onChannelSelectedListener = listener;
|
||||
}
|
||||
|
||||
public String shortSubscriber(Long count) {
|
||||
String curSubString = count > 1 ? subsPluralS : subsS;
|
||||
|
||||
if (count >= 1000000000) {
|
||||
return Long.toString(count / 1000000000) + billion + " " + curSubString;
|
||||
} else if (count >= 1000000) {
|
||||
return Long.toString(count / 1000000) + million + " " + curSubString;
|
||||
} else if (count >= 1000) {
|
||||
return Long.toString(count / 1000) + thousand + " " + curSubString;
|
||||
} else {
|
||||
return Long.toString(count) + " " + curSubString;
|
||||
}
|
||||
public OnInfoItemSelectedListener<PlaylistInfoItem> getOnPlaylistSelectedListener() {
|
||||
return onPlaylistSelectedListener;
|
||||
}
|
||||
|
||||
public static String getDurationString(int duration) {
|
||||
if(duration < 0) {
|
||||
duration = 0;
|
||||
}
|
||||
String output;
|
||||
int days = duration / (24 * 60 * 60); /* greater than a day */
|
||||
duration %= (24 * 60 * 60);
|
||||
int hours = duration / (60 * 60); /* greater than an hour */
|
||||
duration %= (60 * 60);
|
||||
int minutes = duration / 60;
|
||||
int seconds = duration % 60;
|
||||
|
||||
//handle days
|
||||
if (days > 0) {
|
||||
output = String.format(Locale.US, "%d:%02d:%02d:%02d", days, hours, minutes, seconds);
|
||||
} else if(hours > 0) {
|
||||
output = String.format(Locale.US, "%d:%02d:%02d", hours, minutes, seconds);
|
||||
} else {
|
||||
output = String.format(Locale.US, "%d:%02d", minutes, seconds);
|
||||
}
|
||||
return output;
|
||||
public void setOnPlaylistSelectedListener(OnInfoItemSelectedListener<PlaylistInfoItem> listener) {
|
||||
this.onPlaylistSelectedListener = listener;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
package org.schabi.newpipe.info_list;
|
||||
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.View;
|
||||
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 12.02.17.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* InfoItemHolder.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public abstract class InfoItemHolder extends RecyclerView.ViewHolder {
|
||||
public InfoItemHolder(View v) {
|
||||
super(v);
|
||||
}
|
||||
public abstract InfoItem.InfoType infoType();
|
||||
}
|
||||
@@ -3,17 +3,25 @@ package org.schabi.newpipe.info_list;
|
||||
import android.app.Activity;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder.OnInfoItemSelectedListener;
|
||||
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
/*
|
||||
* Created by Christian Schabesberger on 01.08.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
@@ -34,25 +42,32 @@ import java.util.List;
|
||||
*/
|
||||
|
||||
public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||
private static final String TAG = InfoListAdapter.class.toString();
|
||||
private static final String TAG = InfoListAdapter.class.getSimpleName();
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
private static final int HEADER_TYPE = 0;
|
||||
private static final int FOOTER_TYPE = 1;
|
||||
|
||||
private static final int MINI_STREAM_HOLDER_TYPE = 0x100;
|
||||
private static final int STREAM_HOLDER_TYPE = 0x101;
|
||||
private static final int MINI_CHANNEL_HOLDER_TYPE = 0x200;
|
||||
private static final int CHANNEL_HOLDER_TYPE = 0x201;
|
||||
private static final int PLAYLIST_HOLDER_TYPE = 0x301;
|
||||
|
||||
private final InfoItemBuilder infoItemBuilder;
|
||||
private final List<InfoItem> infoItemList;
|
||||
private final ArrayList<InfoItem> infoItemList;
|
||||
private boolean useMiniVariant = false;
|
||||
private boolean showFooter = false;
|
||||
private View header = null;
|
||||
private View footer = null;
|
||||
|
||||
public class HFHolder extends RecyclerView.ViewHolder {
|
||||
public View view;
|
||||
|
||||
public HFHolder(View v) {
|
||||
super(v);
|
||||
view = v;
|
||||
}
|
||||
public View view;
|
||||
}
|
||||
|
||||
public void showFooter(boolean show) {
|
||||
showFooter = show;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public InfoListAdapter(Activity a) {
|
||||
@@ -60,25 +75,71 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
infoItemList = new ArrayList<>();
|
||||
}
|
||||
|
||||
public void setOnStreamInfoItemSelectedListener
|
||||
(InfoItemBuilder.OnInfoItemSelectedListener listener) {
|
||||
infoItemBuilder.setOnStreamInfoItemSelectedListener(listener);
|
||||
public void setOnStreamSelectedListener(OnInfoItemSelectedListener<StreamInfoItem> listener) {
|
||||
infoItemBuilder.setOnStreamSelectedListener(listener);
|
||||
}
|
||||
|
||||
public void setOnChannelInfoItemSelectedListener
|
||||
(InfoItemBuilder.OnInfoItemSelectedListener listener) {
|
||||
infoItemBuilder.setOnChannelInfoItemSelectedListener(listener);
|
||||
public void setOnChannelSelectedListener(OnInfoItemSelectedListener<ChannelInfoItem> listener) {
|
||||
infoItemBuilder.setOnChannelSelectedListener(listener);
|
||||
}
|
||||
|
||||
public void setOnPlaylistSelectedListener(OnInfoItemSelectedListener<PlaylistInfoItem> listener) {
|
||||
infoItemBuilder.setOnPlaylistSelectedListener(listener);
|
||||
}
|
||||
|
||||
public void useMiniItemVariants(boolean useMiniVariant) {
|
||||
this.useMiniVariant = useMiniVariant;
|
||||
}
|
||||
|
||||
public void addInfoItemList(List<InfoItem> data) {
|
||||
if(data != null) {
|
||||
if (data != null) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addInfoItemList() before > infoItemList.size() = " + infoItemList.size() + ", data.size() = " + data.size());
|
||||
}
|
||||
|
||||
int offsetStart = sizeConsideringHeaderOffset();
|
||||
infoItemList.addAll(data);
|
||||
notifyDataSetChanged();
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addInfoItemList() after > offsetStart = " + offsetStart + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter);
|
||||
}
|
||||
|
||||
notifyItemRangeInserted(offsetStart, data.size());
|
||||
|
||||
if (footer != null && showFooter) {
|
||||
int footerNow = sizeConsideringHeaderOffset();
|
||||
notifyItemMoved(offsetStart, footerNow);
|
||||
|
||||
if (DEBUG) Log.d(TAG, "addInfoItemList() footer from " + offsetStart + " to " + footerNow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void addInfoItem(InfoItem data) {
|
||||
if (data != null) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addInfoItem() before > infoItemList.size() = " + infoItemList.size() + ", thread = " + Thread.currentThread());
|
||||
}
|
||||
|
||||
int positionInserted = sizeConsideringHeaderOffset();
|
||||
infoItemList.add(data);
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addInfoItem() after > position = " + positionInserted + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter);
|
||||
}
|
||||
notifyItemInserted(positionInserted);
|
||||
|
||||
if (footer != null && showFooter) {
|
||||
int footerNow = sizeConsideringHeaderOffset();
|
||||
notifyItemMoved(positionInserted, footerNow);
|
||||
|
||||
if (DEBUG) Log.d(TAG, "addInfoItem() footer from " + positionInserted + " to " + footerNow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void clearStreamItemList() {
|
||||
if(infoItemList.isEmpty()) {
|
||||
if (infoItemList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
infoItemList.clear();
|
||||
@@ -86,45 +147,67 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
}
|
||||
|
||||
public void setHeader(View header) {
|
||||
boolean changed = header != this.header;
|
||||
this.header = header;
|
||||
notifyDataSetChanged();
|
||||
if (changed) notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void setFooter(View view) {
|
||||
this.footer = view;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public List<InfoItem> getItemsList() {
|
||||
public void showFooter(boolean show) {
|
||||
if (DEBUG) Log.d(TAG, "showFooter() called with: show = [" + show + "]");
|
||||
if (show == showFooter) return;
|
||||
|
||||
showFooter = show;
|
||||
if (show) notifyItemInserted(sizeConsideringHeaderOffset());
|
||||
else notifyItemRemoved(sizeConsideringHeaderOffset());
|
||||
}
|
||||
|
||||
|
||||
private int sizeConsideringHeaderOffset() {
|
||||
int i = infoItemList.size() + (header != null ? 1 : 0);
|
||||
if (DEBUG) Log.d(TAG, "sizeConsideringHeaderOffset() called → " + i);
|
||||
return i;
|
||||
}
|
||||
|
||||
public ArrayList<InfoItem> getItemsList() {
|
||||
return infoItemList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
int count = infoItemList.size();
|
||||
if(header != null) count++;
|
||||
if(footer != null && showFooter) count++;
|
||||
if (header != null) count++;
|
||||
if (footer != null && showFooter) count++;
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "getItemCount() called, count = " + count + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// don't ask why we have to do that this way... it's android accept it -.-
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if(header != null && position == 0) {
|
||||
return 0;
|
||||
} else if(header != null) {
|
||||
if (DEBUG) Log.d(TAG, "getItemViewType() called with: position = [" + position + "]");
|
||||
|
||||
if (header != null && position == 0) {
|
||||
return HEADER_TYPE;
|
||||
} else if (header != null) {
|
||||
position--;
|
||||
}
|
||||
if(footer != null && position == infoItemList.size() && showFooter) {
|
||||
return 1;
|
||||
if (footer != null && position == infoItemList.size() && showFooter) {
|
||||
return FOOTER_TYPE;
|
||||
}
|
||||
switch(infoItemList.get(position).infoType()) {
|
||||
InfoItem item = infoItemList.get(position);
|
||||
switch (item.info_type) {
|
||||
case STREAM:
|
||||
return 2;
|
||||
return useMiniVariant ? MINI_STREAM_HOLDER_TYPE : STREAM_HOLDER_TYPE;
|
||||
case CHANNEL:
|
||||
return 3;
|
||||
return useMiniVariant ? MINI_CHANNEL_HOLDER_TYPE : CHANNEL_HOLDER_TYPE;
|
||||
case PLAYLIST:
|
||||
return 4;
|
||||
return PLAYLIST_HOLDER_TYPE;
|
||||
default:
|
||||
Log.e(TAG, "Trollolo");
|
||||
return -1;
|
||||
@@ -133,20 +216,22 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int type) {
|
||||
switch(type) {
|
||||
case 0:
|
||||
if (DEBUG) Log.d(TAG, "onCreateViewHolder() called with: parent = [" + parent + "], type = [" + type + "]");
|
||||
switch (type) {
|
||||
case HEADER_TYPE:
|
||||
return new HFHolder(header);
|
||||
case 1:
|
||||
case FOOTER_TYPE:
|
||||
return new HFHolder(footer);
|
||||
case 2:
|
||||
return new StreamInfoItemHolder(LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.stream_item, parent, false));
|
||||
case 3:
|
||||
return new ChannelInfoItemHolder(LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.channel_item, parent, false));
|
||||
case 4:
|
||||
Log.e(TAG, "Playlist is not yet implemented");
|
||||
return null;
|
||||
case MINI_STREAM_HOLDER_TYPE:
|
||||
return new StreamMiniInfoItemHolder(infoItemBuilder, parent);
|
||||
case STREAM_HOLDER_TYPE:
|
||||
return new StreamInfoItemHolder(infoItemBuilder, parent);
|
||||
case MINI_CHANNEL_HOLDER_TYPE:
|
||||
return new ChannelMiniInfoItemHolder(infoItemBuilder, parent);
|
||||
case CHANNEL_HOLDER_TYPE:
|
||||
return new ChannelInfoItemHolder(infoItemBuilder, parent);
|
||||
case PLAYLIST_HOLDER_TYPE:
|
||||
return new PlaylistInfoItemHolder(infoItemBuilder, parent);
|
||||
default:
|
||||
Log.e(TAG, "Trollolo");
|
||||
return null;
|
||||
@@ -154,16 +239,16 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(RecyclerView.ViewHolder holder, int i) {
|
||||
//god damn f*** ANDROID SH**
|
||||
if(holder instanceof InfoItemHolder) {
|
||||
if(header != null) {
|
||||
i--;
|
||||
}
|
||||
infoItemBuilder.buildByHolder((InfoItemHolder) holder, infoItemList.get(i));
|
||||
} else if(holder instanceof HFHolder && i == 0 && header != null) {
|
||||
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
|
||||
if (DEBUG) Log.d(TAG, "onBindViewHolder() called with: holder = [" + holder.getClass().getSimpleName() + "], position = [" + position + "]");
|
||||
if (holder instanceof InfoItemHolder) {
|
||||
// If header isn't null, offset the items by -1
|
||||
if (header != null) position--;
|
||||
|
||||
((InfoItemHolder) holder).updateFromItem(infoItemList.get(position));
|
||||
} else if (holder instanceof HFHolder && position == 0 && header != null) {
|
||||
((HFHolder) holder).view = header;
|
||||
} else if(holder instanceof HFHolder && i == infoItemList.size() && footer != null && showFooter) {
|
||||
} else if (holder instanceof HFHolder && position == sizeConsideringHeaderOffset() && footer != null && showFooter) {
|
||||
((HFHolder) holder).view = footer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
package org.schabi.newpipe.info_list;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 01.08.16.
|
||||
* <p>
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* StreamInfoItemHolder.java is part of NewPipe.
|
||||
* <p>
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
* <p>
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
* <p>
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class StreamInfoItemHolder extends InfoItemHolder {
|
||||
|
||||
public final ImageView itemThumbnailView;
|
||||
public final TextView itemVideoTitleView,
|
||||
itemUploaderView,
|
||||
itemDurationView,
|
||||
itemAdditionalDetails;
|
||||
public final View itemRoot;
|
||||
|
||||
public StreamInfoItemHolder(View v) {
|
||||
super(v);
|
||||
itemRoot = v.findViewById(R.id.itemRoot);
|
||||
itemThumbnailView = (ImageView) v.findViewById(R.id.itemThumbnailView);
|
||||
itemVideoTitleView = (TextView) v.findViewById(R.id.itemVideoTitleView);
|
||||
itemUploaderView = (TextView) v.findViewById(R.id.itemUploaderView);
|
||||
itemDurationView = (TextView) v.findViewById(R.id.itemDurationView);
|
||||
itemAdditionalDetails = (TextView) v.findViewById(R.id.itemAdditionalDetails);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InfoItem.InfoType infoType() {
|
||||
return InfoItem.InfoType.STREAM;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package org.schabi.newpipe.info_list.holder;
|
||||
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 12.02.17.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* ChannelInfoItemHolder .java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class ChannelInfoItemHolder extends ChannelMiniInfoItemHolder {
|
||||
public final TextView itemChannelDescriptionView;
|
||||
|
||||
public ChannelInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
|
||||
super(infoItemBuilder, R.layout.list_channel_item, parent);
|
||||
itemChannelDescriptionView = itemView.findViewById(R.id.itemChannelDescriptionView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFromItem(final InfoItem infoItem) {
|
||||
super.updateFromItem(infoItem);
|
||||
|
||||
if (!(infoItem instanceof ChannelInfoItem)) return;
|
||||
final ChannelInfoItem item = (ChannelInfoItem) infoItem;
|
||||
|
||||
itemChannelDescriptionView.setText(item.description);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getDetailLine(final ChannelInfoItem item) {
|
||||
String details = super.getDetailLine(item);
|
||||
|
||||
if (item.stream_count >= 0) {
|
||||
String formattedVideoAmount = Localization.localizeStreamCount(itemBuilder.getContext(), item.stream_count);
|
||||
|
||||
if (!details.isEmpty()) {
|
||||
details += " • " + formattedVideoAmount;
|
||||
} else {
|
||||
details = formattedVideoAmount;
|
||||
}
|
||||
}
|
||||
return details;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package org.schabi.newpipe.info_list.holder;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
import de.hdodenhof.circleimageview.CircleImageView;
|
||||
|
||||
public class ChannelMiniInfoItemHolder extends InfoItemHolder {
|
||||
public final CircleImageView itemThumbnailView;
|
||||
public final TextView itemTitleView;
|
||||
public final TextView itemAdditionalDetailView;
|
||||
|
||||
ChannelMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) {
|
||||
super(infoItemBuilder, layoutId, parent);
|
||||
|
||||
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
|
||||
itemTitleView = itemView.findViewById(R.id.itemTitleView);
|
||||
itemAdditionalDetailView = itemView.findViewById(R.id.itemAdditionalDetails);
|
||||
}
|
||||
|
||||
public ChannelMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
|
||||
this(infoItemBuilder, R.layout.list_channel_mini_item, parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFromItem(final InfoItem infoItem) {
|
||||
if (!(infoItem instanceof ChannelInfoItem)) return;
|
||||
final ChannelInfoItem item = (ChannelInfoItem) infoItem;
|
||||
|
||||
itemTitleView.setText(item.name);
|
||||
itemAdditionalDetailView.setText(getDetailLine(item));
|
||||
|
||||
itemBuilder.getImageLoader()
|
||||
.displayImage(item.thumbnail_url, itemThumbnailView, ChannelInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS);
|
||||
|
||||
itemView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (itemBuilder.getOnChannelSelectedListener() != null) {
|
||||
itemBuilder.getOnChannelSelectedListener().selected(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected String getDetailLine(final ChannelInfoItem item) {
|
||||
String details = "";
|
||||
if (item.subscriber_count >= 0) {
|
||||
details += Localization.shortSubscriberCount(itemBuilder.getContext(), item.subscriber_count);
|
||||
}
|
||||
return details;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display options for channel thumbnails
|
||||
*/
|
||||
public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
|
||||
new DisplayImageOptions.Builder()
|
||||
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
|
||||
.showImageOnLoading(R.drawable.buddy_channel_item)
|
||||
.showImageForEmptyUri(R.drawable.buddy_channel_item)
|
||||
.showImageOnFail(R.drawable.buddy_channel_item)
|
||||
.build();
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package org.schabi.newpipe.info_list.holder;
|
||||
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 12.02.17.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* InfoItemHolder.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public abstract class InfoItemHolder extends RecyclerView.ViewHolder {
|
||||
protected final InfoItemBuilder itemBuilder;
|
||||
|
||||
public InfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) {
|
||||
super(LayoutInflater.from(infoItemBuilder.getContext()).inflate(layoutId, parent, false));
|
||||
this.itemBuilder = infoItemBuilder;
|
||||
}
|
||||
|
||||
public abstract void updateFromItem(final InfoItem infoItem);
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// ImageLoaderOptions
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
/**
|
||||
* Base display options
|
||||
*/
|
||||
public static final DisplayImageOptions BASE_DISPLAY_IMAGE_OPTIONS =
|
||||
new DisplayImageOptions.Builder()
|
||||
.cacheInMemory(true)
|
||||
.build();
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package org.schabi.newpipe.info_list.holder;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
|
||||
public class PlaylistInfoItemHolder extends InfoItemHolder {
|
||||
public final ImageView itemThumbnailView;
|
||||
public final TextView itemStreamCountView;
|
||||
public final TextView itemTitleView;
|
||||
public final TextView itemUploaderView;
|
||||
|
||||
public PlaylistInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
|
||||
super(infoItemBuilder, R.layout.list_playlist_item, parent);
|
||||
|
||||
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
|
||||
itemTitleView = itemView.findViewById(R.id.itemTitleView);
|
||||
itemStreamCountView = itemView.findViewById(R.id.itemStreamCountView);
|
||||
itemUploaderView = itemView.findViewById(R.id.itemUploaderView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFromItem(final InfoItem infoItem) {
|
||||
if (!(infoItem instanceof PlaylistInfoItem)) return;
|
||||
final PlaylistInfoItem item = (PlaylistInfoItem) infoItem;
|
||||
|
||||
itemTitleView.setText(item.name);
|
||||
itemStreamCountView.setText(item.stream_count + "");
|
||||
itemUploaderView.setText(item.uploader_name);
|
||||
|
||||
itemBuilder.getImageLoader()
|
||||
.displayImage(item.thumbnail_url, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS);
|
||||
|
||||
itemView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (itemBuilder.getOnPlaylistSelectedListener() != null) {
|
||||
itemBuilder.getOnPlaylistSelectedListener().selected(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Display options for playlist thumbnails
|
||||
*/
|
||||
public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
|
||||
new DisplayImageOptions.Builder()
|
||||
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
|
||||
.showImageOnLoading(R.drawable.dummy_thumbnail_playlist)
|
||||
.showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist)
|
||||
.showImageOnFail(R.drawable.dummy_thumbnail_playlist)
|
||||
.build();
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package org.schabi.newpipe.info_list.holder;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 01.08.16.
|
||||
* <p>
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* StreamInfoItemHolder.java is part of NewPipe.
|
||||
* <p>
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
* <p>
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
* <p>
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class StreamInfoItemHolder extends StreamMiniInfoItemHolder {
|
||||
|
||||
public final TextView itemAdditionalDetails;
|
||||
|
||||
public StreamInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
|
||||
super(infoItemBuilder, R.layout.list_stream_item, parent);
|
||||
itemAdditionalDetails = itemView.findViewById(R.id.itemAdditionalDetails);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFromItem(final InfoItem infoItem) {
|
||||
super.updateFromItem(infoItem);
|
||||
|
||||
if (!(infoItem instanceof StreamInfoItem)) return;
|
||||
final StreamInfoItem item = (StreamInfoItem) infoItem;
|
||||
|
||||
itemAdditionalDetails.setText(getStreamInfoDetailLine(item));
|
||||
}
|
||||
|
||||
private String getStreamInfoDetailLine(final StreamInfoItem infoItem) {
|
||||
String viewsAndDate = "";
|
||||
if (infoItem.view_count >= 0) {
|
||||
viewsAndDate = Localization.shortViewCount(itemBuilder.getContext(), infoItem.view_count);
|
||||
}
|
||||
if (!TextUtils.isEmpty(infoItem.upload_date)) {
|
||||
if (viewsAndDate.isEmpty()) {
|
||||
viewsAndDate = infoItem.upload_date;
|
||||
} else {
|
||||
viewsAndDate += " • " + infoItem.upload_date;
|
||||
}
|
||||
}
|
||||
return viewsAndDate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package org.schabi.newpipe.info_list.holder;
|
||||
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||
|
||||
public final ImageView itemThumbnailView;
|
||||
public final TextView itemVideoTitleView;
|
||||
public final TextView itemUploaderView;
|
||||
public final TextView itemDurationView;
|
||||
|
||||
StreamMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) {
|
||||
super(infoItemBuilder, layoutId, parent);
|
||||
|
||||
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
|
||||
itemVideoTitleView = itemView.findViewById(R.id.itemVideoTitleView);
|
||||
itemUploaderView = itemView.findViewById(R.id.itemUploaderView);
|
||||
itemDurationView = itemView.findViewById(R.id.itemDurationView);
|
||||
}
|
||||
|
||||
public StreamMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
|
||||
this(infoItemBuilder, R.layout.list_stream_mini_item, parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFromItem(final InfoItem infoItem) {
|
||||
if (!(infoItem instanceof StreamInfoItem)) return;
|
||||
final StreamInfoItem item = (StreamInfoItem) infoItem;
|
||||
|
||||
itemVideoTitleView.setText(item.name);
|
||||
itemUploaderView.setText(item.uploader_name);
|
||||
|
||||
if (item.duration > 0) {
|
||||
itemDurationView.setText(Localization.getDurationString(item.duration));
|
||||
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), R.color.duration_background_color));
|
||||
itemDurationView.setVisibility(View.VISIBLE);
|
||||
} else if (item.stream_type == StreamType.LIVE_STREAM) {
|
||||
itemDurationView.setText(R.string.duration_live);
|
||||
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), R.color.live_duration_background_color));
|
||||
itemDurationView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
itemDurationView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// Default thumbnail is shown on error, while loading and if the url is empty
|
||||
itemBuilder.getImageLoader()
|
||||
.displayImage(item.thumbnail_url, itemThumbnailView, StreamInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS);
|
||||
|
||||
itemView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (itemBuilder.getOnStreamSelectedListener() != null) {
|
||||
itemBuilder.getOnStreamSelectedListener().selected(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Display options for stream thumbnails
|
||||
*/
|
||||
public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
|
||||
new DisplayImageOptions.Builder()
|
||||
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
|
||||
.showImageOnFail(R.drawable.dummy_thumbnail)
|
||||
.showImageForEmptyUri(R.drawable.dummy_thumbnail)
|
||||
.showImageOnLoading(R.drawable.dummy_thumbnail)
|
||||
.build();
|
||||
}
|
||||
@@ -1,6 +1,24 @@
|
||||
/*
|
||||
* Copyright 2017 Mauricio Colli <mauriciocolli@outlook.com>
|
||||
* BackgroundPlayer.java is part of NewPipe
|
||||
*
|
||||
* License: GPL-3.0+
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.schabi.newpipe.player;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
@@ -22,7 +40,7 @@ import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.stream_info.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
@@ -66,7 +84,6 @@ public class BackgroundPlayer extends Service {
|
||||
private RemoteViews bigNotRemoteView;
|
||||
private final String setAlphaMethodName = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) ? "setImageAlpha" : "setAlpha";
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Service's LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -145,13 +162,13 @@ public class BackgroundPlayer extends Service {
|
||||
setupNotification(notRemoteView);
|
||||
setupNotification(bigNotRemoteView);
|
||||
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, getString(R.string.notification_channel_id))
|
||||
.setOngoing(true)
|
||||
.setSmallIcon(R.drawable.ic_play_circle_filled_white_24dp)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setCustomContentView(notRemoteView)
|
||||
.setCustomBigContentView(bigNotRemoteView);
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) builder.setPriority(Notification.PRIORITY_MAX);
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) builder.setPriority(NotificationCompat.PRIORITY_MAX);
|
||||
return builder;
|
||||
}
|
||||
|
||||
@@ -159,7 +176,7 @@ public class BackgroundPlayer extends Service {
|
||||
//if (videoThumbnail != null) remoteViews.setImageViewBitmap(R.id.notificationCover, videoThumbnail);
|
||||
///else remoteViews.setImageViewResource(R.id.notificationCover, R.drawable.dummy_thumbnail);
|
||||
remoteViews.setTextViewText(R.id.notificationSongName, basePlayerImpl.getVideoTitle());
|
||||
remoteViews.setTextViewText(R.id.notificationArtist, basePlayerImpl.getChannelName());
|
||||
remoteViews.setTextViewText(R.id.notificationArtist, basePlayerImpl.getUploaderName());
|
||||
|
||||
remoteViews.setOnClickPendingIntent(R.id.notificationPlayPause,
|
||||
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT));
|
||||
@@ -342,6 +359,11 @@ public class BackgroundPlayer extends Service {
|
||||
// Disable default behavior
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRepeatModeChanged(int i) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
super.destroy();
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* Copyright 2017 Mauricio Colli <mauriciocolli@outlook.com>
|
||||
* BasePlayer.java is part of NewPipe
|
||||
*
|
||||
* License: GPL-3.0+
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.schabi.newpipe.player;
|
||||
|
||||
import android.animation.Animator;
|
||||
@@ -19,10 +38,12 @@ import android.view.View;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.DefaultLoadControl;
|
||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.RenderersFactory;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
|
||||
@@ -47,6 +68,9 @@ import com.google.android.exoplayer2.util.Util;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
|
||||
|
||||
import org.schabi.newpipe.Downloader;
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import java.io.File;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
@@ -60,7 +84,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
* @author mauriciocolli
|
||||
*/
|
||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||
public abstract class BasePlayer implements ExoPlayer.EventListener, AudioManager.OnAudioFocusChangeListener {
|
||||
public abstract class BasePlayer implements Player.EventListener, AudioManager.OnAudioFocusChangeListener {
|
||||
// TODO: Check api version for deprecated audio manager methods
|
||||
|
||||
public static final boolean DEBUG = false;
|
||||
public static final String TAG = "BasePlayer";
|
||||
|
||||
@@ -86,8 +112,8 @@ public abstract class BasePlayer implements ExoPlayer.EventListener, AudioManage
|
||||
protected String videoUrl = "";
|
||||
protected String videoTitle = "";
|
||||
protected String videoThumbnailUrl = "";
|
||||
protected int videoStartPos = -1;
|
||||
protected String channelName = "";
|
||||
protected long videoStartPos = -1;
|
||||
protected String uploaderName = "";
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Player
|
||||
@@ -135,7 +161,7 @@ public abstract class BasePlayer implements ExoPlayer.EventListener, AudioManage
|
||||
|
||||
private void initExoPlayerCache() {
|
||||
if (cacheDataSourceFactory == null) {
|
||||
DefaultDataSourceFactory dataSourceFactory = new DefaultDataSourceFactory(context, Util.getUserAgent(context, context.getPackageName()), bandwidthMeter);
|
||||
DefaultDataSourceFactory dataSourceFactory = new DefaultDataSourceFactory(context, Downloader.USER_AGENT, bandwidthMeter);
|
||||
File cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME);
|
||||
if (!cacheDir.exists()) {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
@@ -156,7 +182,8 @@ public abstract class BasePlayer implements ExoPlayer.EventListener, AudioManage
|
||||
DefaultTrackSelector defaultTrackSelector = new DefaultTrackSelector(trackSelectionFactory);
|
||||
DefaultLoadControl loadControl = new DefaultLoadControl();
|
||||
|
||||
simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(context, defaultTrackSelector, loadControl);
|
||||
final RenderersFactory renderFactory = new DefaultRenderersFactory(context);
|
||||
simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(renderFactory, defaultTrackSelector, loadControl);
|
||||
simpleExoPlayer.addListener(this);
|
||||
}
|
||||
|
||||
@@ -178,8 +205,8 @@ public abstract class BasePlayer implements ExoPlayer.EventListener, AudioManage
|
||||
videoUrl = intent.getStringExtra(VIDEO_URL);
|
||||
videoTitle = intent.getStringExtra(VIDEO_TITLE);
|
||||
videoThumbnailUrl = intent.getStringExtra(VIDEO_THUMBNAIL_URL);
|
||||
videoStartPos = intent.getIntExtra(START_POSITION, -1);
|
||||
channelName = intent.getStringExtra(CHANNEL_NAME);
|
||||
videoStartPos = intent.getLongExtra(START_POSITION, -1L);
|
||||
uploaderName = intent.getStringExtra(CHANNEL_NAME);
|
||||
setPlaybackSpeed(intent.getFloatExtra(PLAYBACK_SPEED, getPlaybackSpeed()));
|
||||
|
||||
initThumbnail();
|
||||
@@ -195,7 +222,8 @@ public abstract class BasePlayer implements ExoPlayer.EventListener, AudioManage
|
||||
@Override
|
||||
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
|
||||
if (simpleExoPlayer == null) return;
|
||||
if (DEBUG) Log.d(TAG, "onLoadingComplete() called with: imageUri = [" + imageUri + "], view = [" + view + "], loadedImage = [" + loadedImage + "]");
|
||||
if (DEBUG)
|
||||
Log.d(TAG, "onLoadingComplete() called with: imageUri = [" + imageUri + "], view = [" + view + "], loadedImage = [" + loadedImage + "]");
|
||||
videoThumbnail = loadedImage;
|
||||
onThumbnailReceived(loadedImage);
|
||||
}
|
||||
@@ -218,7 +246,7 @@ public abstract class BasePlayer implements ExoPlayer.EventListener, AudioManage
|
||||
isPrepared = false;
|
||||
mediaSource = buildMediaSource(url, format);
|
||||
|
||||
if (simpleExoPlayer.getPlaybackState() != ExoPlayer.STATE_IDLE) simpleExoPlayer.stop();
|
||||
if (simpleExoPlayer.getPlaybackState() != Player.STATE_IDLE) simpleExoPlayer.stop();
|
||||
if (videoStartPos > 0) simpleExoPlayer.seekTo(videoStartPos);
|
||||
simpleExoPlayer.prepare(mediaSource);
|
||||
simpleExoPlayer.setPlayWhenReady(autoPlay);
|
||||
@@ -231,6 +259,10 @@ public abstract class BasePlayer implements ExoPlayer.EventListener, AudioManage
|
||||
simpleExoPlayer.release();
|
||||
}
|
||||
if (progressLoop != null && isProgressLoopRunning.get()) stopProgressLoop();
|
||||
if (audioManager != null) {
|
||||
audioManager.abandonAudioFocus(this);
|
||||
audioManager = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
@@ -321,10 +353,17 @@ public abstract class BasePlayer implements ExoPlayer.EventListener, AudioManage
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isResumeAfterAudioFocusGain() {
|
||||
return sharedPreferences != null && context != null
|
||||
&& sharedPreferences.getBoolean(context.getString(R.string.resume_on_audio_focus_gain_key), false);
|
||||
}
|
||||
|
||||
protected void onAudioFocusGain() {
|
||||
if (DEBUG) Log.d(TAG, "onAudioFocusGain() called");
|
||||
if (simpleExoPlayer != null) simpleExoPlayer.setVolume(DUCK_AUDIO_TO);
|
||||
animateAudio(DUCK_AUDIO_TO, 1f, DUCK_DURATION);
|
||||
|
||||
if (isResumeAfterAudioFocusGain()) simpleExoPlayer.setPlayWhenReady(true);
|
||||
}
|
||||
|
||||
protected void onAudioFocusLoss() {
|
||||
@@ -457,20 +496,21 @@ public abstract class BasePlayer implements ExoPlayer.EventListener, AudioManage
|
||||
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||
if (DEBUG) Log.d(TAG, "onPlayerStateChanged() called with: playWhenReady = [" + playWhenReady + "], playbackState = [" + playbackState + "]");
|
||||
if (DEBUG)
|
||||
Log.d(TAG, "onPlayerStateChanged() called with: playWhenReady = [" + playWhenReady + "], playbackState = [" + playbackState + "]");
|
||||
if (getCurrentState() == STATE_PAUSED_SEEK) {
|
||||
if (DEBUG) Log.d(TAG, "onPlayerStateChanged() currently on PausedSeek");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (playbackState) {
|
||||
case ExoPlayer.STATE_IDLE: // 1
|
||||
case Player.STATE_IDLE: // 1
|
||||
isPrepared = false;
|
||||
break;
|
||||
case ExoPlayer.STATE_BUFFERING: // 2
|
||||
case Player.STATE_BUFFERING: // 2
|
||||
if (isPrepared && getCurrentState() != STATE_LOADING) changeState(STATE_BUFFERING);
|
||||
break;
|
||||
case ExoPlayer.STATE_READY: //3
|
||||
case Player.STATE_READY: //3
|
||||
if (!isPrepared) {
|
||||
isPrepared = true;
|
||||
onPrepared(playWhenReady);
|
||||
@@ -479,7 +519,7 @@ public abstract class BasePlayer implements ExoPlayer.EventListener, AudioManage
|
||||
if (currentState == STATE_PAUSED_SEEK) break;
|
||||
changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED);
|
||||
break;
|
||||
case ExoPlayer.STATE_ENDED: // 4
|
||||
case Player.STATE_ENDED: // 4
|
||||
changeState(STATE_COMPLETED);
|
||||
isPrepared = false;
|
||||
break;
|
||||
@@ -500,7 +540,9 @@ public abstract class BasePlayer implements ExoPlayer.EventListener, AudioManage
|
||||
// General Player
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public abstract void onError(Exception exception);
|
||||
public void onError(Exception exception){
|
||||
destroy();
|
||||
}
|
||||
|
||||
public void onPrepared(boolean playWhenReady) {
|
||||
if (DEBUG) Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]");
|
||||
@@ -519,7 +561,7 @@ public abstract class BasePlayer implements ExoPlayer.EventListener, AudioManage
|
||||
}
|
||||
|
||||
if (!isPlaying()) audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
|
||||
else audioManager.abandonAudioFocus(null);
|
||||
else audioManager.abandonAudioFocus(this);
|
||||
|
||||
simpleExoPlayer.setPlayWhenReady(!isPlaying());
|
||||
}
|
||||
@@ -549,14 +591,15 @@ public abstract class BasePlayer implements ExoPlayer.EventListener, AudioManage
|
||||
|
||||
public void seekBy(int milliSeconds) {
|
||||
if (DEBUG) Log.d(TAG, "seekBy() called with: milliSeconds = [" + milliSeconds + "]");
|
||||
if (simpleExoPlayer == null || (isCompleted() && milliSeconds > 0) || ((milliSeconds < 0 && simpleExoPlayer.getCurrentPosition() == 0))) return;
|
||||
if (simpleExoPlayer == null || (isCompleted() && milliSeconds > 0) || ((milliSeconds < 0 && simpleExoPlayer.getCurrentPosition() == 0)))
|
||||
return;
|
||||
int progress = (int) (simpleExoPlayer.getCurrentPosition() + milliSeconds);
|
||||
if (progress < 0) progress = 0;
|
||||
simpleExoPlayer.seekTo(progress);
|
||||
}
|
||||
|
||||
public boolean isPlaying() {
|
||||
return simpleExoPlayer.getPlaybackState() == ExoPlayer.STATE_READY && simpleExoPlayer.getPlayWhenReady();
|
||||
return simpleExoPlayer.getPlaybackState() == Player.STATE_READY && simpleExoPlayer.getPlayWhenReady();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -677,11 +720,11 @@ public abstract class BasePlayer implements ExoPlayer.EventListener, AudioManage
|
||||
this.videoUrl = videoUrl;
|
||||
}
|
||||
|
||||
public int getVideoStartPos() {
|
||||
public long getVideoStartPos() {
|
||||
return videoStartPos;
|
||||
}
|
||||
|
||||
public void setVideoStartPos(int videoStartPos) {
|
||||
public void setVideoStartPos(long videoStartPos) {
|
||||
this.videoStartPos = videoStartPos;
|
||||
}
|
||||
|
||||
@@ -693,16 +736,16 @@ public abstract class BasePlayer implements ExoPlayer.EventListener, AudioManage
|
||||
this.videoTitle = videoTitle;
|
||||
}
|
||||
|
||||
public String getChannelName() {
|
||||
return channelName;
|
||||
public String getUploaderName() {
|
||||
return uploaderName;
|
||||
}
|
||||
|
||||
public void setChannelName(String channelName) {
|
||||
this.channelName = channelName;
|
||||
public void setUploaderName(String uploaderName) {
|
||||
this.uploaderName = uploaderName;
|
||||
}
|
||||
|
||||
public boolean isCompleted() {
|
||||
return simpleExoPlayer != null && simpleExoPlayer.getPlaybackState() == ExoPlayer.STATE_ENDED;
|
||||
return simpleExoPlayer != null && simpleExoPlayer.getPlaybackState() == Player.STATE_ENDED;
|
||||
}
|
||||
|
||||
public boolean isPrepared() {
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* Copyright 2017 Mauricio Colli <mauriciocolli@outlook.com>
|
||||
* MainVideoPlayer.java is part of NewPipe
|
||||
*
|
||||
* License: GPL-3.0+
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.schabi.newpipe.player;
|
||||
|
||||
import android.app.Activity;
|
||||
@@ -169,14 +188,14 @@ public class MainVideoPlayer extends Activity {
|
||||
@Override
|
||||
public void initViews(View rootView) {
|
||||
super.initViews(rootView);
|
||||
this.titleTextView = (TextView) rootView.findViewById(R.id.titleTextView);
|
||||
this.channelTextView = (TextView) rootView.findViewById(R.id.channelTextView);
|
||||
this.volumeTextView = (TextView) rootView.findViewById(R.id.volumeTextView);
|
||||
this.brightnessTextView = (TextView) rootView.findViewById(R.id.brightnessTextView);
|
||||
this.repeatButton = (ImageButton) rootView.findViewById(R.id.repeatButton);
|
||||
this.titleTextView = rootView.findViewById(R.id.titleTextView);
|
||||
this.channelTextView = rootView.findViewById(R.id.channelTextView);
|
||||
this.volumeTextView = rootView.findViewById(R.id.volumeTextView);
|
||||
this.brightnessTextView = rootView.findViewById(R.id.brightnessTextView);
|
||||
this.repeatButton = rootView.findViewById(R.id.repeatButton);
|
||||
|
||||
this.screenRotationButton = (ImageButton) rootView.findViewById(R.id.screenRotationButton);
|
||||
this.playPauseButton = (ImageButton) rootView.findViewById(R.id.playPauseButton);
|
||||
this.screenRotationButton = rootView.findViewById(R.id.screenRotationButton);
|
||||
this.playPauseButton = rootView.findViewById(R.id.playPauseButton);
|
||||
|
||||
// Due to a bug on lower API, lets set the alpha instead of using a drawable
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) repeatButton.setImageAlpha(77);
|
||||
@@ -205,7 +224,7 @@ public class MainVideoPlayer extends Activity {
|
||||
public void handleIntent(Intent intent) {
|
||||
super.handleIntent(intent);
|
||||
titleTextView.setText(getVideoTitle());
|
||||
channelTextView.setText(getChannelName());
|
||||
channelTextView.setText(getUploaderName());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -422,6 +441,11 @@ public class MainVideoPlayer extends Activity {
|
||||
public ImageButton getPlayPauseButton() {
|
||||
return playPauseButton;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRepeatModeChanged(int i) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private class MySimpleOnGestureListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener {
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* Copyright 2017 Mauricio Colli <mauriciocolli@outlook.com>
|
||||
* PopupVideoPlayer.java is part of NewPipe
|
||||
*
|
||||
* License: GPL-3.0+
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.schabi.newpipe.player;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
@@ -15,6 +34,7 @@ import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
@@ -38,13 +58,29 @@ import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.stream_info.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.player.old.PlayVideoActivity;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.util.Utils;
|
||||
import org.schabi.newpipe.workers.StreamExtractorWorker;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
@@ -87,7 +123,7 @@ public class PopupVideoPlayer extends Service {
|
||||
private DisplayImageOptions displayImageOptions = new DisplayImageOptions.Builder().cacheInMemory(true).build();
|
||||
|
||||
private VideoPlayerImpl playerImpl;
|
||||
private StreamExtractorWorker currentExtractorWorker;
|
||||
private Disposable currentWorker;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Service LifeCycle
|
||||
@@ -105,15 +141,33 @@ public class PopupVideoPlayer extends Service {
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public int onStartCommand(final Intent intent, int flags, int startId) {
|
||||
if (DEBUG) Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "], flags = [" + flags + "], startId = [" + startId + "]");
|
||||
if (DEBUG)
|
||||
Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "], flags = [" + flags + "], startId = [" + startId + "]");
|
||||
if (playerImpl.getPlayer() == null) initPopup();
|
||||
if (!playerImpl.isPlaying()) playerImpl.getPlayer().setPlayWhenReady(true);
|
||||
|
||||
if (imageLoader != null) imageLoader.clearMemoryCache();
|
||||
if (intent.getStringExtra(Constants.KEY_URL) != null) {
|
||||
final int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0);
|
||||
final String url = intent.getStringExtra(Constants.KEY_URL);
|
||||
|
||||
playerImpl.setStartedFromNewPipe(false);
|
||||
currentExtractorWorker = new StreamExtractorWorker(this, 0, intent.getStringExtra(Constants.KEY_URL), new FetcherRunnable(this));
|
||||
currentExtractorWorker.start();
|
||||
|
||||
final FetcherHandler fetcherRunnable = new FetcherHandler(this, serviceId, url);
|
||||
currentWorker = ExtractorHelper.getStreamInfo(serviceId,url,false)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new Consumer<StreamInfo>() {
|
||||
@Override
|
||||
public void accept(@NonNull StreamInfo info) throws Exception {
|
||||
fetcherRunnable.onReceive(info);
|
||||
}
|
||||
}, new Consumer<Throwable>() {
|
||||
@Override
|
||||
public void accept(@NonNull Throwable throwable) throws Exception {
|
||||
fetcherRunnable.onError(throwable);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
playerImpl.setStartedFromNewPipe(true);
|
||||
playerImpl.handleIntent(intent);
|
||||
@@ -137,11 +191,7 @@ public class PopupVideoPlayer extends Service {
|
||||
if (playerImpl.getRootView() != null) windowManager.removeView(playerImpl.getRootView());
|
||||
}
|
||||
if (notificationManager != null) notificationManager.cancel(NOTIFICATION_ID);
|
||||
if (currentExtractorWorker != null) {
|
||||
currentExtractorWorker.cancel();
|
||||
currentExtractorWorker = null;
|
||||
}
|
||||
|
||||
if (currentWorker != null) currentWorker.dispose();
|
||||
savePositionAndSize();
|
||||
}
|
||||
|
||||
@@ -168,9 +218,11 @@ public class PopupVideoPlayer extends Service {
|
||||
float defaultSize = getResources().getDimension(R.dimen.popup_default_width);
|
||||
popupWidth = popupRememberSizeAndPos ? sharedPreferences.getFloat(POPUP_SAVED_WIDTH, defaultSize) : defaultSize;
|
||||
|
||||
final int layoutParamType = Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O ? WindowManager.LayoutParams.TYPE_PHONE : WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
|
||||
|
||||
windowLayoutParams = new WindowManager.LayoutParams(
|
||||
(int) popupWidth, (int) getMinimumVideoHeight(popupWidth),
|
||||
WindowManager.LayoutParams.TYPE_PHONE,
|
||||
layoutParamType,
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
||||
PixelFormat.TRANSLUCENT);
|
||||
windowLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
|
||||
@@ -202,7 +254,7 @@ public class PopupVideoPlayer extends Service {
|
||||
else notRemoteView.setImageViewBitmap(R.id.notificationCover, playerImpl.getVideoThumbnail());
|
||||
|
||||
notRemoteView.setTextViewText(R.id.notificationSongName, playerImpl.getVideoTitle());
|
||||
notRemoteView.setTextViewText(R.id.notificationArtist, playerImpl.getChannelName());
|
||||
notRemoteView.setTextViewText(R.id.notificationArtist, playerImpl.getUploaderName());
|
||||
|
||||
notRemoteView.setOnClickPendingIntent(R.id.notificationPlayPause,
|
||||
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT));
|
||||
@@ -225,7 +277,7 @@ public class PopupVideoPlayer extends Service {
|
||||
break;
|
||||
}
|
||||
|
||||
return new NotificationCompat.Builder(this)
|
||||
return new NotificationCompat.Builder(this, getString(R.string.notification_channel_id))
|
||||
.setOngoing(true)
|
||||
.setSmallIcon(R.drawable.ic_play_arrow_white)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
@@ -273,9 +325,11 @@ public class PopupVideoPlayer extends Service {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void checkPositionBounds() {
|
||||
if (windowLayoutParams.x > screenWidth - windowLayoutParams.width) windowLayoutParams.x = (int) (screenWidth - windowLayoutParams.width);
|
||||
if (windowLayoutParams.x > screenWidth - windowLayoutParams.width)
|
||||
windowLayoutParams.x = (int) (screenWidth - windowLayoutParams.width);
|
||||
if (windowLayoutParams.x < 0) windowLayoutParams.x = 0;
|
||||
if (windowLayoutParams.y > screenHeight - windowLayoutParams.height) windowLayoutParams.y = (int) (screenHeight - windowLayoutParams.height);
|
||||
if (windowLayoutParams.y > screenHeight - windowLayoutParams.height)
|
||||
windowLayoutParams.y = (int) (screenHeight - windowLayoutParams.height);
|
||||
if (windowLayoutParams.y < 0) windowLayoutParams.y = 0;
|
||||
}
|
||||
|
||||
@@ -350,7 +404,7 @@ public class PopupVideoPlayer extends Service {
|
||||
@Override
|
||||
public void initViews(View rootView) {
|
||||
super.initViews(rootView);
|
||||
resizingIndicator = (TextView) rootView.findViewById(R.id.resizing_indicator);
|
||||
resizingIndicator = rootView.findViewById(R.id.resizing_indicator);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -429,7 +483,8 @@ public class PopupVideoPlayer extends Service {
|
||||
hideControls(100, 0);
|
||||
}
|
||||
}
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Broadcast Receiver
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@@ -509,6 +564,10 @@ public class PopupVideoPlayer extends Service {
|
||||
public TextView getResizingIndicator() {
|
||||
return resizingIndicator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRepeatModeChanged(int i) {
|
||||
}
|
||||
}
|
||||
|
||||
private class MySimpleOnGestureListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener {
|
||||
@@ -521,7 +580,8 @@ public class PopupVideoPlayer extends Service {
|
||||
|
||||
@Override
|
||||
public boolean onDoubleTap(MotionEvent e) {
|
||||
if (DEBUG) Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY());
|
||||
if (DEBUG)
|
||||
Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY());
|
||||
if (!playerImpl.isPlaying()) return false;
|
||||
if (e.getX() > popupWidth / 2) playerImpl.onFastForward();
|
||||
else playerImpl.onFastRewind();
|
||||
@@ -615,7 +675,8 @@ public class PopupVideoPlayer extends Service {
|
||||
}
|
||||
|
||||
if (event.getAction() == MotionEvent.ACTION_UP) {
|
||||
if (DEBUG) Log.d(TAG, "onTouch() ACTION_UP > v = [" + v + "], e1.getRaw = [" + event.getRawX() + ", " + event.getRawY() + "]");
|
||||
if (DEBUG)
|
||||
Log.d(TAG, "onTouch() ACTION_UP > v = [" + v + "], e1.getRaw = [" + event.getRawX() + ", " + event.getRawY() + "]");
|
||||
if (isMoving) {
|
||||
isMoving = false;
|
||||
onScrollEnd();
|
||||
@@ -634,32 +695,36 @@ public class PopupVideoPlayer extends Service {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetcher used if open by a link out of NewPipe
|
||||
* Fetcher handler used if open by a link out of NewPipe
|
||||
*/
|
||||
private class FetcherRunnable implements StreamExtractorWorker.OnStreamInfoReceivedListener {
|
||||
private class FetcherHandler {
|
||||
private final int serviceId;
|
||||
private final String url;
|
||||
|
||||
private final Context context;
|
||||
private final Handler mainHandler;
|
||||
|
||||
FetcherRunnable(Context context) {
|
||||
FetcherHandler(Context context, int serviceId, String url) {
|
||||
this.mainHandler = new Handler(PopupVideoPlayer.this.getMainLooper());
|
||||
this.context = context;
|
||||
this.url = url;
|
||||
this.serviceId = serviceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(StreamInfo info) {
|
||||
playerImpl.setVideoTitle(info.title);
|
||||
playerImpl.setVideoUrl(info.webpage_url);
|
||||
playerImpl.setVideoTitle(info.name);
|
||||
playerImpl.setVideoUrl(info.url);
|
||||
playerImpl.setVideoThumbnailUrl(info.thumbnail_url);
|
||||
playerImpl.setChannelName(info.uploader);
|
||||
playerImpl.setUploaderName(info.uploader_name);
|
||||
|
||||
playerImpl.setVideoStreamsList(Utils.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false));
|
||||
playerImpl.setAudioStream(Utils.getHighestQualityAudio(info.audio_streams));
|
||||
playerImpl.setVideoStreamsList(new ArrayList<>(ListHelper.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false)));
|
||||
playerImpl.setAudioStream(ListHelper.getHighestQualityAudio(info.audio_streams));
|
||||
|
||||
int defaultResolution = Utils.getPopupDefaultResolution(context, playerImpl.getVideoStreamsList());
|
||||
int defaultResolution = ListHelper.getPopupDefaultResolutionIndex(context, playerImpl.getVideoStreamsList());
|
||||
playerImpl.setSelectedIndexStream(defaultResolution);
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "FetcherRunnable.StreamExtractor: chosen = "
|
||||
Log.d(TAG, "FetcherHandler.StreamExtractor: chosen = "
|
||||
+ MediaFormat.getNameById(info.video_streams.get(defaultResolution).format) + " "
|
||||
+ info.video_streams.get(defaultResolution).resolution + " > "
|
||||
+ info.video_streams.get(defaultResolution).url);
|
||||
@@ -680,7 +745,7 @@ public class PopupVideoPlayer extends Service {
|
||||
@Override
|
||||
public void onLoadingComplete(final String imageUri, View view, final Bitmap loadedImage) {
|
||||
if (playerImpl == null || playerImpl.getPlayer() == null) return;
|
||||
if (DEBUG) Log.d(TAG, "FetcherRunnable.imageLoader.onLoadingComplete() called with: imageUri = [" + imageUri + "]");
|
||||
if (DEBUG) Log.d(TAG, "FetcherHandler.imageLoader.onLoadingComplete() called with: imageUri = [" + imageUri + "]");
|
||||
mainHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
@@ -693,70 +758,40 @@ public class PopupVideoPlayer extends Service {
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(final int messageId) {
|
||||
protected void onError(final Throwable exception) {
|
||||
if (DEBUG) Log.d(TAG, "onError() called with: exception = [" + exception + "]");
|
||||
exception.printStackTrace();
|
||||
mainHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(context, messageId, Toast.LENGTH_LONG).show();
|
||||
if (exception instanceof ReCaptchaException) {
|
||||
onReCaptchaException();
|
||||
} else if (exception instanceof IOException) {
|
||||
Toast.makeText(context, R.string.network_error, Toast.LENGTH_LONG).show();
|
||||
} else if (exception instanceof YoutubeStreamExtractor.GemaException) {
|
||||
Toast.makeText(context, R.string.blocked_by_gema, Toast.LENGTH_LONG).show();
|
||||
} else if (exception instanceof YoutubeStreamExtractor.LiveStreamException) {
|
||||
Toast.makeText(context, R.string.live_streams_not_supported, Toast.LENGTH_LONG).show();
|
||||
} else if (exception instanceof ContentNotAvailableException) {
|
||||
Toast.makeText(context, R.string.content_not_available, Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
int errorId = exception instanceof YoutubeStreamExtractor.DecryptException ? R.string.youtube_signature_decryption_error :
|
||||
exception instanceof ParsingException ? R.string.parsing_error : R.string.general_error;
|
||||
ErrorActivity.reportError(mainHandler, context, exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(UserAction.REQUESTED_STREAM, NewPipe.getNameOfService(serviceId), url, errorId));
|
||||
}
|
||||
}
|
||||
});
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReCaptchaException() {
|
||||
mainHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(context, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
Toast.makeText(context, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show();
|
||||
// Starting ReCaptcha Challenge Activity
|
||||
Intent intent = new Intent(context, ReCaptchaActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBlockedByGemaError() {
|
||||
mainHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(context, R.string.blocked_by_gema, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContentErrorWithMessage(final int messageId) {
|
||||
mainHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(context, messageId, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContentError() {
|
||||
mainHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(context, R.string.content_not_available, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnrecoverableError(Exception exception) {
|
||||
exception.printStackTrace();
|
||||
stopSelf();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* Copyright 2017 Mauricio Colli <mauriciocolli@outlook.com>
|
||||
* VideoPlayer.java is part of NewPipe
|
||||
*
|
||||
* License: GPL-3.0+
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.schabi.newpipe.player;
|
||||
|
||||
import android.animation.Animator;
|
||||
@@ -26,7 +45,7 @@ import android.widget.ProgressBar;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
@@ -35,8 +54,8 @@ import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.stream_info.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream_info.VideoStream;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
|
||||
import java.io.Serializable;
|
||||
@@ -52,7 +71,7 @@ import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
* @author mauriciocolli
|
||||
*/
|
||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||
public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.VideoListener, SeekBar.OnSeekBarChangeListener, View.OnClickListener, ExoPlayer.EventListener, PopupMenu.OnMenuItemClickListener, PopupMenu.OnDismissListener {
|
||||
public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.VideoListener, SeekBar.OnSeekBarChangeListener, View.OnClickListener, Player.EventListener, PopupMenu.OnMenuItemClickListener, PopupMenu.OnDismissListener {
|
||||
public static final boolean DEBUG = BasePlayer.DEBUG;
|
||||
public final String TAG;
|
||||
|
||||
@@ -73,7 +92,7 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
|
||||
// Player
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public static final int DEFAULT_CONTROLS_HIDE_TIME = 3000; // 3 Seconds
|
||||
public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds
|
||||
private static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f};
|
||||
|
||||
private boolean startedFromNewPipe = true;
|
||||
@@ -132,26 +151,27 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
|
||||
|
||||
public void initViews(View rootView) {
|
||||
this.rootView = rootView;
|
||||
this.aspectRatioFrameLayout = (AspectRatioFrameLayout) rootView.findViewById(R.id.aspectRatioLayout);
|
||||
this.surfaceView = (SurfaceView) rootView.findViewById(R.id.surfaceView);
|
||||
this.aspectRatioFrameLayout = rootView.findViewById(R.id.aspectRatioLayout);
|
||||
this.surfaceView = rootView.findViewById(R.id.surfaceView);
|
||||
this.surfaceForeground = rootView.findViewById(R.id.surfaceForeground);
|
||||
this.loadingPanel = rootView.findViewById(R.id.loading_panel);
|
||||
this.endScreen = (ImageView) rootView.findViewById(R.id.endScreen);
|
||||
this.controlAnimationView = (ImageView) rootView.findViewById(R.id.controlAnimationView);
|
||||
this.endScreen = rootView.findViewById(R.id.endScreen);
|
||||
this.controlAnimationView = rootView.findViewById(R.id.controlAnimationView);
|
||||
this.controlsRoot = rootView.findViewById(R.id.playbackControlRoot);
|
||||
this.currentDisplaySeek = (TextView) rootView.findViewById(R.id.currentDisplaySeek);
|
||||
this.playbackSeekBar = (SeekBar) rootView.findViewById(R.id.playbackSeekBar);
|
||||
this.playbackCurrentTime = (TextView) rootView.findViewById(R.id.playbackCurrentTime);
|
||||
this.playbackEndTime = (TextView) rootView.findViewById(R.id.playbackEndTime);
|
||||
this.playbackSpeed = (TextView) rootView.findViewById(R.id.playbackSpeed);
|
||||
this.currentDisplaySeek = rootView.findViewById(R.id.currentDisplaySeek);
|
||||
this.playbackSeekBar = rootView.findViewById(R.id.playbackSeekBar);
|
||||
this.playbackCurrentTime = rootView.findViewById(R.id.playbackCurrentTime);
|
||||
this.playbackEndTime = rootView.findViewById(R.id.playbackEndTime);
|
||||
this.playbackSpeed = rootView.findViewById(R.id.playbackSpeed);
|
||||
this.bottomControlsRoot = rootView.findViewById(R.id.bottomControls);
|
||||
this.topControlsRoot = rootView.findViewById(R.id.topControls);
|
||||
this.qualityTextView = (TextView) rootView.findViewById(R.id.qualityTextView);
|
||||
this.fullScreenButton = (ImageButton) rootView.findViewById(R.id.fullScreenButton);
|
||||
this.qualityTextView = rootView.findViewById(R.id.qualityTextView);
|
||||
this.fullScreenButton = rootView.findViewById(R.id.fullScreenButton);
|
||||
|
||||
//this.aspectRatioFrameLayout.setAspectRatio(16.0f / 9.0f);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
|
||||
playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN);
|
||||
this.playbackSeekBar.getProgressDrawable().setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY);
|
||||
|
||||
this.qualityPopupMenu = new PopupMenu(context, qualityTextView);
|
||||
@@ -225,7 +245,7 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
|
||||
@Override
|
||||
public MediaSource buildMediaSource(String url, String overrideExtension) {
|
||||
MediaSource mediaSource = super.buildMediaSource(url, overrideExtension);
|
||||
if (!getSelectedVideoStream().isVideoOnly) return mediaSource;
|
||||
if (!getSelectedVideoStream().isVideoOnly || videoOnlyAudioStream == null) return mediaSource;
|
||||
|
||||
Uri audioUri = Uri.parse(videoOnlyAudioStream.url);
|
||||
return new MergingMediaSource(mediaSource, new ExtractorMediaSource(audioUri, cacheDataSourceFactory, extractorsFactory, null, null));
|
||||
@@ -268,7 +288,8 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
|
||||
playbackSeekBar.setProgress(0);
|
||||
|
||||
// Bug on lower api, disabling and enabling the seekBar resets the thumb color -.-, so sets the color again
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
|
||||
playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN);
|
||||
|
||||
animateView(endScreen, false, 0);
|
||||
loadingPanel.setBackgroundColor(Color.BLACK);
|
||||
@@ -323,7 +344,8 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
|
||||
playbackEndTime.setText(getTimeString(playbackSeekBar.getMax()));
|
||||
playbackCurrentTime.setText(playbackEndTime.getText());
|
||||
// Bug on lower api, disabling and enabling the seekBar resets the thumb color -.-, so sets the color again
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
|
||||
playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN);
|
||||
|
||||
animateView(surfaceForeground, true, 100);
|
||||
|
||||
@@ -359,8 +381,8 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
|
||||
if (DEBUG) Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]");
|
||||
|
||||
if (videoStartPos > 0) {
|
||||
playbackSeekBar.setProgress(videoStartPos);
|
||||
playbackCurrentTime.setText(getTimeString(videoStartPos));
|
||||
playbackSeekBar.setProgress((int) videoStartPos);
|
||||
playbackCurrentTime.setText(getTimeString((int) videoStartPos));
|
||||
videoStartPos = -1;
|
||||
}
|
||||
|
||||
@@ -443,11 +465,12 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
|
||||
*/
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem menuItem) {
|
||||
if (DEBUG) Log.d(TAG, "onMenuItemClick() called with: menuItem = [" + menuItem + "], menuItem.getItemId = [" + menuItem.getItemId() + "]");
|
||||
if (DEBUG)
|
||||
Log.d(TAG, "onMenuItemClick() called with: menuItem = [" + menuItem + "], menuItem.getItemId = [" + menuItem.getItemId() + "]");
|
||||
|
||||
if (qualityPopupMenuGroupId == menuItem.getGroupId()) {
|
||||
if (selectedIndexStream == menuItem.getItemId()) return true;
|
||||
setVideoStartPos((int) simpleExoPlayer.getCurrentPosition());
|
||||
setVideoStartPos(simpleExoPlayer.getCurrentPosition());
|
||||
|
||||
selectedIndexStream = menuItem.getItemId();
|
||||
if (!(getCurrentState() == STATE_COMPLETED)) play(wasPlaying);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.player;
|
||||
package org.schabi.newpipe.player.old;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@@ -29,7 +29,7 @@ import android.widget.VideoView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
/**
|
||||
/*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* PlayVideoActivity.java is part of NewPipe.
|
||||
*
|
||||
@@ -122,8 +122,8 @@ public class PlayVideoActivity extends AppCompatActivity {
|
||||
|
||||
position = intent.getIntExtra(START_POSITION, 0)*1000;//convert from seconds to milliseconds
|
||||
|
||||
videoView = (VideoView) findViewById(R.id.video_view);
|
||||
progressBar = (ProgressBar) findViewById(R.id.play_video_progress_bar);
|
||||
videoView = findViewById(R.id.video_view);
|
||||
progressBar = findViewById(R.id.play_video_progress_bar);
|
||||
try {
|
||||
videoView.setMediaController(mediaController);
|
||||
videoView.setVideoURI(Uri.parse(intent.getStringExtra(STREAM_URL)));
|
||||
@@ -146,7 +146,7 @@ public class PlayVideoActivity extends AppCompatActivity {
|
||||
});
|
||||
videoUrl = intent.getStringExtra(VIDEO_URL);
|
||||
|
||||
Button button = (Button) findViewById(R.id.content_button);
|
||||
Button button = findViewById(R.id.content_button);
|
||||
button.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
@@ -8,7 +8,7 @@ import org.acra.sender.ReportSender;
|
||||
import org.acra.sender.ReportSenderException;
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
/**
|
||||
/*
|
||||
* Created by Christian Schabesberger on 13.09.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
|
||||
@@ -6,9 +6,8 @@ import android.support.annotation.NonNull;
|
||||
import org.acra.config.ACRAConfiguration;
|
||||
import org.acra.sender.ReportSender;
|
||||
import org.acra.sender.ReportSenderFactory;
|
||||
import org.schabi.newpipe.report.AcraReportSender;
|
||||
|
||||
/**
|
||||
/*
|
||||
* Created by Christian Schabesberger on 13.09.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
|
||||
@@ -36,7 +36,7 @@ import org.schabi.newpipe.BuildConfig;
|
||||
import org.schabi.newpipe.Downloader;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.Parser;
|
||||
import org.schabi.newpipe.extractor.utils.Parser;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
@@ -47,7 +47,7 @@ import java.util.List;
|
||||
import java.util.TimeZone;
|
||||
import java.util.Vector;
|
||||
|
||||
/**
|
||||
/*
|
||||
* Created by Christian Schabesberger on 24.10.15.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
@@ -95,37 +95,34 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
|
||||
public static void reportError(final Context context, final List<Throwable> el,
|
||||
final Class returnActivity, View rootView, final ErrorInfo errorInfo) {
|
||||
|
||||
if (rootView != null) {
|
||||
Snackbar.make(rootView, R.string.error_snackbar_message, Snackbar.LENGTH_LONG)
|
||||
Snackbar.make(rootView, R.string.error_snackbar_message, 15 * 1000)
|
||||
.setActionTextColor(Color.YELLOW)
|
||||
.setAction(R.string.error_snackbar_action, new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
ActivityCommunicator ac = ActivityCommunicator.getCommunicator();
|
||||
ac.returnActivity = returnActivity;
|
||||
Intent intent = new Intent(context, ErrorActivity.class);
|
||||
intent.putExtra(ERROR_INFO, errorInfo);
|
||||
intent.putExtra(ERROR_LIST, elToSl(el));
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
startErrorActivity(returnActivity, context, errorInfo, el);
|
||||
}
|
||||
}).show();
|
||||
} else {
|
||||
ActivityCommunicator ac = ActivityCommunicator.getCommunicator();
|
||||
ac.returnActivity = returnActivity;
|
||||
Intent intent = new Intent(context, ErrorActivity.class);
|
||||
intent.putExtra(ERROR_INFO, errorInfo);
|
||||
intent.putExtra(ERROR_LIST, elToSl(el));
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
startErrorActivity(returnActivity, context, errorInfo, el);
|
||||
}
|
||||
}
|
||||
|
||||
private static void startErrorActivity(Class returnActivity, Context context, ErrorInfo errorInfo, List<Throwable> el) {
|
||||
ActivityCommunicator ac = ActivityCommunicator.getCommunicator();
|
||||
ac.returnActivity = returnActivity;
|
||||
Intent intent = new Intent(context, ErrorActivity.class);
|
||||
intent.putExtra(ERROR_INFO, errorInfo);
|
||||
intent.putExtra(ERROR_LIST, elToSl(el));
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
public static void reportError(final Context context, final Throwable e,
|
||||
final Class returnActivity, View rootView, final ErrorInfo errorInfo) {
|
||||
List<Throwable> el = null;
|
||||
if(e != null) {
|
||||
if (e != null) {
|
||||
el = new Vector<>();
|
||||
el.add(e);
|
||||
}
|
||||
@@ -137,7 +134,7 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
final Class returnActivity, final View rootView, final ErrorInfo errorInfo) {
|
||||
|
||||
List<Throwable> el = null;
|
||||
if(e != null) {
|
||||
if (e != null) {
|
||||
el = new Vector<>();
|
||||
el.add(e);
|
||||
}
|
||||
@@ -158,12 +155,12 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
public static void reportError(final Context context, final CrashReportData report, final ErrorInfo errorInfo) {
|
||||
// get key first (don't ask about this solution)
|
||||
ReportField key = null;
|
||||
for(ReportField k : report.keySet()) {
|
||||
if(k.toString().equals("STACK_TRACE")) {
|
||||
for (ReportField k : report.keySet()) {
|
||||
if (k.toString().equals("STACK_TRACE")) {
|
||||
key = k;
|
||||
}
|
||||
}
|
||||
String[] el = new String[] { report.get(key) };
|
||||
String[] el = new String[]{report.get(key)};
|
||||
|
||||
Intent intent = new Intent(context, ErrorActivity.class);
|
||||
intent.putExtra(ERROR_INFO, errorInfo);
|
||||
@@ -196,7 +193,7 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
|
||||
Intent intent = getIntent();
|
||||
|
||||
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
@@ -206,11 +203,11 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
actionBar.setDisplayShowTitleEnabled(true);
|
||||
}
|
||||
|
||||
reportButton = (Button) findViewById(R.id.errorReportButton);
|
||||
userCommentBox = (EditText) findViewById(R.id.errorCommentBox);
|
||||
errorView = (TextView) findViewById(R.id.errorView);
|
||||
infoView = (TextView) findViewById(R.id.errorInfosView);
|
||||
errorMessageView = (TextView) findViewById(R.id.errorMessageView);
|
||||
reportButton = findViewById(R.id.errorReportButton);
|
||||
userCommentBox = findViewById(R.id.errorCommentBox);
|
||||
errorView = findViewById(R.id.errorView);
|
||||
infoView = findViewById(R.id.errorInfosView);
|
||||
errorMessageView = findViewById(R.id.errorMessageView);
|
||||
|
||||
ActivityCommunicator ac = ActivityCommunicator.getCommunicator();
|
||||
returnActivity = ac.returnActivity;
|
||||
@@ -240,7 +237,7 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
|
||||
// normal bugreport
|
||||
buildInfo(errorInfo);
|
||||
if(errorInfo.message != 0) {
|
||||
if (errorInfo.message != 0) {
|
||||
errorMessageView.setText(errorInfo.message);
|
||||
} else {
|
||||
errorMessageView.setVisibility(View.GONE);
|
||||
@@ -250,7 +247,7 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
errorView.setText(formErrorText(errorList));
|
||||
|
||||
//print stack trace once again for debugging:
|
||||
for(String e : errorList) {
|
||||
for (String e : errorList) {
|
||||
Log.e(TAG, e);
|
||||
}
|
||||
}
|
||||
@@ -283,7 +280,7 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
|
||||
private String formErrorText(String[] el) {
|
||||
String text = "";
|
||||
if(el != null) {
|
||||
if (el != null) {
|
||||
for (String e : el) {
|
||||
text += "-------------------------------------\n"
|
||||
+ e;
|
||||
@@ -295,13 +292,14 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
|
||||
/**
|
||||
* Get the checked activity.
|
||||
*
|
||||
* @param returnActivity the activity to return to
|
||||
* @return the casted return activity or null
|
||||
*/
|
||||
@Nullable
|
||||
static Class<? extends Activity> getReturnActivity(Class<?> returnActivity) {
|
||||
Class<? extends Activity> checkedReturnActivity = null;
|
||||
if (returnActivity != null){
|
||||
if (returnActivity != null) {
|
||||
if (Activity.class.isAssignableFrom(returnActivity)) {
|
||||
checkedReturnActivity = returnActivity.asSubclass(Activity.class);
|
||||
} else {
|
||||
@@ -323,8 +321,8 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
private void buildInfo(ErrorInfo info) {
|
||||
TextView infoLabelView = (TextView) findViewById(R.id.errorInfoLabelsView);
|
||||
TextView infoView = (TextView) findViewById(R.id.errorInfosView);
|
||||
TextView infoLabelView = findViewById(R.id.errorInfoLabelsView);
|
||||
TextView infoView = findViewById(R.id.errorInfosView);
|
||||
String text = "";
|
||||
|
||||
infoLabelView.setText(getString(R.string.info_labels).replace("\\n", "\n"));
|
||||
@@ -356,7 +354,7 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
.put("ip_range", globIpRange);
|
||||
|
||||
JSONArray exceptionArray = new JSONArray();
|
||||
if(errorList != null) {
|
||||
if (errorList != null) {
|
||||
for (String e : errorList) {
|
||||
exceptionArray.put(e);
|
||||
}
|
||||
@@ -375,7 +373,7 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
private String getUserActionString(UserAction userAction) {
|
||||
if(userAction == null) {
|
||||
if (userAction == null) {
|
||||
return "Your description is in another castle.";
|
||||
} else {
|
||||
return userAction.getMessage();
|
||||
@@ -397,7 +395,7 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
|
||||
private void addGuruMeditaion() {
|
||||
//just an easter egg
|
||||
TextView sorryView = (TextView) findViewById(R.id.errorSorryView);
|
||||
TextView sorryView = findViewById(R.id.errorSorryView);
|
||||
String text = sorryView.getText().toString();
|
||||
text += "\n" + getString(R.string.guru_meditation);
|
||||
sorryView.setText(text);
|
||||
@@ -467,6 +465,7 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
|
||||
private class IpRangeRequester implements Runnable {
|
||||
Handler h = new Handler();
|
||||
|
||||
public void run() {
|
||||
String ipRange = "none";
|
||||
try {
|
||||
@@ -475,7 +474,7 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
|
||||
ipRange = Parser.matchGroup1("([0-9]*\\.[0-9]*\\.)[0-9]*\\.[0-9]*", ip)
|
||||
+ "0.0";
|
||||
} catch(Throwable e) {
|
||||
} catch (Throwable e) {
|
||||
Log.w(TAG, "Error while error: could not get iprange", e);
|
||||
} finally {
|
||||
h.post(new IpRangeReturnRunnable(ipRange));
|
||||
@@ -485,12 +484,14 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
|
||||
private class IpRangeReturnRunnable implements Runnable {
|
||||
String ipRange;
|
||||
|
||||
public IpRangeReturnRunnable(String ipRange) {
|
||||
this.ipRange = ipRange;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
globIpRange = ipRange;
|
||||
if(infoView != null) {
|
||||
if (infoView != null) {
|
||||
String text = infoView.getText().toString();
|
||||
text += "\n" + globIpRange;
|
||||
infoView.setText(text);
|
||||
|
||||
@@ -4,14 +4,16 @@ package org.schabi.newpipe.report;
|
||||
* The user actions that can cause an error.
|
||||
*/
|
||||
public enum UserAction {
|
||||
SEARCHED("searched"),
|
||||
REQUESTED_STREAM("requested stream"),
|
||||
GET_SUGGESTIONS("get suggestions"),
|
||||
SOMETHING_ELSE("something"),
|
||||
USER_REPORT("user report"),
|
||||
LOAD_IMAGE("load image"),
|
||||
UI_ERROR("ui error"),
|
||||
REQUESTED_CHANNEL("requested channel");
|
||||
SUBSCRIPTION("subscription"),
|
||||
LOAD_IMAGE("load image"),
|
||||
SOMETHING_ELSE("something"),
|
||||
SEARCHED("searched"),
|
||||
GET_SUGGESTIONS("get suggestions"),
|
||||
REQUESTED_STREAM("requested stream"),
|
||||
REQUESTED_CHANNEL("requested channel"),
|
||||
REQUESTED_PLAYLIST("requested playlist");
|
||||
|
||||
|
||||
private final String message;
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package org.schabi.newpipe.settings;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.preference.Preference;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
|
||||
public class AppearanceSettingsFragment extends BasePreferenceFragment {
|
||||
/**
|
||||
* Theme that was applied when the settings was opened (or recreated after a theme change)
|
||||
*/
|
||||
private String startThemeKey;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
String themeKey = getString(R.string.theme_key);
|
||||
startThemeKey = defaultPreferences.getString(themeKey, getString(R.string.default_theme_value));
|
||||
findPreference(themeKey).setOnPreferenceChangeListener(themePreferenceChange);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
addPreferencesFromResource(R.xml.appearance_settings);
|
||||
}
|
||||
|
||||
private Preference.OnPreferenceChangeListener themePreferenceChange = new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
defaultPreferences.edit().putBoolean(Constants.KEY_THEME_CHANGE, true).apply();
|
||||
defaultPreferences.edit().putString(getString(R.string.theme_key), newValue.toString()).apply();
|
||||
|
||||
if (!newValue.equals(startThemeKey)) { // If it's not the current theme
|
||||
getActivity().recreate();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.schabi.newpipe.settings;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.preference.PreferenceFragmentCompat;
|
||||
import android.view.View;
|
||||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
|
||||
public abstract class BasePreferenceFragment extends PreferenceFragmentCompat {
|
||||
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
|
||||
protected boolean DEBUG = MainActivity.DEBUG;
|
||||
|
||||
protected SharedPreferences defaultPreferences;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
defaultPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
setDivider(null);
|
||||
updateTitle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
updateTitle();
|
||||
}
|
||||
|
||||
private void updateTitle() {
|
||||
if (getActivity() instanceof AppCompatActivity) {
|
||||
ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
|
||||
if (actionBar != null) actionBar.setTitle(getPreferenceScreen().getTitle());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.schabi.newpipe.settings;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
addPreferencesFromResource(R.xml.content_settings);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package org.schabi.newpipe.settings;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.preference.Preference;
|
||||
import android.util.Log;
|
||||
|
||||
import com.nononsenseapps.filepicker.FilePickerActivity;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
public class DownloadSettingsFragment extends BasePreferenceFragment {
|
||||
private static final int REQUEST_DOWNLOAD_PATH = 0x1235;
|
||||
private static final int REQUEST_DOWNLOAD_AUDIO_PATH = 0x1236;
|
||||
|
||||
private String DOWNLOAD_PATH_PREFERENCE;
|
||||
private String DOWNLOAD_PATH_AUDIO_PREFERENCE;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
initKeys();
|
||||
updatePreferencesSummary();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
addPreferencesFromResource(R.xml.download_settings);
|
||||
}
|
||||
|
||||
private void initKeys() {
|
||||
DOWNLOAD_PATH_PREFERENCE = getString(R.string.download_path_key);
|
||||
DOWNLOAD_PATH_AUDIO_PREFERENCE = getString(R.string.download_path_audio_key);
|
||||
}
|
||||
|
||||
private void updatePreferencesSummary() {
|
||||
findPreference(DOWNLOAD_PATH_PREFERENCE).setSummary(defaultPreferences.getString(DOWNLOAD_PATH_PREFERENCE, getString(R.string.download_path_summary)));
|
||||
findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE).setSummary(defaultPreferences.getString(DOWNLOAD_PATH_AUDIO_PREFERENCE, getString(R.string.download_path_audio_summary)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceTreeClick(Preference preference) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onPreferenceTreeClick() called with: preference = [" + preference + "]");
|
||||
}
|
||||
|
||||
if (preference.getKey().equals(DOWNLOAD_PATH_PREFERENCE) || preference.getKey().equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) {
|
||||
Intent i = new Intent(getActivity(), FilePickerActivity.class)
|
||||
.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false)
|
||||
.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true)
|
||||
.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR);
|
||||
if (preference.getKey().equals(DOWNLOAD_PATH_PREFERENCE)) {
|
||||
startActivityForResult(i, REQUEST_DOWNLOAD_PATH);
|
||||
} else if (preference.getKey().equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) {
|
||||
startActivityForResult(i, REQUEST_DOWNLOAD_AUDIO_PATH);
|
||||
}
|
||||
}
|
||||
|
||||
return super.onPreferenceTreeClick(preference);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onActivityResult() called with: requestCode = [" + requestCode + "], resultCode = [" + resultCode + "], data = [" + data + "]");
|
||||
}
|
||||
|
||||
if ((requestCode == REQUEST_DOWNLOAD_PATH || requestCode == REQUEST_DOWNLOAD_AUDIO_PATH) && resultCode == Activity.RESULT_OK) {
|
||||
String key = getString(requestCode == REQUEST_DOWNLOAD_PATH ? R.string.download_path_key : R.string.download_path_audio_key);
|
||||
String path = data.getData().getPath();
|
||||
defaultPreferences.edit().putString(key, path).apply();
|
||||
updatePreferencesSummary();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.schabi.newpipe.settings;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
public class HistorySettingsFragment extends BasePreferenceFragment {
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
addPreferencesFromResource(R.xml.history_settings);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.schabi.newpipe.settings;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
public class MainSettingsFragment extends BasePreferenceFragment {
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
addPreferencesFromResource(R.xml.main_settings);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Created by k3b on 07.01.2016.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
@@ -30,13 +30,11 @@ import org.schabi.newpipe.R;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import us.shandian.giga.util.Utility;
|
||||
|
||||
/**
|
||||
* Helper for global settings
|
||||
*/
|
||||
|
||||
/**
|
||||
/*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* NewPipeSettings.java is part of NewPipe.
|
||||
*
|
||||
@@ -60,7 +58,13 @@ public class NewPipeSettings {
|
||||
}
|
||||
|
||||
public static void initSettings(Context context) {
|
||||
PreferenceManager.setDefaultValues(context, R.xml.settings, false);
|
||||
PreferenceManager.setDefaultValues(context, R.xml.appearance_settings, true);
|
||||
PreferenceManager.setDefaultValues(context, R.xml.content_settings, true);
|
||||
PreferenceManager.setDefaultValues(context, R.xml.download_settings, true);
|
||||
PreferenceManager.setDefaultValues(context, R.xml.history_settings, true);
|
||||
PreferenceManager.setDefaultValues(context, R.xml.main_settings, true);
|
||||
PreferenceManager.setDefaultValues(context, R.xml.video_audio_settings, true);
|
||||
|
||||
getVideoDownloadFolder(context);
|
||||
getAudioDownloadFolder(context);
|
||||
}
|
||||
@@ -93,14 +97,13 @@ public class NewPipeSettings {
|
||||
|
||||
final File folder = getFolder(defaultDirectoryName);
|
||||
SharedPreferences.Editor spEditor = prefs.edit();
|
||||
spEditor.putString(key
|
||||
, new File(folder,"NewPipe").getAbsolutePath());
|
||||
spEditor.putString(key, new File(folder, "NewPipe").getAbsolutePath());
|
||||
spEditor.apply();
|
||||
return folder;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static File getFolder(String defaultDirectoryName) {
|
||||
return new File(Environment.getExternalStorageDirectory(),defaultDirectoryName);
|
||||
return new File(Environment.getExternalStorageDirectory(), defaultDirectoryName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,20 @@ package org.schabi.newpipe.settings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.preference.Preference;
|
||||
import android.support.v7.preference.PreferenceFragmentCompat;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
|
||||
/**
|
||||
/*
|
||||
* Created by Christian Schabesberger on 31.08.15.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
@@ -31,7 +35,7 @@ import org.schabi.newpipe.util.ThemeHelper;
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class SettingsActivity extends AppCompatActivity {
|
||||
public class SettingsActivity extends AppCompatActivity implements BasePreferenceFragment.OnPreferenceStartFragmentCallback {
|
||||
|
||||
public static void initSettings(Context context) {
|
||||
NewPipeSettings.initSettings(context);
|
||||
@@ -43,21 +47,25 @@ public class SettingsActivity extends AppCompatActivity {
|
||||
super.onCreate(savedInstanceBundle);
|
||||
setContentView(R.layout.settings_layout);
|
||||
|
||||
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
if (savedInstanceBundle == null) {
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.fragment_holder, new MainSettingsFragment())
|
||||
.commit();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
actionBar.setTitle(R.string.settings);
|
||||
actionBar.setDisplayShowTitleEnabled(true);
|
||||
}
|
||||
|
||||
if (savedInstanceBundle == null) {
|
||||
getFragmentManager().beginTransaction()
|
||||
.replace(R.id.fragment_holder, new SettingsFragment())
|
||||
.commit();
|
||||
}
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -68,4 +76,15 @@ public class SettingsActivity extends AppCompatActivity {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference preference) {
|
||||
Fragment fragment = Fragment.instantiate(this, preference.getFragment(), preference.getExtras());
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out)
|
||||
.replace(R.id.fragment_holder, fragment)
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
package org.schabi.newpipe.settings;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceFragment;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.preference.PreferenceScreen;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.nononsenseapps.filepicker.FilePickerActivity;
|
||||
|
||||
import org.schabi.newpipe.App;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
|
||||
import info.guardianproject.netcipher.proxy.OrbotHelper;
|
||||
|
||||
public class SettingsFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private static final int REQUEST_INSTALL_ORBOT = 0x1234;
|
||||
private static final int REQUEST_DOWNLOAD_PATH = 0x1235;
|
||||
private static final int REQUEST_DOWNLOAD_AUDIO_PATH = 0x1236;
|
||||
|
||||
private String DOWNLOAD_PATH_PREFERENCE;
|
||||
private String DOWNLOAD_PATH_AUDIO_PREFERENCE;
|
||||
private String USE_TOR_KEY;
|
||||
private String THEME;
|
||||
|
||||
private String currentTheme;
|
||||
private SharedPreferences defaultPreferences;
|
||||
|
||||
private Activity activity;
|
||||
|
||||
@Override
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
activity = getActivity();
|
||||
addPreferencesFromResource(R.xml.settings);
|
||||
|
||||
defaultPreferences = PreferenceManager.getDefaultSharedPreferences(activity);
|
||||
initKeys();
|
||||
updatePreferencesSummary();
|
||||
|
||||
currentTheme = defaultPreferences.getString(THEME, getString(R.string.default_theme_value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
defaultPreferences.registerOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
defaultPreferences.unregisterOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
|
||||
private void initKeys() {
|
||||
DOWNLOAD_PATH_PREFERENCE = getString(R.string.download_path_key);
|
||||
DOWNLOAD_PATH_AUDIO_PREFERENCE = getString(R.string.download_path_audio_key);
|
||||
THEME = getString(R.string.theme_key);
|
||||
USE_TOR_KEY = getString(R.string.use_tor_key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) {
|
||||
if (MainActivity.DEBUG) Log.d("TAG", "onPreferenceTreeClick() called with: preferenceScreen = [" + preferenceScreen + "], preference = [" + preference + "]");
|
||||
if (preference.getKey().equals(DOWNLOAD_PATH_PREFERENCE) || preference.getKey().equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) {
|
||||
Intent i = new Intent(activity, FilePickerActivity.class)
|
||||
.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false)
|
||||
.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true)
|
||||
.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR);
|
||||
if (preference.getKey().equals(DOWNLOAD_PATH_PREFERENCE)) {
|
||||
startActivityForResult(i, REQUEST_DOWNLOAD_PATH);
|
||||
} else if (preference.getKey().equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) {
|
||||
startActivityForResult(i, REQUEST_DOWNLOAD_AUDIO_PATH);
|
||||
}
|
||||
}
|
||||
return super.onPreferenceTreeClick(preferenceScreen, preference);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (MainActivity.DEBUG) Log.d("TAG", "onActivityResult() called with: requestCode = [" + requestCode + "], resultCode = [" + resultCode + "], data = [" + data + "]");
|
||||
|
||||
if ((requestCode == REQUEST_DOWNLOAD_PATH || requestCode == REQUEST_DOWNLOAD_AUDIO_PATH) && resultCode == Activity.RESULT_OK) {
|
||||
String key = getString(requestCode == REQUEST_DOWNLOAD_PATH ? R.string.download_path_key : R.string.download_path_audio_key);
|
||||
String path = data.getData().getPath();
|
||||
defaultPreferences.edit().putString(key, path).apply();
|
||||
updatePreferencesSummary();
|
||||
} else if (requestCode == REQUEST_INSTALL_ORBOT) {
|
||||
// try to start tor regardless of resultCode since clicking back after
|
||||
// installing the app does not necessarily return RESULT_OK
|
||||
App.configureTor(OrbotHelper.requestStartTor(activity));
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Update ONLY the summary of some preferences that don't fire in the onSharedPreferenceChanged or CAN'T be update via xml (%s)
|
||||
*
|
||||
* For example, the download_path use the startActivityForResult, firing the onStop of this fragment,
|
||||
* unregistering the listener (unregisterOnSharedPreferenceChangeListener)
|
||||
*/
|
||||
private void updatePreferencesSummary() {
|
||||
findPreference(DOWNLOAD_PATH_PREFERENCE).setSummary(defaultPreferences.getString(DOWNLOAD_PATH_PREFERENCE, getString(R.string.download_path_summary)));
|
||||
findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE).setSummary(defaultPreferences.getString(DOWNLOAD_PATH_AUDIO_PREFERENCE, getString(R.string.download_path_audio_summary)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
if (MainActivity.DEBUG) Log.d("TAG", "onSharedPreferenceChanged() called with: sharedPreferences = [" + sharedPreferences + "], key = [" + key + "]");
|
||||
String summary = null;
|
||||
|
||||
if (key.equals(USE_TOR_KEY)) {
|
||||
if (defaultPreferences.getBoolean(USE_TOR_KEY, false)) {
|
||||
if (OrbotHelper.isOrbotInstalled(activity)) {
|
||||
App.configureTor(true);
|
||||
OrbotHelper.requestStartTor(activity);
|
||||
} else {
|
||||
Intent intent = OrbotHelper.getOrbotInstallIntent(activity);
|
||||
startActivityForResult(intent, REQUEST_INSTALL_ORBOT);
|
||||
}
|
||||
} else App.configureTor(false);
|
||||
return;
|
||||
} else if (key.equals(THEME)) {
|
||||
summary = sharedPreferences.getString(THEME, getString(R.string.default_theme_value));
|
||||
if (!summary.equals(currentTheme)) { // If it's not the current theme
|
||||
getActivity().recreate();
|
||||
}
|
||||
|
||||
defaultPreferences.edit().putBoolean(Constants.KEY_THEME_CHANGE, true).apply();
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(summary)) findPreference(key).setSummary(summary);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.schabi.newpipe.settings;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
public class VideoAudioSettingsFragment extends BasePreferenceFragment {
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
addPreferencesFromResource(R.xml.video_audio_settings);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user