mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2024-12-23 16:40:32 +00:00
Merged the latest changes
This commit is contained in:
commit
d2aaa6f691
38
.github/CONTRIBUTING.md
vendored
38
.github/CONTRIBUTING.md
vendored
@ -12,57 +12,53 @@ add a comment to it. You'll see exactly what is sent, the system is 100% transpa
|
|||||||
## Issue reporting/feature requests
|
## Issue reporting/feature requests
|
||||||
|
|
||||||
* Search the [existing issues](https://github.com/TeamNewPipe/NewPipe/issues) first to make sure your issue/feature
|
* Search the [existing issues](https://github.com/TeamNewPipe/NewPipe/issues) first to make sure your issue/feature
|
||||||
hasn't been reported/requested before
|
hasn't been reported/requested before.
|
||||||
* Check whether your issue/feature is already fixed/implemented
|
* Check whether your issue/feature is already fixed/implemented.
|
||||||
* Check if the issue still exists in the latest release/beta version
|
* Check if the issue still exists in the latest release/beta version.
|
||||||
* If you are an Android/Java developer, you are always welcome to fix/implement an issue/a feature yourself. PRs welcome!
|
* If you are an Android/Java developer, you are always welcome to fix an issue or implement a feature yourself. PRs welcome!
|
||||||
* We use English for development. Issues in other languages will be closed and ignored.
|
* We use English for development. Issues in other languages will be closed and ignored.
|
||||||
* Please only add *one* issue at a time. Do not put multiple issues into one thread.
|
* Please only add *one* issue at a time. Do not put multiple issues into one thread.
|
||||||
* When reporting a bug please give us a context, and a description how to reproduce it.
|
* Follow the template! Issues or feature requests not matching the template might be closed.
|
||||||
* Issues that only contain a generated bug report, but no description might be closed.
|
|
||||||
|
|
||||||
## Bug Fixing
|
## 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
|
* 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,
|
<a href="mailto:tnp@newpipe.schabi.org">tnp@newpipe.schabi.org</a> to let us know that you intend to help. We'll send you further instructions. You may, on request,
|
||||||
register at our [Sentry](https://sentry.schabi.org) instance (see section "Crash reporting" for more information.
|
register at our [Sentry](https://sentry.schabi.org) instance (see section "Crash reporting" for more information).
|
||||||
|
|
||||||
## Translation
|
## Translation
|
||||||
|
|
||||||
* NewPipe can be translated via [Weblate](https://hosted.weblate.org/projects/newpipe/strings/). You can log in there
|
* NewPipe is translated via [Weblate](https://hosted.weblate.org/projects/newpipe/strings/). You can log in there
|
||||||
with your GitHub account.
|
with your GitHub account.
|
||||||
|
* If the language you want to translate is not on Weblate, you can add it: see [How to add a new language](https://github.com/TeamNewPipe/NewPipe/wiki/How-to-add-a-new-language-to-NewPipe) in the wiki.
|
||||||
|
|
||||||
## Code contribution
|
## Code contribution
|
||||||
|
|
||||||
* Stick to NewPipe's style conventions (well, just look the other code and then do it the same way :))
|
* Stick to NewPipe's style conventions: follow [checkStyle](https://github.com/checkstyle/checkstyle). It will run each time you build the project.
|
||||||
* Do not bring non-free software (e.g., binary blobs) into the project. Also, make sure you do not introduce Google
|
* Do not bring non-free software (e.g. binary blobs) into the project. Also, make sure you do not introduce Google
|
||||||
libraries.
|
libraries.
|
||||||
* Stick to [F-Droid contribution guidelines](https://f-droid.org/wiki/page/Inclusion_Policy)
|
* 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
|
* Make changes on a separate branch with a meaningful name, not on the master neither dev branch. This is commonly known as *feature branch workflow*. You
|
||||||
may then send your changes as a pull request on GitHub. Patches to the email address mentioned in this document might
|
may then send your changes as a pull request (PR) on GitHub.
|
||||||
not be considered, GitHub is the primary platform. (This only affects you if you are a member of TeamNewPipe)
|
|
||||||
* When submitting changes, you confirm that your code is licensed under the terms of the
|
* 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).
|
[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
|
* 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!
|
description. Untested code will **not** be merged!
|
||||||
* Try to figure out yourself why builds on our CI fail.
|
* 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,
|
* Make sure your PR is up-to-date with the rest of the code. Often, a simple click on "Update branch" will do the job,
|
||||||
but if not, you are asked to merge the master branch manually and resolve the problems on your own. That will make the
|
but if not, you are asked to rebase the dev branch manually and resolve the problems on your own. You can find help [on the wiki](https://github.com/TeamNewPipe/NewPipe/wiki/How-to-merge-a-PR). That will make the
|
||||||
maintainers' jobs way easier.
|
maintainers' jobs way easier.
|
||||||
* Please show intention to maintain your features and code after you contributed it. Unmaintained code is a hassle for
|
* 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
|
the core developers, and just adds work. If you do not intend to maintain features you contributed, please think again
|
||||||
about submission, or clearly state that in the description of your PR.
|
about submission, or clearly state that in the description of your PR.
|
||||||
* Respond yourselves if someone requests changes or otherwise raises issues about your PRs.
|
* Respond yourselves if someone requests changes or otherwise raises issues about your PRs.
|
||||||
* Check if your contributions align with the [fdroid inclusion guidelines](https://f-droid.org/en/docs/Inclusion_Policy/).
|
|
||||||
* Check if your submission can be build with the current fdroid build server setup.
|
|
||||||
* Send PR that only cover one specific issue/solution/bug. Do not send PRs that are huge and consists of multiple
|
* Send PR that only cover one specific issue/solution/bug. Do not send PRs that are huge and consists of multiple
|
||||||
independent solutions.
|
independent solutions.
|
||||||
|
|
||||||
## Communication
|
## Communication
|
||||||
|
|
||||||
* WE DO NOW HAVE A MAILING LIST: [newpipe@list.schabi.org](https://list.schabi.org/cgi-bin/mailman/listinfo/newpipe).
|
|
||||||
* There is an IRC channel on Freenode which is regularly visited by the core team and other developers:
|
* 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)!
|
[#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
|
* If you want to get in touch with the core team or one of our other contributors you can send an email to
|
||||||
tnp(at)schabi.org. Please do not send issue reports, they will be ignored and remain unanswered! Use the GitHub issue
|
<a href="mailto:tnp@newpipe.schabi.org">tnp@newpipe.schabi.org</a>. Please do not send issue reports, they will be ignored and remain unanswered! Use the GitHub issue
|
||||||
tracker described above!
|
tracker described above!
|
||||||
* Feel free to post suggestions, changes, ideas etc. on GitHub, IRC or the mailing list!
|
* Feel free to post suggestions, changes, ideas etc. on GitHub or IRC!
|
||||||
|
3
.github/ISSUE_TEMPLATE.md
vendored
3
.github/ISSUE_TEMPLATE.md
vendored
@ -1,3 +0,0 @@
|
|||||||
- [ ] I carefully read the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md) and agree to them.
|
|
||||||
- [ ] I checked if the issue/feature exists in the latest version.
|
|
||||||
- [ ] I did use the [incredible bugreport to markdown converter](https://teamnewpipe.github.io/CrashReportToMarkdown/) to paste bug reports.
|
|
46
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
46
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a bug report to help us improve
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Oh no, a bug! It happens. Thanks for reporting an issue with NewPipe. If this is your first bug report, read the following information before proceeding:
|
||||||
|
|
||||||
|
Please note, we only support the latest version of NewPipe. In order to check your app version, open the left drawer and click on "About". If you don't have the latest version, upgrade to it and reproduce the problem before opening the issue. The release page (https://github.com/TeamNewPipe/NewPipe/releases/latest) is where you can get it.
|
||||||
|
|
||||||
|
P.S.: Our contribution guidelines might be a nice document to read before you fill out the report :) You can find it at https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md
|
||||||
|
|
||||||
|
To make it easier for us to help you please enter detailed information in the template we have provided below. If a section isn't relevant, just delete it, though it would be helpful to still provide as much detail as possible.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- The comments between these brackets won't show up in the submitted issue (as you can see in the preview). -->
|
||||||
|
|
||||||
|
### Version
|
||||||
|
<!-- Which version are you using? Hopefully the latest! We just told you that above! -->
|
||||||
|
-
|
||||||
|
|
||||||
|
### Steps to reproduce the bug
|
||||||
|
<!--
|
||||||
|
1. Go to '...'
|
||||||
|
2. Press on '....'
|
||||||
|
3. Swipe down to '....'
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- If you can't cause the bug to show up again reliably (and hence don't have a proper set of steps to give us), please still try to give as many details as possible on how you think you encountered the bug. -->
|
||||||
|
|
||||||
|
### Expected behavior
|
||||||
|
<!-- Tell us what you expect to happen. -->
|
||||||
|
|
||||||
|
### Actual behaviour
|
||||||
|
<!-- Tell us what happens instead. -->
|
||||||
|
|
||||||
|
### Screenshots/Screen recordings
|
||||||
|
<!-- If applicable, add screenshots or a screen recording to help explain your problem. GitHub supports uploading them directly in the issue text box. If your file is too big for Github to accept, feel free to paste a link from an image/video hoster here instead. -->
|
||||||
|
|
||||||
|
### Logs
|
||||||
|
<!-- If your bug includes a crash (where you're shown the Error Report page with a bunch of info), copy it to the clipboard (there is a share button for this), head over to our bug report to markdown converter at https://teamnewpipe.github.io/CrashReportToMarkdown/ and paste it. Copy the converted text (it is MUCH easier to read this way) from there and paste it here: -->
|
||||||
|
|
||||||
|
<!-- That's right, here! -->
|
39
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
39
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
labels: enhancement
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
<!-- Hey. Our contribution guidelines (https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md) might be an appropriate
|
||||||
|
document to read before you fill out the request :) -->
|
||||||
|
|
||||||
|
<!-- The comments between these brackets won't show up in the submitted issue (as you can see in the preview). -->
|
||||||
|
|
||||||
|
#### Describe the feature you want
|
||||||
|
<!-- A clear and concise description of what you want to happen. PLEASE MAKE SURE it is one feature ONLY. You should open separate issues for separate feature requests, because those issues will be used to track their status.
|
||||||
|
Example: *I think it would be nice if you add feature Y which makes X possible.*
|
||||||
|
|
||||||
|
Optionally, also describe alternatives you've considered.
|
||||||
|
Example: *Z is also a good alternative. Not as good as Y, but at least...* or *I considered Z, but that didn't turn out to be a good idea because...* -->
|
||||||
|
|
||||||
|
<!-- Write below this -->
|
||||||
|
|
||||||
|
#### Is your feature request related to a problem? Please describe it
|
||||||
|
<!-- A clear and concise description of what the problem is. Maybe the developers could brainstorm and come up with a better solution to your problem. If they exist, link to related Issues and/or PRs for developers to keep track easier.
|
||||||
|
Example: *I want to do X, but there is no way to do it.* -->
|
||||||
|
|
||||||
|
<!-- Write below this -->
|
||||||
|
|
||||||
|
#### Additional context
|
||||||
|
<!-- Add any other context, like screenshots, about the feature request here.
|
||||||
|
Example: *Here's a photo of my cat!* -->
|
||||||
|
|
||||||
|
<!-- Write below this -->
|
||||||
|
|
||||||
|
#### How will you/everyone benefit from this feature?
|
||||||
|
<!-- Convince us! How does it change your NewPipe experience and/or your life?
|
||||||
|
The better this paragraph is, the more likely a developer will think about working on it.
|
||||||
|
Example: *This feature will help us colonize the galaxy! -->
|
||||||
|
|
||||||
|
<!-- Write below this -->
|
27
.github/PULL_REQUEST_TEMPLATE.md
vendored
27
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1 +1,28 @@
|
|||||||
|
<!-- Hey there. Thank you so much for improving NewPipe. Please take a moment to fill out the following suggestion on how to structure this PR description. Having roughly the same layout helps everyone considerably :)-->
|
||||||
|
|
||||||
|
#### What is it?
|
||||||
|
- [ ] Bug fix (user facing)
|
||||||
|
- [ ] Feature (user facing)
|
||||||
|
- [ ] Code base improvement (dev facing)
|
||||||
|
- [ ] Meta improvement to the project (dev facing)
|
||||||
|
|
||||||
|
#### Description of the changes in your PR
|
||||||
|
<!-- While bullet points are the norm in this section, feel free to write a text instead if you can't fit it in a list -->
|
||||||
|
- record videos
|
||||||
|
- create clones
|
||||||
|
- take over the world
|
||||||
|
|
||||||
|
#### Fixes the following issue(s)
|
||||||
|
<!-- Also add reddit or other links which are relevant to your change. -->
|
||||||
|
-
|
||||||
|
|
||||||
|
#### Relies on the following changes
|
||||||
|
<!-- Delete this if it doesn't apply to you. -->
|
||||||
|
-
|
||||||
|
|
||||||
|
#### Testing apk
|
||||||
|
<!-- Ensure that you have your changes on a new branch which has a meaningful name. This name will be used as a suffix for the app ID to allow installing and testing multiple versions of NewPipe. Do NOT name your branches like "patch-0" and "feature-1". For example, if your PR implements a bug fix for comments, an appropriate branch name would be "commentfix". -->
|
||||||
|
debug.zip
|
||||||
|
|
||||||
|
#### Agreement
|
||||||
- [ ] I carefully read the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md) and agree to them.
|
- [ ] I carefully read the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md) and agree to them.
|
||||||
|
8
.gitignore
vendored
8
.gitignore
vendored
@ -10,3 +10,11 @@
|
|||||||
*~
|
*~
|
||||||
.weblate
|
.weblate
|
||||||
*.class
|
*.class
|
||||||
|
|
||||||
|
# vscode / eclipse files
|
||||||
|
*.classpath
|
||||||
|
*.project
|
||||||
|
*.settings
|
||||||
|
bin/
|
||||||
|
.vscode/
|
||||||
|
*.code-workspace
|
||||||
|
@ -5,13 +5,13 @@ android:
|
|||||||
components:
|
components:
|
||||||
# The BuildTools version used by NewPipe
|
# The BuildTools version used by NewPipe
|
||||||
- tools
|
- tools
|
||||||
- build-tools-28.0.3
|
- build-tools-29.0.3
|
||||||
|
|
||||||
# The SDK version used to compile NewPipe
|
# The SDK version used to compile NewPipe
|
||||||
- android-28
|
- android-29
|
||||||
|
|
||||||
before_install:
|
before_install:
|
||||||
- yes | sdkmanager "platforms;android-28"
|
- yes | sdkmanager "platforms;android-29"
|
||||||
script: ./gradlew -Dorg.gradle.jvmargs=-Xmx1536m assembleDebug lintDebug testDebugUnitTest
|
script: ./gradlew -Dorg.gradle.jvmargs=-Xmx1536m assembleDebug lintDebug testDebugUnitTest
|
||||||
|
|
||||||
licenses:
|
licenses:
|
||||||
|
@ -4,16 +4,16 @@
|
|||||||
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://f-droid.org/wiki/images/0/06/F-Droid-button_get-it-on.png"></a></p>
|
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://f-droid.org/wiki/images/0/06/F-Droid-button_get-it-on.png"></a></p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/TeamNewPipe/NewPipe" alt="GitHub release"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a>
|
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub release"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a>
|
||||||
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="License: GPLv3"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg"></a>
|
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="License: GPLv3"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg"></a>
|
||||||
<a href="https://travis-ci.org/TeamNewPipe/NewPipe" alt="Build Status"><img src="https://travis-ci.org/TeamNewPipe/NewPipe.svg"></a>
|
<a href="https://travis-ci.org/TeamNewPipe/NewPipe" alt="Build Status"><img src="https://travis-ci.org/TeamNewPipe/NewPipe.svg"></a>
|
||||||
<a href="https://hosted.weblate.org/engage/NewPipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/NewPipe/-/svg-badge.svg"></a>
|
<a href="https://hosted.weblate.org/engage/newpipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
|
||||||
<a href="http://webchat.freenode.net/?channels=%23newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
|
<a href="http://webchat.freenode.net/?channels=%23newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
|
||||||
<a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
|
<a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
|
||||||
</p>
|
</p>
|
||||||
<hr>
|
<hr>
|
||||||
<p align="center"><a href="#screenshots">Screenshots</a> • <a href="#description">Description</a> • <a href="#features">Features</a> • <a href="#updates">Updates</a> • <a href="#contribution">Contribution</a> • <a href="#donate">Donate</a> • <a href="#license">License</a></p>
|
<p align="center"><a href="#screenshots">Screenshots</a> • <a href="#description">Description</a> • <a href="#features">Features</a> • <a href="#updates">Updates</a> • <a href="#contribution">Contribution</a> • <a href="#donate">Donate</a> • <a href="#license">License</a></p>
|
||||||
<p align="center"><a href="https://newpipe.schabi.org">Website</a> • <a href="https://newpipe.schabi.org/blog/">Blog</a> • <a href="https://newpipe.schabi.org/press/">Press</a></p>
|
<p align="center"><a href="https://newpipe.schabi.org">Website</a> • <a href="https://newpipe.schabi.org/blog/">Blog</a> • <a href="https://newpipe.schabi.org/FAQ/">FAQ</a> • <a href="https://newpipe.schabi.org/press/">Press</a></p>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<b>WARNING: THIS IS A BETA VERSION, THEREFORE YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE VIA OUR GITHUB REPOSITORY.</b>
|
<b>WARNING: THIS IS A BETA VERSION, THEREFORE YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE VIA OUR GITHUB REPOSITORY.</b>
|
||||||
|
2
app/.gitignore
vendored
2
app/.gitignore
vendored
@ -1,3 +1,3 @@
|
|||||||
.gitignore
|
.gitignore
|
||||||
/build
|
/build
|
||||||
app.iml
|
*.iml
|
||||||
|
225
app/build.gradle
225
app/build.gradle
@ -2,33 +2,57 @@ apply plugin: 'com.android.application'
|
|||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
apply plugin: 'kotlin-android-extensions'
|
apply plugin: 'kotlin-android-extensions'
|
||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
|
apply plugin: 'checkstyle'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 28
|
compileSdkVersion 29
|
||||||
buildToolsVersion '28.0.3'
|
buildToolsVersion '29.0.3'
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "org.schabi.newpipe"
|
applicationId "org.schabi.newpipe"
|
||||||
|
resValue "string", "app_name", "NewPipe"
|
||||||
minSdkVersion 19
|
minSdkVersion 19
|
||||||
targetSdkVersion 28
|
targetSdkVersion 29
|
||||||
versionCode 800
|
versionCode 950
|
||||||
versionName "0.18.0"
|
versionName "0.19.5"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
|
||||||
|
javaCompileOptions {
|
||||||
|
annotationProcessorOptions {
|
||||||
|
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
debug {
|
||||||
|
multiDexEnabled true
|
||||||
|
debuggable true
|
||||||
|
|
||||||
|
// suffix the app id and the app name with git branch name
|
||||||
|
def workingBranch = getGitWorkingBranch()
|
||||||
|
def normalizedWorkingBranch = workingBranch.replaceAll("[^A-Za-z]+", "").toLowerCase()
|
||||||
|
if (normalizedWorkingBranch.isEmpty() || workingBranch == "master" || workingBranch == "dev") {
|
||||||
|
// default values when branch name could not be determined or is master or dev
|
||||||
|
applicationIdSuffix ".debug"
|
||||||
|
resValue "string", "app_name", "NewPipe Debug"
|
||||||
|
} else {
|
||||||
|
applicationIdSuffix ".debug." + normalizedWorkingBranch
|
||||||
|
resValue "string", "app_name", "NewPipe " + workingBranch
|
||||||
|
archivesBaseName = 'NewPipe_' + normalizedWorkingBranch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep the release build type at the end of the list to override 'archivesBaseName' of
|
||||||
|
// debug build. This seems to be a Gradle bug, therefore
|
||||||
|
// TODO: update Gradle version
|
||||||
release {
|
release {
|
||||||
minifyEnabled true
|
minifyEnabled true
|
||||||
shrinkResources true
|
shrinkResources true
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
}
|
archivesBaseName = 'app'
|
||||||
|
|
||||||
debug {
|
|
||||||
multiDexEnabled true
|
|
||||||
debuggable true
|
|
||||||
applicationIdSuffix ".debug"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,70 +66,161 @@ android {
|
|||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
encoding 'utf-8'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Required and used only by groupie
|
||||||
|
androidExtensions {
|
||||||
|
experimental = true
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
androidxLibVersion = '1.0.0'
|
icepickVersion = '3.2.0'
|
||||||
exoPlayerLibVersion = '2.10.8'
|
checkstyleVersion = '8.32'
|
||||||
roomDbLibVersion = '2.1.0'
|
stethoVersion = '1.5.1'
|
||||||
leakCanaryLibVersion = '1.5.4' //1.6.1
|
leakCanaryVersion = '2.2'
|
||||||
okHttpLibVersion = '3.12.6'
|
exoPlayerVersion = '2.11.6'
|
||||||
icepickLibVersion = '3.2.0'
|
androidxLifecycleVersion = '2.2.0'
|
||||||
stethoLibVersion = '1.5.0'
|
androidxRoomVersion = '2.2.5'
|
||||||
|
groupieVersion = '2.8.0'
|
||||||
|
markwonVersion = '4.3.1'
|
||||||
|
}
|
||||||
|
|
||||||
|
configurations {
|
||||||
|
checkstyle
|
||||||
|
ktlint
|
||||||
|
}
|
||||||
|
|
||||||
|
checkstyle {
|
||||||
|
configFile rootProject.file('checkstyle.xml')
|
||||||
|
ignoreFailures false
|
||||||
|
showViolations true
|
||||||
|
toolVersion = checkstyleVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
task runCheckstyle(type: Checkstyle) {
|
||||||
|
source 'src'
|
||||||
|
include '**/*.java'
|
||||||
|
exclude '**/gen/**'
|
||||||
|
exclude '**/R.java'
|
||||||
|
exclude '**/BuildConfig.java'
|
||||||
|
exclude 'main/java/us/shandian/giga/**'
|
||||||
|
|
||||||
|
classpath = configurations.checkstyle
|
||||||
|
|
||||||
|
showViolations true
|
||||||
|
|
||||||
|
reports {
|
||||||
|
xml.enabled true
|
||||||
|
html.enabled true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task runKtlint(type: JavaExec) {
|
||||||
|
main = "com.pinterest.ktlint.Main"
|
||||||
|
classpath = configurations.ktlint
|
||||||
|
args "src/**/*.kt"
|
||||||
|
}
|
||||||
|
|
||||||
|
task formatKtlint(type: JavaExec) {
|
||||||
|
main = "com.pinterest.ktlint.Main"
|
||||||
|
classpath = configurations.ktlint
|
||||||
|
args "-F", "src/**/*.kt"
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEvaluate {
|
||||||
|
preDebugBuild.dependsOn runCheckstyle, runKtlint
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
|
|
||||||
androidTestImplementation('androidx.test.espresso:espresso-core:3.1.0', {
|
implementation "frankiesardo:icepick:${icepickVersion}"
|
||||||
|
kapt "frankiesardo:icepick-processor:${icepickVersion}"
|
||||||
|
|
||||||
|
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
|
||||||
|
ktlint "com.pinterest:ktlint:0.35.0"
|
||||||
|
|
||||||
|
debugImplementation "com.facebook.stetho:stetho:${stethoVersion}"
|
||||||
|
debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoVersion}"
|
||||||
|
|
||||||
|
debugImplementation "com.squareup.leakcanary:leakcanary-android:${leakCanaryVersion}"
|
||||||
|
implementation "com.squareup.leakcanary:leakcanary-object-watcher-android:${leakCanaryVersion}"
|
||||||
|
|
||||||
|
debugImplementation "androidx.multidex:multidex:2.0.1"
|
||||||
|
|
||||||
|
testImplementation 'junit:junit:4.13'
|
||||||
|
testImplementation 'org.mockito:mockito-core:3.3.3'
|
||||||
|
|
||||||
|
androidTestImplementation "androidx.test.ext:junit:1.1.1"
|
||||||
|
androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}"
|
||||||
|
androidTestImplementation "androidx.test.espresso:espresso-core:3.2.0", {
|
||||||
exclude module: 'support-annotations'
|
exclude module: 'support-annotations'
|
||||||
})
|
}
|
||||||
|
|
||||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:ff61e284'
|
implementation 'com.github.TeamNewPipe:NewPipeExtractor:a70cb0283ffc3bba2709815673a5a7940aab0a3a'
|
||||||
testImplementation 'junit:junit:4.12'
|
|
||||||
testImplementation 'org.mockito:mockito-core:2.23.0'
|
|
||||||
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
implementation "com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751"
|
||||||
implementation "androidx.legacy:legacy-support-v4:${androidxLibVersion}"
|
implementation "org.jsoup:jsoup:1.13.1"
|
||||||
implementation "com.google.android.material:material:${androidxLibVersion}"
|
|
||||||
implementation "androidx.recyclerview:recyclerview:${androidxLibVersion}"
|
|
||||||
implementation "androidx.legacy:legacy-preference-v14:${androidxLibVersion}"
|
|
||||||
implementation "androidx.cardview:cardview:${androidxLibVersion}"
|
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
|
||||||
|
|
||||||
// Originally in NewPipeExtractor
|
implementation "com.squareup.okhttp3:okhttp:3.12.11"
|
||||||
implementation 'com.grack:nanojson:1.1'
|
|
||||||
implementation 'org.jsoup:jsoup:1.9.2'
|
|
||||||
|
|
||||||
implementation 'ch.acra:acra:4.9.2' //4.11
|
implementation "com.google.android.exoplayer:exoplayer:${exoPlayerVersion}"
|
||||||
|
implementation "com.google.android.exoplayer:extension-mediasession:${exoPlayerVersion}"
|
||||||
|
|
||||||
implementation 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'
|
implementation "com.google.android.material:material:1.1.0"
|
||||||
implementation 'de.hdodenhof:circleimageview:2.2.0'
|
|
||||||
implementation 'com.nononsenseapps:filepicker:4.2.1'
|
|
||||||
|
|
||||||
implementation "com.google.android.exoplayer:exoplayer:${exoPlayerLibVersion}"
|
implementation "androidx.appcompat:appcompat:1.1.0"
|
||||||
implementation "com.google.android.exoplayer:extension-mediasession:${exoPlayerLibVersion}"
|
implementation "androidx.preference:preference:1.1.1"
|
||||||
|
implementation "androidx.recyclerview:recyclerview:1.1.0"
|
||||||
|
implementation "androidx.cardview:cardview:1.0.0"
|
||||||
|
implementation "androidx.constraintlayout:constraintlayout:1.1.3"
|
||||||
|
|
||||||
debugImplementation "com.facebook.stetho:stetho:${stethoLibVersion}"
|
implementation "androidx.lifecycle:lifecycle-livedata:${androidxLifecycleVersion}"
|
||||||
debugImplementation "com.facebook.stetho:stetho-urlconnection:${stethoLibVersion}"
|
implementation "androidx.lifecycle:lifecycle-viewmodel:${androidxLifecycleVersion}"
|
||||||
debugImplementation 'androidx.multidex:multidex:2.0.1'
|
implementation "androidx.lifecycle:lifecycle-extensions:${androidxLifecycleVersion}"
|
||||||
|
|
||||||
implementation 'io.reactivex.rxjava2:rxjava:2.2.2'
|
implementation "androidx.room:room-runtime:${androidxRoomVersion}"
|
||||||
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
|
implementation "androidx.room:room-rxjava2:${androidxRoomVersion}"
|
||||||
implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1'
|
kapt "androidx.room:room-compiler:${androidxRoomVersion}"
|
||||||
implementation 'org.ocpsoft.prettytime:prettytime:4.0.1.Final'
|
|
||||||
|
|
||||||
implementation "androidx.room:room-runtime:${roomDbLibVersion}"
|
implementation "com.xwray:groupie:${groupieVersion}"
|
||||||
implementation "androidx.room:room-rxjava2:${roomDbLibVersion}"
|
implementation "com.xwray:groupie-kotlin-android-extensions:${groupieVersion}"
|
||||||
kapt "androidx.room:room-compiler:${roomDbLibVersion}"
|
|
||||||
|
|
||||||
implementation "frankiesardo:icepick:${icepickLibVersion}"
|
implementation "de.hdodenhof:circleimageview:3.1.0"
|
||||||
kapt "frankiesardo:icepick-processor:${icepickLibVersion}"
|
implementation "com.nostra13.universalimageloader:universal-image-loader:1.9.5"
|
||||||
|
|
||||||
debugImplementation "com.squareup.leakcanary:leakcanary-android:${leakCanaryLibVersion}"
|
implementation "io.noties.markwon:core:${markwonVersion}"
|
||||||
releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:${leakCanaryLibVersion}"
|
implementation "io.noties.markwon:linkify:${markwonVersion}"
|
||||||
|
|
||||||
implementation "com.squareup.okhttp3:okhttp:${okHttpLibVersion}"
|
implementation "com.nononsenseapps:filepicker:4.2.1"
|
||||||
debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoLibVersion}"
|
|
||||||
|
implementation "ch.acra:acra-core:5.5.0"
|
||||||
|
|
||||||
|
implementation "io.reactivex.rxjava2:rxjava:2.2.19"
|
||||||
|
implementation "io.reactivex.rxjava2:rxandroid:2.1.1"
|
||||||
|
implementation "com.jakewharton.rxbinding2:rxbinding:2.2.0"
|
||||||
|
|
||||||
|
implementation "org.ocpsoft.prettytime:prettytime:4.0.5.Final"
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getGitWorkingBranch() {
|
||||||
|
try {
|
||||||
|
def gitProcess = "git rev-parse --abbrev-ref HEAD".execute()
|
||||||
|
gitProcess.waitFor()
|
||||||
|
if (gitProcess.exitValue() == 0) {
|
||||||
|
return gitProcess.text.trim()
|
||||||
|
} else {
|
||||||
|
// not a git repository
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
// git was not found
|
||||||
|
return ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
479
app/schemas/org.schabi.newpipe.database.AppDatabase/2.json
Normal file
479
app/schemas/org.schabi.newpipe.database.AppDatabase/2.json
Normal file
@ -0,0 +1,479 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 2,
|
||||||
|
"identityHash": "b7856223e2595ddf20a3ce6243ce9527",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "subscriptions",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `name` TEXT, `avatar_url` TEXT, `subscriber_count` INTEGER, `description` TEXT)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "avatarUrl",
|
||||||
|
"columnName": "avatar_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriberCount",
|
||||||
|
"columnName": "subscriber_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "description",
|
||||||
|
"columnName": "description",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_subscriptions_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX `index_subscriptions_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "search_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "creationDate",
|
||||||
|
"columnName": "creation_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "search",
|
||||||
|
"columnName": "search",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_search_history_search",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"search"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX `index_search_history_search` ON `${TABLE_NAME}` (`search`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "streams",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, `stream_type` TEXT, `duration` INTEGER, `uploader` TEXT, `thumbnail_url` TEXT)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamType",
|
||||||
|
"columnName": "stream_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "duration",
|
||||||
|
"columnName": "duration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_streams_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX `index_streams_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, `repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accessDate",
|
||||||
|
"columnName": "access_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "repeatCount",
|
||||||
|
"columnName": "repeat_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"access_date"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_stream_history_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX `index_stream_history_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_state",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "progressTime",
|
||||||
|
"columnName": "progress_time",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `thumbnail_url` TEXT)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_playlists_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX `index_playlists_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlist_stream_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "playlistUid",
|
||||||
|
"columnName": "playlist_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "index",
|
||||||
|
"columnName": "join_index",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_playlist_id_join_index",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX `index_playlist_stream_join_playlist_id_join_index` ON `${TABLE_NAME}` (`playlist_id`, `join_index`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX `index_playlist_stream_join_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "playlists",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"playlist_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "remote_playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamCount",
|
||||||
|
"columnName": "stream_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_remote_playlists_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX `index_remote_playlists_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_remote_playlists_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"b7856223e2595ddf20a3ce6243ce9527\")"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
707
app/schemas/org.schabi.newpipe.database.AppDatabase/3.json
Normal file
707
app/schemas/org.schabi.newpipe.database.AppDatabase/3.json
Normal file
@ -0,0 +1,707 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 3,
|
||||||
|
"identityHash": "9f825b1ee281480bedd38b971feac327",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "subscriptions",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `name` TEXT, `avatar_url` TEXT, `subscriber_count` INTEGER, `description` TEXT)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "avatarUrl",
|
||||||
|
"columnName": "avatar_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriberCount",
|
||||||
|
"columnName": "subscriber_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "description",
|
||||||
|
"columnName": "description",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_subscriptions_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX `index_subscriptions_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "search_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "creationDate",
|
||||||
|
"columnName": "creation_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "search",
|
||||||
|
"columnName": "search",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_search_history_search",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"search"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX `index_search_history_search` ON `${TABLE_NAME}` (`search`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "streams",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `stream_type` TEXT NOT NULL, `duration` INTEGER NOT NULL, `uploader` TEXT NOT NULL, `thumbnail_url` TEXT, `view_count` INTEGER, `textual_upload_date` TEXT, `upload_date` INTEGER, `is_upload_date_approximation` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamType",
|
||||||
|
"columnName": "stream_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "duration",
|
||||||
|
"columnName": "duration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "viewCount",
|
||||||
|
"columnName": "view_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "textualUploadDate",
|
||||||
|
"columnName": "textual_upload_date",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploadDate",
|
||||||
|
"columnName": "upload_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isUploadDateApproximation",
|
||||||
|
"columnName": "is_upload_date_approximation",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_streams_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX `index_streams_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, `repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accessDate",
|
||||||
|
"columnName": "access_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "repeatCount",
|
||||||
|
"columnName": "repeat_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"access_date"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_stream_history_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX `index_stream_history_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_state",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "progressTime",
|
||||||
|
"columnName": "progress_time",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `thumbnail_url` TEXT)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_playlists_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX `index_playlists_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlist_stream_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "playlistUid",
|
||||||
|
"columnName": "playlist_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "index",
|
||||||
|
"columnName": "join_index",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_playlist_id_join_index",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX `index_playlist_stream_join_playlist_id_join_index` ON `${TABLE_NAME}` (`playlist_id`, `join_index`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX `index_playlist_stream_join_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "playlists",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"playlist_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "remote_playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamCount",
|
||||||
|
"columnName": "stream_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_remote_playlists_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX `index_remote_playlists_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_remote_playlists_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `subscription_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamId",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX `index_feed_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon_id` INTEGER NOT NULL, `sort_order` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "icon",
|
||||||
|
"columnName": "icon_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sortOrder",
|
||||||
|
"columnName": "sort_order",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_sort_order",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"sort_order"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX `index_feed_group_sort_order` ON `${TABLE_NAME}` (`sort_order`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group_subscription_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`group_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`group_id`, `subscription_id`), FOREIGN KEY(`group_id`) REFERENCES `feed_group`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "feedGroupId",
|
||||||
|
"columnName": "group_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"group_id",
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_subscription_join_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX `index_feed_group_subscription_join_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "feed_group",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"group_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_last_updated",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`subscription_id` INTEGER NOT NULL, `last_updated` INTEGER, PRIMARY KEY(`subscription_id`), FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastUpdated",
|
||||||
|
"columnName": "last_updated",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9f825b1ee281480bedd38b971feac327')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,119 @@
|
|||||||
|
package org.schabi.newpipe.database
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.database.sqlite.SQLiteDatabase
|
||||||
|
import androidx.room.Room
|
||||||
|
import androidx.room.testing.MigrationTestHelper
|
||||||
|
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamType
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class AppDatabaseTest {
|
||||||
|
companion object {
|
||||||
|
private const val DEFAULT_SERVICE_ID = 0
|
||||||
|
private const val DEFAULT_URL = "https://www.youtube.com/watch?v=cDphUib5iG4"
|
||||||
|
private const val DEFAULT_TITLE = "Test Title"
|
||||||
|
private val DEFAULT_TYPE = StreamType.VIDEO_STREAM
|
||||||
|
private const val DEFAULT_DURATION = 480L
|
||||||
|
private const val DEFAULT_UPLOADER_NAME = "Uploader Test"
|
||||||
|
private const val DEFAULT_THUMBNAIL = "https://example.com/example.jpg"
|
||||||
|
|
||||||
|
private const val DEFAULT_SECOND_SERVICE_ID = 0
|
||||||
|
private const val DEFAULT_SECOND_URL = "https://www.youtube.com/watch?v=ncQU6iBn5Fc"
|
||||||
|
}
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val testHelper = MigrationTestHelper(InstrumentationRegistry.getInstrumentation(),
|
||||||
|
AppDatabase::class.java.canonicalName, FrameworkSQLiteOpenHelperFactory())
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun migrateDatabaseFrom2to3() {
|
||||||
|
val databaseInV2 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_2)
|
||||||
|
|
||||||
|
databaseInV2.run {
|
||||||
|
insert("streams", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply {
|
||||||
|
// put("uid", null)
|
||||||
|
put("service_id", DEFAULT_SERVICE_ID)
|
||||||
|
put("url", DEFAULT_URL)
|
||||||
|
put("title", DEFAULT_TITLE)
|
||||||
|
put("stream_type", DEFAULT_TYPE.name)
|
||||||
|
put("duration", DEFAULT_DURATION)
|
||||||
|
put("uploader", DEFAULT_UPLOADER_NAME)
|
||||||
|
put("thumbnail_url", DEFAULT_THUMBNAIL)
|
||||||
|
})
|
||||||
|
insert("streams", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply {
|
||||||
|
// put("uid", null)
|
||||||
|
put("service_id", DEFAULT_SECOND_SERVICE_ID)
|
||||||
|
put("url", DEFAULT_SECOND_URL)
|
||||||
|
// put("title", null)
|
||||||
|
// put("stream_type", null)
|
||||||
|
// put("duration", null)
|
||||||
|
// put("uploader", null)
|
||||||
|
// put("thumbnail_url", null)
|
||||||
|
})
|
||||||
|
insert("streams", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply {
|
||||||
|
// put("uid", null)
|
||||||
|
put("service_id", DEFAULT_SERVICE_ID)
|
||||||
|
// put("url", null)
|
||||||
|
// put("title", null)
|
||||||
|
// put("stream_type", null)
|
||||||
|
// put("duration", null)
|
||||||
|
// put("uploader", null)
|
||||||
|
// put("thumbnail_url", null)
|
||||||
|
})
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
testHelper.runMigrationsAndValidate(AppDatabase.DATABASE_NAME, Migrations.DB_VER_3,
|
||||||
|
true, Migrations.MIGRATION_2_3)
|
||||||
|
|
||||||
|
val migratedDatabaseV3 = getMigratedDatabase()
|
||||||
|
val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst()
|
||||||
|
|
||||||
|
// Only expect 2, the one with the null url will be ignored
|
||||||
|
assertEquals(2, listFromDB.size)
|
||||||
|
|
||||||
|
val streamFromMigratedDatabase = listFromDB[0]
|
||||||
|
assertEquals(DEFAULT_SERVICE_ID, streamFromMigratedDatabase.serviceId)
|
||||||
|
assertEquals(DEFAULT_URL, streamFromMigratedDatabase.url)
|
||||||
|
assertEquals(DEFAULT_TITLE, streamFromMigratedDatabase.title)
|
||||||
|
assertEquals(DEFAULT_TYPE, streamFromMigratedDatabase.streamType)
|
||||||
|
assertEquals(DEFAULT_DURATION, streamFromMigratedDatabase.duration)
|
||||||
|
assertEquals(DEFAULT_UPLOADER_NAME, streamFromMigratedDatabase.uploader)
|
||||||
|
assertEquals(DEFAULT_THUMBNAIL, streamFromMigratedDatabase.thumbnailUrl)
|
||||||
|
assertNull(streamFromMigratedDatabase.viewCount)
|
||||||
|
assertNull(streamFromMigratedDatabase.textualUploadDate)
|
||||||
|
assertNull(streamFromMigratedDatabase.uploadDate)
|
||||||
|
assertNull(streamFromMigratedDatabase.isUploadDateApproximation)
|
||||||
|
|
||||||
|
val secondStreamFromMigratedDatabase = listFromDB[1]
|
||||||
|
assertEquals(DEFAULT_SECOND_SERVICE_ID, secondStreamFromMigratedDatabase.serviceId)
|
||||||
|
assertEquals(DEFAULT_SECOND_URL, secondStreamFromMigratedDatabase.url)
|
||||||
|
assertEquals("", secondStreamFromMigratedDatabase.title)
|
||||||
|
// Should fallback to VIDEO_STREAM
|
||||||
|
assertEquals(StreamType.VIDEO_STREAM, secondStreamFromMigratedDatabase.streamType)
|
||||||
|
assertEquals(0, secondStreamFromMigratedDatabase.duration)
|
||||||
|
assertEquals("", secondStreamFromMigratedDatabase.uploader)
|
||||||
|
assertEquals("", secondStreamFromMigratedDatabase.thumbnailUrl)
|
||||||
|
assertNull(secondStreamFromMigratedDatabase.viewCount)
|
||||||
|
assertNull(secondStreamFromMigratedDatabase.textualUploadDate)
|
||||||
|
assertNull(secondStreamFromMigratedDatabase.uploadDate)
|
||||||
|
assertNull(secondStreamFromMigratedDatabase.isUploadDateApproximation)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMigratedDatabase(): AppDatabase {
|
||||||
|
val database: AppDatabase = Room.databaseBuilder(ApplicationProvider.getApplicationContext(),
|
||||||
|
AppDatabase::class.java, AppDatabase.DATABASE_NAME)
|
||||||
|
.build()
|
||||||
|
testHelper.closeWhenFinished(database)
|
||||||
|
return database
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,10 @@
|
|||||||
package org.schabi.newpipe.report;
|
package org.schabi.newpipe.report;
|
||||||
|
|
||||||
import android.os.Parcel;
|
import android.os.Parcel;
|
||||||
import androidx.test.filters.LargeTest;
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|
||||||
|
|
||||||
|
import androidx.test.filters.LargeTest;
|
||||||
|
|
||||||
|
import androidx.test.runner.AndroidJUnit4;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
@ -12,15 +13,16 @@ import org.schabi.newpipe.report.ErrorActivity.ErrorInfo;
|
|||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instrumented tests for {@link ErrorInfo}
|
* Instrumented tests for {@link ErrorInfo}.
|
||||||
*/
|
*/
|
||||||
@RunWith(AndroidJUnit4.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
@LargeTest
|
@LargeTest
|
||||||
public class ErrorInfoTest {
|
public class ErrorInfoTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void errorInfo_testParcelable() {
|
public void errorInfoTestParcelable() {
|
||||||
ErrorInfo info = ErrorInfo.make(UserAction.USER_REPORT, "youtube", "request", R.string.general_error);
|
ErrorInfo info = ErrorInfo.make(UserAction.USER_REPORT, "youtube", "request",
|
||||||
|
R.string.general_error);
|
||||||
// Obtain a Parcel object and write the parcelable object to it:
|
// Obtain a Parcel object and write the parcelable object to it:
|
||||||
Parcel parcel = Parcel.obtain();
|
Parcel parcel = Parcel.obtain();
|
||||||
info.writeToParcel(parcel, 0);
|
info.writeToParcel(parcel, 0);
|
||||||
|
@ -6,12 +6,5 @@
|
|||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".DebugApp"
|
android:name=".DebugApp"
|
||||||
android:label="NewPipe Debug"
|
tools:replace="android:name" />
|
||||||
tools:replace="android:name, android:label">
|
|
||||||
<activity
|
|
||||||
android:name=".MainActivity"
|
|
||||||
android:label="NewPipe Debug"
|
|
||||||
tools:replace="android:label"/>
|
|
||||||
</application>
|
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
@ -1,104 +0,0 @@
|
|||||||
package org.schabi.newpipe;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.multidex.MultiDex;
|
|
||||||
|
|
||||||
import com.facebook.stetho.Stetho;
|
|
||||||
import com.facebook.stetho.okhttp3.StethoInterceptor;
|
|
||||||
import com.squareup.leakcanary.AndroidHeapDumper;
|
|
||||||
import com.squareup.leakcanary.DefaultLeakDirectoryProvider;
|
|
||||||
import com.squareup.leakcanary.HeapDumper;
|
|
||||||
import com.squareup.leakcanary.LeakCanary;
|
|
||||||
import com.squareup.leakcanary.LeakDirectoryProvider;
|
|
||||||
import com.squareup.leakcanary.RefWatcher;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import okhttp3.OkHttpClient;
|
|
||||||
|
|
||||||
public class DebugApp extends App {
|
|
||||||
private static final String TAG = DebugApp.class.toString();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void attachBaseContext(Context base) {
|
|
||||||
super.attachBaseContext(base);
|
|
||||||
MultiDex.install(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate() {
|
|
||||||
super.onCreate();
|
|
||||||
initStetho();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Downloader getDownloader() {
|
|
||||||
return DownloaderImpl.init(new OkHttpClient.Builder()
|
|
||||||
.addNetworkInterceptor(new StethoInterceptor()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initStetho() {
|
|
||||||
// Create an InitializerBuilder
|
|
||||||
Stetho.InitializerBuilder initializerBuilder =
|
|
||||||
Stetho.newInitializerBuilder(this);
|
|
||||||
|
|
||||||
// Enable Chrome DevTools
|
|
||||||
initializerBuilder.enableWebKitInspector(
|
|
||||||
Stetho.defaultInspectorModulesProvider(this)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Enable command line interface
|
|
||||||
initializerBuilder.enableDumpapp(
|
|
||||||
Stetho.defaultDumperPluginsProvider(getApplicationContext())
|
|
||||||
);
|
|
||||||
|
|
||||||
// Use the InitializerBuilder to generate an Initializer
|
|
||||||
Stetho.Initializer initializer = initializerBuilder.build();
|
|
||||||
|
|
||||||
// Initialize Stetho with the Initializer
|
|
||||||
Stetho.initialize(initializer);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean isDisposedRxExceptionsReported() {
|
|
||||||
return PreferenceManager.getDefaultSharedPreferences(this)
|
|
||||||
.getBoolean(getString(R.string.allow_disposed_exceptions_key), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected RefWatcher installLeakCanary() {
|
|
||||||
return LeakCanary.refWatcher(this)
|
|
||||||
.heapDumper(new ToggleableHeapDumper(this))
|
|
||||||
// give each object 10 seconds to be gc'ed, before leak canary gets nosy on it
|
|
||||||
.watchDelay(10, TimeUnit.SECONDS)
|
|
||||||
.buildAndInstall();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class ToggleableHeapDumper implements HeapDumper {
|
|
||||||
private final HeapDumper dumper;
|
|
||||||
private final SharedPreferences preferences;
|
|
||||||
private final String dumpingAllowanceKey;
|
|
||||||
|
|
||||||
ToggleableHeapDumper(@NonNull final Context context) {
|
|
||||||
LeakDirectoryProvider leakDirectoryProvider = new DefaultLeakDirectoryProvider(context);
|
|
||||||
this.dumper = new AndroidHeapDumper(context, leakDirectoryProvider);
|
|
||||||
this.preferences = PreferenceManager.getDefaultSharedPreferences(context);
|
|
||||||
this.dumpingAllowanceKey = context.getString(R.string.allow_heap_dumping_key);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isDumpingAllowed() {
|
|
||||||
return preferences.getBoolean(dumpingAllowanceKey, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public File dumpHeap() {
|
|
||||||
return isDumpingAllowed() ? dumper.dumpHeap() : HeapDumper.RETRY_LATER;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
59
app/src/debug/java/org/schabi/newpipe/DebugApp.kt
Normal file
59
app/src/debug/java/org/schabi/newpipe/DebugApp.kt
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package org.schabi.newpipe
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.multidex.MultiDex
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import com.facebook.stetho.Stetho
|
||||||
|
import com.facebook.stetho.okhttp3.StethoInterceptor
|
||||||
|
import leakcanary.AppWatcher
|
||||||
|
import leakcanary.LeakCanary
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import org.schabi.newpipe.extractor.downloader.Downloader
|
||||||
|
|
||||||
|
class DebugApp : App() {
|
||||||
|
override fun attachBaseContext(base: Context) {
|
||||||
|
super.attachBaseContext(base)
|
||||||
|
MultiDex.install(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
initStetho()
|
||||||
|
|
||||||
|
// Give each object 10 seconds to be GC'ed, before LeakCanary gets nosy on it
|
||||||
|
AppWatcher.config = AppWatcher.config.copy(watchDurationMillis = 10000)
|
||||||
|
LeakCanary.config = LeakCanary.config.copy(dumpHeap = PreferenceManager
|
||||||
|
.getDefaultSharedPreferences(this).getBoolean(getString(
|
||||||
|
R.string.allow_heap_dumping_key), false))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDownloader(): Downloader {
|
||||||
|
val downloader = DownloaderImpl.init(OkHttpClient.Builder()
|
||||||
|
.addNetworkInterceptor(StethoInterceptor()))
|
||||||
|
setCookiesToDownloader(downloader)
|
||||||
|
return downloader
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initStetho() {
|
||||||
|
// Create an InitializerBuilder
|
||||||
|
val initializerBuilder = Stetho.newInitializerBuilder(this)
|
||||||
|
|
||||||
|
// Enable Chrome DevTools
|
||||||
|
initializerBuilder.enableWebKitInspector(Stetho.defaultInspectorModulesProvider(this))
|
||||||
|
|
||||||
|
// Enable command line interface
|
||||||
|
initializerBuilder.enableDumpapp(
|
||||||
|
Stetho.defaultDumperPluginsProvider(applicationContext))
|
||||||
|
|
||||||
|
// Use the InitializerBuilder to generate an Initializer
|
||||||
|
val initializer = initializerBuilder.build()
|
||||||
|
|
||||||
|
// Initialize Stetho with the Initializer
|
||||||
|
Stetho.initialize(initializer)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isDisposedRxExceptionsReported(): Boolean {
|
||||||
|
return PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
.getBoolean(getString(R.string.allow_disposed_exceptions_key), false)
|
||||||
|
}
|
||||||
|
}
|
@ -1,22 +1,27 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="org.schabi.newpipe">
|
package="org.schabi.newpipe">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.touchscreen"
|
||||||
|
android:required="false" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".App"
|
android:name=".App"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
android:banner="@mipmap/newpipe_tv_banner"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:logo="@mipmap/ic_launcher"
|
android:logo="@mipmap/ic_launcher"
|
||||||
|
android:requestLegacyExternalStorage="true"
|
||||||
android:theme="@style/OpeningTheme"
|
android:theme="@style/OpeningTheme"
|
||||||
tools:ignore="AllowBackup">
|
tools:ignore="AllowBackup">
|
||||||
<activity
|
<activity
|
||||||
@ -24,12 +29,14 @@
|
|||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:launchMode="singleTask">
|
android:launchMode="singleTask">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<receiver android:name="androidx.media.session.MediaButtonReceiver" >
|
<receiver android:name="androidx.media.session.MediaButtonReceiver">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
@ -50,34 +57,36 @@
|
|||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".player.BackgroundPlayerActivity"
|
android:name=".player.BackgroundPlayerActivity"
|
||||||
android:launchMode="singleTask"
|
android:label="@string/title_activity_background_player"
|
||||||
android:label="@string/title_activity_background_player"/>
|
android:launchMode="singleTask" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".player.PopupVideoPlayerActivity"
|
android:name=".player.PopupVideoPlayerActivity"
|
||||||
android:launchMode="singleTask"
|
android:label="@string/title_activity_popup_player"
|
||||||
android:label="@string/title_activity_popup_player"/>
|
android:launchMode="singleTask" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".player.PopupVideoPlayer"
|
android:name=".player.PopupVideoPlayer"
|
||||||
android:exported="false"/>
|
android:exported="false" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".player.MainVideoPlayer"
|
android:name=".player.MainVideoPlayer"
|
||||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:launchMode="singleTask"/>
|
android:launchMode="singleTask"
|
||||||
|
android:theme="@style/VideoPlayerTheme" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".settings.SettingsActivity"
|
android:name=".settings.SettingsActivity"
|
||||||
android:label="@string/settings"/>
|
android:label="@string/settings" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".about.AboutActivity"
|
android:name=".about.AboutActivity"
|
||||||
android:label="@string/title_activity_about"/>
|
android:label="@string/title_activity_about" />
|
||||||
|
|
||||||
<service android:name=".local.subscription.services.SubscriptionsImportService"/>
|
<service android:name=".local.subscription.services.SubscriptionsImportService" />
|
||||||
<service android:name=".local.subscription.services.SubscriptionsExportService"/>
|
<service android:name=".local.subscription.services.SubscriptionsExportService" />
|
||||||
|
<service android:name=".local.feed.service.FeedLoadService" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".PanicResponderActivity"
|
android:name=".PanicResponderActivity"
|
||||||
@ -85,25 +94,25 @@
|
|||||||
android:noHistory="true"
|
android:noHistory="true"
|
||||||
android:theme="@android:style/Theme.NoDisplay">
|
android:theme="@android:style/Theme.NoDisplay">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="info.guardianproject.panic.action.TRIGGER"/>
|
<action android:name="info.guardianproject.panic.action.TRIGGER" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ExitActivity"
|
android:name=".ExitActivity"
|
||||||
android:label="@string/general_error"
|
android:label="@string/general_error"
|
||||||
android:theme="@android:style/Theme.NoDisplay"/>
|
android:theme="@android:style/Theme.NoDisplay" />
|
||||||
<activity android:name=".report.ErrorActivity"/>
|
<activity android:name=".report.ErrorActivity" />
|
||||||
|
|
||||||
<!-- giga get related -->
|
<!-- giga get related -->
|
||||||
<activity
|
<activity
|
||||||
android:name=".download.DownloadActivity"
|
android:name=".download.DownloadActivity"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:launchMode="singleTask"/>
|
android:launchMode="singleTask" />
|
||||||
|
|
||||||
<service android:name="us.shandian.giga.service.DownloadManagerService"/>
|
<service android:name="us.shandian.giga.service.DownloadManagerService" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".util.FilePickerActivityHelper"
|
android:name=".util.FilePickerActivityHelper"
|
||||||
@ -117,7 +126,7 @@
|
|||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ReCaptchaActivity"
|
android:name=".ReCaptchaActivity"
|
||||||
android:label="@string/reCaptchaActivity"/>
|
android:label="@string/recaptcha" />
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
@ -126,7 +135,7 @@
|
|||||||
android:grantUriPermissions="true">
|
android:grantUriPermissions="true">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
android:resource="@xml/nnf_provider_paths"/>
|
android:resource="@xml/nnf_provider_paths" />
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
@ -138,152 +147,197 @@
|
|||||||
|
|
||||||
<!-- Youtube filter -->
|
<!-- Youtube filter -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW"/>
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
|
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||||
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
|
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE"/>
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data android:scheme="http"/>
|
<data android:scheme="http" />
|
||||||
<data android:scheme="https"/>
|
<data android:scheme="https" />
|
||||||
<data android:host="youtube.com"/>
|
<data android:host="youtube.com" />
|
||||||
<data android:host="m.youtube.com"/>
|
<data android:host="m.youtube.com" />
|
||||||
<data android:host="www.youtube.com"/>
|
<data android:host="www.youtube.com" />
|
||||||
|
<data android:host="music.youtube.com" />
|
||||||
<!-- video prefix -->
|
<!-- video prefix -->
|
||||||
<data android:pathPrefix="/v/"/>
|
<data android:pathPrefix="/v/" />
|
||||||
<data android:pathPrefix="/embed/"/>
|
<data android:pathPrefix="/embed/" />
|
||||||
<data android:pathPrefix="/watch"/>
|
<data android:pathPrefix="/watch" />
|
||||||
<data android:pathPrefix="/attribution_link"/>
|
<data android:pathPrefix="/attribution_link" />
|
||||||
<!-- channel prefix -->
|
<!-- channel prefix -->
|
||||||
<data android:pathPrefix="/channel/"/>
|
<data android:pathPrefix="/channel/" />
|
||||||
<data android:pathPrefix="/user/"/>
|
<data android:pathPrefix="/user/" />
|
||||||
|
<data android:pathPrefix="/c/" />
|
||||||
<!-- playlist prefix -->
|
<!-- playlist prefix -->
|
||||||
<data android:pathPrefix="/playlist"/>
|
<data android:pathPrefix="/playlist" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW"/>
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
|
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||||
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
|
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE"/>
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data android:scheme="http"/>
|
<data android:scheme="http" />
|
||||||
<data android:scheme="https"/>
|
<data android:scheme="https" />
|
||||||
<data android:host="youtu.be"/>
|
<data android:host="youtu.be" />
|
||||||
<data android:pathPrefix="/"/>
|
<data android:pathPrefix="/" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW"/>
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
|
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||||
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
|
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE"/>
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data android:scheme="http"/>
|
<data android:scheme="http" />
|
||||||
<data android:scheme="https"/>
|
<data android:scheme="https" />
|
||||||
<data android:host="www.youtube-nocookie.com"/>
|
<data android:host="www.youtube-nocookie.com" />
|
||||||
<data android:pathPrefix="/embed/"/>
|
<data android:pathPrefix="/embed/" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW"/>
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
|
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||||
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
|
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE"/>
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data android:scheme="vnd.youtube"/>
|
<data android:scheme="vnd.youtube" />
|
||||||
<data android:scheme="vnd.youtube.launch"/>
|
<data android:scheme="vnd.youtube.launch" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<!-- Hooktube filter -->
|
<!-- Hooktube filter -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW"/>
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
|
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||||
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
|
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE"/>
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data android:scheme="http"/>
|
<data android:scheme="http" />
|
||||||
<data android:scheme="https"/>
|
<data android:scheme="https" />
|
||||||
<data android:host="hooktube.com"/>
|
<data android:host="hooktube.com" />
|
||||||
<data android:host="*.hooktube.com"/>
|
<data android:host="*.hooktube.com" />
|
||||||
<!-- video prefix -->
|
<!-- video prefix -->
|
||||||
<data android:pathPrefix="/v/"/>
|
<data android:pathPrefix="/v/" />
|
||||||
<data android:pathPrefix="/embed/"/>
|
<data android:pathPrefix="/embed/" />
|
||||||
<data android:pathPrefix="/watch"/>
|
<data android:pathPrefix="/watch" />
|
||||||
<!-- channel prefix -->
|
<!-- channel prefix -->
|
||||||
<data android:pathPrefix="/channel/"/>
|
<data android:pathPrefix="/channel/" />
|
||||||
<data android:pathPrefix="/user/"/>
|
<data android:pathPrefix="/user/" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<!-- Invidious filter -->
|
<!-- Invidious filter -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW"/>
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
|
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||||
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
|
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE"/>
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data android:scheme="http"/>
|
<data android:scheme="http" />
|
||||||
<data android:scheme="https"/>
|
<data android:scheme="https" />
|
||||||
<data android:host="invidio.us"/>
|
<data android:host="invidio.us" />
|
||||||
<data android:host="dev.invidio.us"/>
|
<data android:host="dev.invidio.us" />
|
||||||
<data android:host="www.invidio.us"/>
|
<data android:host="www.invidio.us" />
|
||||||
<data android:host="invidious.snopyta.org"/>
|
<data android:host="invidious.snopyta.org" />
|
||||||
<data android:host="de.invidious.snopyta.org"/>
|
<data android:host="fi.invidious.snopyta.org" />
|
||||||
<data android:host="fi.invidious.snopyta.org"/>
|
<data android:host="yewtu.be" />
|
||||||
<data android:host="vid.wxzm.sx"/>
|
<data android:host="invidious.ggc-project.de" />
|
||||||
<data android:host="invidious.kabi.tk"/>
|
<data android:host="yt.maisputain.ovh" />
|
||||||
<data android:host="invidiou.sh"/>
|
<data android:host="invidious.13ad.de" />
|
||||||
<data android:host="www.invidiou.sh"/>
|
<data android:host="invidious.toot.koeln" />
|
||||||
<data android:host="no.invidiou.sh"/>
|
<data android:host="invidious.fdn.fr" />
|
||||||
<data android:host="invidious.enkirton.net"/>
|
<data android:host="watch.nettohikari.com" />
|
||||||
<data android:host="tube.poal.co"/>
|
<data android:host="invidious.snwmds.net" />
|
||||||
<data android:host="invidious.13ad.de"/>
|
<data android:host="invidious.snwmds.org" />
|
||||||
<data android:host="yt.elukerio.org"/>
|
<data android:host="invidious.snwmds.com" />
|
||||||
<!-- video prefix -->
|
<data android:host="invidious.sunsetravens.com" />
|
||||||
<data android:pathPrefix="/embed/"/>
|
<data android:host="invidious.gachirangers.com" />
|
||||||
<data android:pathPrefix="/watch"/>
|
<data android:pathPrefix="/" />
|
||||||
<!-- channel prefix -->
|
|
||||||
<data android:pathPrefix="/channel/"/>
|
|
||||||
<data android:pathPrefix="/user/"/>
|
|
||||||
<!-- playlist prefix -->
|
|
||||||
<data android:pathPrefix="/playlist"/>
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<!-- Soundcloud filter -->
|
<!-- Soundcloud filter -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW"/>
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
|
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||||
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
|
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE"/>
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data android:scheme="http"/>
|
<data android:scheme="http" />
|
||||||
<data android:scheme="https"/>
|
<data android:scheme="https" />
|
||||||
<data android:host="soundcloud.com"/>
|
<data android:host="soundcloud.com" />
|
||||||
<data android:host="m.soundcloud.com"/>
|
<data android:host="m.soundcloud.com" />
|
||||||
<data android:host="www.soundcloud.com"/>
|
<data android:host="www.soundcloud.com" />
|
||||||
<data android:pathPrefix="/"/>
|
<data android:pathPrefix="/" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<!-- Share filter -->
|
<!-- Share filter -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND"/>
|
<action android:name="android.intent.action.SEND" />
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<data android:mimeType="text/plain"/>
|
<data android:mimeType="text/plain" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- MediaCCC filter -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||||
|
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="http" />
|
||||||
|
<data android:scheme="https" />
|
||||||
|
<data android:host="media.ccc.de" />
|
||||||
|
<!-- video prefix -->
|
||||||
|
<data android:pathPrefix="/v/" />
|
||||||
|
<!-- channel prefix-->
|
||||||
|
<data android:pathPrefix="/c/" />
|
||||||
|
<data android:pathPrefix="/b/" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- PeerTube filter -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||||
|
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="http" />
|
||||||
|
<data android:scheme="https" />
|
||||||
|
|
||||||
|
<data android:host="framatube.org" />
|
||||||
|
<data android:host="media.assassinate-you.net" />
|
||||||
|
<data android:host="peertube.co.uk" />
|
||||||
|
<data android:host="peertube.cpy.re" />
|
||||||
|
<data android:host="peertube.mastodon.host" />
|
||||||
|
<data android:host="peertube.fr" />
|
||||||
|
<data android:host="peertube.live" />
|
||||||
|
<data android:host="peertube.video" />
|
||||||
|
<data android:host="tube.privacytools.io" />
|
||||||
|
<data android:host="video.ploud.fr" />
|
||||||
|
<data android:host="video.lqdn.fr" />
|
||||||
|
<data android:host="skeptikon.fr" />
|
||||||
|
|
||||||
|
<data android:pathPrefix="/videos/" /> <!-- it contains playlists -->
|
||||||
|
<data android:pathPrefix="/accounts/" />
|
||||||
|
<data android:pathPrefix="/video-channels/" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".RouterActivity$FetcherService"
|
android:name=".RouterActivity$FetcherService"
|
||||||
android:exported="false"/>
|
android:exported="false" />
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
@ -0,0 +1,329 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2018 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.fragment.app;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.annotation.IntDef;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.lifecycle.Lifecycle;
|
||||||
|
import androidx.viewpager.widget.PagerAdapter;
|
||||||
|
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
// TODO: Replace this deprecated class with its ViewPager2 counterpart
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a copy from {@link androidx.fragment.app.FragmentStatePagerAdapter}.
|
||||||
|
* <p>
|
||||||
|
* It includes a workaround to fix the menu visibility when the adapter is restored.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* When restoring the state of this adapter, all the fragments' menu visibility were set to false,
|
||||||
|
* effectively disabling the menu from the user until he switched pages or another event
|
||||||
|
* that triggered the menu to be visible again happened.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* <b>Check out the changes in:</b>
|
||||||
|
* </p>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link #saveState()}</li>
|
||||||
|
* <li>{@link #restoreState(Parcelable, ClassLoader)}</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapter {
|
||||||
|
private static final String TAG = "FragmentStatePagerAdapt";
|
||||||
|
private static final boolean DEBUG = false;
|
||||||
|
|
||||||
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
|
@IntDef({BEHAVIOR_SET_USER_VISIBLE_HINT, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT})
|
||||||
|
private @interface Behavior { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that {@link Fragment#setUserVisibleHint(boolean)} will be called when the current
|
||||||
|
* fragment changes.
|
||||||
|
*
|
||||||
|
* @deprecated This behavior relies on the deprecated
|
||||||
|
* {@link Fragment#setUserVisibleHint(boolean)} API. Use
|
||||||
|
* {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT} to switch to its replacement,
|
||||||
|
* {@link FragmentTransaction#setMaxLifecycle}.
|
||||||
|
* @see #FragmentStatePagerAdapterMenuWorkaround(FragmentManager, int)
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
public static final int BEHAVIOR_SET_USER_VISIBLE_HINT = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that only the current fragment will be in the {@link Lifecycle.State#RESUMED}
|
||||||
|
* state. All other Fragments are capped at {@link Lifecycle.State#STARTED}.
|
||||||
|
*
|
||||||
|
* @see #FragmentStatePagerAdapterMenuWorkaround(FragmentManager, int)
|
||||||
|
*/
|
||||||
|
public static final int BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT = 1;
|
||||||
|
|
||||||
|
private final FragmentManager mFragmentManager;
|
||||||
|
private final int mBehavior;
|
||||||
|
private FragmentTransaction mCurTransaction = null;
|
||||||
|
|
||||||
|
private ArrayList<Fragment.SavedState> mSavedState = new ArrayList<Fragment.SavedState>();
|
||||||
|
private ArrayList<Fragment> mFragments = new ArrayList<Fragment>();
|
||||||
|
private Fragment mCurrentPrimaryItem = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for {@link FragmentStatePagerAdapterMenuWorkaround}
|
||||||
|
* that sets the fragment manager for the adapter. This is the equivalent of calling
|
||||||
|
* {@link #FragmentStatePagerAdapterMenuWorkaround(FragmentManager, int)} and passing in
|
||||||
|
* {@link #BEHAVIOR_SET_USER_VISIBLE_HINT}.
|
||||||
|
*
|
||||||
|
* <p>Fragments will have {@link Fragment#setUserVisibleHint(boolean)} called whenever the
|
||||||
|
* current Fragment changes.</p>
|
||||||
|
*
|
||||||
|
* @param fm fragment manager that will interact with this adapter
|
||||||
|
* @deprecated use {@link #FragmentStatePagerAdapterMenuWorkaround(FragmentManager, int)} with
|
||||||
|
* {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT}
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
public FragmentStatePagerAdapterMenuWorkaround(@NonNull final FragmentManager fm) {
|
||||||
|
this(fm, BEHAVIOR_SET_USER_VISIBLE_HINT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for {@link FragmentStatePagerAdapterMenuWorkaround}.
|
||||||
|
*
|
||||||
|
* If {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT} is passed in, then only the current
|
||||||
|
* Fragment is in the {@link Lifecycle.State#RESUMED} state, while all other fragments are
|
||||||
|
* capped at {@link Lifecycle.State#STARTED}. If {@link #BEHAVIOR_SET_USER_VISIBLE_HINT} is
|
||||||
|
* passed, all fragments are in the {@link Lifecycle.State#RESUMED} state and there will be
|
||||||
|
* callbacks to {@link Fragment#setUserVisibleHint(boolean)}.
|
||||||
|
*
|
||||||
|
* @param fm fragment manager that will interact with this adapter
|
||||||
|
* @param behavior determines if only current fragments are in a resumed state
|
||||||
|
*/
|
||||||
|
public FragmentStatePagerAdapterMenuWorkaround(@NonNull final FragmentManager fm,
|
||||||
|
@Behavior final int behavior) {
|
||||||
|
mFragmentManager = fm;
|
||||||
|
mBehavior = behavior;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param position the position of the item you want
|
||||||
|
* @return the {@link Fragment} associated with a specified position
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public abstract Fragment getItem(int position);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void startUpdate(@NonNull final ViewGroup container) {
|
||||||
|
if (container.getId() == View.NO_ID) {
|
||||||
|
throw new IllegalStateException("ViewPager with adapter " + this
|
||||||
|
+ " requires a view id");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Object instantiateItem(@NonNull final ViewGroup container, final int position) {
|
||||||
|
// If we already have this item instantiated, there is nothing
|
||||||
|
// to do. This can happen when we are restoring the entire pager
|
||||||
|
// from its saved state, where the fragment manager has already
|
||||||
|
// taken care of restoring the fragments we previously had instantiated.
|
||||||
|
if (mFragments.size() > position) {
|
||||||
|
Fragment f = mFragments.get(position);
|
||||||
|
if (f != null) {
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mCurTransaction == null) {
|
||||||
|
mCurTransaction = mFragmentManager.beginTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
Fragment fragment = getItem(position);
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
|
||||||
|
}
|
||||||
|
if (mSavedState.size() > position) {
|
||||||
|
Fragment.SavedState fss = mSavedState.get(position);
|
||||||
|
if (fss != null) {
|
||||||
|
fragment.setInitialSavedState(fss);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (mFragments.size() <= position) {
|
||||||
|
mFragments.add(null);
|
||||||
|
}
|
||||||
|
fragment.setMenuVisibility(false);
|
||||||
|
if (mBehavior == BEHAVIOR_SET_USER_VISIBLE_HINT) {
|
||||||
|
fragment.setUserVisibleHint(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
mFragments.set(position, fragment);
|
||||||
|
mCurTransaction.add(container.getId(), fragment);
|
||||||
|
|
||||||
|
if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
|
||||||
|
mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroyItem(@NonNull final ViewGroup container, final int position,
|
||||||
|
@NonNull final Object object) {
|
||||||
|
Fragment fragment = (Fragment) object;
|
||||||
|
|
||||||
|
if (mCurTransaction == null) {
|
||||||
|
mCurTransaction = mFragmentManager.beginTransaction();
|
||||||
|
}
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.v(TAG, "Removing item #" + position + ": f=" + object
|
||||||
|
+ " v=" + ((Fragment) object).getView());
|
||||||
|
}
|
||||||
|
while (mSavedState.size() <= position) {
|
||||||
|
mSavedState.add(null);
|
||||||
|
}
|
||||||
|
mSavedState.set(position, fragment.isAdded()
|
||||||
|
? mFragmentManager.saveFragmentInstanceState(fragment) : null);
|
||||||
|
mFragments.set(position, null);
|
||||||
|
|
||||||
|
mCurTransaction.remove(fragment);
|
||||||
|
if (fragment == mCurrentPrimaryItem) {
|
||||||
|
mCurrentPrimaryItem = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings({"ReferenceEquality", "deprecation"})
|
||||||
|
public void setPrimaryItem(@NonNull final ViewGroup container, final int position,
|
||||||
|
@NonNull final Object object) {
|
||||||
|
Fragment fragment = (Fragment) object;
|
||||||
|
if (fragment != mCurrentPrimaryItem) {
|
||||||
|
if (mCurrentPrimaryItem != null) {
|
||||||
|
mCurrentPrimaryItem.setMenuVisibility(false);
|
||||||
|
if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
|
||||||
|
if (mCurTransaction == null) {
|
||||||
|
mCurTransaction = mFragmentManager.beginTransaction();
|
||||||
|
}
|
||||||
|
mCurTransaction.setMaxLifecycle(mCurrentPrimaryItem, Lifecycle.State.STARTED);
|
||||||
|
} else {
|
||||||
|
mCurrentPrimaryItem.setUserVisibleHint(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fragment.setMenuVisibility(true);
|
||||||
|
if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
|
||||||
|
if (mCurTransaction == null) {
|
||||||
|
mCurTransaction = mFragmentManager.beginTransaction();
|
||||||
|
}
|
||||||
|
mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED);
|
||||||
|
} else {
|
||||||
|
fragment.setUserVisibleHint(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
mCurrentPrimaryItem = fragment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void finishUpdate(@NonNull final ViewGroup container) {
|
||||||
|
if (mCurTransaction != null) {
|
||||||
|
mCurTransaction.commitNowAllowingStateLoss();
|
||||||
|
mCurTransaction = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isViewFromObject(@NonNull final View view, @NonNull final Object object) {
|
||||||
|
return ((Fragment) object).getView() == view;
|
||||||
|
}
|
||||||
|
|
||||||
|
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
|
private final String selectedFragment = "selected_fragment";
|
||||||
|
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public Parcelable saveState() {
|
||||||
|
Bundle state = null;
|
||||||
|
if (mSavedState.size() > 0) {
|
||||||
|
state = new Bundle();
|
||||||
|
Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()];
|
||||||
|
mSavedState.toArray(fss);
|
||||||
|
state.putParcelableArray("states", fss);
|
||||||
|
}
|
||||||
|
for (int i = 0; i < mFragments.size(); i++) {
|
||||||
|
Fragment f = mFragments.get(i);
|
||||||
|
if (f != null && f.isAdded()) {
|
||||||
|
if (state == null) {
|
||||||
|
state = new Bundle();
|
||||||
|
}
|
||||||
|
String key = "f" + i;
|
||||||
|
mFragmentManager.putFragment(state, key, f);
|
||||||
|
|
||||||
|
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
|
// Check if it's the same fragment instance
|
||||||
|
if (f == mCurrentPrimaryItem) {
|
||||||
|
state.putString(selectedFragment, key);
|
||||||
|
}
|
||||||
|
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void restoreState(@Nullable final Parcelable state, @Nullable final ClassLoader loader) {
|
||||||
|
if (state != null) {
|
||||||
|
Bundle bundle = (Bundle) state;
|
||||||
|
bundle.setClassLoader(loader);
|
||||||
|
Parcelable[] fss = bundle.getParcelableArray("states");
|
||||||
|
mSavedState.clear();
|
||||||
|
mFragments.clear();
|
||||||
|
if (fss != null) {
|
||||||
|
for (int i = 0; i < fss.length; i++) {
|
||||||
|
mSavedState.add((Fragment.SavedState) fss[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Iterable<String> keys = bundle.keySet();
|
||||||
|
for (String key: keys) {
|
||||||
|
if (key.startsWith("f")) {
|
||||||
|
int index = Integer.parseInt(key.substring(1));
|
||||||
|
Fragment f = mFragmentManager.getFragment(bundle, key);
|
||||||
|
if (f != null) {
|
||||||
|
while (mFragments.size() <= index) {
|
||||||
|
mFragments.add(null);
|
||||||
|
}
|
||||||
|
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
|
final boolean wasSelected = bundle.getString(selectedFragment, "")
|
||||||
|
.equals(key);
|
||||||
|
f.setMenuVisibility(wasSelected);
|
||||||
|
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
|
mFragments.set(index, f);
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Bad fragment at key " + key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,7 @@ import android.view.View;
|
|||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.OverScroller;
|
import android.widget.OverScroller;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
@ -15,10 +16,11 @@ import org.schabi.newpipe.R;
|
|||||||
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
|
|
||||||
// check this https://stackoverflow.com/questions/56849221/recyclerview-fling-causes-laggy-while-appbarlayout-is-scrolling/57997489#57997489
|
// See https://stackoverflow.com/questions/56849221#57997489
|
||||||
public final class FlingBehavior extends AppBarLayout.Behavior {
|
public final class FlingBehavior extends AppBarLayout.Behavior {
|
||||||
|
private final Rect focusScrollRect = new Rect();
|
||||||
|
|
||||||
public FlingBehavior(Context context, AttributeSet attrs) {
|
public FlingBehavior(final Context context, final AttributeSet attrs) {
|
||||||
super(context, attrs);
|
super(context, attrs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -26,7 +28,40 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
|
|||||||
private final Rect globalRect = new Rect();
|
private final Rect globalRect = new Rect();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onInterceptTouchEvent(CoordinatorLayout parent, AppBarLayout child, MotionEvent ev) {
|
public boolean onRequestChildRectangleOnScreen(
|
||||||
|
@NonNull final CoordinatorLayout coordinatorLayout, @NonNull final AppBarLayout child,
|
||||||
|
@NonNull final Rect rectangle, final boolean immediate) {
|
||||||
|
|
||||||
|
focusScrollRect.set(rectangle);
|
||||||
|
|
||||||
|
coordinatorLayout.offsetDescendantRectToMyCoords(child, focusScrollRect);
|
||||||
|
|
||||||
|
int height = coordinatorLayout.getHeight();
|
||||||
|
|
||||||
|
if (focusScrollRect.top <= 0 && focusScrollRect.bottom >= height) {
|
||||||
|
// the child is too big to fit inside ourselves completely, ignore request
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int dy;
|
||||||
|
|
||||||
|
if (focusScrollRect.bottom > height) {
|
||||||
|
dy = focusScrollRect.top;
|
||||||
|
} else if (focusScrollRect.top < 0) {
|
||||||
|
// scrolling up
|
||||||
|
dy = -(height - focusScrollRect.bottom);
|
||||||
|
} else {
|
||||||
|
// nothing to do
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int consumed = scroll(coordinatorLayout, child, dy, getMaxDragOffset(child), 0);
|
||||||
|
|
||||||
|
return consumed == dy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean onInterceptTouchEvent(final CoordinatorLayout parent, final AppBarLayout child,
|
||||||
|
final MotionEvent ev) {
|
||||||
final ViewGroup playQueue = child.findViewById(R.id.playQueuePanel);
|
final ViewGroup playQueue = child.findViewById(R.id.playQueuePanel);
|
||||||
if (playQueue != null) {
|
if (playQueue != null) {
|
||||||
final boolean visible = playQueue.getGlobalVisibleRect(globalRect);
|
final boolean visible = playQueue.getGlobalVisibleRect(globalRect);
|
||||||
@ -36,30 +71,36 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
allowScroll = true;
|
allowScroll = true;
|
||||||
|
switch (ev.getActionMasked()) {
|
||||||
if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
|
case MotionEvent.ACTION_DOWN:
|
||||||
// remove reference to old nested scrolling child
|
// remove reference to old nested scrolling child
|
||||||
resetNestedScrollingChild();
|
resetNestedScrollingChild();
|
||||||
// Stop fling when your finger touches the screen
|
// Stop fling when your finger touches the screen
|
||||||
stopAppBarLayoutFling();
|
stopAppBarLayoutFling();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return super.onInterceptTouchEvent(parent, child, ev);
|
return super.onInterceptTouchEvent(parent, child, ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes, int type) {
|
public boolean onStartNestedScroll(@NotNull final CoordinatorLayout parent, @NotNull final AppBarLayout child,
|
||||||
|
@NotNull final View directTargetChild, final View target, final int nestedScrollAxes, final int type) {
|
||||||
return allowScroll && super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes, type);
|
return allowScroll && super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes, type);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onNestedFling(@NotNull CoordinatorLayout coordinatorLayout, @NotNull AppBarLayout child, @NotNull View target, float velocityX, float velocityY, boolean consumed) {
|
public boolean onNestedFling(@NotNull final CoordinatorLayout coordinatorLayout, @NotNull final AppBarLayout child,
|
||||||
|
@NotNull final View target, final float velocityX, final float velocityY, final boolean consumed) {
|
||||||
return allowScroll && super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
|
return allowScroll && super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private OverScroller getScrollerField() {
|
private OverScroller getScrollerField() {
|
||||||
try {
|
try {
|
||||||
Class<?> headerBehaviorType = this.getClass().getSuperclass().getSuperclass().getSuperclass();
|
Class<?> headerBehaviorType = this.getClass()
|
||||||
|
.getSuperclass().getSuperclass().getSuperclass();
|
||||||
if (headerBehaviorType != null) {
|
if (headerBehaviorType != null) {
|
||||||
Field field = headerBehaviorType.getDeclaredField("scroller");
|
Field field = headerBehaviorType.getDeclaredField("scroller");
|
||||||
field.setAccessible(true);
|
field.setAccessible(true);
|
||||||
@ -86,12 +127,14 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void resetNestedScrollingChild(){
|
private void resetNestedScrollingChild() {
|
||||||
Field field = getLastNestedScrollingChildRefField();
|
Field field = getLastNestedScrollingChildRefField();
|
||||||
if(field != null){
|
if (field != null) {
|
||||||
try {
|
try {
|
||||||
Object value = field.get(this);
|
Object value = field.get(this);
|
||||||
if(value != null) field.set(this, null);
|
if (value != null) {
|
||||||
|
field.set(this, null);
|
||||||
|
}
|
||||||
} catch (IllegalAccessException e) {
|
} catch (IllegalAccessException e) {
|
||||||
// ?
|
// ?
|
||||||
}
|
}
|
||||||
@ -100,7 +143,8 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
|
|||||||
|
|
||||||
private void stopAppBarLayoutFling() {
|
private void stopAppBarLayoutFling() {
|
||||||
OverScroller scroller = getScrollerField();
|
OverScroller scroller = getScrollerField();
|
||||||
if (scroller != null) scroller.forceFinished(true);
|
if (scroller != null) {
|
||||||
|
scroller.forceFinished(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -23,17 +23,25 @@ package org.schabi.newpipe;
|
|||||||
/**
|
/**
|
||||||
* Singleton:
|
* Singleton:
|
||||||
* Used to send data between certain Activity/Services within the same process.
|
* Used to send data between certain Activity/Services within the same process.
|
||||||
* This can be considered as an ugly hack inside the Android universe. **/
|
* This can be considered as an ugly hack inside the Android universe.
|
||||||
|
**/
|
||||||
public class ActivityCommunicator {
|
public class ActivityCommunicator {
|
||||||
|
|
||||||
private static ActivityCommunicator activityCommunicator;
|
private static ActivityCommunicator activityCommunicator;
|
||||||
|
private volatile Class returnActivity;
|
||||||
|
|
||||||
public static ActivityCommunicator getCommunicator() {
|
public static ActivityCommunicator getCommunicator() {
|
||||||
if(activityCommunicator == null) {
|
if (activityCommunicator == null) {
|
||||||
activityCommunicator = new ActivityCommunicator();
|
activityCommunicator = new ActivityCommunicator();
|
||||||
}
|
}
|
||||||
return activityCommunicator;
|
return activityCommunicator;
|
||||||
}
|
}
|
||||||
|
|
||||||
public volatile Class returnActivity;
|
public Class getReturnActivity() {
|
||||||
|
return returnActivity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReturnActivity(final Class returnActivity) {
|
||||||
|
this.returnActivity = returnActivity;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,21 +5,20 @@ import android.app.Application;
|
|||||||
import android.app.NotificationChannel;
|
import android.app.NotificationChannel;
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache;
|
import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache;
|
||||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||||
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
|
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
|
||||||
import com.squareup.leakcanary.LeakCanary;
|
|
||||||
import com.squareup.leakcanary.RefWatcher;
|
|
||||||
|
|
||||||
import org.acra.ACRA;
|
import org.acra.ACRA;
|
||||||
import org.acra.config.ACRAConfiguration;
|
|
||||||
import org.acra.config.ACRAConfigurationException;
|
import org.acra.config.ACRAConfigurationException;
|
||||||
import org.acra.config.ConfigurationBuilder;
|
import org.acra.config.CoreConfiguration;
|
||||||
|
import org.acra.config.CoreConfigurationBuilder;
|
||||||
import org.acra.sender.ReportSenderFactory;
|
import org.acra.sender.ReportSenderFactory;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||||
@ -27,7 +26,7 @@ import org.schabi.newpipe.report.AcraReportSenderFactory;
|
|||||||
import org.schabi.newpipe.report.ErrorActivity;
|
import org.schabi.newpipe.report.ErrorActivity;
|
||||||
import org.schabi.newpipe.report.UserAction;
|
import org.schabi.newpipe.report.UserAction;
|
||||||
import org.schabi.newpipe.settings.SettingsActivity;
|
import org.schabi.newpipe.settings.SettingsActivity;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExceptionUtils;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
import org.schabi.newpipe.util.StateSaver;
|
import org.schabi.newpipe.util.StateSaver;
|
||||||
@ -66,15 +65,17 @@ import io.reactivex.plugins.RxJavaPlugins;
|
|||||||
|
|
||||||
public class App extends Application {
|
public class App extends Application {
|
||||||
protected static final String TAG = App.class.toString();
|
protected static final String TAG = App.class.toString();
|
||||||
private RefWatcher refWatcher;
|
|
||||||
private static App app;
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private static final Class<? extends ReportSenderFactory>[]
|
private static final Class<? extends ReportSenderFactory>[]
|
||||||
reportSenderFactoryClasses = new Class[]{AcraReportSenderFactory.class};
|
REPORT_SENDER_FACTORY_CLASSES = new Class[]{AcraReportSenderFactory.class};
|
||||||
|
private static App app;
|
||||||
|
|
||||||
|
public static App getApp() {
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void attachBaseContext(Context base) {
|
protected void attachBaseContext(final Context base) {
|
||||||
super.attachBaseContext(base);
|
super.attachBaseContext(base);
|
||||||
|
|
||||||
initACRA();
|
initACRA();
|
||||||
@ -84,13 +85,6 @@ public class App extends Application {
|
|||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
|
|
||||||
if (LeakCanary.isInAnalyzerProcess(this)) {
|
|
||||||
// This process is dedicated to LeakCanary for heap analysis.
|
|
||||||
// You should not init your app in this process.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
refWatcher = installLeakCanary();
|
|
||||||
|
|
||||||
app = this;
|
app = this;
|
||||||
|
|
||||||
// Initialize settings first because others inits can use its values
|
// Initialize settings first because others inits can use its values
|
||||||
@ -99,7 +93,7 @@ public class App extends Application {
|
|||||||
NewPipe.init(getDownloader(),
|
NewPipe.init(getDownloader(),
|
||||||
Localization.getPreferredLocalization(this),
|
Localization.getPreferredLocalization(this),
|
||||||
Localization.getPreferredContentCountry(this));
|
Localization.getPreferredContentCountry(this));
|
||||||
Localization.init();
|
Localization.init(getApplicationContext());
|
||||||
|
|
||||||
StateSaver.init(this);
|
StateSaver.init(this);
|
||||||
initNotificationChannel();
|
initNotificationChannel();
|
||||||
@ -116,31 +110,47 @@ public class App extends Application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected Downloader getDownloader() {
|
protected Downloader getDownloader() {
|
||||||
return DownloaderImpl.init(null);
|
DownloaderImpl downloader = DownloaderImpl.init(null);
|
||||||
|
setCookiesToDownloader(downloader);
|
||||||
|
return downloader;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setCookiesToDownloader(final DownloaderImpl downloader) {
|
||||||
|
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
|
||||||
|
getApplicationContext());
|
||||||
|
final String key = getApplicationContext().getString(R.string.recaptcha_cookies_key);
|
||||||
|
downloader.setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, prefs.getString(key, ""));
|
||||||
|
downloader.updateYoutubeRestrictedModeCookies(getApplicationContext());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void configureRxJavaErrorHandler() {
|
private void configureRxJavaErrorHandler() {
|
||||||
// https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling
|
// https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling
|
||||||
RxJavaPlugins.setErrorHandler(new Consumer<Throwable>() {
|
RxJavaPlugins.setErrorHandler(new Consumer<Throwable>() {
|
||||||
@Override
|
@Override
|
||||||
public void accept(@NonNull Throwable throwable) {
|
public void accept(@NonNull final Throwable throwable) {
|
||||||
Log.e(TAG, "RxJavaPlugins.ErrorHandler called with -> : " +
|
Log.e(TAG, "RxJavaPlugins.ErrorHandler called with -> : "
|
||||||
"throwable = [" + throwable.getClass().getName() + "]");
|
+ "throwable = [" + throwable.getClass().getName() + "]");
|
||||||
|
|
||||||
|
final Throwable actualThrowable;
|
||||||
if (throwable instanceof UndeliverableException) {
|
if (throwable instanceof UndeliverableException) {
|
||||||
// As UndeliverableException is a wrapper, get the cause of it to get the "real" exception
|
// As UndeliverableException is a wrapper,
|
||||||
throwable = throwable.getCause();
|
// get the cause of it to get the "real" exception
|
||||||
|
actualThrowable = throwable.getCause();
|
||||||
|
} else {
|
||||||
|
actualThrowable = throwable;
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<Throwable> errors;
|
final List<Throwable> errors;
|
||||||
if (throwable instanceof CompositeException) {
|
if (actualThrowable instanceof CompositeException) {
|
||||||
errors = ((CompositeException) throwable).getExceptions();
|
errors = ((CompositeException) actualThrowable).getExceptions();
|
||||||
} else {
|
} else {
|
||||||
errors = Collections.singletonList(throwable);
|
errors = Collections.singletonList(actualThrowable);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final Throwable error : errors) {
|
for (final Throwable error : errors) {
|
||||||
if (isThrowableIgnored(error)) return;
|
if (isThrowableIgnored(error)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (isThrowableCritical(error)) {
|
if (isThrowableCritical(error)) {
|
||||||
reportException(error);
|
reportException(error);
|
||||||
return;
|
return;
|
||||||
@ -150,22 +160,24 @@ public class App extends Application {
|
|||||||
// Out-of-lifecycle exceptions should only be reported if a debug user wishes so,
|
// Out-of-lifecycle exceptions should only be reported if a debug user wishes so,
|
||||||
// When exception is not reported, log it
|
// When exception is not reported, log it
|
||||||
if (isDisposedRxExceptionsReported()) {
|
if (isDisposedRxExceptionsReported()) {
|
||||||
reportException(throwable);
|
reportException(actualThrowable);
|
||||||
} else {
|
} else {
|
||||||
Log.e(TAG, "RxJavaPlugin: Undeliverable Exception received: ", throwable);
|
Log.e(TAG, "RxJavaPlugin: Undeliverable Exception received: ", actualThrowable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isThrowableIgnored(@NonNull final Throwable throwable) {
|
private boolean isThrowableIgnored(@NonNull final Throwable throwable) {
|
||||||
// Don't crash the application over a simple network problem
|
// Don't crash the application over a simple network problem
|
||||||
return ExtractorHelper.hasAssignableCauseThrowable(throwable,
|
return ExceptionUtils.hasAssignableCause(throwable,
|
||||||
IOException.class, SocketException.class, // network api cancellation
|
// network api cancellation
|
||||||
InterruptedException.class, InterruptedIOException.class); // blocking code disposed
|
IOException.class, SocketException.class,
|
||||||
|
// blocking code disposed
|
||||||
|
InterruptedException.class, InterruptedIOException.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isThrowableCritical(@NonNull final Throwable throwable) {
|
private boolean isThrowableCritical(@NonNull final Throwable throwable) {
|
||||||
// Though these exceptions cannot be ignored
|
// Though these exceptions cannot be ignored
|
||||||
return ExtractorHelper.hasAssignableCauseThrowable(throwable,
|
return ExceptionUtils.hasAssignableCause(throwable,
|
||||||
NullPointerException.class, IllegalArgumentException.class, // bug in app
|
NullPointerException.class, IllegalArgumentException.class, // bug in app
|
||||||
OnErrorNotImplementedException.class, MissingBackpressureException.class,
|
OnErrorNotImplementedException.class, MissingBackpressureException.class,
|
||||||
IllegalStateException.class); // bug in operator
|
IllegalStateException.class); // bug in operator
|
||||||
@ -190,8 +202,8 @@ public class App extends Application {
|
|||||||
|
|
||||||
private void initACRA() {
|
private void initACRA() {
|
||||||
try {
|
try {
|
||||||
final ACRAConfiguration acraConfig = new ConfigurationBuilder(this)
|
final CoreConfiguration acraConfig = new CoreConfigurationBuilder(this)
|
||||||
.setReportSenderFactoryClasses(reportSenderFactoryClasses)
|
.setReportSenderFactoryClasses(REPORT_SENDER_FACTORY_CLASSES)
|
||||||
.setBuildConfigClass(BuildConfig.class)
|
.setBuildConfigClass(BuildConfig.class)
|
||||||
.build();
|
.build();
|
||||||
ACRA.init(this, acraConfig);
|
ACRA.init(this, acraConfig);
|
||||||
@ -202,7 +214,7 @@ public class App extends Application {
|
|||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||||
"Could not initialize ACRA crash report", R.string.app_ui_crash));
|
"Could not initialize ACRA crash report", R.string.app_ui_crash));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -230,11 +242,11 @@ public class App extends Application {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Set up notification channel for app update.
|
* Set up notification channel for app update.
|
||||||
|
*
|
||||||
* @param importance
|
* @param importance
|
||||||
*/
|
*/
|
||||||
@TargetApi(Build.VERSION_CODES.O)
|
@TargetApi(Build.VERSION_CODES.O)
|
||||||
private void setUpUpdateNotificationChannel(int importance) {
|
private void setUpUpdateNotificationChannel(final int importance) {
|
||||||
|
|
||||||
final String appUpdateId
|
final String appUpdateId
|
||||||
= getString(R.string.app_update_notification_channel_id);
|
= getString(R.string.app_update_notification_channel_id);
|
||||||
final CharSequence appUpdateName
|
final CharSequence appUpdateName
|
||||||
@ -251,21 +263,7 @@ public class App extends Application {
|
|||||||
appUpdateNotificationManager.createNotificationChannel(appUpdateChannel);
|
appUpdateNotificationManager.createNotificationChannel(appUpdateChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public static RefWatcher getRefWatcher(Context context) {
|
|
||||||
final App application = (App) context.getApplicationContext();
|
|
||||||
return application.refWatcher;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected RefWatcher installLeakCanary() {
|
|
||||||
return RefWatcher.DISABLED;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected boolean isDisposedRxExceptionsReported() {
|
protected boolean isDisposedRxExceptionsReported() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static App getApp() {
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -2,32 +2,31 @@ package org.schabi.newpipe;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import androidx.fragment.app.FragmentManager;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.fragment.app.FragmentManager;
|
||||||
|
|
||||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||||
import com.squareup.leakcanary.RefWatcher;
|
|
||||||
|
|
||||||
import icepick.Icepick;
|
import icepick.Icepick;
|
||||||
import icepick.State;
|
import icepick.State;
|
||||||
|
import leakcanary.AppWatcher;
|
||||||
|
|
||||||
public abstract class BaseFragment extends Fragment {
|
public abstract class BaseFragment extends Fragment {
|
||||||
|
public static final ImageLoader IMAGE_LOADER = ImageLoader.getInstance();
|
||||||
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
|
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
|
||||||
protected final boolean DEBUG = MainActivity.DEBUG;
|
protected final boolean DEBUG = MainActivity.DEBUG;
|
||||||
|
|
||||||
protected AppCompatActivity activity;
|
protected AppCompatActivity activity;
|
||||||
public static final ImageLoader imageLoader = ImageLoader.getInstance();
|
//These values are used for controlling fragments when they are part of the frontpage
|
||||||
|
|
||||||
//These values are used for controlling framgents when they are part of the frontpage
|
|
||||||
@State
|
@State
|
||||||
protected boolean useAsFrontPage = false;
|
protected boolean useAsFrontPage = false;
|
||||||
protected boolean mIsVisibleToUser = false;
|
private boolean mIsVisibleToUser = false;
|
||||||
|
|
||||||
public void useAsFrontPage(boolean value) {
|
public void useAsFrontPage(final boolean value) {
|
||||||
useAsFrontPage = value;
|
useAsFrontPage = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,7 +35,7 @@ public abstract class BaseFragment extends Fragment {
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAttach(Context context) {
|
public void onAttach(final Context context) {
|
||||||
super.onAttach(context);
|
super.onAttach(context);
|
||||||
activity = (AppCompatActivity) context;
|
activity = (AppCompatActivity) context;
|
||||||
}
|
}
|
||||||
@ -48,43 +47,49 @@ public abstract class BaseFragment extends Fragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(final Bundle savedInstanceState) {
|
||||||
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onCreate() called with: "
|
||||||
|
+ "savedInstanceState = [" + savedInstanceState + "]");
|
||||||
|
}
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
Icepick.restoreInstanceState(this, savedInstanceState);
|
||||||
if (savedInstanceState != null) onRestoreInstanceState(savedInstanceState);
|
if (savedInstanceState != null) {
|
||||||
|
onRestoreInstanceState(savedInstanceState);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onViewCreated(View rootView, Bundle savedInstanceState) {
|
public void onViewCreated(final View rootView, final Bundle savedInstanceState) {
|
||||||
super.onViewCreated(rootView, savedInstanceState);
|
super.onViewCreated(rootView, savedInstanceState);
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "onViewCreated() called with: rootView = [" + rootView + "], savedInstanceState = [" + savedInstanceState + "]");
|
Log.d(TAG, "onViewCreated() called with: "
|
||||||
|
+ "rootView = [" + rootView + "], "
|
||||||
|
+ "savedInstanceState = [" + savedInstanceState + "]");
|
||||||
}
|
}
|
||||||
initViews(rootView, savedInstanceState);
|
initViews(rootView, savedInstanceState);
|
||||||
initListeners();
|
initListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSaveInstanceState(Bundle outState) {
|
public void onSaveInstanceState(final Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
Icepick.saveInstanceState(this, outState);
|
Icepick.saveInstanceState(this, outState);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
|
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
|
|
||||||
RefWatcher refWatcher = App.getRefWatcher(getActivity());
|
AppWatcher.INSTANCE.getObjectWatcher().watch(this);
|
||||||
if (refWatcher != null) refWatcher.watch(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setUserVisibleHint(boolean isVisibleToUser) {
|
public void setUserVisibleHint(final boolean isVisibleToUser) {
|
||||||
super.setUserVisibleHint(isVisibleToUser);
|
super.setUserVisibleHint(isVisibleToUser);
|
||||||
mIsVisibleToUser = isVisibleToUser;
|
mIsVisibleToUser = isVisibleToUser;
|
||||||
}
|
}
|
||||||
@ -93,7 +98,7 @@ public abstract class BaseFragment extends Fragment {
|
|||||||
// Init
|
// Init
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void initListeners() {
|
protected void initListeners() {
|
||||||
@ -103,10 +108,12 @@ public abstract class BaseFragment extends Fragment {
|
|||||||
// Utils
|
// Utils
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
public void setTitle(String title) {
|
public void setTitle(final String title) {
|
||||||
if (DEBUG) Log.d(TAG, "setTitle() called with: title = [" + title + "]");
|
if (DEBUG) {
|
||||||
if((!useAsFrontPage || mIsVisibleToUser)
|
Log.d(TAG, "setTitle() called with: title = [" + title + "]");
|
||||||
&& (activity != null && activity.getSupportActionBar() != null)) {
|
}
|
||||||
|
if ((!useAsFrontPage || mIsVisibleToUser)
|
||||||
|
&& (activity != null && activity.getSupportActionBar() != null)) {
|
||||||
activity.getSupportActionBar().setDisplayShowTitleEnabled(true);
|
activity.getSupportActionBar().setDisplayShowTitleEnabled(true);
|
||||||
activity.getSupportActionBar().setTitle(title);
|
activity.getSupportActionBar().setTitle(title);
|
||||||
}
|
}
|
||||||
|
@ -12,12 +12,16 @@ import android.net.ConnectivityManager;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import androidx.core.app.NotificationCompat;
|
|
||||||
import androidx.core.app.NotificationManagerCompat;
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import org.json.JSONException;
|
import androidx.core.app.NotificationCompat;
|
||||||
import org.json.JSONObject;
|
import androidx.core.app.NotificationManagerCompat;
|
||||||
|
|
||||||
|
import com.grack.nanojson.JsonObject;
|
||||||
|
import com.grack.nanojson.JsonParser;
|
||||||
|
import com.grack.nanojson.JsonParserException;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||||
import org.schabi.newpipe.report.ErrorActivity;
|
import org.schabi.newpipe.report.ErrorActivity;
|
||||||
import org.schabi.newpipe.report.UserAction;
|
import org.schabi.newpipe.report.UserAction;
|
||||||
|
|
||||||
@ -30,11 +34,6 @@ import java.security.cert.CertificateEncodingException;
|
|||||||
import java.security.cert.CertificateException;
|
import java.security.cert.CertificateException;
|
||||||
import java.security.cert.CertificateFactory;
|
import java.security.cert.CertificateFactory;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import okhttp3.OkHttpClient;
|
|
||||||
import okhttp3.Request;
|
|
||||||
import okhttp3.Response;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AsyncTask to check if there is a newer version of the NewPipe github apk available or not.
|
* AsyncTask to check if there is a newer version of the NewPipe github apk available or not.
|
||||||
@ -42,149 +41,44 @@ import okhttp3.Response;
|
|||||||
* the notification, the user will be directed to the download link.
|
* the notification, the user will be directed to the download link.
|
||||||
*/
|
*/
|
||||||
public class CheckForNewAppVersionTask extends AsyncTask<Void, Void, String> {
|
public class CheckForNewAppVersionTask extends AsyncTask<Void, Void, String> {
|
||||||
|
|
||||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||||
private static final String TAG = CheckForNewAppVersionTask.class.getSimpleName();
|
private static final String TAG = CheckForNewAppVersionTask.class.getSimpleName();
|
||||||
private static final Application app = App.getApp();
|
|
||||||
private static final String GITHUB_APK_SHA1 = "B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15";
|
|
||||||
private static final String newPipeApiUrl = "https://newpipe.schabi.org/api/data.json";
|
|
||||||
private static final int timeoutPeriod = 30;
|
|
||||||
|
|
||||||
private SharedPreferences mPrefs;
|
private static final Application APP = App.getApp();
|
||||||
private OkHttpClient client;
|
private static final String GITHUB_APK_SHA1
|
||||||
|
= "B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15";
|
||||||
@Override
|
private static final String NEWPIPE_API_URL = "https://newpipe.schabi.org/api/data.json";
|
||||||
protected void onPreExecute() {
|
|
||||||
|
|
||||||
mPrefs = PreferenceManager.getDefaultSharedPreferences(app);
|
|
||||||
|
|
||||||
// Check if user has enabled/ disabled update checking
|
|
||||||
// and if the current apk is a github one or not.
|
|
||||||
if (!mPrefs.getBoolean(app.getString(R.string.update_app_key), true)
|
|
||||||
|| !isGithubApk()) {
|
|
||||||
this.cancel(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String doInBackground(Void... voids) {
|
|
||||||
|
|
||||||
if(isCancelled() || !isConnected()) return null;
|
|
||||||
|
|
||||||
// Make a network request to get latest NewPipe data.
|
|
||||||
if (client == null) {
|
|
||||||
|
|
||||||
client = new OkHttpClient
|
|
||||||
.Builder()
|
|
||||||
.readTimeout(timeoutPeriod, TimeUnit.SECONDS)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
Request request = new Request.Builder()
|
|
||||||
.url(newPipeApiUrl)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
try {
|
|
||||||
Response response = client.newCall(request).execute();
|
|
||||||
return response.body().string();
|
|
||||||
} catch (IOException ex) {
|
|
||||||
// connectivity problems, do not alarm user and fail silently
|
|
||||||
if (DEBUG) Log.w(TAG, Log.getStackTraceString(ex));
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(String response) {
|
|
||||||
|
|
||||||
// Parse the json from the response.
|
|
||||||
if (response != null) {
|
|
||||||
|
|
||||||
try {
|
|
||||||
JSONObject mainObject = new JSONObject(response);
|
|
||||||
JSONObject flavoursObject = mainObject.getJSONObject("flavors");
|
|
||||||
JSONObject githubObject = flavoursObject.getJSONObject("github");
|
|
||||||
JSONObject githubStableObject = githubObject.getJSONObject("stable");
|
|
||||||
|
|
||||||
String versionName = githubStableObject.getString("version");
|
|
||||||
String versionCode = githubStableObject.getString("version_code");
|
|
||||||
String apkLocationUrl = githubStableObject.getString("apk");
|
|
||||||
|
|
||||||
compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode);
|
|
||||||
|
|
||||||
} catch (JSONException ex) {
|
|
||||||
// connectivity problems, do not alarm user and fail silently
|
|
||||||
if (DEBUG) Log.w(TAG, Log.getStackTraceString(ex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to compare the current and latest available app version.
|
* Method to get the apk's SHA1 key. See https://stackoverflow.com/questions/9293019/#22506133.
|
||||||
* If a newer version is available, we show the update notification.
|
*
|
||||||
* @param versionName
|
* @return String with the apk's SHA1 fingeprint in hexadecimal
|
||||||
* @param apkLocationUrl
|
|
||||||
*/
|
|
||||||
private void compareAppVersionAndShowNotification(String versionName,
|
|
||||||
String apkLocationUrl,
|
|
||||||
String versionCode) {
|
|
||||||
|
|
||||||
int NOTIFICATION_ID = 2000;
|
|
||||||
|
|
||||||
if (BuildConfig.VERSION_CODE < Integer.valueOf(versionCode)) {
|
|
||||||
|
|
||||||
// A pending intent to open the apk location url in the browser.
|
|
||||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl));
|
|
||||||
PendingIntent pendingIntent
|
|
||||||
= PendingIntent.getActivity(app, 0, intent, 0);
|
|
||||||
|
|
||||||
NotificationCompat.Builder notificationBuilder = new NotificationCompat
|
|
||||||
.Builder(app, app.getString(R.string.app_update_notification_channel_id))
|
|
||||||
.setSmallIcon(R.drawable.ic_newpipe_update)
|
|
||||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
||||||
.setContentIntent(pendingIntent)
|
|
||||||
.setAutoCancel(true)
|
|
||||||
.setContentTitle(app.getString(R.string.app_update_notification_content_title))
|
|
||||||
.setContentText(app.getString(R.string.app_update_notification_content_text)
|
|
||||||
+ " " + versionName);
|
|
||||||
|
|
||||||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(app);
|
|
||||||
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method to get the apk's SHA1 key.
|
|
||||||
* https://stackoverflow.com/questions/9293019/get-certificate-fingerprint-from-android-app#22506133
|
|
||||||
*/
|
*/
|
||||||
private static String getCertificateSHA1Fingerprint() {
|
private static String getCertificateSHA1Fingerprint() {
|
||||||
|
final PackageManager pm = APP.getPackageManager();
|
||||||
PackageManager pm = app.getPackageManager();
|
final String packageName = APP.getPackageName();
|
||||||
String packageName = app.getPackageName();
|
final int flags = PackageManager.GET_SIGNATURES;
|
||||||
int flags = PackageManager.GET_SIGNATURES;
|
|
||||||
PackageInfo packageInfo = null;
|
PackageInfo packageInfo = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
packageInfo = pm.getPackageInfo(packageName, flags);
|
packageInfo = pm.getPackageInfo(packageName, flags);
|
||||||
} catch (PackageManager.NameNotFoundException ex) {
|
} catch (PackageManager.NameNotFoundException e) {
|
||||||
ErrorActivity.reportError(app, ex, null, null,
|
ErrorActivity.reportError(APP, e, null, null,
|
||||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||||
"Could not find package info", R.string.app_ui_crash));
|
"Could not find package info", R.string.app_ui_crash));
|
||||||
}
|
}
|
||||||
|
|
||||||
Signature[] signatures = packageInfo.signatures;
|
final Signature[] signatures = packageInfo.signatures;
|
||||||
byte[] cert = signatures[0].toByteArray();
|
final byte[] cert = signatures[0].toByteArray();
|
||||||
InputStream input = new ByteArrayInputStream(cert);
|
final InputStream input = new ByteArrayInputStream(cert);
|
||||||
|
|
||||||
CertificateFactory cf = null;
|
|
||||||
X509Certificate c = null;
|
X509Certificate c = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
cf = CertificateFactory.getInstance("X509");
|
final CertificateFactory cf = CertificateFactory.getInstance("X509");
|
||||||
c = (X509Certificate) cf.generateCertificate(input);
|
c = (X509Certificate) cf.generateCertificate(input);
|
||||||
} catch (CertificateException ex) {
|
} catch (CertificateException e) {
|
||||||
ErrorActivity.reportError(app, ex, null, null,
|
ErrorActivity.reportError(APP, e, null, null,
|
||||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||||
"Certificate error", R.string.app_ui_crash));
|
"Certificate error", R.string.app_ui_crash));
|
||||||
}
|
}
|
||||||
@ -193,14 +87,10 @@ public class CheckForNewAppVersionTask extends AsyncTask<Void, Void, String> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
MessageDigest md = MessageDigest.getInstance("SHA1");
|
MessageDigest md = MessageDigest.getInstance("SHA1");
|
||||||
byte[] publicKey = md.digest(c.getEncoded());
|
final byte[] publicKey = md.digest(c.getEncoded());
|
||||||
hexString = byte2HexFormatted(publicKey);
|
hexString = byte2HexFormatted(publicKey);
|
||||||
} catch (NoSuchAlgorithmException ex1) {
|
} catch (NoSuchAlgorithmException | CertificateEncodingException e) {
|
||||||
ErrorActivity.reportError(app, ex1, null, null,
|
ErrorActivity.reportError(APP, e, null, null,
|
||||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
|
||||||
"Could not retrieve SHA1 key", R.string.app_ui_crash));
|
|
||||||
} catch (CertificateEncodingException ex2) {
|
|
||||||
ErrorActivity.reportError(app, ex2, null, null,
|
|
||||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||||
"Could not retrieve SHA1 key", R.string.app_ui_crash));
|
"Could not retrieve SHA1 key", R.string.app_ui_crash));
|
||||||
}
|
}
|
||||||
@ -208,31 +98,124 @@ public class CheckForNewAppVersionTask extends AsyncTask<Void, Void, String> {
|
|||||||
return hexString;
|
return hexString;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String byte2HexFormatted(byte[] arr) {
|
private static String byte2HexFormatted(final byte[] arr) {
|
||||||
|
final StringBuilder str = new StringBuilder(arr.length * 2);
|
||||||
StringBuilder str = new StringBuilder(arr.length * 2);
|
|
||||||
|
|
||||||
for (int i = 0; i < arr.length; i++) {
|
for (int i = 0; i < arr.length; i++) {
|
||||||
String h = Integer.toHexString(arr[i]);
|
String h = Integer.toHexString(arr[i]);
|
||||||
int l = h.length();
|
final int l = h.length();
|
||||||
if (l == 1) h = "0" + h;
|
if (l == 1) {
|
||||||
if (l > 2) h = h.substring(l - 2, l);
|
h = "0" + h;
|
||||||
|
}
|
||||||
|
if (l > 2) {
|
||||||
|
h = h.substring(l - 2, l);
|
||||||
|
}
|
||||||
str.append(h.toUpperCase());
|
str.append(h.toUpperCase());
|
||||||
if (i < (arr.length - 1)) str.append(':');
|
if (i < (arr.length - 1)) {
|
||||||
|
str.append(':');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return str.toString();
|
return str.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isGithubApk() {
|
public static boolean isGithubApk() {
|
||||||
|
|
||||||
return getCertificateSHA1Fingerprint().equals(GITHUB_APK_SHA1);
|
return getCertificateSHA1Fingerprint().equals(GITHUB_APK_SHA1);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isConnected() {
|
@Override
|
||||||
|
protected void onPreExecute() {
|
||||||
|
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(APP);
|
||||||
|
|
||||||
ConnectivityManager cm =
|
// Check if user has enabled/disabled update checking
|
||||||
(ConnectivityManager) app.getSystemService(Context.CONNECTIVITY_SERVICE);
|
// and if the current apk is a github one or not.
|
||||||
|
if (!prefs.getBoolean(APP.getString(R.string.update_app_key), true) || !isGithubApk()) {
|
||||||
|
this.cancel(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String doInBackground(final Void... voids) {
|
||||||
|
if (isCancelled() || !isConnected()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a network request to get latest NewPipe data.
|
||||||
|
try {
|
||||||
|
return DownloaderImpl.getInstance().get(NEWPIPE_API_URL).responseBody();
|
||||||
|
} catch (IOException | ReCaptchaException e) {
|
||||||
|
// connectivity problems, do not alarm user and fail silently
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.w(TAG, Log.getStackTraceString(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(final String response) {
|
||||||
|
// Parse the json from the response.
|
||||||
|
if (response != null) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
final JsonObject githubStableObject = JsonParser.object().from(response)
|
||||||
|
.getObject("flavors").getObject("github").getObject("stable");
|
||||||
|
|
||||||
|
final String versionName = githubStableObject.getString("version");
|
||||||
|
final int versionCode = githubStableObject.getInt("version_code");
|
||||||
|
final String apkLocationUrl = githubStableObject.getString("apk");
|
||||||
|
|
||||||
|
compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode);
|
||||||
|
|
||||||
|
} catch (JsonParserException e) {
|
||||||
|
// connectivity problems, do not alarm user and fail silently
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.w(TAG, Log.getStackTraceString(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to compare the current and latest available app version.
|
||||||
|
* If a newer version is available, we show the update notification.
|
||||||
|
*
|
||||||
|
* @param versionName Name of new version
|
||||||
|
* @param apkLocationUrl Url with the new apk
|
||||||
|
* @param versionCode Code of new version
|
||||||
|
*/
|
||||||
|
private void compareAppVersionAndShowNotification(final String versionName,
|
||||||
|
final String apkLocationUrl,
|
||||||
|
final int versionCode) {
|
||||||
|
int notificationId = 2000;
|
||||||
|
|
||||||
|
if (BuildConfig.VERSION_CODE < versionCode) {
|
||||||
|
|
||||||
|
// A pending intent to open the apk location url in the browser.
|
||||||
|
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl));
|
||||||
|
final PendingIntent pendingIntent
|
||||||
|
= PendingIntent.getActivity(APP, 0, intent, 0);
|
||||||
|
|
||||||
|
final NotificationCompat.Builder notificationBuilder = new NotificationCompat
|
||||||
|
.Builder(APP, APP.getString(R.string.app_update_notification_channel_id))
|
||||||
|
.setSmallIcon(R.drawable.ic_newpipe_update)
|
||||||
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||||
|
.setContentIntent(pendingIntent)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setContentTitle(APP.getString(R.string.app_update_notification_content_title))
|
||||||
|
.setContentText(APP.getString(R.string.app_update_notification_content_text)
|
||||||
|
+ " " + versionName);
|
||||||
|
|
||||||
|
final NotificationManagerCompat notificationManager
|
||||||
|
= NotificationManagerCompat.from(APP);
|
||||||
|
notificationManager.notify(notificationId, notificationBuilder.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isConnected() {
|
||||||
|
final ConnectivityManager cm =
|
||||||
|
(ConnectivityManager) APP.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||||
return cm.getActiveNetworkInfo() != null
|
return cm.getActiveNetworkInfo() != null
|
||||||
&& cm.getActiveNetworkInfo().isConnected();
|
&& cm.getActiveNetworkInfo().isConnected();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,18 @@
|
|||||||
package org.schabi.newpipe;
|
package org.schabi.newpipe;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.text.TextUtils;
|
import android.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||||
import org.schabi.newpipe.extractor.downloader.Request;
|
import org.schabi.newpipe.extractor.downloader.Request;
|
||||||
import org.schabi.newpipe.extractor.downloader.Response;
|
import org.schabi.newpipe.extractor.downloader.Response;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||||
|
import org.schabi.newpipe.util.CookieUtils;
|
||||||
|
import org.schabi.newpipe.util.InfoCache;
|
||||||
import org.schabi.newpipe.util.TLSSocketFactoryCompat;
|
import org.schabi.newpipe.util.TLSSocketFactoryCompat;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -17,6 +23,7 @@ import java.security.KeyStoreException;
|
|||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@ -26,9 +33,6 @@ import javax.net.ssl.TrustManager;
|
|||||||
import javax.net.ssl.TrustManagerFactory;
|
import javax.net.ssl.TrustManagerFactory;
|
||||||
import javax.net.ssl.X509TrustManager;
|
import javax.net.ssl.X509TrustManager;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import okhttp3.CipherSuite;
|
import okhttp3.CipherSuite;
|
||||||
import okhttp3.ConnectionSpec;
|
import okhttp3.ConnectionSpec;
|
||||||
import okhttp3.OkHttpClient;
|
import okhttp3.OkHttpClient;
|
||||||
@ -37,42 +41,139 @@ import okhttp3.ResponseBody;
|
|||||||
|
|
||||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||||
|
|
||||||
public class DownloaderImpl extends Downloader {
|
public final class DownloaderImpl extends Downloader {
|
||||||
public static final String USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:43.0) Gecko/20100101 Firefox/43.0";
|
public static final String USER_AGENT
|
||||||
|
= "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:68.0) Gecko/20100101 Firefox/68.0";
|
||||||
|
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE_KEY
|
||||||
|
= "youtube_restricted_mode_key";
|
||||||
|
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE = "PREF=f2=8000000";
|
||||||
|
public static final String YOUTUBE_DOMAIN = "youtube.com";
|
||||||
|
|
||||||
private static DownloaderImpl instance;
|
private static DownloaderImpl instance;
|
||||||
private String mCookies;
|
private Map<String, String> mCookies;
|
||||||
private OkHttpClient client;
|
private OkHttpClient client;
|
||||||
|
|
||||||
private DownloaderImpl(OkHttpClient.Builder builder) {
|
private DownloaderImpl(final OkHttpClient.Builder builder) {
|
||||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
|
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
|
||||||
enableModernTLS(builder);
|
enableModernTLS(builder);
|
||||||
}
|
}
|
||||||
this.client = builder
|
this.client = builder
|
||||||
.readTimeout(30, TimeUnit.SECONDS)
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
//.cache(new Cache(new File(context.getExternalCacheDir(), "okhttp"), 16 * 1024 * 1024))
|
// .cache(new Cache(new File(context.getExternalCacheDir(), "okhttp"),
|
||||||
|
// 16 * 1024 * 1024))
|
||||||
.build();
|
.build();
|
||||||
|
this.mCookies = new HashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* It's recommended to call exactly once in the entire lifetime of the application.
|
* It's recommended to call exactly once in the entire lifetime of the application.
|
||||||
*
|
*
|
||||||
* @param builder if null, default builder will be used
|
* @param builder if null, default builder will be used
|
||||||
|
* @return a new instance of {@link DownloaderImpl}
|
||||||
*/
|
*/
|
||||||
public static DownloaderImpl init(@Nullable OkHttpClient.Builder builder) {
|
public static DownloaderImpl init(@Nullable final OkHttpClient.Builder builder) {
|
||||||
return instance = new DownloaderImpl(builder != null ? builder : new OkHttpClient.Builder());
|
instance = new DownloaderImpl(
|
||||||
|
builder != null ? builder : new OkHttpClient.Builder());
|
||||||
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static DownloaderImpl getInstance() {
|
public static DownloaderImpl getInstance() {
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getCookies() {
|
/**
|
||||||
return mCookies;
|
* Enable TLS 1.2 and 1.1 on Android Kitkat. This function is mostly taken
|
||||||
|
* from the documentation of OkHttpClient.Builder.sslSocketFactory(_,_).
|
||||||
|
* <p>
|
||||||
|
* If there is an error, the function will safely fall back to doing nothing
|
||||||
|
* and printing the error to the console.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param builder The HTTPClient Builder on which TLS is enabled on (will be modified in-place)
|
||||||
|
*/
|
||||||
|
private static void enableModernTLS(final OkHttpClient.Builder builder) {
|
||||||
|
try {
|
||||||
|
// get the default TrustManager
|
||||||
|
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
|
||||||
|
TrustManagerFactory.getDefaultAlgorithm());
|
||||||
|
trustManagerFactory.init((KeyStore) null);
|
||||||
|
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
|
||||||
|
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
|
||||||
|
throw new IllegalStateException("Unexpected default trust managers:"
|
||||||
|
+ Arrays.toString(trustManagers));
|
||||||
|
}
|
||||||
|
X509TrustManager trustManager = (X509TrustManager) trustManagers[0];
|
||||||
|
|
||||||
|
// insert our own TLSSocketFactory
|
||||||
|
SSLSocketFactory sslSocketFactory = TLSSocketFactoryCompat.getInstance();
|
||||||
|
|
||||||
|
builder.sslSocketFactory(sslSocketFactory, trustManager);
|
||||||
|
|
||||||
|
// This will try to enable all modern CipherSuites(+2 more)
|
||||||
|
// that are supported on the device.
|
||||||
|
// Necessary because some servers (e.g. Framatube.org)
|
||||||
|
// don't support the old cipher suites.
|
||||||
|
// https://github.com/square/okhttp/issues/4053#issuecomment-402579554
|
||||||
|
List<CipherSuite> cipherSuites = new ArrayList<>();
|
||||||
|
cipherSuites.addAll(ConnectionSpec.MODERN_TLS.cipherSuites());
|
||||||
|
cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA);
|
||||||
|
cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA);
|
||||||
|
ConnectionSpec legacyTLS = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
|
||||||
|
.cipherSuites(cipherSuites.toArray(new CipherSuite[0]))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
builder.connectionSpecs(Arrays.asList(legacyTLS, ConnectionSpec.CLEARTEXT));
|
||||||
|
} catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) {
|
||||||
|
if (DEBUG) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setCookies(String cookies) {
|
public String getCookies(final String url) {
|
||||||
mCookies = cookies;
|
List<String> resultCookies = new ArrayList<>();
|
||||||
|
if (url.contains(YOUTUBE_DOMAIN)) {
|
||||||
|
String youtubeCookie = getCookie(YOUTUBE_RESTRICTED_MODE_COOKIE_KEY);
|
||||||
|
if (youtubeCookie != null) {
|
||||||
|
resultCookies.add(youtubeCookie);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Recaptcha cookie is always added TODO: not sure if this is necessary
|
||||||
|
String recaptchaCookie = getCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY);
|
||||||
|
if (recaptchaCookie != null) {
|
||||||
|
resultCookies.add(recaptchaCookie);
|
||||||
|
}
|
||||||
|
return CookieUtils.concatCookies(resultCookies);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCookie(final String key) {
|
||||||
|
return mCookies.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCookie(final String key, final String cookie) {
|
||||||
|
mCookies.put(key, cookie);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeCookie(final String key) {
|
||||||
|
mCookies.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateYoutubeRestrictedModeCookies(final Context context) {
|
||||||
|
String restrictedModeEnabledKey =
|
||||||
|
context.getString(R.string.youtube_restricted_mode_enabled);
|
||||||
|
boolean restrictedModeEnabled = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
.getBoolean(restrictedModeEnabledKey, false);
|
||||||
|
updateYoutubeRestrictedModeCookies(restrictedModeEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateYoutubeRestrictedModeCookies(final boolean youtubeRestrictedModeEnabled) {
|
||||||
|
if (youtubeRestrictedModeEnabled) {
|
||||||
|
setCookie(YOUTUBE_RESTRICTED_MODE_COOKIE_KEY,
|
||||||
|
YOUTUBE_RESTRICTED_MODE_COOKIE);
|
||||||
|
} else {
|
||||||
|
removeCookie(YOUTUBE_RESTRICTED_MODE_COOKIE_KEY);
|
||||||
|
}
|
||||||
|
InfoCache.getInstance().clearCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -81,7 +182,7 @@ public class DownloaderImpl extends Downloader {
|
|||||||
* @param url an url pointing to the content
|
* @param url an url pointing to the content
|
||||||
* @return the size of the content, in bytes
|
* @return the size of the content, in bytes
|
||||||
*/
|
*/
|
||||||
public long getContentLength(String url) throws IOException {
|
public long getContentLength(final String url) throws IOException {
|
||||||
try {
|
try {
|
||||||
final Response response = head(url);
|
final Response response = head(url);
|
||||||
return Long.parseLong(response.getHeader("Content-Length"));
|
return Long.parseLong(response.getHeader("Content-Length"));
|
||||||
@ -92,14 +193,15 @@ public class DownloaderImpl extends Downloader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public InputStream stream(String siteUrl) throws IOException {
|
public InputStream stream(final String siteUrl) throws IOException {
|
||||||
try {
|
try {
|
||||||
final okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder()
|
final okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder()
|
||||||
.method("GET", null).url(siteUrl)
|
.method("GET", null).url(siteUrl)
|
||||||
.addHeader("User-Agent", USER_AGENT);
|
.addHeader("User-Agent", USER_AGENT);
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(mCookies)) {
|
String cookies = getCookies(siteUrl);
|
||||||
requestBuilder.addHeader("Cookie", mCookies);
|
if (!cookies.isEmpty()) {
|
||||||
|
requestBuilder.addHeader("Cookie", cookies);
|
||||||
}
|
}
|
||||||
|
|
||||||
final okhttp3.Request request = requestBuilder.build();
|
final okhttp3.Request request = requestBuilder.build();
|
||||||
@ -122,7 +224,8 @@ public class DownloaderImpl extends Downloader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Response execute(@NonNull Request request) throws IOException, ReCaptchaException {
|
public Response execute(@NonNull final Request request)
|
||||||
|
throws IOException, ReCaptchaException {
|
||||||
final String httpMethod = request.httpMethod();
|
final String httpMethod = request.httpMethod();
|
||||||
final String url = request.url();
|
final String url = request.url();
|
||||||
final Map<String, List<String>> headers = request.headers();
|
final Map<String, List<String>> headers = request.headers();
|
||||||
@ -137,8 +240,9 @@ public class DownloaderImpl extends Downloader {
|
|||||||
.method(httpMethod, requestBody).url(url)
|
.method(httpMethod, requestBody).url(url)
|
||||||
.addHeader("User-Agent", USER_AGENT);
|
.addHeader("User-Agent", USER_AGENT);
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(mCookies)) {
|
String cookies = getCookies(url);
|
||||||
requestBuilder.addHeader("Cookie", mCookies);
|
if (!cookies.isEmpty()) {
|
||||||
|
requestBuilder.addHeader("Cookie", cookies);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (Map.Entry<String, List<String>> pair : headers.entrySet()) {
|
for (Map.Entry<String, List<String>> pair : headers.entrySet()) {
|
||||||
@ -171,49 +275,8 @@ public class DownloaderImpl extends Downloader {
|
|||||||
responseBodyToReturn = body.string();
|
responseBodyToReturn = body.string();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Response(response.code(), response.message(), response.headers().toMultimap(), responseBodyToReturn);
|
final String latestUrl = response.request().url().toString();
|
||||||
}
|
return new Response(response.code(), response.message(), response.headers().toMultimap(),
|
||||||
|
responseBodyToReturn, latestUrl);
|
||||||
/**
|
|
||||||
* Enable TLS 1.2 and 1.1 on Android Kitkat. This function is mostly taken from the documentation of
|
|
||||||
* OkHttpClient.Builder.sslSocketFactory(_,_)
|
|
||||||
* <p>
|
|
||||||
* If there is an error, the function will safely fall back to doing nothing and printing the error to the console.
|
|
||||||
*
|
|
||||||
* @param builder The HTTPClient Builder on which TLS is enabled on (will be modified in-place)
|
|
||||||
*/
|
|
||||||
private static void enableModernTLS(OkHttpClient.Builder builder) {
|
|
||||||
try {
|
|
||||||
// get the default TrustManager
|
|
||||||
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
|
|
||||||
TrustManagerFactory.getDefaultAlgorithm());
|
|
||||||
trustManagerFactory.init((KeyStore) null);
|
|
||||||
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
|
|
||||||
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
|
|
||||||
throw new IllegalStateException("Unexpected default trust managers:"
|
|
||||||
+ Arrays.toString(trustManagers));
|
|
||||||
}
|
|
||||||
X509TrustManager trustManager = (X509TrustManager) trustManagers[0];
|
|
||||||
|
|
||||||
// insert our own TLSSocketFactory
|
|
||||||
SSLSocketFactory sslSocketFactory = TLSSocketFactoryCompat.getInstance();
|
|
||||||
|
|
||||||
builder.sslSocketFactory(sslSocketFactory, trustManager);
|
|
||||||
|
|
||||||
// This will try to enable all modern CipherSuites(+2 more) that are supported on the device.
|
|
||||||
// Necessary because some servers (e.g. Framatube.org) don't support the old cipher suites.
|
|
||||||
// https://github.com/square/okhttp/issues/4053#issuecomment-402579554
|
|
||||||
List<CipherSuite> cipherSuites = new ArrayList<>();
|
|
||||||
cipherSuites.addAll(ConnectionSpec.MODERN_TLS.cipherSuites());
|
|
||||||
cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA);
|
|
||||||
cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA);
|
|
||||||
ConnectionSpec legacyTLS = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
|
|
||||||
.cipherSuites(cipherSuites.toArray(new CipherSuite[0]))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
builder.connectionSpecs(Arrays.asList(legacyTLS, ConnectionSpec.CLEARTEXT));
|
|
||||||
} catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) {
|
|
||||||
if (DEBUG) e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
package org.schabi.newpipe;
|
package org.schabi.newpipe;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
@ -27,9 +26,20 @@ import android.os.Bundle;
|
|||||||
|
|
||||||
public class ExitActivity extends Activity {
|
public class ExitActivity extends Activity {
|
||||||
|
|
||||||
|
public static void exitAndRemoveFromRecentApps(final Activity activity) {
|
||||||
|
Intent intent = new Intent(activity, ExitActivity.class);
|
||||||
|
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
| Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
|
||||||
|
| Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
| Intent.FLAG_ACTIVITY_NO_ANIMATION);
|
||||||
|
|
||||||
|
activity.startActivity(intent);
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("NewApi")
|
@SuppressLint("NewApi")
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(final Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= 21) {
|
if (Build.VERSION.SDK_INT >= 21) {
|
||||||
@ -40,15 +50,4 @@ public class ExitActivity extends Activity {
|
|||||||
|
|
||||||
System.exit(0);
|
System.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void exitAndRemoveFromRecentApps(Activity activity) {
|
|
||||||
Intent intent = new Intent(activity, ExitActivity.class);
|
|
||||||
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
| Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
|
|
||||||
| Intent.FLAG_ACTIVITY_CLEAR_TASK
|
|
||||||
| Intent.FLAG_ACTIVITY_NO_ANIMATION);
|
|
||||||
|
|
||||||
activity.startActivity(intent);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ public class ImageDownloader extends BaseImageDownloader {
|
|||||||
private final SharedPreferences preferences;
|
private final SharedPreferences preferences;
|
||||||
private final String downloadThumbnailKey;
|
private final String downloadThumbnailKey;
|
||||||
|
|
||||||
public ImageDownloader(Context context) {
|
public ImageDownloader(final Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
this.resources = context.getResources();
|
this.resources = context.getResources();
|
||||||
this.preferences = PreferenceManager.getDefaultSharedPreferences(context);
|
this.preferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
@ -31,7 +31,7 @@ public class ImageDownloader extends BaseImageDownloader {
|
|||||||
|
|
||||||
@SuppressLint("ResourceType")
|
@SuppressLint("ResourceType")
|
||||||
@Override
|
@Override
|
||||||
public InputStream getStream(String imageUri, Object extra) throws IOException {
|
public InputStream getStream(final String imageUri, final Object extra) throws IOException {
|
||||||
if (isDownloadingThumbnail()) {
|
if (isDownloadingThumbnail()) {
|
||||||
return super.getStream(imageUri, extra);
|
return super.getStream(imageUri, extra);
|
||||||
} else {
|
} else {
|
||||||
@ -39,7 +39,8 @@ public class ImageDownloader extends BaseImageDownloader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException {
|
protected InputStream getStreamFromNetwork(final String imageUri, final Object extra)
|
||||||
|
throws IOException {
|
||||||
final DownloaderImpl downloader = (DownloaderImpl) NewPipe.getDownloader();
|
final DownloaderImpl downloader = (DownloaderImpl) NewPipe.getDownloader();
|
||||||
return downloader.stream(imageUri);
|
return downloader.stream(imageUri);
|
||||||
}
|
}
|
||||||
|
@ -53,31 +53,36 @@ import org.schabi.newpipe.fragments.MainFragment;
|
|||||||
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
||||||
import org.schabi.newpipe.fragments.list.search.SearchFragment;
|
import org.schabi.newpipe.fragments.list.search.SearchFragment;
|
||||||
import org.schabi.newpipe.player.VideoPlayer;
|
import org.schabi.newpipe.player.VideoPlayer;
|
||||||
|
import org.schabi.newpipe.player.event.OnKeyDownListener;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.report.ErrorActivity;
|
import org.schabi.newpipe.report.ErrorActivity;
|
||||||
import org.schabi.newpipe.util.*;
|
import org.schabi.newpipe.util.*;
|
||||||
|
import org.schabi.newpipe.views.FocusOverlayView;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||||
|
|
||||||
public class MainActivity extends AppCompatActivity {
|
public class MainActivity extends AppCompatActivity {
|
||||||
private static final String TAG = "MainActivity";
|
private static final String TAG = "MainActivity";
|
||||||
public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release");
|
public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release");
|
||||||
|
|
||||||
private ActionBarDrawerToggle toggle = null;
|
private ActionBarDrawerToggle toggle;
|
||||||
private DrawerLayout drawer = null;
|
private DrawerLayout drawer;
|
||||||
private NavigationView drawerItems = null;
|
private NavigationView drawerItems;
|
||||||
private TextView headerServiceView = null;
|
private ImageView headerServiceIcon;
|
||||||
private Button toggleServiceButton = null;
|
private TextView headerServiceView;
|
||||||
|
private Button toggleServiceButton;
|
||||||
|
|
||||||
private boolean servicesShown = false;
|
private boolean servicesShown = false;
|
||||||
private ImageView serviceArrow;
|
private ImageView serviceArrow;
|
||||||
|
|
||||||
private static final int ITEM_ID_SUBSCRIPTIONS = - 1;
|
private static final int ITEM_ID_SUBSCRIPTIONS = -1;
|
||||||
private static final int ITEM_ID_FEED = - 2;
|
private static final int ITEM_ID_FEED = -2;
|
||||||
private static final int ITEM_ID_BOOKMARKS = - 3;
|
private static final int ITEM_ID_BOOKMARKS = -3;
|
||||||
private static final int ITEM_ID_DOWNLOADS = - 4;
|
private static final int ITEM_ID_DOWNLOADS = -4;
|
||||||
private static final int ITEM_ID_HISTORY = - 5;
|
private static final int ITEM_ID_HISTORY = -5;
|
||||||
private static final int ITEM_ID_SETTINGS = 0;
|
private static final int ITEM_ID_SETTINGS = 0;
|
||||||
private static final int ITEM_ID_ABOUT = 1;
|
private static final int ITEM_ID_ABOUT = 1;
|
||||||
|
|
||||||
@ -88,25 +93,24 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(final Bundle savedInstanceState) {
|
||||||
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onCreate() called with: "
|
||||||
|
+ "savedInstanceState = [" + savedInstanceState + "]");
|
||||||
|
}
|
||||||
|
|
||||||
// enable TLS1.1/1.2 for kitkat devices, to fix download and play for mediaCCC sources
|
// enable TLS1.1/1.2 for kitkat devices, to fix download and play for mediaCCC sources
|
||||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
|
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
|
||||||
TLSSocketFactoryCompat.setAsDefault();
|
TLSSocketFactoryCompat.setAsDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this));
|
ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this));
|
||||||
|
|
||||||
|
assureCorrectAppLanguage(this);
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_main);
|
setContentView(R.layout.activity_main);
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
if (getSupportFragmentManager() != null
|
||||||
Window w = getWindow();
|
&& getSupportFragmentManager().getBackStackEntryCount() == 0) {
|
||||||
w.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (getSupportFragmentManager() != null && getSupportFragmentManager().getBackStackEntryCount() == 0) {
|
|
||||||
initFragments();
|
initFragments();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,6 +120,10 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
ErrorActivity.reportUiError(this, e);
|
ErrorActivity.reportUiError(this, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (AndroidTvUtils.isTv(this)) {
|
||||||
|
FocusOverlayView.setupFocusObserver(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupDrawer() throws Exception {
|
private void setupDrawer() throws Exception {
|
||||||
@ -131,49 +139,52 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
for (final String ks : service.getKioskList().getAvailableKiosks()) {
|
for (final String ks : service.getKioskList().getAvailableKiosks()) {
|
||||||
drawerItems.getMenu()
|
drawerItems.getMenu()
|
||||||
.add(R.id.menu_tabs_group, kioskId, 0, KioskTranslator.getTranslatedKioskName(ks, this))
|
.add(R.id.menu_tabs_group, kioskId, 0, KioskTranslator
|
||||||
.setIcon(KioskTranslator.getKioskIcons(ks, this));
|
.getTranslatedKioskName(ks, this))
|
||||||
kioskId ++;
|
.setIcon(KioskTranslator.getKioskIcon(ks, this));
|
||||||
|
kioskId++;
|
||||||
}
|
}
|
||||||
|
|
||||||
drawerItems.getMenu()
|
drawerItems.getMenu()
|
||||||
.add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER, R.string.tab_subscriptions)
|
.add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER,
|
||||||
|
R.string.tab_subscriptions)
|
||||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_channel));
|
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_channel));
|
||||||
drawerItems.getMenu()
|
drawerItems.getMenu()
|
||||||
.add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_whats_new)
|
.add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_feed_title)
|
||||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.rss));
|
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_rss));
|
||||||
drawerItems.getMenu()
|
drawerItems.getMenu()
|
||||||
.add(R.id.menu_tabs_group, ITEM_ID_BOOKMARKS, ORDER, R.string.tab_bookmarks)
|
.add(R.id.menu_tabs_group, ITEM_ID_BOOKMARKS, ORDER, R.string.tab_bookmarks)
|
||||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_bookmark));
|
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_bookmark));
|
||||||
drawerItems.getMenu()
|
drawerItems.getMenu()
|
||||||
.add(R.id.menu_tabs_group, ITEM_ID_DOWNLOADS, ORDER, R.string.downloads)
|
.add(R.id.menu_tabs_group, ITEM_ID_DOWNLOADS, ORDER, R.string.downloads)
|
||||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.download));
|
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_file_download));
|
||||||
drawerItems.getMenu()
|
drawerItems.getMenu()
|
||||||
.add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history)
|
.add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history)
|
||||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.history));
|
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_history));
|
||||||
|
|
||||||
//Settings and About
|
//Settings and About
|
||||||
drawerItems.getMenu()
|
drawerItems.getMenu()
|
||||||
.add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings)
|
.add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings)
|
||||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.settings));
|
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_settings));
|
||||||
drawerItems.getMenu()
|
drawerItems.getMenu()
|
||||||
.add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
|
.add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
|
||||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.info));
|
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_info_outline));
|
||||||
|
|
||||||
toggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.drawer_open, R.string.drawer_close);
|
toggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.drawer_open,
|
||||||
|
R.string.drawer_close);
|
||||||
toggle.syncState();
|
toggle.syncState();
|
||||||
drawer.addDrawerListener(toggle);
|
drawer.addDrawerListener(toggle);
|
||||||
drawer.addDrawerListener(new DrawerLayout.SimpleDrawerListener() {
|
drawer.addDrawerListener(new DrawerLayout.SimpleDrawerListener() {
|
||||||
private int lastService;
|
private int lastService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDrawerOpened(View drawerView) {
|
public void onDrawerOpened(final View drawerView) {
|
||||||
lastService = ServiceHelper.getSelectedServiceId(MainActivity.this);
|
lastService = ServiceHelper.getSelectedServiceId(MainActivity.this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDrawerClosed(View drawerView) {
|
public void onDrawerClosed(final View drawerView) {
|
||||||
if(servicesShown) {
|
if (servicesShown) {
|
||||||
toggleServices();
|
toggleServices();
|
||||||
}
|
}
|
||||||
if (lastService != ServiceHelper.getSelectedServiceId(MainActivity.this)) {
|
if (lastService != ServiceHelper.getSelectedServiceId(MainActivity.this)) {
|
||||||
@ -186,7 +197,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
setupDrawerHeader();
|
setupDrawerHeader();
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean drawerItemSelected(MenuItem item) {
|
private boolean drawerItemSelected(final MenuItem item) {
|
||||||
switch (item.getGroupId()) {
|
switch (item.getGroupId()) {
|
||||||
case R.id.menu_services_group:
|
case R.id.menu_services_group:
|
||||||
changeService(item);
|
changeService(item);
|
||||||
@ -209,19 +220,21 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void changeService(MenuItem item) {
|
private void changeService(final MenuItem item) {
|
||||||
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(false);
|
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this))
|
||||||
|
.setChecked(false);
|
||||||
ServiceHelper.setSelectedServiceId(this, item.getItemId());
|
ServiceHelper.setSelectedServiceId(this, item.getItemId());
|
||||||
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true);
|
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this))
|
||||||
|
.setChecked(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void tabSelected(MenuItem item) throws ExtractionException {
|
private void tabSelected(final MenuItem item) throws ExtractionException {
|
||||||
switch(item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
case ITEM_ID_SUBSCRIPTIONS:
|
case ITEM_ID_SUBSCRIPTIONS:
|
||||||
NavigationHelper.openSubscriptionFragment(getSupportFragmentManager());
|
NavigationHelper.openSubscriptionFragment(getSupportFragmentManager());
|
||||||
break;
|
break;
|
||||||
case ITEM_ID_FEED:
|
case ITEM_ID_FEED:
|
||||||
NavigationHelper.openWhatsNewFragment(getSupportFragmentManager());
|
NavigationHelper.openFeedFragment(getSupportFragmentManager());
|
||||||
break;
|
break;
|
||||||
case ITEM_ID_BOOKMARKS:
|
case ITEM_ID_BOOKMARKS:
|
||||||
NavigationHelper.openBookmarksFragment(getSupportFragmentManager());
|
NavigationHelper.openBookmarksFragment(getSupportFragmentManager());
|
||||||
@ -239,19 +252,20 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
int kioskId = 0;
|
int kioskId = 0;
|
||||||
for (final String ks : service.getKioskList().getAvailableKiosks()) {
|
for (final String ks : service.getKioskList().getAvailableKiosks()) {
|
||||||
if(kioskId == item.getItemId()) {
|
if (kioskId == item.getItemId()) {
|
||||||
serviceName = ks;
|
serviceName = ks;
|
||||||
}
|
}
|
||||||
kioskId ++;
|
kioskId++;
|
||||||
}
|
}
|
||||||
|
|
||||||
NavigationHelper.openKioskFragment(getSupportFragmentManager(), currentServiceId, serviceName);
|
NavigationHelper.openKioskFragment(getSupportFragmentManager(), currentServiceId,
|
||||||
|
serviceName);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void optionsAboutSelected(MenuItem item) {
|
private void optionsAboutSelected(final MenuItem item) {
|
||||||
switch(item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
case ITEM_ID_SETTINGS:
|
case ITEM_ID_SETTINGS:
|
||||||
NavigationHelper.openSettings(this);
|
NavigationHelper.openSettings(this);
|
||||||
break;
|
break;
|
||||||
@ -263,14 +277,27 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
private void setupDrawerHeader() {
|
private void setupDrawerHeader() {
|
||||||
NavigationView navigationView = findViewById(R.id.navigation);
|
NavigationView navigationView = findViewById(R.id.navigation);
|
||||||
View hView = navigationView.getHeaderView(0);
|
View hView = navigationView.getHeaderView(0);
|
||||||
|
|
||||||
serviceArrow = hView.findViewById(R.id.drawer_arrow);
|
serviceArrow = hView.findViewById(R.id.drawer_arrow);
|
||||||
|
headerServiceIcon = hView.findViewById(R.id.drawer_header_service_icon);
|
||||||
headerServiceView = hView.findViewById(R.id.drawer_header_service_view);
|
headerServiceView = hView.findViewById(R.id.drawer_header_service_view);
|
||||||
toggleServiceButton = hView.findViewById(R.id.drawer_header_action_button);
|
toggleServiceButton = hView.findViewById(R.id.drawer_header_action_button);
|
||||||
toggleServiceButton.setOnClickListener(view -> {
|
toggleServiceButton.setOnClickListener(view -> toggleServices());
|
||||||
toggleServices();
|
|
||||||
});
|
// If the current app name is bigger than the default "NewPipe" (7 chars),
|
||||||
|
// let the text view grow a little more as well.
|
||||||
|
if (getString(R.string.app_name).length() > "NewPipe".length()) {
|
||||||
|
final TextView headerTitle = hView.findViewById(R.id.drawer_header_newpipe_title);
|
||||||
|
final ViewGroup.LayoutParams layoutParams = headerTitle.getLayoutParams();
|
||||||
|
layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||||
|
headerTitle.setLayoutParams(layoutParams);
|
||||||
|
headerTitle.setMaxLines(2);
|
||||||
|
headerTitle.setMinWidth(getResources()
|
||||||
|
.getDimensionPixelSize(R.dimen.drawer_header_newpipe_title_default_width));
|
||||||
|
headerTitle.setMaxWidth(getResources()
|
||||||
|
.getDimensionPixelSize(R.dimen.drawer_header_newpipe_title_max_width));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void toggleServices() {
|
private void toggleServices() {
|
||||||
@ -280,8 +307,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
drawerItems.getMenu().removeGroup(R.id.menu_tabs_group);
|
drawerItems.getMenu().removeGroup(R.id.menu_tabs_group);
|
||||||
drawerItems.getMenu().removeGroup(R.id.menu_options_about_group);
|
drawerItems.getMenu().removeGroup(R.id.menu_options_about_group);
|
||||||
|
|
||||||
|
if (servicesShown) {
|
||||||
if(servicesShown) {
|
|
||||||
showServices();
|
showServices();
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
@ -293,57 +319,64 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void showServices() {
|
private void showServices() {
|
||||||
serviceArrow.setImageResource(R.drawable.ic_arrow_up_white);
|
serviceArrow.setImageResource(R.drawable.ic_arrow_drop_up_white_24dp);
|
||||||
|
|
||||||
for(StreamingService s : NewPipe.getServices()) {
|
for (StreamingService s : NewPipe.getServices()) {
|
||||||
final String title = s.getServiceInfo().getName() +
|
final String title = s.getServiceInfo().getName()
|
||||||
(ServiceHelper.isBeta(s) ? " (beta)" : "");
|
+ (ServiceHelper.isBeta(s) ? " (beta)" : "");
|
||||||
|
|
||||||
MenuItem menuItem = drawerItems.getMenu()
|
MenuItem menuItem = drawerItems.getMenu()
|
||||||
.add(R.id.menu_services_group, s.getServiceId(), ORDER, title)
|
.add(R.id.menu_services_group, s.getServiceId(), ORDER, title)
|
||||||
.setIcon(ServiceHelper.getIcon(s.getServiceId()));
|
.setIcon(ServiceHelper.getIcon(s.getServiceId()));
|
||||||
|
|
||||||
// peertube specifics
|
// peertube specifics
|
||||||
if(s.getServiceId() == 3){
|
if (s.getServiceId() == 3) {
|
||||||
enhancePeertubeMenu(s, menuItem);
|
enhancePeertubeMenu(s, menuItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true);
|
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this))
|
||||||
|
.setChecked(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void enhancePeertubeMenu(StreamingService s, MenuItem menuItem) {
|
private void enhancePeertubeMenu(final StreamingService s, final MenuItem menuItem) {
|
||||||
PeertubeInstance currentInstace = PeertubeHelper.getCurrentInstance();
|
PeertubeInstance currentInstace = PeertubeHelper.getCurrentInstance();
|
||||||
menuItem.setTitle(currentInstace.getName() + (ServiceHelper.isBeta(s) ? " (beta)" : ""));
|
menuItem.setTitle(currentInstace.getName() + (ServiceHelper.isBeta(s) ? " (beta)" : ""));
|
||||||
Spinner spinner = (Spinner) LayoutInflater.from(this).inflate(R.layout.instance_spinner_layout, null);
|
Spinner spinner = (Spinner) LayoutInflater.from(this)
|
||||||
|
.inflate(R.layout.instance_spinner_layout, null);
|
||||||
List<PeertubeInstance> instances = PeertubeHelper.getInstanceList(this);
|
List<PeertubeInstance> instances = PeertubeHelper.getInstanceList(this);
|
||||||
List<String> items = new ArrayList<>();
|
List<String> items = new ArrayList<>();
|
||||||
int defaultSelect = 0;
|
int defaultSelect = 0;
|
||||||
for(PeertubeInstance instance: instances){
|
for (PeertubeInstance instance : instances) {
|
||||||
items.add(instance.getName());
|
items.add(instance.getName());
|
||||||
if(instance.getUrl().equals(currentInstace.getUrl())){
|
if (instance.getUrl().equals(currentInstace.getUrl())) {
|
||||||
defaultSelect = items.size()-1;
|
defaultSelect = items.size() - 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ArrayAdapter<String> adapter = new ArrayAdapter<>(this, R.layout.instance_spinner_item, items);
|
ArrayAdapter<String> adapter = new ArrayAdapter<>(this,
|
||||||
|
R.layout.instance_spinner_item, items);
|
||||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||||
spinner.setAdapter(adapter);
|
spinner.setAdapter(adapter);
|
||||||
spinner.setSelection(defaultSelect, false);
|
spinner.setSelection(defaultSelect, false);
|
||||||
spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
public void onItemSelected(final AdapterView<?> parent, final View view,
|
||||||
|
final int position, final long id) {
|
||||||
PeertubeInstance newInstance = instances.get(position);
|
PeertubeInstance newInstance = instances.get(position);
|
||||||
if(newInstance.getUrl().equals(PeertubeHelper.getCurrentInstance().getUrl())) return;
|
if (newInstance.getUrl().equals(PeertubeHelper.getCurrentInstance().getUrl())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
PeertubeHelper.selectInstance(newInstance, getApplicationContext());
|
PeertubeHelper.selectInstance(newInstance, getApplicationContext());
|
||||||
changeService(menuItem);
|
changeService(menuItem);
|
||||||
drawer.closeDrawers();
|
drawer.closeDrawers();
|
||||||
new Handler(Looper.getMainLooper()).postDelayed(() -> {
|
new Handler(Looper.getMainLooper()).postDelayed(() -> {
|
||||||
getSupportFragmentManager().popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
|
getSupportFragmentManager().popBackStack(null,
|
||||||
|
FragmentManager.POP_BACK_STACK_INCLUSIVE);
|
||||||
recreate();
|
recreate();
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onNothingSelected(AdapterView<?> parent) {
|
public void onNothingSelected(final AdapterView<?> parent) {
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -351,7 +384,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void showTabs() throws ExtractionException {
|
private void showTabs() throws ExtractionException {
|
||||||
serviceArrow.setImageResource(R.drawable.ic_arrow_down_white);
|
serviceArrow.setImageResource(R.drawable.ic_arrow_drop_down_white_24dp);
|
||||||
|
|
||||||
//Tabs
|
//Tabs
|
||||||
int currentServiceId = ServiceHelper.getSelectedServiceId(this);
|
int currentServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||||
@ -361,34 +394,35 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
for (final String ks : service.getKioskList().getAvailableKiosks()) {
|
for (final String ks : service.getKioskList().getAvailableKiosks()) {
|
||||||
drawerItems.getMenu()
|
drawerItems.getMenu()
|
||||||
.add(R.id.menu_tabs_group, kioskId, ORDER, KioskTranslator.getTranslatedKioskName(ks, this))
|
.add(R.id.menu_tabs_group, kioskId, ORDER,
|
||||||
.setIcon(KioskTranslator.getKioskIcons(ks, this));
|
KioskTranslator.getTranslatedKioskName(ks, this))
|
||||||
kioskId ++;
|
.setIcon(KioskTranslator.getKioskIcon(ks, this));
|
||||||
|
kioskId++;
|
||||||
}
|
}
|
||||||
|
|
||||||
drawerItems.getMenu()
|
drawerItems.getMenu()
|
||||||
.add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER, R.string.tab_subscriptions)
|
.add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER, R.string.tab_subscriptions)
|
||||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_channel));
|
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_channel));
|
||||||
drawerItems.getMenu()
|
drawerItems.getMenu()
|
||||||
.add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_whats_new)
|
.add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_feed_title)
|
||||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.rss));
|
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_rss));
|
||||||
drawerItems.getMenu()
|
drawerItems.getMenu()
|
||||||
.add(R.id.menu_tabs_group, ITEM_ID_BOOKMARKS, ORDER, R.string.tab_bookmarks)
|
.add(R.id.menu_tabs_group, ITEM_ID_BOOKMARKS, ORDER, R.string.tab_bookmarks)
|
||||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_bookmark));
|
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_bookmark));
|
||||||
drawerItems.getMenu()
|
drawerItems.getMenu()
|
||||||
.add(R.id.menu_tabs_group, ITEM_ID_DOWNLOADS, ORDER, R.string.downloads)
|
.add(R.id.menu_tabs_group, ITEM_ID_DOWNLOADS, ORDER, R.string.downloads)
|
||||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.download));
|
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_file_download));
|
||||||
drawerItems.getMenu()
|
drawerItems.getMenu()
|
||||||
.add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history)
|
.add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history)
|
||||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.history));
|
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_history));
|
||||||
|
|
||||||
//Settings and About
|
//Settings and About
|
||||||
drawerItems.getMenu()
|
drawerItems.getMenu()
|
||||||
.add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings)
|
.add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings)
|
||||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.settings));
|
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_settings));
|
||||||
drawerItems.getMenu()
|
drawerItems.getMenu()
|
||||||
.add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
|
.add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
|
||||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.info));
|
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_info_outline));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -401,15 +435,21 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
|
assureCorrectAppLanguage(this);
|
||||||
|
// Change the date format to match the selected language on resume
|
||||||
|
Localization.init(getApplicationContext());
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
|
||||||
// close drawer on return, and don't show animation, so its looks like the drawer isn't open
|
// Close drawer on return, and don't show animation,
|
||||||
// when the user returns to MainActivity
|
// so it looks like the drawer isn't open when the user returns to MainActivity
|
||||||
drawer.closeDrawer(GravityCompat.START, false);
|
drawer.closeDrawer(GravityCompat.START, false);
|
||||||
try {
|
try {
|
||||||
String selectedServiceName = NewPipe.getService(
|
final int selectedServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||||
ServiceHelper.getSelectedServiceId(this)).getServiceInfo().getName();
|
final String selectedServiceName = NewPipe.getService(selectedServiceId)
|
||||||
|
.getServiceInfo().getName();
|
||||||
headerServiceView.setText(selectedServiceName);
|
headerServiceView.setText(selectedServiceName);
|
||||||
|
headerServiceIcon.setImageResource(ServiceHelper.getIcon(selectedServiceId));
|
||||||
|
|
||||||
headerServiceView.post(() -> headerServiceView.setSelected(true));
|
headerServiceView.post(() -> headerServiceView.setSelected(true));
|
||||||
toggleServiceButton.setContentDescription(
|
toggleServiceButton.setContentDescription(
|
||||||
getString(R.string.drawer_header_description) + selectedServiceName);
|
getString(R.string.drawer_header_description) + selectedServiceName);
|
||||||
@ -419,28 +459,42 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
|
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||||
if (sharedPreferences.getBoolean(Constants.KEY_THEME_CHANGE, false)) {
|
if (sharedPreferences.getBoolean(Constants.KEY_THEME_CHANGE, false)) {
|
||||||
if (DEBUG) Log.d(TAG, "Theme has changed, recreating activity...");
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "Theme has changed, recreating activity...");
|
||||||
|
}
|
||||||
sharedPreferences.edit().putBoolean(Constants.KEY_THEME_CHANGE, false).apply();
|
sharedPreferences.edit().putBoolean(Constants.KEY_THEME_CHANGE, false).apply();
|
||||||
// https://stackoverflow.com/questions/10844112/runtimeexception-performing-pause-of-activity-that-is-not-resumed
|
// https://stackoverflow.com/questions/10844112/
|
||||||
// Briefly, let the activity resume properly posting the recreate call to end of the message queue
|
// Briefly, let the activity resume
|
||||||
|
// properly posting the recreate call to end of the message queue
|
||||||
new Handler(Looper.getMainLooper()).post(MainActivity.this::recreate);
|
new Handler(Looper.getMainLooper()).post(MainActivity.this::recreate);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sharedPreferences.getBoolean(Constants.KEY_MAIN_PAGE_CHANGE, false)) {
|
if (sharedPreferences.getBoolean(Constants.KEY_MAIN_PAGE_CHANGE, false)) {
|
||||||
if (DEBUG) Log.d(TAG, "main page has changed, recreating main fragment...");
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "main page has changed, recreating main fragment...");
|
||||||
|
}
|
||||||
sharedPreferences.edit().putBoolean(Constants.KEY_MAIN_PAGE_CHANGE, false).apply();
|
sharedPreferences.edit().putBoolean(Constants.KEY_MAIN_PAGE_CHANGE, false).apply();
|
||||||
NavigationHelper.openMainActivity(this);
|
NavigationHelper.openMainActivity(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final boolean isHistoryEnabled = sharedPreferences.getBoolean(
|
||||||
|
getString(R.string.enable_watch_history_key), true);
|
||||||
|
drawerItems.getMenu().findItem(ITEM_ID_HISTORY).setVisible(isHistoryEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onNewIntent(Intent intent) {
|
protected void onNewIntent(final Intent intent) {
|
||||||
if (DEBUG) Log.d(TAG, "onNewIntent() called with: intent = [" + intent + "]");
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onNewIntent() called with: intent = [" + intent + "]");
|
||||||
|
}
|
||||||
if (intent != null) {
|
if (intent != null) {
|
||||||
// Return if launched from a launcher (e.g. Nova Launcher, Pixel Launcher ...)
|
// Return if launched from a launcher (e.g. Nova Launcher, Pixel Launcher ...)
|
||||||
// to not destroy the already created backstack
|
// to not destroy the already created backstack
|
||||||
String action = intent.getAction();
|
String action = intent.getAction();
|
||||||
if ((action != null && action.equals(Intent.ACTION_MAIN)) && intent.hasCategory(Intent.CATEGORY_LAUNCHER)) return;
|
if ((action != null && action.equals(Intent.ACTION_MAIN))
|
||||||
|
&& intent.hasCategory(Intent.CATEGORY_LAUNCHER)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
super.onNewIntent(intent);
|
super.onNewIntent(intent);
|
||||||
@ -448,17 +502,33 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
handleIntent(intent);
|
handleIntent(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onKeyDown(final int keyCode, final KeyEvent event) {
|
||||||
|
final Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_player_holder);
|
||||||
|
if (fragment instanceof OnKeyDownListener && !bottomSheetHiddenOrCollapsed()) {
|
||||||
|
// Provide keyDown event to fragment which then sends this event to the main player service
|
||||||
|
return ((OnKeyDownListener) fragment).onKeyDown(keyCode) || super.onKeyDown(keyCode, event);
|
||||||
|
}
|
||||||
|
return super.onKeyDown(keyCode, event);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBackPressed() {
|
public void onBackPressed() {
|
||||||
if (DEBUG) Log.d(TAG, "onBackPressed() called");
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onBackPressed() called");
|
||||||
|
}
|
||||||
|
|
||||||
final FrameLayout bottomSheetLayout = findViewById(R.id.fragment_player_holder);
|
if (AndroidTvUtils.isTv(this)) {
|
||||||
final BottomSheetBehavior<FrameLayout> bottomSheetBehavior = BottomSheetBehavior.from(bottomSheetLayout);
|
View drawerPanel = findViewById(R.id.navigation);
|
||||||
|
if (drawer.isDrawerOpen(drawerPanel)) {
|
||||||
|
drawer.closeDrawers();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final int sheetState = bottomSheetBehavior.getState();
|
// In case bottomSheet is not visible on the screen or collapsed we can assume that the user
|
||||||
// In case bottomSheet is not visible on the screen or collapsed we can assume that the user interacts with a fragment
|
// interacts with a fragment inside fragment_holder so all back presses should be handled by it
|
||||||
// inside fragment_holder so all back presses should be handled by it
|
if (bottomSheetHiddenOrCollapsed()) {
|
||||||
if (sheetState == BottomSheetBehavior.STATE_HIDDEN || sheetState == BottomSheetBehavior.STATE_COLLAPSED) {
|
|
||||||
final Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
|
final Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
|
||||||
// If current fragment implements BackPressable (i.e. can/wanna handle back press) delegate the back press to it
|
// If current fragment implements BackPressable (i.e. can/wanna handle back press) delegate the back press to it
|
||||||
if (fragment instanceof BackPressable) {
|
if (fragment instanceof BackPressable) {
|
||||||
@ -469,21 +539,27 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
final Fragment fragmentPlayer = getSupportFragmentManager().findFragmentById(R.id.fragment_player_holder);
|
final Fragment fragmentPlayer = getSupportFragmentManager().findFragmentById(R.id.fragment_player_holder);
|
||||||
// If current fragment implements BackPressable (i.e. can/wanna handle back press) delegate the back press to it
|
// If current fragment implements BackPressable (i.e. can/wanna handle back press) delegate the back press to it
|
||||||
if (fragmentPlayer instanceof BackPressable) {
|
if (fragmentPlayer instanceof BackPressable) {
|
||||||
if (!((BackPressable) fragmentPlayer).onBackPressed())
|
if (!((BackPressable) fragmentPlayer).onBackPressed()) {
|
||||||
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
final FrameLayout bottomSheetLayout = findViewById(R.id.fragment_player_holder);
|
||||||
|
BottomSheetBehavior.from(bottomSheetLayout).setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getSupportFragmentManager().getBackStackEntryCount() == 1) {
|
if (getSupportFragmentManager().getBackStackEntryCount() == 1) {
|
||||||
finish();
|
finish();
|
||||||
} else super.onBackPressed();
|
} else {
|
||||||
|
super.onBackPressed();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
public void onRequestPermissionsResult(final int requestCode,
|
||||||
for (int i: grantResults){
|
@NonNull final String[] permissions,
|
||||||
if (i == PackageManager.PERMISSION_DENIED){
|
@NonNull final int[] grantResults) {
|
||||||
|
for (int i : grantResults) {
|
||||||
|
if (i == PackageManager.PERMISSION_DENIED) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -537,16 +613,16 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onCreateOptionsMenu(Menu menu) {
|
public boolean onCreateOptionsMenu(final Menu menu) {
|
||||||
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "]");
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "]");
|
||||||
|
}
|
||||||
super.onCreateOptionsMenu(menu);
|
super.onCreateOptionsMenu(menu);
|
||||||
|
|
||||||
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
|
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
|
||||||
if (!(fragment instanceof SearchFragment)) {
|
if (!(fragment instanceof SearchFragment)) {
|
||||||
findViewById(R.id.toolbar).findViewById(R.id.toolbar_search_container).setVisibility(View.GONE);
|
findViewById(R.id.toolbar).findViewById(R.id.toolbar_search_container)
|
||||||
|
.setVisibility(View.GONE);
|
||||||
MenuInflater inflater = getMenuInflater();
|
|
||||||
inflater.inflate(R.menu.main_menu, menu);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ActionBar actionBar = getSupportActionBar();
|
ActionBar actionBar = getSupportActionBar();
|
||||||
@ -560,22 +636,16 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||||
if (DEBUG) Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]");
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]");
|
||||||
|
}
|
||||||
int id = item.getItemId();
|
int id = item.getItemId();
|
||||||
|
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case android.R.id.home:
|
case android.R.id.home:
|
||||||
onHomeButtonPressed();
|
onHomeButtonPressed();
|
||||||
return true;
|
return true;
|
||||||
case R.id.action_show_downloads:
|
|
||||||
return NavigationHelper.openDownloads(this);
|
|
||||||
case R.id.action_history:
|
|
||||||
NavigationHelper.openStatisticFragment(getSupportFragmentManager());
|
|
||||||
return true;
|
|
||||||
case R.id.action_settings:
|
|
||||||
NavigationHelper.openSettings(this);
|
|
||||||
return true;
|
|
||||||
default:
|
default:
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
@ -586,7 +656,9 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
private void initFragments() {
|
private void initFragments() {
|
||||||
if (DEBUG) Log.d(TAG, "initFragments() called");
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "initFragments() called");
|
||||||
|
}
|
||||||
StateSaver.clearStateFiles();
|
StateSaver.clearStateFiles();
|
||||||
if (getIntent() != null && getIntent().hasExtra(Constants.KEY_LINK_TYPE)) {
|
if (getIntent() != null && getIntent().hasExtra(Constants.KEY_LINK_TYPE)) {
|
||||||
// When user watch a video inside popup and then tries to open the video in main player while the app is closed
|
// When user watch a video inside popup and then tries to open the video in main player while the app is closed
|
||||||
@ -595,7 +667,9 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
NavigationHelper.openMainFragment(getSupportFragmentManager());
|
NavigationHelper.openMainFragment(getSupportFragmentManager());
|
||||||
|
|
||||||
handleIntent(getIntent());
|
handleIntent(getIntent());
|
||||||
} else NavigationHelper.gotoMainFragment(getSupportFragmentManager());
|
} else {
|
||||||
|
NavigationHelper.gotoMainFragment(getSupportFragmentManager());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
@ -603,12 +677,14 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
private void updateDrawerNavigation() {
|
private void updateDrawerNavigation() {
|
||||||
if (getSupportActionBar() == null) return;
|
if (getSupportActionBar() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final Toolbar toolbar = findViewById(R.id.toolbar);
|
final Toolbar toolbar = findViewById(R.id.toolbar);
|
||||||
final DrawerLayout drawer = findViewById(R.id.drawer_layout);
|
|
||||||
|
|
||||||
final Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
|
final Fragment fragment = getSupportFragmentManager()
|
||||||
|
.findFragmentById(R.id.fragment_holder);
|
||||||
if (fragment instanceof MainFragment) {
|
if (fragment instanceof MainFragment) {
|
||||||
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
|
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
|
||||||
if (toggle != null) {
|
if (toggle != null) {
|
||||||
@ -623,23 +699,18 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateDrawerHeaderString(String content) {
|
private void handleIntent(final Intent intent) {
|
||||||
NavigationView navigationView = findViewById(R.id.navigation);
|
|
||||||
View hView = navigationView.getHeaderView(0);
|
|
||||||
Button action = hView.findViewById(R.id.drawer_header_action_button);
|
|
||||||
|
|
||||||
action.setContentDescription(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleIntent(Intent intent) {
|
|
||||||
try {
|
try {
|
||||||
if (DEBUG) Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]");
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]");
|
||||||
|
}
|
||||||
|
|
||||||
if (intent.hasExtra(Constants.KEY_LINK_TYPE)) {
|
if (intent.hasExtra(Constants.KEY_LINK_TYPE)) {
|
||||||
String url = intent.getStringExtra(Constants.KEY_URL);
|
String url = intent.getStringExtra(Constants.KEY_URL);
|
||||||
int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0);
|
int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0);
|
||||||
String title = intent.getStringExtra(Constants.KEY_TITLE);
|
String title = intent.getStringExtra(Constants.KEY_TITLE);
|
||||||
switch (((StreamingService.LinkType) intent.getSerializableExtra(Constants.KEY_LINK_TYPE))) {
|
switch (((StreamingService.LinkType) intent
|
||||||
|
.getSerializableExtra(Constants.KEY_LINK_TYPE))) {
|
||||||
case STREAM:
|
case STREAM:
|
||||||
boolean autoPlay = intent.getBooleanExtra(VideoDetailFragment.AUTO_PLAY, false);
|
boolean autoPlay = intent.getBooleanExtra(VideoDetailFragment.AUTO_PLAY, false);
|
||||||
final String intentCacheKey = intent.getStringExtra(VideoPlayer.PLAY_QUEUE_KEY);
|
final String intentCacheKey = intent.getStringExtra(VideoPlayer.PLAY_QUEUE_KEY);
|
||||||
@ -661,7 +732,9 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
} else if (intent.hasExtra(Constants.KEY_OPEN_SEARCH)) {
|
} else if (intent.hasExtra(Constants.KEY_OPEN_SEARCH)) {
|
||||||
String searchString = intent.getStringExtra(Constants.KEY_SEARCH_STRING);
|
String searchString = intent.getStringExtra(Constants.KEY_SEARCH_STRING);
|
||||||
if (searchString == null) searchString = "";
|
if (searchString == null) {
|
||||||
|
searchString = "";
|
||||||
|
}
|
||||||
int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0);
|
int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0);
|
||||||
NavigationHelper.openSearchFragment(
|
NavigationHelper.openSearchFragment(
|
||||||
getSupportFragmentManager(),
|
getSupportFragmentManager(),
|
||||||
@ -675,4 +748,15 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
ErrorActivity.reportUiError(this, e);
|
ErrorActivity.reportUiError(this, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
|
* Utils
|
||||||
|
* */
|
||||||
|
|
||||||
|
private boolean bottomSheetHiddenOrCollapsed() {
|
||||||
|
final FrameLayout bottomSheetLayout = findViewById(R.id.fragment_player_holder);
|
||||||
|
final BottomSheetBehavior<FrameLayout> bottomSheetBehavior = BottomSheetBehavior.from(bottomSheetLayout);
|
||||||
|
|
||||||
|
final int sheetState = bottomSheetBehavior.getState();
|
||||||
|
return sheetState == BottomSheetBehavior.STATE_HIDDEN || sheetState == BottomSheetBehavior.STATE_COLLAPSED;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,42 +1,54 @@
|
|||||||
package org.schabi.newpipe;
|
package org.schabi.newpipe;
|
||||||
|
|
||||||
import androidx.room.Room;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.room.Room;
|
||||||
|
|
||||||
import org.schabi.newpipe.database.AppDatabase;
|
import org.schabi.newpipe.database.AppDatabase;
|
||||||
|
|
||||||
import static org.schabi.newpipe.database.AppDatabase.DATABASE_NAME;
|
import static org.schabi.newpipe.database.AppDatabase.DATABASE_NAME;
|
||||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_11_12;
|
import static org.schabi.newpipe.database.Migrations.MIGRATION_1_2;
|
||||||
|
import static org.schabi.newpipe.database.Migrations.MIGRATION_2_3;
|
||||||
|
|
||||||
public final class NewPipeDatabase {
|
public final class NewPipeDatabase {
|
||||||
|
|
||||||
private static volatile AppDatabase databaseInstance;
|
private static volatile AppDatabase databaseInstance;
|
||||||
|
|
||||||
private NewPipeDatabase() {
|
private NewPipeDatabase() {
|
||||||
//no instance
|
//no instance
|
||||||
}
|
}
|
||||||
|
|
||||||
private static AppDatabase getDatabase(Context context) {
|
private static AppDatabase getDatabase(final Context context) {
|
||||||
return Room
|
return Room
|
||||||
.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
|
.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
|
||||||
.addMigrations(MIGRATION_11_12)
|
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
|
||||||
.fallbackToDestructiveMigration()
|
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public static AppDatabase getInstance(@NonNull Context context) {
|
public static AppDatabase getInstance(@NonNull final Context context) {
|
||||||
AppDatabase result = databaseInstance;
|
AppDatabase result = databaseInstance;
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
synchronized (NewPipeDatabase.class) {
|
synchronized (NewPipeDatabase.class) {
|
||||||
result = databaseInstance;
|
result = databaseInstance;
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
databaseInstance = (result = getDatabase(context));
|
databaseInstance = getDatabase(context);
|
||||||
|
result = databaseInstance;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void checkpoint() {
|
||||||
|
if (databaseInstance == null) {
|
||||||
|
throw new IllegalStateException("database is not initialized");
|
||||||
|
}
|
||||||
|
Cursor c = databaseInstance.query("pragma wal_checkpoint(full)", null);
|
||||||
|
if (c.moveToFirst() && c.getInt(0) == 1) {
|
||||||
|
throw new RuntimeException("Checkpoint was blocked from completing");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
package org.schabi.newpipe;
|
package org.schabi.newpipe;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
@ -26,17 +25,18 @@ import android.os.Bundle;
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
public class PanicResponderActivity extends Activity {
|
public class PanicResponderActivity extends Activity {
|
||||||
|
|
||||||
public static final String PANIC_TRIGGER_ACTION = "info.guardianproject.panic.action.TRIGGER";
|
public static final String PANIC_TRIGGER_ACTION = "info.guardianproject.panic.action.TRIGGER";
|
||||||
|
|
||||||
@SuppressLint("NewApi")
|
@SuppressLint("NewApi")
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(final Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
Intent intent = getIntent();
|
Intent intent = getIntent();
|
||||||
if (intent != null && PANIC_TRIGGER_ACTION.equals(intent.getAction())) {
|
if (intent != null && PANIC_TRIGGER_ACTION.equals(intent.getAction())) {
|
||||||
// TODO explicitly clear the search results once they are restored when the app restarts
|
// TODO: Explicitly clear the search results
|
||||||
// or if the app reloads the current video after being killed, that should be cleared also
|
// once they are restored when the app restarts
|
||||||
|
// or if the app reloads the current video after being killed,
|
||||||
|
// that should be cleared also
|
||||||
ExitActivity.exitAndRemoveFromRecentApps(this);
|
ExitActivity.exitAndRemoveFromRecentApps(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,20 +1,32 @@
|
|||||||
package org.schabi.newpipe;
|
package org.schabi.newpipe;
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.graphics.Bitmap;
|
import android.content.SharedPreferences;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import androidx.core.app.NavUtils;
|
import android.util.Log;
|
||||||
import androidx.appcompat.app.ActionBar;
|
import android.view.Menu;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import androidx.appcompat.widget.Toolbar;
|
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.webkit.CookieManager;
|
import android.webkit.CookieManager;
|
||||||
|
import android.webkit.WebResourceRequest;
|
||||||
import android.webkit.WebSettings;
|
import android.webkit.WebSettings;
|
||||||
import android.webkit.WebView;
|
import android.webkit.WebView;
|
||||||
import android.webkit.WebViewClient;
|
import android.webkit.WebViewClient;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.RequiresApi;
|
||||||
|
import androidx.appcompat.app.ActionBar;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
import androidx.core.app.NavUtils;
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.net.URLDecoder;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Created by beneth <bmauduit@beneth.fr> on 06.12.16.
|
* Created by beneth <bmauduit@beneth.fr> on 06.12.16.
|
||||||
*
|
*
|
||||||
@ -37,126 +49,200 @@ import android.webkit.WebViewClient;
|
|||||||
public class ReCaptchaActivity extends AppCompatActivity {
|
public class ReCaptchaActivity extends AppCompatActivity {
|
||||||
public static final int RECAPTCHA_REQUEST = 10;
|
public static final int RECAPTCHA_REQUEST = 10;
|
||||||
public static final String RECAPTCHA_URL_EXTRA = "recaptcha_url_extra";
|
public static final String RECAPTCHA_URL_EXTRA = "recaptcha_url_extra";
|
||||||
|
|
||||||
public static final String TAG = ReCaptchaActivity.class.toString();
|
public static final String TAG = ReCaptchaActivity.class.toString();
|
||||||
public static final String YT_URL = "https://www.youtube.com";
|
public static final String YT_URL = "https://www.youtube.com";
|
||||||
|
public static final String RECAPTCHA_COOKIES_KEY = "recaptcha_cookies";
|
||||||
|
|
||||||
private String url;
|
private WebView webView;
|
||||||
|
private String foundCookies = "";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(final Bundle savedInstanceState) {
|
||||||
|
ThemeHelper.setTheme(this);
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_recaptcha);
|
setContentView(R.layout.activity_recaptcha);
|
||||||
|
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||||
|
setSupportActionBar(toolbar);
|
||||||
|
|
||||||
url = getIntent().getStringExtra(RECAPTCHA_URL_EXTRA);
|
String url = getIntent().getStringExtra(RECAPTCHA_URL_EXTRA);
|
||||||
if (url == null || url.isEmpty()) {
|
if (url == null || url.isEmpty()) {
|
||||||
url = YT_URL;
|
url = YT_URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set return to Cancel by default
|
||||||
// Set return to Cancel by default
|
|
||||||
setResult(RESULT_CANCELED);
|
setResult(RESULT_CANCELED);
|
||||||
|
|
||||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
|
||||||
setSupportActionBar(toolbar);
|
|
||||||
|
|
||||||
ActionBar actionBar = getSupportActionBar();
|
webView = findViewById(R.id.reCaptchaWebView);
|
||||||
if (actionBar != null) {
|
|
||||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
|
||||||
actionBar.setTitle(R.string.reCaptcha_title);
|
|
||||||
actionBar.setDisplayShowTitleEnabled(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
WebView myWebView = findViewById(R.id.reCaptchaWebView);
|
// enable Javascript
|
||||||
|
WebSettings webSettings = webView.getSettings();
|
||||||
// Enable Javascript
|
|
||||||
WebSettings webSettings = myWebView.getSettings();
|
|
||||||
webSettings.setJavaScriptEnabled(true);
|
webSettings.setJavaScriptEnabled(true);
|
||||||
|
|
||||||
ReCaptchaWebViewClient webClient = new ReCaptchaWebViewClient(this);
|
webView.setWebViewClient(new WebViewClient() {
|
||||||
myWebView.setWebViewClient(webClient);
|
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||||
|
@Override
|
||||||
|
public boolean shouldOverrideUrlLoading(final WebView view,
|
||||||
|
final WebResourceRequest request) {
|
||||||
|
String url = request.getUrl().toString();
|
||||||
|
if (MainActivity.DEBUG) {
|
||||||
|
Log.d(TAG, "shouldOverrideUrlLoading: request.url=" + url);
|
||||||
|
}
|
||||||
|
|
||||||
// Cleaning cache, history and cookies from webView
|
handleCookiesFromUrl(url);
|
||||||
myWebView.clearCache(true);
|
return false;
|
||||||
myWebView.clearHistory();
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldOverrideUrlLoading(final WebView view, final String url) {
|
||||||
|
if (MainActivity.DEBUG) {
|
||||||
|
Log.d(TAG, "shouldOverrideUrlLoading: url=" + url);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCookiesFromUrl(url);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPageFinished(final WebView view, final String url) {
|
||||||
|
super.onPageFinished(view, url);
|
||||||
|
handleCookiesFromUrl(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// cleaning cache, history and cookies from webView
|
||||||
|
webView.clearCache(true);
|
||||||
|
webView.clearHistory();
|
||||||
android.webkit.CookieManager cookieManager = CookieManager.getInstance();
|
android.webkit.CookieManager cookieManager = CookieManager.getInstance();
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
cookieManager.removeAllCookies(aBoolean -> {});
|
cookieManager.removeAllCookies(aBoolean -> {
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
cookieManager.removeAllCookie();
|
cookieManager.removeAllCookie();
|
||||||
}
|
}
|
||||||
|
|
||||||
myWebView.loadUrl(url);
|
webView.loadUrl(url);
|
||||||
}
|
|
||||||
|
|
||||||
private class ReCaptchaWebViewClient extends WebViewClient {
|
|
||||||
private final Activity context;
|
|
||||||
private String mCookies;
|
|
||||||
|
|
||||||
ReCaptchaWebViewClient(Activity ctx) {
|
|
||||||
context = ctx;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPageStarted(WebView view, String url, Bitmap favicon) {
|
|
||||||
// TODO: Start Loader
|
|
||||||
super.onPageStarted(view, url, favicon);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPageFinished(WebView view, String url) {
|
|
||||||
String cookies = CookieManager.getInstance().getCookie(url);
|
|
||||||
|
|
||||||
// TODO: Stop Loader
|
|
||||||
|
|
||||||
// find cookies : s_gl & goojf and Add cookies to Downloader
|
|
||||||
if (find_access_cookies(cookies)) {
|
|
||||||
// Give cookies to Downloader class
|
|
||||||
DownloaderImpl.getInstance().setCookies(mCookies);
|
|
||||||
|
|
||||||
// Closing activity and return to parent
|
|
||||||
setResult(RESULT_OK);
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean find_access_cookies(String cookies) {
|
|
||||||
boolean ret = false;
|
|
||||||
String c_s_gl = "";
|
|
||||||
String c_goojf = "";
|
|
||||||
|
|
||||||
String[] parts = cookies.split("; ");
|
|
||||||
for (String part : parts) {
|
|
||||||
if (part.trim().startsWith("s_gl")) {
|
|
||||||
c_s_gl = part.trim();
|
|
||||||
}
|
|
||||||
if (part.trim().startsWith("goojf")) {
|
|
||||||
c_goojf = part.trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (c_s_gl.length() > 0 && c_goojf.length() > 0) {
|
|
||||||
ret = true;
|
|
||||||
//mCookies = c_s_gl + "; " + c_goojf;
|
|
||||||
// Youtube seems to also need the other cookies:
|
|
||||||
mCookies = cookies;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onCreateOptionsMenu(final Menu menu) {
|
||||||
|
getMenuInflater().inflate(R.menu.menu_recaptcha, menu);
|
||||||
|
|
||||||
|
ActionBar actionBar = getSupportActionBar();
|
||||||
|
if (actionBar != null) {
|
||||||
|
actionBar.setDisplayHomeAsUpEnabled(false);
|
||||||
|
actionBar.setTitle(R.string.title_activity_recaptcha);
|
||||||
|
actionBar.setSubtitle(R.string.subtitle_activity_recaptcha);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBackPressed() {
|
||||||
|
saveCookiesAndFinish();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||||
int id = item.getItemId();
|
int id = item.getItemId();
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case android.R.id.home: {
|
case R.id.menu_item_done:
|
||||||
Intent intent = new Intent(this, org.schabi.newpipe.MainActivity.class);
|
saveCookiesAndFinish();
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
|
||||||
NavUtils.navigateUpTo(this, intent);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void saveCookiesAndFinish() {
|
||||||
|
handleCookiesFromUrl(webView.getUrl()); // try to get cookies of unclosed page
|
||||||
|
if (MainActivity.DEBUG) {
|
||||||
|
Log.d(TAG, "saveCookiesAndFinish: foundCookies=" + foundCookies);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundCookies.isEmpty()) {
|
||||||
|
// save cookies to preferences
|
||||||
|
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
|
||||||
|
getApplicationContext());
|
||||||
|
final String key = getApplicationContext().getString(R.string.recaptcha_cookies_key);
|
||||||
|
prefs.edit().putString(key, foundCookies).apply();
|
||||||
|
|
||||||
|
// give cookies to Downloader class
|
||||||
|
DownloaderImpl.getInstance().setCookie(RECAPTCHA_COOKIES_KEY, foundCookies);
|
||||||
|
setResult(RESULT_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
Intent intent = new Intent(this, org.schabi.newpipe.MainActivity.class);
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||||
|
NavUtils.navigateUpTo(this, intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void handleCookiesFromUrl(@Nullable final String url) {
|
||||||
|
if (MainActivity.DEBUG) {
|
||||||
|
Log.d(TAG, "handleCookiesFromUrl: url=" + (url == null ? "null" : url));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String cookies = CookieManager.getInstance().getCookie(url);
|
||||||
|
handleCookies(cookies);
|
||||||
|
|
||||||
|
// sometimes cookies are inside the url
|
||||||
|
int abuseStart = url.indexOf("google_abuse=");
|
||||||
|
if (abuseStart != -1) {
|
||||||
|
int abuseEnd = url.indexOf("+path");
|
||||||
|
|
||||||
|
try {
|
||||||
|
String abuseCookie = url.substring(abuseStart + 13, abuseEnd);
|
||||||
|
abuseCookie = URLDecoder.decode(abuseCookie, "UTF-8");
|
||||||
|
handleCookies(abuseCookie);
|
||||||
|
} catch (UnsupportedEncodingException | StringIndexOutOfBoundsException e) {
|
||||||
|
if (MainActivity.DEBUG) {
|
||||||
|
e.printStackTrace();
|
||||||
|
Log.d(TAG, "handleCookiesFromUrl: invalid google abuse starting at "
|
||||||
|
+ abuseStart + " and ending at " + abuseEnd + " for url " + url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleCookies(@Nullable final String cookies) {
|
||||||
|
if (MainActivity.DEBUG) {
|
||||||
|
Log.d(TAG, "handleCookies: cookies=" + (cookies == null ? "null" : cookies));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cookies == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
addYoutubeCookies(cookies);
|
||||||
|
// add here methods to extract cookies for other services
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addYoutubeCookies(@NonNull final String cookies) {
|
||||||
|
if (cookies.contains("s_gl=") || cookies.contains("goojf=")
|
||||||
|
|| cookies.contains("VISITOR_INFO1_LIVE=")
|
||||||
|
|| cookies.contains("GOOGLE_ABUSE_EXEMPTION=")) {
|
||||||
|
// youtube seems to also need the other cookies:
|
||||||
|
addCookie(cookies);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addCookie(final String cookie) {
|
||||||
|
if (foundCookies.contains(cookie)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundCookies.isEmpty() || foundCookies.endsWith("; ")) {
|
||||||
|
foundCookies += cookie;
|
||||||
|
} else if (foundCookies.endsWith(";")) {
|
||||||
|
foundCookies += " " + cookie;
|
||||||
|
} else {
|
||||||
|
foundCookies += "; " + cookie;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,12 +9,6 @@ import android.content.SharedPreferences;
|
|||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import androidx.annotation.DrawableRes;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.core.app.NotificationCompat;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.view.ContextThemeWrapper;
|
import android.view.ContextThemeWrapper;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
@ -26,6 +20,13 @@ import android.widget.RadioButton;
|
|||||||
import android.widget.RadioGroup;
|
import android.widget.RadioGroup;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.appcompat.content.res.AppCompatResources;
|
||||||
|
import androidx.core.app.NotificationCompat;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
|
|
||||||
import org.schabi.newpipe.download.DownloadDialog;
|
import org.schabi.newpipe.download.DownloadDialog;
|
||||||
@ -44,12 +45,15 @@ import org.schabi.newpipe.player.playqueue.PlayQueue;
|
|||||||
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||||
import org.schabi.newpipe.report.UserAction;
|
import org.schabi.newpipe.report.UserAction;
|
||||||
|
import org.schabi.newpipe.util.AndroidTvUtils;
|
||||||
import org.schabi.newpipe.util.Constants;
|
import org.schabi.newpipe.util.Constants;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
import org.schabi.newpipe.util.ListHelper;
|
import org.schabi.newpipe.util.ListHelper;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.PermissionHelper;
|
import org.schabi.newpipe.util.PermissionHelper;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
import org.schabi.newpipe.util.urlfinder.UrlFinder;
|
||||||
|
import org.schabi.newpipe.views.FocusOverlayView;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -73,29 +77,31 @@ import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCap
|
|||||||
import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr;
|
import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the url from the intent and open it in the chosen preferred player
|
* Get the url from the intent and open it in the chosen preferred player.
|
||||||
*/
|
*/
|
||||||
public class RouterActivity extends AppCompatActivity {
|
public class RouterActivity extends AppCompatActivity {
|
||||||
|
public static final String INTERNAL_ROUTE_KEY = "internalRoute";
|
||||||
|
/**
|
||||||
|
* Removes invisible separators (\p{Z}) and punctuation characters including
|
||||||
|
* brackets (\p{P}). See http://www.regular-expressions.info/unicode.html for
|
||||||
|
* more details.
|
||||||
|
*/
|
||||||
|
private static final String REGEX_REMOVE_FROM_URL = "[\\p{Z}\\p{P}]";
|
||||||
|
protected final CompositeDisposable disposables = new CompositeDisposable();
|
||||||
@State
|
@State
|
||||||
protected int currentServiceId = -1;
|
protected int currentServiceId = -1;
|
||||||
private StreamingService currentService;
|
|
||||||
@State
|
@State
|
||||||
protected LinkType currentLinkType;
|
protected LinkType currentLinkType;
|
||||||
@State
|
@State
|
||||||
protected int selectedRadioPosition = -1;
|
protected int selectedRadioPosition = -1;
|
||||||
protected int selectedPreviously = -1;
|
protected int selectedPreviously = -1;
|
||||||
|
|
||||||
protected String currentUrl;
|
protected String currentUrl;
|
||||||
protected boolean internalRoute = false;
|
protected boolean internalRoute = false;
|
||||||
protected final CompositeDisposable disposables = new CompositeDisposable();
|
private StreamingService currentService;
|
||||||
|
|
||||||
private boolean selectionIsDownload = false;
|
private boolean selectionIsDownload = false;
|
||||||
|
|
||||||
public static final String internalRouteKey = "internalRoute";
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(final Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
Icepick.restoreInstanceState(this, savedInstanceState);
|
||||||
|
|
||||||
@ -108,14 +114,14 @@ public class RouterActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internalRoute = getIntent().getBooleanExtra(internalRouteKey, false);
|
internalRoute = getIntent().getBooleanExtra(INTERNAL_ROUTE_KEY, false);
|
||||||
|
|
||||||
setTheme(ThemeHelper.isLightThemeSelected(this)
|
setTheme(ThemeHelper.isLightThemeSelected(this)
|
||||||
? R.style.RouterActivityThemeLight : R.style.RouterActivityThemeDark);
|
? R.style.RouterActivityThemeLight : R.style.RouterActivityThemeDark);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onSaveInstanceState(Bundle outState) {
|
protected void onSaveInstanceState(final Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
Icepick.saveInstanceState(this, outState);
|
Icepick.saveInstanceState(this, outState);
|
||||||
}
|
}
|
||||||
@ -134,7 +140,7 @@ public class RouterActivity extends AppCompatActivity {
|
|||||||
disposables.clear();
|
disposables.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleUrl(String url) {
|
private void handleUrl(final String url) {
|
||||||
disposables.add(Observable
|
disposables.add(Observable
|
||||||
.fromCallable(() -> {
|
.fromCallable(() -> {
|
||||||
if (currentServiceId == -1) {
|
if (currentServiceId == -1) {
|
||||||
@ -159,13 +165,14 @@ public class RouterActivity extends AppCompatActivity {
|
|||||||
}, this::handleError));
|
}, this::handleError));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleError(Throwable error) {
|
private void handleError(final Throwable error) {
|
||||||
error.printStackTrace();
|
error.printStackTrace();
|
||||||
|
|
||||||
if (error instanceof ExtractionException) {
|
if (error instanceof ExtractionException) {
|
||||||
Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show();
|
Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show();
|
||||||
} else {
|
} else {
|
||||||
ExtractorHelper.handleGeneralException(this, -1, null, error, UserAction.SOMETHING_ELSE, null);
|
ExtractorHelper.handleGeneralException(this, -1, null, error,
|
||||||
|
UserAction.SOMETHING_ELSE, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
finish();
|
finish();
|
||||||
@ -177,8 +184,11 @@ public class RouterActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected void onSuccess() {
|
protected void onSuccess() {
|
||||||
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
|
final SharedPreferences preferences = PreferenceManager
|
||||||
final String selectedChoiceKey = preferences.getString(getString(R.string.preferred_open_action_key), getString(R.string.preferred_open_action_default));
|
.getDefaultSharedPreferences(this);
|
||||||
|
final String selectedChoiceKey = preferences
|
||||||
|
.getString(getString(R.string.preferred_open_action_key),
|
||||||
|
getString(R.string.preferred_open_action_default));
|
||||||
|
|
||||||
final String showInfoKey = getString(R.string.show_info_key);
|
final String showInfoKey = getString(R.string.show_info_key);
|
||||||
final String videoPlayerKey = getString(R.string.video_player_key);
|
final String videoPlayerKey = getString(R.string.video_player_key);
|
||||||
@ -188,7 +198,8 @@ public class RouterActivity extends AppCompatActivity {
|
|||||||
final String alwaysAskKey = getString(R.string.always_ask_open_action_key);
|
final String alwaysAskKey = getString(R.string.always_ask_open_action_key);
|
||||||
|
|
||||||
if (selectedChoiceKey.equals(alwaysAskKey)) {
|
if (selectedChoiceKey.equals(alwaysAskKey)) {
|
||||||
final List<AdapterChoiceItem> choices = getChoicesForService(currentService, currentLinkType);
|
final List<AdapterChoiceItem> choices
|
||||||
|
= getChoicesForService(currentService, currentLinkType);
|
||||||
|
|
||||||
switch (choices.size()) {
|
switch (choices.size()) {
|
||||||
case 1:
|
case 1:
|
||||||
@ -206,20 +217,26 @@ public class RouterActivity extends AppCompatActivity {
|
|||||||
} else if (selectedChoiceKey.equals(downloadKey)) {
|
} else if (selectedChoiceKey.equals(downloadKey)) {
|
||||||
handleChoice(downloadKey);
|
handleChoice(downloadKey);
|
||||||
} else {
|
} else {
|
||||||
final boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false);
|
final boolean isExtVideoEnabled = preferences.getBoolean(
|
||||||
final boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false);
|
getString(R.string.use_external_video_player_key), false);
|
||||||
final boolean isVideoPlayerSelected = selectedChoiceKey.equals(videoPlayerKey) || selectedChoiceKey.equals(popupPlayerKey);
|
final boolean isExtAudioEnabled = preferences.getBoolean(
|
||||||
|
getString(R.string.use_external_audio_player_key), false);
|
||||||
|
final boolean isVideoPlayerSelected = selectedChoiceKey.equals(videoPlayerKey)
|
||||||
|
|| selectedChoiceKey.equals(popupPlayerKey);
|
||||||
final boolean isAudioPlayerSelected = selectedChoiceKey.equals(backgroundPlayerKey);
|
final boolean isAudioPlayerSelected = selectedChoiceKey.equals(backgroundPlayerKey);
|
||||||
|
|
||||||
if (currentLinkType != LinkType.STREAM) {
|
if (currentLinkType != LinkType.STREAM) {
|
||||||
if (isExtAudioEnabled && isAudioPlayerSelected || isExtVideoEnabled && isVideoPlayerSelected) {
|
if (isExtAudioEnabled && isAudioPlayerSelected
|
||||||
Toast.makeText(this, R.string.external_player_unsupported_link_type, Toast.LENGTH_LONG).show();
|
|| isExtVideoEnabled && isVideoPlayerSelected) {
|
||||||
|
Toast.makeText(this, R.string.external_player_unsupported_link_type,
|
||||||
|
Toast.LENGTH_LONG).show();
|
||||||
handleChoice(showInfoKey);
|
handleChoice(showInfoKey);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<StreamingService.ServiceInfo.MediaCapability> capabilities = currentService.getServiceInfo().getMediaCapabilities();
|
final List<StreamingService.ServiceInfo.MediaCapability> capabilities
|
||||||
|
= currentService.getServiceInfo().getMediaCapabilities();
|
||||||
|
|
||||||
boolean serviceSupportsChoice = false;
|
boolean serviceSupportsChoice = false;
|
||||||
if (isVideoPlayerSelected) {
|
if (isVideoPlayerSelected) {
|
||||||
@ -241,7 +258,8 @@ public class RouterActivity extends AppCompatActivity {
|
|||||||
final Context themeWrapperContext = getThemeWrapperContext();
|
final Context themeWrapperContext = getThemeWrapperContext();
|
||||||
|
|
||||||
final LayoutInflater inflater = LayoutInflater.from(themeWrapperContext);
|
final LayoutInflater inflater = LayoutInflater.from(themeWrapperContext);
|
||||||
final LinearLayout rootLayout = (LinearLayout) inflater.inflate(R.layout.preferred_player_dialog_view, null, false);
|
final LinearLayout rootLayout = (LinearLayout) inflater.inflate(
|
||||||
|
R.layout.preferred_player_dialog_view, null, false);
|
||||||
final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list);
|
final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list);
|
||||||
|
|
||||||
final DialogInterface.OnClickListener dialogButtonsClickListener = (dialog, which) -> {
|
final DialogInterface.OnClickListener dialogButtonsClickListener = (dialog, which) -> {
|
||||||
@ -252,7 +270,9 @@ public class RouterActivity extends AppCompatActivity {
|
|||||||
handleChoice(choice.key);
|
handleChoice(choice.key);
|
||||||
|
|
||||||
if (which == DialogInterface.BUTTON_POSITIVE) {
|
if (which == DialogInterface.BUTTON_POSITIVE) {
|
||||||
preferences.edit().putString(getString(R.string.preferred_open_action_key), choice.key).apply();
|
preferences.edit()
|
||||||
|
.putString(getString(R.string.preferred_open_action_key), choice.key)
|
||||||
|
.apply();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -263,7 +283,9 @@ public class RouterActivity extends AppCompatActivity {
|
|||||||
.setNegativeButton(R.string.just_once, dialogButtonsClickListener)
|
.setNegativeButton(R.string.just_once, dialogButtonsClickListener)
|
||||||
.setPositiveButton(R.string.always, dialogButtonsClickListener)
|
.setPositiveButton(R.string.always, dialogButtonsClickListener)
|
||||||
.setOnDismissListener((dialog) -> {
|
.setOnDismissListener((dialog) -> {
|
||||||
if (!selectionIsDownload) finish();
|
if (!selectionIsDownload) {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.create();
|
.create();
|
||||||
|
|
||||||
@ -272,10 +294,13 @@ public class RouterActivity extends AppCompatActivity {
|
|||||||
setDialogButtonsState(alertDialog, radioGroup.getCheckedRadioButtonId() != -1);
|
setDialogButtonsState(alertDialog, radioGroup.getCheckedRadioButtonId() != -1);
|
||||||
});
|
});
|
||||||
|
|
||||||
radioGroup.setOnCheckedChangeListener((group, checkedId) -> setDialogButtonsState(alertDialog, true));
|
radioGroup.setOnCheckedChangeListener((group, checkedId) ->
|
||||||
|
setDialogButtonsState(alertDialog, true));
|
||||||
final View.OnClickListener radioButtonsClickListener = v -> {
|
final View.OnClickListener radioButtonsClickListener = v -> {
|
||||||
final int indexOfChild = radioGroup.indexOfChild(v);
|
final int indexOfChild = radioGroup.indexOfChild(v);
|
||||||
if (indexOfChild == -1) return;
|
if (indexOfChild == -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
selectedPreviously = selectedRadioPosition;
|
selectedPreviously = selectedRadioPosition;
|
||||||
selectedRadioPosition = indexOfChild;
|
selectedRadioPosition = indexOfChild;
|
||||||
@ -287,18 +312,23 @@ public class RouterActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
int id = 12345;
|
int id = 12345;
|
||||||
for (AdapterChoiceItem item : choices) {
|
for (AdapterChoiceItem item : choices) {
|
||||||
final RadioButton radioButton = (RadioButton) inflater.inflate(R.layout.list_radio_icon_item, null);
|
final RadioButton radioButton
|
||||||
|
= (RadioButton) inflater.inflate(R.layout.list_radio_icon_item, null);
|
||||||
radioButton.setText(item.description);
|
radioButton.setText(item.description);
|
||||||
radioButton.setCompoundDrawablesWithIntrinsicBounds(item.icon, 0, 0, 0);
|
radioButton.setCompoundDrawablesWithIntrinsicBounds(
|
||||||
|
AppCompatResources.getDrawable(getApplicationContext(), item.icon),
|
||||||
|
null, null, null);
|
||||||
radioButton.setChecked(false);
|
radioButton.setChecked(false);
|
||||||
radioButton.setId(id++);
|
radioButton.setId(id++);
|
||||||
radioButton.setLayoutParams(new RadioGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
radioButton.setLayoutParams(new RadioGroup.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||||
radioButton.setOnClickListener(radioButtonsClickListener);
|
radioButton.setOnClickListener(radioButtonsClickListener);
|
||||||
radioGroup.addView(radioButton);
|
radioGroup.addView(radioButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedRadioPosition == -1) {
|
if (selectedRadioPosition == -1) {
|
||||||
final String lastSelectedPlayer = preferences.getString(getString(R.string.preferred_open_action_last_selected_key), null);
|
final String lastSelectedPlayer = preferences.getString(
|
||||||
|
getString(R.string.preferred_open_action_last_selected_key), null);
|
||||||
if (!TextUtils.isEmpty(lastSelectedPlayer)) {
|
if (!TextUtils.isEmpty(lastSelectedPlayer)) {
|
||||||
for (int i = 0; i < choices.size(); i++) {
|
for (int i = 0; i < choices.size(); i++) {
|
||||||
AdapterChoiceItem c = choices.get(i);
|
AdapterChoiceItem c = choices.get(i);
|
||||||
@ -317,48 +347,64 @@ public class RouterActivity extends AppCompatActivity {
|
|||||||
selectedPreviously = selectedRadioPosition;
|
selectedPreviously = selectedRadioPosition;
|
||||||
|
|
||||||
alertDialog.show();
|
alertDialog.show();
|
||||||
|
|
||||||
|
if (AndroidTvUtils.isTv(this)) {
|
||||||
|
FocusOverlayView.setupFocusObserver(alertDialog);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<AdapterChoiceItem> getChoicesForService(StreamingService service, LinkType linkType) {
|
private List<AdapterChoiceItem> getChoicesForService(final StreamingService service,
|
||||||
|
final LinkType linkType) {
|
||||||
final Context context = getThemeWrapperContext();
|
final Context context = getThemeWrapperContext();
|
||||||
|
|
||||||
final List<AdapterChoiceItem> returnList = new ArrayList<>();
|
final List<AdapterChoiceItem> returnList = new ArrayList<>();
|
||||||
final List<StreamingService.ServiceInfo.MediaCapability> capabilities = service.getServiceInfo().getMediaCapabilities();
|
final List<StreamingService.ServiceInfo.MediaCapability> capabilities
|
||||||
|
= service.getServiceInfo().getMediaCapabilities();
|
||||||
|
|
||||||
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
|
final SharedPreferences preferences = PreferenceManager
|
||||||
boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false);
|
.getDefaultSharedPreferences(this);
|
||||||
boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false);
|
boolean isExtVideoEnabled = preferences.getBoolean(
|
||||||
|
getString(R.string.use_external_video_player_key), false);
|
||||||
|
boolean isExtAudioEnabled = preferences.getBoolean(
|
||||||
|
getString(R.string.use_external_audio_player_key), false);
|
||||||
|
|
||||||
returnList.add(new AdapterChoiceItem(getString(R.string.show_info_key), getString(R.string.show_info),
|
returnList.add(new AdapterChoiceItem(getString(R.string.show_info_key),
|
||||||
resolveResourceIdFromAttr(context, R.attr.info)));
|
getString(R.string.show_info),
|
||||||
|
resolveResourceIdFromAttr(context, R.attr.ic_info_outline)));
|
||||||
|
|
||||||
if (capabilities.contains(VIDEO) && !(isExtVideoEnabled && linkType != LinkType.STREAM)) {
|
if (capabilities.contains(VIDEO) && !(isExtVideoEnabled && linkType != LinkType.STREAM)) {
|
||||||
returnList.add(new AdapterChoiceItem(getString(R.string.video_player_key), getString(R.string.video_player),
|
returnList.add(new AdapterChoiceItem(getString(R.string.video_player_key),
|
||||||
resolveResourceIdFromAttr(context, R.attr.play)));
|
getString(R.string.video_player),
|
||||||
returnList.add(new AdapterChoiceItem(getString(R.string.popup_player_key), getString(R.string.popup_player),
|
resolveResourceIdFromAttr(context, R.attr.ic_play_arrow)));
|
||||||
resolveResourceIdFromAttr(context, R.attr.popup)));
|
returnList.add(new AdapterChoiceItem(getString(R.string.popup_player_key),
|
||||||
|
getString(R.string.popup_player),
|
||||||
|
resolveResourceIdFromAttr(context, R.attr.ic_popup)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (capabilities.contains(AUDIO) && !(isExtAudioEnabled && linkType != LinkType.STREAM)) {
|
if (capabilities.contains(AUDIO) && !(isExtAudioEnabled && linkType != LinkType.STREAM)) {
|
||||||
returnList.add(new AdapterChoiceItem(getString(R.string.background_player_key), getString(R.string.background_player),
|
returnList.add(new AdapterChoiceItem(getString(R.string.background_player_key),
|
||||||
resolveResourceIdFromAttr(context, R.attr.audio)));
|
getString(R.string.background_player),
|
||||||
|
resolveResourceIdFromAttr(context, R.attr.ic_headset)));
|
||||||
}
|
}
|
||||||
|
|
||||||
returnList.add(new AdapterChoiceItem(getString(R.string.download_key), getString(R.string.download),
|
returnList.add(new AdapterChoiceItem(getString(R.string.download_key),
|
||||||
resolveResourceIdFromAttr(context, R.attr.download)));
|
getString(R.string.download),
|
||||||
|
resolveResourceIdFromAttr(context, R.attr.ic_file_download)));
|
||||||
|
|
||||||
return returnList;
|
return returnList;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Context getThemeWrapperContext() {
|
private Context getThemeWrapperContext() {
|
||||||
return new ContextThemeWrapper(this,
|
return new ContextThemeWrapper(this, ThemeHelper.isLightThemeSelected(this)
|
||||||
ThemeHelper.isLightThemeSelected(this) ? R.style.LightTheme : R.style.DarkTheme);
|
? R.style.LightTheme : R.style.DarkTheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setDialogButtonsState(AlertDialog dialog, boolean state) {
|
private void setDialogButtonsState(final AlertDialog dialog, final boolean state) {
|
||||||
final Button negativeButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE);
|
final Button negativeButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE);
|
||||||
final Button positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
|
final Button positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
|
||||||
if (negativeButton == null || positiveButton == null) return;
|
if (negativeButton == null || positiveButton == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
negativeButton.setEnabled(state);
|
negativeButton.setEnabled(state);
|
||||||
positiveButton.setEnabled(state);
|
positiveButton.setEnabled(state);
|
||||||
@ -374,21 +420,25 @@ public class RouterActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void handleChoice(final String selectedChoiceKey) {
|
private void handleChoice(final String selectedChoiceKey) {
|
||||||
final List<String> validChoicesList = Arrays.asList(getResources().getStringArray(R.array.preferred_open_action_values_list));
|
final List<String> validChoicesList = Arrays.asList(getResources()
|
||||||
|
.getStringArray(R.array.preferred_open_action_values_list));
|
||||||
if (validChoicesList.contains(selectedChoiceKey)) {
|
if (validChoicesList.contains(selectedChoiceKey)) {
|
||||||
PreferenceManager.getDefaultSharedPreferences(this).edit()
|
PreferenceManager.getDefaultSharedPreferences(this).edit()
|
||||||
.putString(getString(R.string.preferred_open_action_last_selected_key), selectedChoiceKey)
|
.putString(getString(
|
||||||
|
R.string.preferred_open_action_last_selected_key), selectedChoiceKey)
|
||||||
.apply();
|
.apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedChoiceKey.equals(getString(R.string.popup_player_key)) && !PermissionHelper.isPopupEnabled(this)) {
|
if (selectedChoiceKey.equals(getString(R.string.popup_player_key))
|
||||||
|
&& !PermissionHelper.isPopupEnabled(this)) {
|
||||||
PermissionHelper.showPopupEnablementToast(this);
|
PermissionHelper.showPopupEnablementToast(this);
|
||||||
finish();
|
finish();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedChoiceKey.equals(getString(R.string.download_key))) {
|
if (selectedChoiceKey.equals(getString(R.string.download_key))) {
|
||||||
if (PermissionHelper.checkStoragePermissions(this, PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
|
if (PermissionHelper.checkStoragePermissions(this,
|
||||||
|
PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
|
||||||
selectionIsDownload = true;
|
selectionIsDownload = true;
|
||||||
openDownloadDialog();
|
openDownloadDialog();
|
||||||
}
|
}
|
||||||
@ -416,7 +466,8 @@ public class RouterActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final Intent intent = new Intent(this, FetcherService.class);
|
final Intent intent = new Intent(this, FetcherService.class);
|
||||||
final Choice choice = new Choice(currentService.getServiceId(), currentLinkType, currentUrl, selectedChoiceKey);
|
final Choice choice = new Choice(currentService.getServiceId(), currentLinkType,
|
||||||
|
currentUrl, selectedChoiceKey);
|
||||||
intent.putExtra(FetcherService.KEY_CHOICE, choice);
|
intent.putExtra(FetcherService.KEY_CHOICE, choice);
|
||||||
startService(intent);
|
startService(intent);
|
||||||
|
|
||||||
@ -429,12 +480,11 @@ public class RouterActivity extends AppCompatActivity {
|
|||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe((@NonNull StreamInfo result) -> {
|
.subscribe((@NonNull StreamInfo result) -> {
|
||||||
List<VideoStream> sortedVideoStreams = ListHelper.getSortedStreamVideosList(this,
|
List<VideoStream> sortedVideoStreams = ListHelper
|
||||||
result.getVideoStreams(),
|
.getSortedStreamVideosList(this, result.getVideoStreams(),
|
||||||
result.getVideoOnlyStreams(),
|
result.getVideoOnlyStreams(), false);
|
||||||
false);
|
int selectedVideoStreamIndex = ListHelper
|
||||||
int selectedVideoStreamIndex = ListHelper.getDefaultResolutionIndex(this,
|
.getDefaultResolutionIndex(this, sortedVideoStreams);
|
||||||
sortedVideoStreams);
|
|
||||||
|
|
||||||
FragmentManager fm = getSupportFragmentManager();
|
FragmentManager fm = getSupportFragmentManager();
|
||||||
DownloadDialog downloadDialog = DownloadDialog.newInstance(result);
|
DownloadDialog downloadDialog = DownloadDialog.newInstance(result);
|
||||||
@ -452,7 +502,9 @@ public class RouterActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
public void onRequestPermissionsResult(final int requestCode,
|
||||||
|
@NonNull final String[] permissions,
|
||||||
|
@NonNull final int[] grantResults) {
|
||||||
for (int i : grantResults) {
|
for (int i : grantResults) {
|
||||||
if (i == PackageManager.PERMISSION_DENIED) {
|
if (i == PackageManager.PERMISSION_DENIED) {
|
||||||
finish();
|
finish();
|
||||||
@ -464,196 +516,10 @@ public class RouterActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class AdapterChoiceItem {
|
|
||||||
final String description, key;
|
|
||||||
@DrawableRes
|
|
||||||
final int icon;
|
|
||||||
|
|
||||||
AdapterChoiceItem(String key, String description, int icon) {
|
|
||||||
this.description = description;
|
|
||||||
this.key = key;
|
|
||||||
this.icon = icon;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class Choice implements Serializable {
|
|
||||||
final int serviceId;
|
|
||||||
final String url, playerChoice;
|
|
||||||
final LinkType linkType;
|
|
||||||
|
|
||||||
Choice(int serviceId, LinkType linkType, String url, String playerChoice) {
|
|
||||||
this.serviceId = serviceId;
|
|
||||||
this.linkType = linkType;
|
|
||||||
this.url = url;
|
|
||||||
this.playerChoice = playerChoice;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return serviceId + ":" + url + " > " + linkType + " ::: " + playerChoice;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Service Fetcher
|
// Service Fetcher
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
public static class FetcherService extends IntentService {
|
|
||||||
|
|
||||||
private static final int ID = 456;
|
|
||||||
public static final String KEY_CHOICE = "key_choice";
|
|
||||||
private Disposable fetcher;
|
|
||||||
|
|
||||||
public FetcherService() {
|
|
||||||
super(FetcherService.class.getSimpleName());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate() {
|
|
||||||
super.onCreate();
|
|
||||||
startForeground(ID, createNotification().build());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onHandleIntent(@Nullable Intent intent) {
|
|
||||||
if (intent == null) return;
|
|
||||||
|
|
||||||
final Serializable serializable = intent.getSerializableExtra(KEY_CHOICE);
|
|
||||||
if (!(serializable instanceof Choice)) return;
|
|
||||||
Choice playerChoice = (Choice) serializable;
|
|
||||||
handleChoice(playerChoice);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void handleChoice(Choice choice) {
|
|
||||||
Single<? extends Info> single = null;
|
|
||||||
UserAction userAction = UserAction.SOMETHING_ELSE;
|
|
||||||
|
|
||||||
switch (choice.linkType) {
|
|
||||||
case STREAM:
|
|
||||||
single = ExtractorHelper.getStreamInfo(choice.serviceId, choice.url, false);
|
|
||||||
userAction = UserAction.REQUESTED_STREAM;
|
|
||||||
break;
|
|
||||||
case CHANNEL:
|
|
||||||
single = ExtractorHelper.getChannelInfo(choice.serviceId, choice.url, false);
|
|
||||||
userAction = UserAction.REQUESTED_CHANNEL;
|
|
||||||
break;
|
|
||||||
case PLAYLIST:
|
|
||||||
single = ExtractorHelper.getPlaylistInfo(choice.serviceId, choice.url, false);
|
|
||||||
userAction = UserAction.REQUESTED_PLAYLIST;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (single != null) {
|
|
||||||
final UserAction finalUserAction = userAction;
|
|
||||||
final Consumer<Info> resultHandler = getResultHandler(choice);
|
|
||||||
fetcher = single
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(info -> {
|
|
||||||
resultHandler.accept(info);
|
|
||||||
if (fetcher != null) fetcher.dispose();
|
|
||||||
}, throwable -> ExtractorHelper.handleGeneralException(this,
|
|
||||||
choice.serviceId, choice.url, throwable, finalUserAction, ", opened with " + choice.playerChoice));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Consumer<Info> getResultHandler(Choice choice) {
|
|
||||||
return info -> {
|
|
||||||
final String videoPlayerKey = getString(R.string.video_player_key);
|
|
||||||
final String backgroundPlayerKey = getString(R.string.background_player_key);
|
|
||||||
final String popupPlayerKey = getString(R.string.popup_player_key);
|
|
||||||
|
|
||||||
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
|
|
||||||
boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false);
|
|
||||||
boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false);
|
|
||||||
;
|
|
||||||
|
|
||||||
PlayQueue playQueue;
|
|
||||||
String playerChoice = choice.playerChoice;
|
|
||||||
|
|
||||||
if (info instanceof StreamInfo) {
|
|
||||||
if (playerChoice.equals(backgroundPlayerKey) && isExtAudioEnabled) {
|
|
||||||
NavigationHelper.playOnExternalAudioPlayer(this, (StreamInfo) info);
|
|
||||||
|
|
||||||
} else if (playerChoice.equals(videoPlayerKey) && isExtVideoEnabled) {
|
|
||||||
NavigationHelper.playOnExternalVideoPlayer(this, (StreamInfo) info);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
playQueue = new SinglePlayQueue((StreamInfo) info);
|
|
||||||
|
|
||||||
if (playerChoice.equals(videoPlayerKey)) {
|
|
||||||
openMainPlayer(playQueue, choice);
|
|
||||||
} else if (playerChoice.equals(backgroundPlayerKey)) {
|
|
||||||
NavigationHelper.enqueueOnBackgroundPlayer(this, playQueue, true);
|
|
||||||
} else if (playerChoice.equals(popupPlayerKey)) {
|
|
||||||
NavigationHelper.enqueueOnPopupPlayer(this, playQueue, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (info instanceof ChannelInfo || info instanceof PlaylistInfo) {
|
|
||||||
playQueue = info instanceof ChannelInfo ? new ChannelPlayQueue((ChannelInfo) info) : new PlaylistPlayQueue((PlaylistInfo) info);
|
|
||||||
|
|
||||||
if (playerChoice.equals(videoPlayerKey)) {
|
|
||||||
openMainPlayer(playQueue, choice);
|
|
||||||
} else if (playerChoice.equals(backgroundPlayerKey)) {
|
|
||||||
NavigationHelper.playOnBackgroundPlayer(this, playQueue, true);
|
|
||||||
} else if (playerChoice.equals(popupPlayerKey)) {
|
|
||||||
NavigationHelper.playOnPopupPlayer(this, playQueue, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private void openMainPlayer(PlayQueue playQueue, Choice choice) {
|
|
||||||
NavigationHelper.playOnMainPlayer(this, playQueue, choice.linkType,
|
|
||||||
choice.url, "", true, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
stopForeground(true);
|
|
||||||
if (fetcher != null) fetcher.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
private NotificationCompat.Builder createNotification() {
|
|
||||||
return new NotificationCompat.Builder(this, getString(R.string.notification_channel_id))
|
|
||||||
.setOngoing(true)
|
|
||||||
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
|
|
||||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
||||||
.setContentTitle(getString(R.string.preferred_player_fetcher_notification_title))
|
|
||||||
.setContentText(getString(R.string.preferred_player_fetcher_notification_message));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Utils
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes invisible separators (\p{Z}) and punctuation characters including
|
|
||||||
* brackets (\p{P}). See http://www.regular-expressions.info/unicode.html for
|
|
||||||
* more details.
|
|
||||||
*/
|
|
||||||
private final static String REGEX_REMOVE_FROM_URL = "[\\p{Z}\\p{P}]";
|
|
||||||
|
|
||||||
private String getUrl(Intent intent) {
|
|
||||||
// first gather data and find service
|
|
||||||
String videoUrl = null;
|
|
||||||
if (intent.getData() != null) {
|
|
||||||
// this means the video was called though another app
|
|
||||||
videoUrl = intent.getData().toString();
|
|
||||||
} else if (intent.getStringExtra(Intent.EXTRA_TEXT) != null) {
|
|
||||||
//this means that vidoe was called through share menu
|
|
||||||
String extraText = intent.getStringExtra(Intent.EXTRA_TEXT);
|
|
||||||
final String[] uris = getUris(extraText);
|
|
||||||
videoUrl = uris.length > 0 ? uris[0] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return videoUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String removeHeadingGibberish(final String input) {
|
private String removeHeadingGibberish(final String input) {
|
||||||
int start = 0;
|
int start = 0;
|
||||||
for (int i = input.indexOf("://") - 1; i >= 0; i--) {
|
for (int i = input.indexOf("://") - 1; i >= 0; i--) {
|
||||||
@ -662,9 +528,13 @@ public class RouterActivity extends AppCompatActivity {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return input.substring(start, input.length());
|
return input.substring(start);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Utils
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
private String trim(final String input) {
|
private String trim(final String input) {
|
||||||
if (input == null || input.length() < 1) {
|
if (input == null || input.length() < 1) {
|
||||||
return input;
|
return input;
|
||||||
@ -674,7 +544,7 @@ public class RouterActivity extends AppCompatActivity {
|
|||||||
output = output.substring(1);
|
output = output.substring(1);
|
||||||
}
|
}
|
||||||
while (output.length() > 0
|
while (output.length() > 0
|
||||||
&& output.substring(output.length() - 1, output.length()).matches(REGEX_REMOVE_FROM_URL)) {
|
&& output.substring(output.length() - 1).matches(REGEX_REMOVE_FROM_URL)) {
|
||||||
output = output.substring(0, output.length() - 1);
|
output = output.substring(0, output.length() - 1);
|
||||||
}
|
}
|
||||||
return output;
|
return output;
|
||||||
@ -705,4 +575,200 @@ public class RouterActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
return result.toArray(new String[result.size()]);
|
return result.toArray(new String[result.size()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class AdapterChoiceItem {
|
||||||
|
final String description;
|
||||||
|
final String key;
|
||||||
|
@DrawableRes
|
||||||
|
final int icon;
|
||||||
|
|
||||||
|
AdapterChoiceItem(final String key, final String description, final int icon) {
|
||||||
|
this.description = description;
|
||||||
|
this.key = key;
|
||||||
|
this.icon = icon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Choice implements Serializable {
|
||||||
|
final int serviceId;
|
||||||
|
final String url;
|
||||||
|
final String playerChoice;
|
||||||
|
final LinkType linkType;
|
||||||
|
|
||||||
|
Choice(final int serviceId, final LinkType linkType,
|
||||||
|
final String url, final String playerChoice) {
|
||||||
|
this.serviceId = serviceId;
|
||||||
|
this.linkType = linkType;
|
||||||
|
this.url = url;
|
||||||
|
this.playerChoice = playerChoice;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return serviceId + ":" + url + " > " + linkType + " ::: " + playerChoice;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class FetcherService extends IntentService {
|
||||||
|
|
||||||
|
public static final String KEY_CHOICE = "key_choice";
|
||||||
|
private static final int ID = 456;
|
||||||
|
private Disposable fetcher;
|
||||||
|
|
||||||
|
public FetcherService() {
|
||||||
|
super(FetcherService.class.getSimpleName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
startForeground(ID, createNotification().build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onHandleIntent(@Nullable final Intent intent) {
|
||||||
|
if (intent == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Serializable serializable = intent.getSerializableExtra(KEY_CHOICE);
|
||||||
|
if (!(serializable instanceof Choice)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Choice playerChoice = (Choice) serializable;
|
||||||
|
handleChoice(playerChoice);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handleChoice(final Choice choice) {
|
||||||
|
Single<? extends Info> single = null;
|
||||||
|
UserAction userAction = UserAction.SOMETHING_ELSE;
|
||||||
|
|
||||||
|
switch (choice.linkType) {
|
||||||
|
case STREAM:
|
||||||
|
single = ExtractorHelper.getStreamInfo(choice.serviceId, choice.url, false);
|
||||||
|
userAction = UserAction.REQUESTED_STREAM;
|
||||||
|
break;
|
||||||
|
case CHANNEL:
|
||||||
|
single = ExtractorHelper.getChannelInfo(choice.serviceId, choice.url, false);
|
||||||
|
userAction = UserAction.REQUESTED_CHANNEL;
|
||||||
|
break;
|
||||||
|
case PLAYLIST:
|
||||||
|
single = ExtractorHelper.getPlaylistInfo(choice.serviceId, choice.url, false);
|
||||||
|
userAction = UserAction.REQUESTED_PLAYLIST;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (single != null) {
|
||||||
|
final UserAction finalUserAction = userAction;
|
||||||
|
final Consumer<Info> resultHandler = getResultHandler(choice);
|
||||||
|
fetcher = single
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(info -> {
|
||||||
|
resultHandler.accept(info);
|
||||||
|
if (fetcher != null) {
|
||||||
|
fetcher.dispose();
|
||||||
|
}
|
||||||
|
}, throwable -> ExtractorHelper.handleGeneralException(this,
|
||||||
|
choice.serviceId, choice.url, throwable, finalUserAction,
|
||||||
|
", opened with " + choice.playerChoice));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Consumer<Info> getResultHandler(final Choice choice) {
|
||||||
|
return info -> {
|
||||||
|
final String videoPlayerKey = getString(R.string.video_player_key);
|
||||||
|
final String backgroundPlayerKey = getString(R.string.background_player_key);
|
||||||
|
final String popupPlayerKey = getString(R.string.popup_player_key);
|
||||||
|
|
||||||
|
final SharedPreferences preferences = PreferenceManager
|
||||||
|
.getDefaultSharedPreferences(this);
|
||||||
|
boolean isExtVideoEnabled = preferences.getBoolean(
|
||||||
|
getString(R.string.use_external_video_player_key), false);
|
||||||
|
boolean isExtAudioEnabled = preferences.getBoolean(
|
||||||
|
getString(R.string.use_external_audio_player_key), false);
|
||||||
|
|
||||||
|
PlayQueue playQueue;
|
||||||
|
String playerChoice = choice.playerChoice;
|
||||||
|
|
||||||
|
if (info instanceof StreamInfo) {
|
||||||
|
if (playerChoice.equals(backgroundPlayerKey) && isExtAudioEnabled) {
|
||||||
|
NavigationHelper.playOnExternalAudioPlayer(this, (StreamInfo) info);
|
||||||
|
|
||||||
|
} else if (playerChoice.equals(videoPlayerKey) && isExtVideoEnabled) {
|
||||||
|
NavigationHelper.playOnExternalVideoPlayer(this, (StreamInfo) info);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
playQueue = new SinglePlayQueue((StreamInfo) info);
|
||||||
|
|
||||||
|
if (playerChoice.equals(videoPlayerKey)) {
|
||||||
|
openMainPlayer(playQueue, choice);
|
||||||
|
} else if (playerChoice.equals(backgroundPlayerKey)) {
|
||||||
|
NavigationHelper.enqueueOnBackgroundPlayer(this, playQueue, true);
|
||||||
|
} else if (playerChoice.equals(popupPlayerKey)) {
|
||||||
|
NavigationHelper.enqueueOnPopupPlayer(this, playQueue, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info instanceof ChannelInfo || info instanceof PlaylistInfo) {
|
||||||
|
playQueue = info instanceof ChannelInfo
|
||||||
|
? new ChannelPlayQueue((ChannelInfo) info)
|
||||||
|
: new PlaylistPlayQueue((PlaylistInfo) info);
|
||||||
|
|
||||||
|
if (playerChoice.equals(videoPlayerKey)) {
|
||||||
|
openMainPlayer(playQueue, choice);
|
||||||
|
} else if (playerChoice.equals(backgroundPlayerKey)) {
|
||||||
|
NavigationHelper.playOnBackgroundPlayer(this, playQueue, true);
|
||||||
|
} else if (playerChoice.equals(popupPlayerKey)) {
|
||||||
|
NavigationHelper.playOnPopupPlayer(this, playQueue, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openMainPlayer(PlayQueue playQueue, Choice choice) {
|
||||||
|
NavigationHelper.playOnMainPlayer(this, playQueue, choice.linkType,
|
||||||
|
choice.url, "", true, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
stopForeground(true);
|
||||||
|
if (fetcher != null) {
|
||||||
|
fetcher.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private NotificationCompat.Builder createNotification() {
|
||||||
|
return new NotificationCompat.Builder(this, getString(R.string.notification_channel_id))
|
||||||
|
.setOngoing(true)
|
||||||
|
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
|
||||||
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||||
|
.setContentTitle(
|
||||||
|
getString(R.string.preferred_player_fetcher_notification_title))
|
||||||
|
.setContentText(
|
||||||
|
getString(R.string.preferred_player_fetcher_notification_message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Utils
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private String getUrl(final Intent intent) {
|
||||||
|
String foundUrl = null;
|
||||||
|
if (intent.getData() != null) {
|
||||||
|
// Called from another app
|
||||||
|
foundUrl = intent.getData().toString();
|
||||||
|
} else if (intent.getStringExtra(Intent.EXTRA_TEXT) != null) {
|
||||||
|
// Called from the share menu
|
||||||
|
final String extraText = intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||||
|
foundUrl = UrlFinder.firstUrlFromInput(extraText);
|
||||||
|
}
|
||||||
|
|
||||||
|
return foundUrl;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,48 +1,67 @@
|
|||||||
package org.schabi.newpipe.about;
|
package org.schabi.newpipe.about;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import com.google.android.material.tabs.TabLayout;
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.appcompat.widget.Toolbar;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.fragment.app.FragmentPagerAdapter;
|
import androidx.fragment.app.FragmentPagerAdapter;
|
||||||
import androidx.fragment.app.FragmentStatePagerAdapter;
|
import androidx.fragment.app.FragmentStatePagerAdapter;
|
||||||
import androidx.viewpager.widget.PagerAdapter;
|
import androidx.viewpager.widget.PagerAdapter;
|
||||||
import androidx.viewpager.widget.ViewPager;
|
import androidx.viewpager.widget.ViewPager;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import androidx.appcompat.widget.Toolbar;
|
import com.google.android.material.tabs.TabLayout;
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.BuildConfig;
|
import org.schabi.newpipe.BuildConfig;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
public class AboutActivity extends AppCompatActivity {
|
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||||
|
import static org.schabi.newpipe.util.ShareUtils.openUrlInBrowser;
|
||||||
|
|
||||||
|
public class AboutActivity extends AppCompatActivity {
|
||||||
/**
|
/**
|
||||||
* List of all software components
|
* List of all software components.
|
||||||
*/
|
*/
|
||||||
private static final SoftwareComponent[] SOFTWARE_COMPONENTS = new SoftwareComponent[]{
|
private static final SoftwareComponent[] SOFTWARE_COMPONENTS = new SoftwareComponent[]{
|
||||||
new SoftwareComponent("Giga Get", "2014", "Peter Cai", "https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL2),
|
new SoftwareComponent("Giga Get", "2014 - 2015", "Peter Cai",
|
||||||
new SoftwareComponent("NewPipe Extractor", "2017", "Christian Schabesberger", "https://github.com/TeamNewPipe/NewPipeExtractor", StandardLicenses.GPL3),
|
"https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL2),
|
||||||
new SoftwareComponent("Jsoup", "2017", "Jonathan Hedley", "https://github.com/jhy/jsoup", StandardLicenses.MIT),
|
new SoftwareComponent("NewPipe Extractor", "2017 - 2020", "Christian Schabesberger",
|
||||||
new SoftwareComponent("Rhino", "2015", "Mozilla", "https://www.mozilla.org/rhino/", StandardLicenses.MPL2),
|
"https://github.com/TeamNewPipe/NewPipeExtractor", StandardLicenses.GPL3),
|
||||||
new SoftwareComponent("ACRA", "2013", "Kevin Gaudin", "http://www.acra.ch", StandardLicenses.APACHE2),
|
new SoftwareComponent("Jsoup", "2017", "Jonathan Hedley",
|
||||||
new SoftwareComponent("Universal Image Loader", "2011 - 2015", "Sergey Tarasevich", "https://github.com/nostra13/Android-Universal-Image-Loader", StandardLicenses.APACHE2),
|
"https://github.com/jhy/jsoup", StandardLicenses.MIT),
|
||||||
new SoftwareComponent("CircleImageView", "2014 - 2017", "Henning Dodenhof", "https://github.com/hdodenhof/CircleImageView", StandardLicenses.APACHE2),
|
new SoftwareComponent("Rhino", "2015", "Mozilla",
|
||||||
new SoftwareComponent("NoNonsense-FilePicker", "2016", "Jonas Kalderstam", "https://github.com/spacecowboy/NoNonsense-FilePicker", StandardLicenses.MPL2),
|
"https://www.mozilla.org/rhino/", StandardLicenses.MPL2),
|
||||||
new SoftwareComponent("ExoPlayer", "2014-2017", "Google Inc", "https://github.com/google/ExoPlayer", StandardLicenses.APACHE2),
|
new SoftwareComponent("ACRA", "2013", "Kevin Gaudin",
|
||||||
new SoftwareComponent("RxAndroid", "2015", "The RxAndroid authors", "https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2),
|
"http://www.acra.ch", StandardLicenses.APACHE2),
|
||||||
new SoftwareComponent("RxJava", "2016-present", "RxJava Contributors", "https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2),
|
new SoftwareComponent("Universal Image Loader", "2011 - 2015", "Sergey Tarasevich",
|
||||||
new SoftwareComponent("RxBinding", "2015", "Jake Wharton", "https://github.com/JakeWharton/RxBinding", StandardLicenses.APACHE2)
|
"https://github.com/nostra13/Android-Universal-Image-Loader",
|
||||||
|
StandardLicenses.APACHE2),
|
||||||
|
new SoftwareComponent("CircleImageView", "2014 - 2020", "Henning Dodenhof",
|
||||||
|
"https://github.com/hdodenhof/CircleImageView", StandardLicenses.APACHE2),
|
||||||
|
new SoftwareComponent("NoNonsense-FilePicker", "2016", "Jonas Kalderstam",
|
||||||
|
"https://github.com/spacecowboy/NoNonsense-FilePicker", StandardLicenses.MPL2),
|
||||||
|
new SoftwareComponent("ExoPlayer", "2014 - 2020", "Google Inc",
|
||||||
|
"https://github.com/google/ExoPlayer", StandardLicenses.APACHE2),
|
||||||
|
new SoftwareComponent("RxAndroid", "2015 - 2018", "The RxAndroid authors",
|
||||||
|
"https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2),
|
||||||
|
new SoftwareComponent("RxJava", "2016 - 2020", "RxJava Contributors",
|
||||||
|
"https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2),
|
||||||
|
new SoftwareComponent("RxBinding", "2015 - 2018", "Jake Wharton",
|
||||||
|
"https://github.com/JakeWharton/RxBinding", StandardLicenses.APACHE2),
|
||||||
|
new SoftwareComponent("PrettyTime", "2012 - 2020", "Lincoln Baxter, III",
|
||||||
|
"https://github.com/ocpsoft/prettytime", StandardLicenses.APACHE2),
|
||||||
|
new SoftwareComponent("Markwon", "2017 - 2020", "Noties",
|
||||||
|
"https://github.com/noties/Markwon", StandardLicenses.APACHE2),
|
||||||
|
new SoftwareComponent("Groupie", "2016", "Lisa Wray",
|
||||||
|
"https://github.com/lisawray/groupie", StandardLicenses.MIT)
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -61,9 +80,11 @@ public class AboutActivity extends AppCompatActivity {
|
|||||||
private ViewPager mViewPager;
|
private ViewPager mViewPager;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(final Bundle savedInstanceState) {
|
||||||
|
assureCorrectAppLanguage(this);
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
ThemeHelper.setTheme(this);
|
ThemeHelper.setTheme(this);
|
||||||
|
this.setTitle(getString(R.string.title_activity_about));
|
||||||
|
|
||||||
setContentView(R.layout.activity_about);
|
setContentView(R.layout.activity_about);
|
||||||
|
|
||||||
@ -82,28 +103,14 @@ public class AboutActivity extends AppCompatActivity {
|
|||||||
tabLayout.setupWithViewPager(mViewPager);
|
tabLayout.setupWithViewPager(mViewPager);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onCreateOptionsMenu(Menu menu) {
|
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||||
// Inflate the menu; this adds items to the action bar if it is present.
|
|
||||||
getMenuInflater().inflate(R.menu.menu_about, menu);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
|
|
||||||
int id = item.getItemId();
|
int id = item.getItemId();
|
||||||
|
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case android.R.id.home:
|
case android.R.id.home:
|
||||||
finish();
|
finish();
|
||||||
return true;
|
return true;
|
||||||
case R.id.action_settings:
|
|
||||||
NavigationHelper.openSettings(this);
|
|
||||||
return true;
|
|
||||||
case R.id.action_show_downloads:
|
|
||||||
return NavigationHelper.openDownloads(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
@ -113,21 +120,20 @@ public class AboutActivity extends AppCompatActivity {
|
|||||||
* A placeholder fragment containing a simple view.
|
* A placeholder fragment containing a simple view.
|
||||||
*/
|
*/
|
||||||
public static class AboutFragment extends Fragment {
|
public static class AboutFragment extends Fragment {
|
||||||
|
public AboutFragment() { }
|
||||||
public AboutFragment() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a new instance of this fragment for the given section
|
* Created a new instance of this fragment for the given section number.
|
||||||
* number.
|
*
|
||||||
|
* @return New instance of {@link AboutFragment}
|
||||||
*/
|
*/
|
||||||
public static AboutFragment newInstance() {
|
public static AboutFragment newInstance() {
|
||||||
return new AboutFragment();
|
return new AboutFragment();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
|
||||||
Bundle savedInstanceState) {
|
final Bundle savedInstanceState) {
|
||||||
View rootView = inflater.inflate(R.layout.fragment_about, container, false);
|
View rootView = inflater.inflate(R.layout.fragment_about, container, false);
|
||||||
Context context = this.getContext();
|
Context context = this.getContext();
|
||||||
|
|
||||||
@ -135,40 +141,37 @@ public class AboutActivity extends AppCompatActivity {
|
|||||||
version.setText(BuildConfig.VERSION_NAME);
|
version.setText(BuildConfig.VERSION_NAME);
|
||||||
|
|
||||||
View githubLink = rootView.findViewById(R.id.github_link);
|
View githubLink = rootView.findViewById(R.id.github_link);
|
||||||
githubLink.setOnClickListener(nv -> openWebsite(context.getString(R.string.github_url), context));
|
githubLink.setOnClickListener(nv ->
|
||||||
|
openUrlInBrowser(context, context.getString(R.string.github_url)));
|
||||||
|
|
||||||
View donationLink = rootView.findViewById(R.id.donation_link);
|
View donationLink = rootView.findViewById(R.id.donation_link);
|
||||||
donationLink.setOnClickListener(v -> openWebsite(context.getString(R.string.donation_url), context));
|
donationLink.setOnClickListener(v ->
|
||||||
|
openUrlInBrowser(context, context.getString(R.string.donation_url)));
|
||||||
|
|
||||||
View websiteLink = rootView.findViewById(R.id.website_link);
|
View websiteLink = rootView.findViewById(R.id.website_link);
|
||||||
websiteLink.setOnClickListener(nv -> openWebsite(context.getString(R.string.website_url), context));
|
websiteLink.setOnClickListener(nv ->
|
||||||
|
openUrlInBrowser(context, context.getString(R.string.website_url)));
|
||||||
|
|
||||||
View privacyPolicyLink = rootView.findViewById(R.id.privacy_policy_link);
|
View privacyPolicyLink = rootView.findViewById(R.id.privacy_policy_link);
|
||||||
privacyPolicyLink.setOnClickListener(v -> openWebsite(context.getString(R.string.privacy_policy_url), context));
|
privacyPolicyLink.setOnClickListener(v ->
|
||||||
|
openUrlInBrowser(context, context.getString(R.string.privacy_policy_url)));
|
||||||
|
|
||||||
return rootView;
|
return rootView;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openWebsite(String url, Context context) {
|
|
||||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
|
||||||
context.startActivity(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link FragmentPagerAdapter} that returns a fragment corresponding to
|
* A {@link FragmentPagerAdapter} that returns a fragment corresponding to
|
||||||
* one of the sections/tabs/pages.
|
* one of the sections/tabs/pages.
|
||||||
*/
|
*/
|
||||||
public class SectionsPagerAdapter extends FragmentPagerAdapter {
|
public class SectionsPagerAdapter extends FragmentPagerAdapter {
|
||||||
|
public SectionsPagerAdapter(final FragmentManager fm) {
|
||||||
public SectionsPagerAdapter(FragmentManager fm) {
|
|
||||||
super(fm);
|
super(fm);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Fragment getItem(int position) {
|
public Fragment getItem(final int position) {
|
||||||
switch (position) {
|
switch (position) {
|
||||||
case 0:
|
case 0:
|
||||||
return AboutFragment.newInstance();
|
return AboutFragment.newInstance();
|
||||||
@ -185,7 +188,7 @@ public class AboutActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CharSequence getPageTitle(int position) {
|
public CharSequence getPageTitle(final int position) {
|
||||||
switch (position) {
|
switch (position) {
|
||||||
case 0:
|
case 0:
|
||||||
return getString(R.string.tab_about);
|
return getString(R.string.tab_about);
|
||||||
|
@ -5,18 +5,17 @@ import android.os.Parcel;
|
|||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A software license
|
* Class for storing information about a software license.
|
||||||
*/
|
*/
|
||||||
public class License implements Parcelable {
|
public class License implements Parcelable {
|
||||||
|
|
||||||
public static final Creator<License> CREATOR = new Creator<License>() {
|
public static final Creator<License> CREATOR = new Creator<License>() {
|
||||||
@Override
|
@Override
|
||||||
public License createFromParcel(Parcel source) {
|
public License createFromParcel(final Parcel source) {
|
||||||
return new License(source);
|
return new License(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public License[] newArray(int size) {
|
public License[] newArray(final int size) {
|
||||||
return new License[size];
|
return new License[size];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -24,16 +23,22 @@ public class License implements Parcelable {
|
|||||||
private final String name;
|
private final String name;
|
||||||
private String filename;
|
private String filename;
|
||||||
|
|
||||||
public License(String name, String abbreviation, String filename) {
|
public License(final String name, final String abbreviation, final String filename) {
|
||||||
if(name == null) throw new NullPointerException("name is null");
|
if (name == null) {
|
||||||
if(abbreviation == null) throw new NullPointerException("abbreviation is null");
|
throw new NullPointerException("name is null");
|
||||||
if(filename == null) throw new NullPointerException("filename is null");
|
}
|
||||||
|
if (abbreviation == null) {
|
||||||
|
throw new NullPointerException("abbreviation is null");
|
||||||
|
}
|
||||||
|
if (filename == null) {
|
||||||
|
throw new NullPointerException("filename is null");
|
||||||
|
}
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.filename = filename;
|
this.filename = filename;
|
||||||
this.abbreviation = abbreviation;
|
this.abbreviation = abbreviation;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected License(Parcel in) {
|
protected License(final Parcel in) {
|
||||||
this.filename = in.readString();
|
this.filename = in.readString();
|
||||||
this.abbreviation = in.readString();
|
this.abbreviation = in.readString();
|
||||||
this.name = in.readString();
|
this.name = in.readString();
|
||||||
@ -61,7 +66,7 @@ public class License implements Parcelable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void writeToParcel(Parcel dest, int flags) {
|
public void writeToParcel(final Parcel dest, final int flags) {
|
||||||
dest.writeString(this.filename);
|
dest.writeString(this.filename);
|
||||||
dest.writeString(this.abbreviation);
|
dest.writeString(this.abbreviation);
|
||||||
dest.writeString(this.name);
|
dest.writeString(this.name);
|
||||||
|
@ -2,29 +2,33 @@ package org.schabi.newpipe.about;
|
|||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.view.ContextMenu;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.MenuInflater;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import android.view.*;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.util.ShareUtils;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Comparator;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fragment containing the software licenses
|
* Fragment containing the software licenses.
|
||||||
*/
|
*/
|
||||||
public class LicenseFragment extends Fragment {
|
public class LicenseFragment extends Fragment {
|
||||||
|
|
||||||
private static final String ARG_COMPONENTS = "components";
|
private static final String ARG_COMPONENTS = "components";
|
||||||
private SoftwareComponent[] softwareComponents;
|
private SoftwareComponent[] softwareComponents;
|
||||||
private SoftwareComponent mComponentForContextMenu;
|
private SoftwareComponent componentForContextMenu;
|
||||||
|
|
||||||
public static LicenseFragment newInstance(SoftwareComponent[] softwareComponents) {
|
public static LicenseFragment newInstance(final SoftwareComponent[] softwareComponents) {
|
||||||
if(softwareComponents == null) {
|
if (softwareComponents == null) {
|
||||||
throw new NullPointerException("softwareComponents is null");
|
throw new NullPointerException("softwareComponents is null");
|
||||||
}
|
}
|
||||||
LicenseFragment fragment = new LicenseFragment();
|
LicenseFragment fragment = new LicenseFragment();
|
||||||
@ -35,57 +39,50 @@ public class LicenseFragment extends Fragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows a popup containing the license
|
* Shows a popup containing the license.
|
||||||
|
*
|
||||||
* @param context the context to use
|
* @param context the context to use
|
||||||
* @param license the license to show
|
* @param license the license to show
|
||||||
*/
|
*/
|
||||||
public static void showLicense(Context context, License license) {
|
private static void showLicense(final Context context, final License license) {
|
||||||
new LicenseFragmentHelper((Activity) context).execute(license);
|
new LicenseFragmentHelper((Activity) context).execute(license);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
public void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
softwareComponents = (SoftwareComponent[]) getArguments().getParcelableArray(ARG_COMPONENTS);
|
softwareComponents = (SoftwareComponent[]) getArguments()
|
||||||
|
.getParcelableArray(ARG_COMPONENTS);
|
||||||
|
|
||||||
// Sort components by name
|
// Sort components by name
|
||||||
Arrays.sort(softwareComponents, new Comparator<SoftwareComponent>() {
|
Arrays.sort(softwareComponents, (o1, o2) -> o1.getName().compareTo(o2.getName()));
|
||||||
@Override
|
|
||||||
public int compare(SoftwareComponent o1, SoftwareComponent o2) {
|
|
||||||
return o1.getName().compareTo(o2.getName());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
|
||||||
View rootView = inflater.inflate(R.layout.fragment_licenses, container, false);
|
@Nullable final Bundle savedInstanceState) {
|
||||||
ViewGroup softwareComponentsView = rootView.findViewById(R.id.software_components);
|
final View rootView = inflater.inflate(R.layout.fragment_licenses, container, false);
|
||||||
|
final ViewGroup softwareComponentsView = rootView.findViewById(R.id.software_components);
|
||||||
|
|
||||||
View licenseLink = rootView.findViewById(R.id.app_read_license);
|
final View licenseLink = rootView.findViewById(R.id.app_read_license);
|
||||||
licenseLink.setOnClickListener(new OnReadFullLicenseClickListener());
|
licenseLink.setOnClickListener(v ->
|
||||||
|
showLicense(getActivity(), StandardLicenses.GPL3));
|
||||||
|
|
||||||
for (final SoftwareComponent component : softwareComponents) {
|
for (final SoftwareComponent component : softwareComponents) {
|
||||||
View componentView = inflater.inflate(R.layout.item_software_component, container, false);
|
final View componentView = inflater
|
||||||
TextView softwareName = componentView.findViewById(R.id.name);
|
.inflate(R.layout.item_software_component, container, false);
|
||||||
TextView copyright = componentView.findViewById(R.id.copyright);
|
final TextView softwareName = componentView.findViewById(R.id.name);
|
||||||
|
final TextView copyright = componentView.findViewById(R.id.copyright);
|
||||||
softwareName.setText(component.getName());
|
softwareName.setText(component.getName());
|
||||||
copyright.setText(getContext().getString(R.string.copyright,
|
copyright.setText(getString(R.string.copyright,
|
||||||
component.getYears(),
|
component.getYears(),
|
||||||
component.getCopyrightOwner(),
|
component.getCopyrightOwner(),
|
||||||
component.getLicense().getAbbreviation()));
|
component.getLicense().getAbbreviation()));
|
||||||
|
|
||||||
componentView.setTag(component);
|
componentView.setTag(component);
|
||||||
componentView.setOnClickListener(new View.OnClickListener() {
|
componentView.setOnClickListener(v ->
|
||||||
@Override
|
showLicense(getActivity(), component.getLicense()));
|
||||||
public void onClick(View v) {
|
|
||||||
Context context = v.getContext();
|
|
||||||
if (context != null) {
|
|
||||||
showLicense(context, component.getLicense());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
softwareComponentsView.addView(componentView);
|
softwareComponentsView.addView(componentView);
|
||||||
registerForContextMenu(componentView);
|
registerForContextMenu(componentView);
|
||||||
}
|
}
|
||||||
@ -93,41 +90,30 @@ public class LicenseFragment extends Fragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
|
public void onCreateContextMenu(final ContextMenu menu, final View v,
|
||||||
MenuInflater inflater = getActivity().getMenuInflater();
|
final ContextMenu.ContextMenuInfo menuInfo) {
|
||||||
SoftwareComponent component = (SoftwareComponent) v.getTag();
|
final MenuInflater inflater = getActivity().getMenuInflater();
|
||||||
|
final SoftwareComponent component = (SoftwareComponent) v.getTag();
|
||||||
menu.setHeaderTitle(component.getName());
|
menu.setHeaderTitle(component.getName());
|
||||||
inflater.inflate(R.menu.software_component, menu);
|
inflater.inflate(R.menu.software_component, menu);
|
||||||
super.onCreateContextMenu(menu, v, menuInfo);
|
super.onCreateContextMenu(menu, v, menuInfo);
|
||||||
mComponentForContextMenu = (SoftwareComponent) v.getTag();
|
componentForContextMenu = (SoftwareComponent) v.getTag();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onContextItemSelected(MenuItem item) {
|
public boolean onContextItemSelected(final MenuItem item) {
|
||||||
// item.getMenuInfo() is null so we use the tag of the view
|
// item.getMenuInfo() is null so we use the tag of the view
|
||||||
final SoftwareComponent component = mComponentForContextMenu;
|
final SoftwareComponent component = componentForContextMenu;
|
||||||
if (component == null) {
|
if (component == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
case R.id.action_website:
|
case R.id.action_website:
|
||||||
openWebsite(component.getLink());
|
ShareUtils.openUrlInBrowser(getActivity(), component.getLink());
|
||||||
return true;
|
return true;
|
||||||
case R.id.action_show_license:
|
case R.id.action_show_license:
|
||||||
showLicense(getContext(), component.getLicense());
|
showLicense(getActivity(), component.getLicense());
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openWebsite(String componentLink) {
|
|
||||||
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(componentLink));
|
|
||||||
startActivity(browserIntent);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class OnReadFullLicenseClickListener implements View.OnClickListener {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
LicenseFragment.showLicense(v.getContext(), StandardLicenses.GPL3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -2,30 +2,95 @@ package org.schabi.newpipe.about;
|
|||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
|
import android.util.Base64;
|
||||||
|
import android.webkit.WebView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import android.webkit.WebView;
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||||
|
|
||||||
public class LicenseFragmentHelper extends AsyncTask<Object, Void, Integer> {
|
public class LicenseFragmentHelper extends AsyncTask<Object, Void, Integer> {
|
||||||
|
private final WeakReference<Activity> weakReference;
|
||||||
final WeakReference<Activity> weakReference;
|
|
||||||
private License license;
|
private License license;
|
||||||
|
|
||||||
public LicenseFragmentHelper(@Nullable Activity activity) {
|
public LicenseFragmentHelper(@Nullable final Activity activity) {
|
||||||
weakReference = new WeakReference<>(activity);
|
weakReference = new WeakReference<>(activity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param context the context to use
|
||||||
|
* @param license the license
|
||||||
|
* @return String which contains a HTML formatted license page
|
||||||
|
* styled according to the context's theme
|
||||||
|
*/
|
||||||
|
private static String getFormattedLicense(@NonNull final Context context,
|
||||||
|
@NonNull final License license) {
|
||||||
|
final StringBuilder licenseContent = new StringBuilder();
|
||||||
|
final String webViewData;
|
||||||
|
try {
|
||||||
|
final BufferedReader in = new BufferedReader(new InputStreamReader(
|
||||||
|
context.getAssets().open(license.getFilename()), StandardCharsets.UTF_8));
|
||||||
|
String str;
|
||||||
|
while ((str = in.readLine()) != null) {
|
||||||
|
licenseContent.append(str);
|
||||||
|
}
|
||||||
|
in.close();
|
||||||
|
|
||||||
|
// split the HTML file and insert the stylesheet into the HEAD of the file
|
||||||
|
webViewData = licenseContent.toString().replace("</head>",
|
||||||
|
"<style>" + getLicenseStylesheet(context) + "</style></head>");
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Could not get license file: " + license.getFilename(), e);
|
||||||
|
}
|
||||||
|
return webViewData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param context
|
||||||
|
* @return String which is a CSS stylesheet according to the context's theme
|
||||||
|
*/
|
||||||
|
private static String getLicenseStylesheet(final Context context) {
|
||||||
|
final boolean isLightTheme = ThemeHelper.isLightThemeSelected(context);
|
||||||
|
return "body{padding:12px 15px;margin:0;"
|
||||||
|
+ "background:#" + getHexRGBColor(context, isLightTheme
|
||||||
|
? R.color.light_license_background_color
|
||||||
|
: R.color.dark_license_background_color) + ";"
|
||||||
|
+ "color:#" + getHexRGBColor(context, isLightTheme
|
||||||
|
? R.color.light_license_text_color
|
||||||
|
: R.color.dark_license_text_color) + "}"
|
||||||
|
+ "a[href]{color:#" + getHexRGBColor(context, isLightTheme
|
||||||
|
? R.color.light_youtube_primary_color
|
||||||
|
: R.color.dark_youtube_primary_color) + "}"
|
||||||
|
+ "pre{white-space:pre-wrap}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cast R.color to a hexadecimal color value.
|
||||||
|
*
|
||||||
|
* @param context the context to use
|
||||||
|
* @param color the color number from R.color
|
||||||
|
* @return a six characters long String with hexadecimal RGB values
|
||||||
|
*/
|
||||||
|
private static String getHexRGBColor(final Context context, final int color) {
|
||||||
|
return context.getResources().getString(color).substring(3);
|
||||||
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private Activity getActivity() {
|
private Activity getActivity() {
|
||||||
Activity activity = weakReference.get();
|
final Activity activity = weakReference.get();
|
||||||
|
|
||||||
if (activity != null && activity.isFinishing()) {
|
if (activity != null && activity.isFinishing()) {
|
||||||
return null;
|
return null;
|
||||||
@ -35,99 +100,29 @@ public class LicenseFragmentHelper extends AsyncTask<Object, Void, Integer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Integer doInBackground(Object... objects) {
|
protected Integer doInBackground(final Object... objects) {
|
||||||
license = (License) objects[0];
|
license = (License) objects[0];
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPostExecute(Integer result) {
|
protected void onPostExecute(final Integer result) {
|
||||||
Activity activity = getActivity();
|
final Activity activity = getActivity();
|
||||||
if (activity == null) {
|
if (activity == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String webViewData = getFormattedLicense(activity, license);
|
final String webViewData = Base64.encodeToString(getFormattedLicense(activity, license)
|
||||||
AlertDialog.Builder alert = new AlertDialog.Builder(activity);
|
.getBytes(StandardCharsets.UTF_8), Base64.NO_PADDING);
|
||||||
|
final WebView webView = new WebView(activity);
|
||||||
|
webView.loadData(webViewData, "text/html; charset=UTF-8", "base64");
|
||||||
|
|
||||||
|
final AlertDialog.Builder alert = new AlertDialog.Builder(activity);
|
||||||
alert.setTitle(license.getName());
|
alert.setTitle(license.getName());
|
||||||
|
alert.setView(webView);
|
||||||
WebView wv = new WebView(activity);
|
assureCorrectAppLanguage(activity);
|
||||||
wv.loadData(webViewData, "text/html; charset=UTF-8", null);
|
alert.setNegativeButton(activity.getString(R.string.finish),
|
||||||
|
(dialog, which) -> dialog.dismiss());
|
||||||
alert.setView(wv);
|
|
||||||
alert.setNegativeButton(android.R.string.ok, new DialogInterface.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
|
||||||
dialog.dismiss();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
alert.show();
|
alert.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param context the context to use
|
|
||||||
* @param license the license
|
|
||||||
* @return String which contains a HTML formatted license page styled according to the context's theme
|
|
||||||
*/
|
|
||||||
public static String getFormattedLicense(Context context, License license) {
|
|
||||||
if(context == null) {
|
|
||||||
throw new NullPointerException("context is null");
|
|
||||||
}
|
|
||||||
if(license == null) {
|
|
||||||
throw new NullPointerException("license is null");
|
|
||||||
}
|
|
||||||
|
|
||||||
StringBuilder licenseContent = new StringBuilder();
|
|
||||||
String webViewData;
|
|
||||||
try {
|
|
||||||
BufferedReader in = new BufferedReader(new InputStreamReader(context.getAssets().open(license.getFilename()), "UTF-8"));
|
|
||||||
String str;
|
|
||||||
while ((str = in.readLine()) != null) {
|
|
||||||
licenseContent.append(str);
|
|
||||||
}
|
|
||||||
in.close();
|
|
||||||
|
|
||||||
// split the HTML file and insert the stylesheet into the HEAD of the file
|
|
||||||
String[] insert = licenseContent.toString().split("</head>");
|
|
||||||
webViewData = insert[0] + "<style type=\"text/css\">"
|
|
||||||
+ getLicenseStylesheet(context) + "</style></head>"
|
|
||||||
+ insert[1];
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new NullPointerException("could not get license file:" + getLicenseStylesheet(context));
|
|
||||||
}
|
|
||||||
return webViewData;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param context
|
|
||||||
* @return String which is a CSS stylesheet according to the context's theme
|
|
||||||
*/
|
|
||||||
public static String getLicenseStylesheet(Context context) {
|
|
||||||
boolean isLightTheme = ThemeHelper.isLightThemeSelected(context);
|
|
||||||
return "body{padding:12px 15px;margin:0;background:#"
|
|
||||||
+ getHexRGBColor(context, isLightTheme
|
|
||||||
? R.color.light_license_background_color
|
|
||||||
: R.color.dark_license_background_color)
|
|
||||||
+ ";color:#"
|
|
||||||
+ getHexRGBColor(context, isLightTheme
|
|
||||||
? R.color.light_license_text_color
|
|
||||||
: R.color.dark_license_text_color) + ";}"
|
|
||||||
+ "a[href]{color:#"
|
|
||||||
+ getHexRGBColor(context, isLightTheme
|
|
||||||
? R.color.light_youtube_primary_color
|
|
||||||
: R.color.dark_youtube_primary_color) + ";}"
|
|
||||||
+ "pre{white-space: pre-wrap;}";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cast R.color to a hexadecimal color value
|
|
||||||
* @param context the context to use
|
|
||||||
* @param color the color number from R.color
|
|
||||||
* @return a six characters long String with hexadecimal RGB values
|
|
||||||
*/
|
|
||||||
public static String getHexRGBColor(Context context, int color) {
|
|
||||||
return context.getResources().getString(color).substring(3);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -4,19 +4,44 @@ import android.os.Parcel;
|
|||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
|
|
||||||
public class SoftwareComponent implements Parcelable {
|
public class SoftwareComponent implements Parcelable {
|
||||||
|
|
||||||
public static final Creator<SoftwareComponent> CREATOR = new Creator<SoftwareComponent>() {
|
public static final Creator<SoftwareComponent> CREATOR = new Creator<SoftwareComponent>() {
|
||||||
@Override
|
@Override
|
||||||
public SoftwareComponent createFromParcel(Parcel source) {
|
public SoftwareComponent createFromParcel(final Parcel source) {
|
||||||
return new SoftwareComponent(source);
|
return new SoftwareComponent(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SoftwareComponent[] newArray(int size) {
|
public SoftwareComponent[] newArray(final int size) {
|
||||||
return new SoftwareComponent[size];
|
return new SoftwareComponent[size];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private final License license;
|
||||||
|
private final String name;
|
||||||
|
private final String years;
|
||||||
|
private final String copyrightOwner;
|
||||||
|
private final String link;
|
||||||
|
private final String version;
|
||||||
|
|
||||||
|
public SoftwareComponent(final String name, final String years, final String copyrightOwner,
|
||||||
|
final String link, final License license) {
|
||||||
|
this.name = name;
|
||||||
|
this.years = years;
|
||||||
|
this.copyrightOwner = copyrightOwner;
|
||||||
|
this.link = link;
|
||||||
|
this.license = license;
|
||||||
|
this.version = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected SoftwareComponent(final Parcel in) {
|
||||||
|
this.name = in.readString();
|
||||||
|
this.license = in.readParcelable(License.class.getClassLoader());
|
||||||
|
this.copyrightOwner = in.readString();
|
||||||
|
this.link = in.readString();
|
||||||
|
this.years = in.readString();
|
||||||
|
this.version = in.readString();
|
||||||
|
}
|
||||||
|
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
@ -37,31 +62,6 @@ public class SoftwareComponent implements Parcelable {
|
|||||||
return version;
|
return version;
|
||||||
}
|
}
|
||||||
|
|
||||||
private final License license;
|
|
||||||
private final String name;
|
|
||||||
private final String years;
|
|
||||||
private final String copyrightOwner;
|
|
||||||
private final String link;
|
|
||||||
private final String version;
|
|
||||||
|
|
||||||
public SoftwareComponent(String name, String years, String copyrightOwner, String link, License license) {
|
|
||||||
this.name = name;
|
|
||||||
this.years = years;
|
|
||||||
this.copyrightOwner = copyrightOwner;
|
|
||||||
this.link = link;
|
|
||||||
this.license = license;
|
|
||||||
this.version = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected SoftwareComponent(Parcel in) {
|
|
||||||
this.name = in.readString();
|
|
||||||
this.license = in.readParcelable(License.class.getClassLoader());
|
|
||||||
this.copyrightOwner = in.readString();
|
|
||||||
this.link = in.readString();
|
|
||||||
this.years = in.readString();
|
|
||||||
this.version = in.readString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public License getLicense() {
|
public License getLicense() {
|
||||||
return license;
|
return license;
|
||||||
}
|
}
|
||||||
@ -72,7 +72,7 @@ public class SoftwareComponent implements Parcelable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void writeToParcel(Parcel dest, int flags) {
|
public void writeToParcel(final Parcel dest, final int flags) {
|
||||||
dest.writeString(name);
|
dest.writeString(name);
|
||||||
dest.writeParcelable(license, flags);
|
dest.writeParcelable(license, flags);
|
||||||
dest.writeString(copyrightOwner);
|
dest.writeString(copyrightOwner);
|
||||||
|
@ -1,12 +1,19 @@
|
|||||||
package org.schabi.newpipe.about;
|
package org.schabi.newpipe.about;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Standard software licenses
|
* Class containing information about standard software licenses.
|
||||||
*/
|
*/
|
||||||
public final class StandardLicenses {
|
public final class StandardLicenses {
|
||||||
public static final License GPL2 = new License("GNU General Public License, Version 2.0", "GPLv2", "gpl_2.html");
|
public static final License GPL2
|
||||||
public static final License GPL3 = new License("GNU General Public License, Version 3.0", "GPLv3", "gpl_3.html");
|
= new License("GNU General Public License, Version 2.0", "GPLv2", "gpl_2.html");
|
||||||
public static final License APACHE2 = new License("Apache License, Version 2.0", "ALv2", "apache2.html");
|
public static final License GPL3
|
||||||
public static final License MPL2 = new License("Mozilla Public License, Version 2.0", "MPL 2.0", "mpl2.html");
|
= new License("GNU General Public License, Version 3.0", "GPLv3", "gpl_3.html");
|
||||||
public static final License MIT = new License("MIT License", "MIT", "mit.html");
|
public static final License APACHE2
|
||||||
|
= new License("Apache License, Version 2.0", "ALv2", "apache2.html");
|
||||||
|
public static final License MPL2
|
||||||
|
= new License("Mozilla Public License, Version 2.0", "MPL 2.0", "mpl2.html");
|
||||||
|
public static final License MIT
|
||||||
|
= new License("MIT License", "MIT", "mit.html");
|
||||||
|
|
||||||
|
private StandardLicenses() { }
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,12 @@ import androidx.room.Database;
|
|||||||
import androidx.room.RoomDatabase;
|
import androidx.room.RoomDatabase;
|
||||||
import androidx.room.TypeConverters;
|
import androidx.room.TypeConverters;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.database.feed.dao.FeedDAO;
|
||||||
|
import org.schabi.newpipe.database.feed.dao.FeedGroupDAO;
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedEntity;
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedGroupEntity;
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity;
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity;
|
||||||
import org.schabi.newpipe.database.history.dao.SearchHistoryDAO;
|
import org.schabi.newpipe.database.history.dao.SearchHistoryDAO;
|
||||||
import org.schabi.newpipe.database.history.dao.StreamHistoryDAO;
|
import org.schabi.newpipe.database.history.dao.StreamHistoryDAO;
|
||||||
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
||||||
@ -21,24 +27,22 @@ import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
|||||||
import org.schabi.newpipe.database.subscription.SubscriptionDAO;
|
import org.schabi.newpipe.database.subscription.SubscriptionDAO;
|
||||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||||
|
|
||||||
import static org.schabi.newpipe.database.Migrations.DB_VER_12_0;
|
import static org.schabi.newpipe.database.Migrations.DB_VER_3;
|
||||||
|
|
||||||
@TypeConverters({Converters.class})
|
@TypeConverters({Converters.class})
|
||||||
@Database(
|
@Database(
|
||||||
entities = {
|
entities = {
|
||||||
SubscriptionEntity.class, SearchHistoryEntry.class,
|
SubscriptionEntity.class, SearchHistoryEntry.class,
|
||||||
StreamEntity.class, StreamHistoryEntity.class, StreamStateEntity.class,
|
StreamEntity.class, StreamHistoryEntity.class, StreamStateEntity.class,
|
||||||
PlaylistEntity.class, PlaylistStreamEntity.class, PlaylistRemoteEntity.class
|
PlaylistEntity.class, PlaylistStreamEntity.class, PlaylistRemoteEntity.class,
|
||||||
|
FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class,
|
||||||
|
FeedLastUpdatedEntity.class
|
||||||
},
|
},
|
||||||
version = DB_VER_12_0,
|
version = DB_VER_3
|
||||||
exportSchema = false
|
|
||||||
)
|
)
|
||||||
public abstract class AppDatabase extends RoomDatabase {
|
public abstract class AppDatabase extends RoomDatabase {
|
||||||
|
|
||||||
public static final String DATABASE_NAME = "newpipe.db";
|
public static final String DATABASE_NAME = "newpipe.db";
|
||||||
|
|
||||||
public abstract SubscriptionDAO subscriptionDAO();
|
|
||||||
|
|
||||||
public abstract SearchHistoryDAO searchHistoryDAO();
|
public abstract SearchHistoryDAO searchHistoryDAO();
|
||||||
|
|
||||||
public abstract StreamDAO streamDAO();
|
public abstract StreamDAO streamDAO();
|
||||||
@ -52,4 +56,10 @@ public abstract class AppDatabase extends RoomDatabase {
|
|||||||
public abstract PlaylistStreamDAO playlistStreamDAO();
|
public abstract PlaylistStreamDAO playlistStreamDAO();
|
||||||
|
|
||||||
public abstract PlaylistRemoteDAO playlistRemoteDAO();
|
public abstract PlaylistRemoteDAO playlistRemoteDAO();
|
||||||
|
|
||||||
|
public abstract FeedDAO feedDAO();
|
||||||
|
|
||||||
|
public abstract FeedGroupDAO feedGroupDAO();
|
||||||
|
|
||||||
|
public abstract SubscriptionDAO subscriptionDAO();
|
||||||
}
|
}
|
||||||
|
@ -15,13 +15,13 @@ import io.reactivex.Flowable;
|
|||||||
public interface BasicDAO<Entity> {
|
public interface BasicDAO<Entity> {
|
||||||
/* Inserts */
|
/* Inserts */
|
||||||
@Insert(onConflict = OnConflictStrategy.FAIL)
|
@Insert(onConflict = OnConflictStrategy.FAIL)
|
||||||
long insert(final Entity entity);
|
long insert(Entity entity);
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.FAIL)
|
@Insert(onConflict = OnConflictStrategy.FAIL)
|
||||||
List<Long> insertAll(final Entity... entities);
|
List<Long> insertAll(Entity... entities);
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.FAIL)
|
@Insert(onConflict = OnConflictStrategy.FAIL)
|
||||||
List<Long> insertAll(final Collection<Entity> entities);
|
List<Long> insertAll(Collection<Entity> entities);
|
||||||
|
|
||||||
/* Searches */
|
/* Searches */
|
||||||
Flowable<List<Entity>> getAll();
|
Flowable<List<Entity>> getAll();
|
||||||
@ -30,17 +30,17 @@ public interface BasicDAO<Entity> {
|
|||||||
|
|
||||||
/* Deletes */
|
/* Deletes */
|
||||||
@Delete
|
@Delete
|
||||||
void delete(final Entity entity);
|
void delete(Entity entity);
|
||||||
|
|
||||||
@Delete
|
@Delete
|
||||||
int delete(final Collection<Entity> entities);
|
int delete(Collection<Entity> entities);
|
||||||
|
|
||||||
int deleteAll();
|
int deleteAll();
|
||||||
|
|
||||||
/* Updates */
|
/* Updates */
|
||||||
@Update
|
@Update
|
||||||
int update(final Entity entity);
|
int update(Entity entity);
|
||||||
|
|
||||||
@Update
|
@Update
|
||||||
void update(final Collection<Entity> entities);
|
void update(Collection<Entity> entities);
|
||||||
}
|
}
|
||||||
|
@ -3,38 +3,58 @@ package org.schabi.newpipe.database;
|
|||||||
import androidx.room.TypeConverter;
|
import androidx.room.TypeConverter;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
|
import org.schabi.newpipe.local.subscription.FeedGroupIcon;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
public class Converters {
|
public final class Converters {
|
||||||
|
private Converters() { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a long value to a date
|
* Convert a long value to a date.
|
||||||
|
*
|
||||||
* @param value the long value
|
* @param value the long value
|
||||||
* @return the date
|
* @return the date
|
||||||
*/
|
*/
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
public static Date fromTimestamp(Long value) {
|
public static Date fromTimestamp(final Long value) {
|
||||||
return value == null ? null : new Date(value);
|
return value == null ? null : new Date(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a date to a long value
|
* Convert a date to a long value.
|
||||||
|
*
|
||||||
* @param date the date
|
* @param date the date
|
||||||
* @return the long value
|
* @return the long value
|
||||||
*/
|
*/
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
public static Long dateToTimestamp(Date date) {
|
public static Long dateToTimestamp(final Date date) {
|
||||||
return date == null ? null : date.getTime();
|
return date == null ? null : date.getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
public static StreamType streamTypeOf(String value) {
|
public static StreamType streamTypeOf(final String value) {
|
||||||
return StreamType.valueOf(value);
|
return StreamType.valueOf(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
public static String stringOf(StreamType streamType) {
|
public static String stringOf(final StreamType streamType) {
|
||||||
return streamType.name();
|
return streamType.name();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static Integer integerOf(final FeedGroupIcon feedGroupIcon) {
|
||||||
|
return feedGroupIcon.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static FeedGroupIcon feedGroupIconOf(final Integer id) {
|
||||||
|
for (FeedGroupIcon icon : FeedGroupIcon.values()) {
|
||||||
|
if (icon.getId() == id) {
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalArgumentException("There's no feed group icon with the id \"" + id + "\"");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package org.schabi.newpipe.database;
|
package org.schabi.newpipe.database;
|
||||||
|
|
||||||
public interface LocalItem {
|
public interface LocalItem {
|
||||||
|
LocalItemType getLocalItemType();
|
||||||
|
|
||||||
enum LocalItemType {
|
enum LocalItemType {
|
||||||
PLAYLIST_LOCAL_ITEM,
|
PLAYLIST_LOCAL_ITEM,
|
||||||
PLAYLIST_REMOTE_ITEM,
|
PLAYLIST_REMOTE_ITEM,
|
||||||
@ -8,6 +10,4 @@ public interface LocalItem {
|
|||||||
PLAYLIST_STREAM_ITEM,
|
PLAYLIST_STREAM_ITEM,
|
||||||
STATISTIC_STREAM_ITEM,
|
STATISTIC_STREAM_ITEM,
|
||||||
}
|
}
|
||||||
|
|
||||||
LocalItemType getLocalItemType();
|
|
||||||
}
|
}
|
||||||
|
@ -1,74 +1,164 @@
|
|||||||
package org.schabi.newpipe.database;
|
package org.schabi.newpipe.database;
|
||||||
|
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase;
|
|
||||||
import androidx.room.migration.Migration;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.room.migration.Migration;
|
||||||
|
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||||
|
|
||||||
import org.schabi.newpipe.BuildConfig;
|
import org.schabi.newpipe.BuildConfig;
|
||||||
|
|
||||||
public class Migrations {
|
public final class Migrations {
|
||||||
|
public static final int DB_VER_1 = 1;
|
||||||
|
public static final int DB_VER_2 = 2;
|
||||||
|
public static final int DB_VER_3 = 3;
|
||||||
|
|
||||||
public static final int DB_VER_11_0 = 1;
|
|
||||||
public static final int DB_VER_12_0 = 2;
|
|
||||||
|
|
||||||
public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release");
|
|
||||||
private static final String TAG = Migrations.class.getName();
|
private static final String TAG = Migrations.class.getName();
|
||||||
|
public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release");
|
||||||
|
|
||||||
public static final Migration MIGRATION_11_12 = new Migration(DB_VER_11_0, DB_VER_12_0) {
|
public static final Migration MIGRATION_1_2 = new Migration(DB_VER_1, DB_VER_2) {
|
||||||
@Override
|
@Override
|
||||||
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
||||||
if(DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "Start migrating database");
|
Log.d(TAG, "Start migrating database");
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
* Unfortunately these queries must be hardcoded due to the possibility of
|
* Unfortunately these queries must be hardcoded due to the possibility of
|
||||||
* schema and names changing at a later date, thus invalidating the older migration
|
* schema and names changing at a later date, thus invalidating the older migration
|
||||||
* scripts if they are not hardcoded.
|
* scripts if they are not hardcoded.
|
||||||
* */
|
* */
|
||||||
|
|
||||||
// Not much we can do about this, since room doesn't create tables before migration.
|
// Not much we can do about this, since room doesn't create tables before migration.
|
||||||
// It's either this or blasting the entire database anew.
|
// It's either this or blasting the entire database anew.
|
||||||
database.execSQL("CREATE INDEX `index_search_history_search` ON `search_history` (`search`)");
|
database.execSQL("CREATE INDEX `index_search_history_search` "
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS `streams` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, `stream_type` TEXT, `duration` INTEGER, `uploader` TEXT, `thumbnail_url` TEXT)");
|
+ "ON `search_history` (`search`)");
|
||||||
database.execSQL("CREATE UNIQUE INDEX `index_streams_service_id_url` ON `streams` (`service_id`, `url`)");
|
database.execSQL("CREATE TABLE IF NOT EXISTS `streams` "
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS `stream_history` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, `repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )");
|
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
|
||||||
database.execSQL("CREATE INDEX `index_stream_history_stream_id` ON `stream_history` (`stream_id`)");
|
+ "`service_id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, "
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS `stream_state` (`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )");
|
+ "`stream_type` TEXT, `duration` INTEGER, `uploader` TEXT, "
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS `playlists` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `thumbnail_url` TEXT)");
|
+ "`thumbnail_url` TEXT)");
|
||||||
|
database.execSQL("CREATE UNIQUE INDEX `index_streams_service_id_url` "
|
||||||
|
+ "ON `streams` (`service_id`, `url`)");
|
||||||
|
database.execSQL("CREATE TABLE IF NOT EXISTS `stream_history` "
|
||||||
|
+ "(`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, "
|
||||||
|
+ "`repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), "
|
||||||
|
+ "FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) "
|
||||||
|
+ "ON UPDATE CASCADE ON DELETE CASCADE )");
|
||||||
|
database.execSQL("CREATE INDEX `index_stream_history_stream_id` "
|
||||||
|
+ "ON `stream_history` (`stream_id`)");
|
||||||
|
database.execSQL("CREATE TABLE IF NOT EXISTS `stream_state` "
|
||||||
|
+ "(`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, "
|
||||||
|
+ "PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) "
|
||||||
|
+ "REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )");
|
||||||
|
database.execSQL("CREATE TABLE IF NOT EXISTS `playlists` "
|
||||||
|
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
|
||||||
|
+ "`name` TEXT, `thumbnail_url` TEXT)");
|
||||||
database.execSQL("CREATE INDEX `index_playlists_name` ON `playlists` (`name`)");
|
database.execSQL("CREATE INDEX `index_playlists_name` ON `playlists` (`name`)");
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS `playlist_stream_join` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)");
|
database.execSQL("CREATE TABLE IF NOT EXISTS `playlist_stream_join` "
|
||||||
database.execSQL("CREATE UNIQUE INDEX `index_playlist_stream_join_playlist_id_join_index` ON `playlist_stream_join` (`playlist_id`, `join_index`)");
|
+ "(`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, "
|
||||||
database.execSQL("CREATE INDEX `index_playlist_stream_join_stream_id` ON `playlist_stream_join` (`stream_id`)");
|
+ "`join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), "
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS `remote_playlists` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)");
|
+ "FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) "
|
||||||
database.execSQL("CREATE INDEX `index_remote_playlists_name` ON `remote_playlists` (`name`)");
|
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, "
|
||||||
database.execSQL("CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` ON `remote_playlists` (`service_id`, `url`)");
|
+ "FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) "
|
||||||
|
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)");
|
||||||
|
database.execSQL("CREATE UNIQUE INDEX "
|
||||||
|
+ "`index_playlist_stream_join_playlist_id_join_index` "
|
||||||
|
+ "ON `playlist_stream_join` (`playlist_id`, `join_index`)");
|
||||||
|
database.execSQL("CREATE INDEX `index_playlist_stream_join_stream_id` "
|
||||||
|
+ "ON `playlist_stream_join` (`stream_id`)");
|
||||||
|
database.execSQL("CREATE TABLE IF NOT EXISTS `remote_playlists` "
|
||||||
|
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
|
||||||
|
+ "`service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, "
|
||||||
|
+ "`thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)");
|
||||||
|
database.execSQL("CREATE INDEX `index_remote_playlists_name` "
|
||||||
|
+ "ON `remote_playlists` (`name`)");
|
||||||
|
database.execSQL("CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` "
|
||||||
|
+ "ON `remote_playlists` (`service_id`, `url`)");
|
||||||
|
|
||||||
// Populate streams table with existing entries in watch history
|
// Populate streams table with existing entries in watch history
|
||||||
// Latest data first, thus ignoring older entries with the same indices
|
// Latest data first, thus ignoring older entries with the same indices
|
||||||
database.execSQL("INSERT OR IGNORE INTO streams (service_id, url, title, " +
|
database.execSQL("INSERT OR IGNORE INTO streams (service_id, url, title, "
|
||||||
"stream_type, duration, uploader, thumbnail_url) " +
|
+ "stream_type, duration, uploader, thumbnail_url) "
|
||||||
|
|
||||||
"SELECT service_id, url, title, 'VIDEO_STREAM', duration, " +
|
+ "SELECT service_id, url, title, 'VIDEO_STREAM', duration, "
|
||||||
"uploader, thumbnail_url " +
|
+ "uploader, thumbnail_url "
|
||||||
|
|
||||||
"FROM watch_history " +
|
+ "FROM watch_history "
|
||||||
"ORDER BY creation_date DESC");
|
+ "ORDER BY creation_date DESC");
|
||||||
|
|
||||||
// Once the streams have PKs, join them with the normalized history table
|
// Once the streams have PKs, join them with the normalized history table
|
||||||
// and populate it with the remaining data from watch history
|
// and populate it with the remaining data from watch history
|
||||||
database.execSQL("INSERT INTO stream_history (stream_id, access_date, repeat_count)" +
|
database.execSQL("INSERT INTO stream_history (stream_id, access_date, repeat_count)"
|
||||||
"SELECT uid, creation_date, 1 " +
|
+ "SELECT uid, creation_date, 1 "
|
||||||
"FROM watch_history INNER JOIN streams " +
|
+ "FROM watch_history INNER JOIN streams "
|
||||||
"ON watch_history.service_id == streams.service_id " +
|
+ "ON watch_history.service_id == streams.service_id "
|
||||||
"AND watch_history.url == streams.url " +
|
+ "AND watch_history.url == streams.url "
|
||||||
"ORDER BY creation_date DESC");
|
+ "ORDER BY creation_date DESC");
|
||||||
|
|
||||||
database.execSQL("DROP TABLE IF EXISTS watch_history");
|
database.execSQL("DROP TABLE IF EXISTS watch_history");
|
||||||
|
|
||||||
if(DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "Stop migrating database");
|
Log.d(TAG, "Stop migrating database");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static final Migration MIGRATION_2_3 = new Migration(DB_VER_2, DB_VER_3) {
|
||||||
|
@Override
|
||||||
|
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
||||||
|
// Add NOT NULLs and new fields
|
||||||
|
database.execSQL("CREATE TABLE IF NOT EXISTS streams_new "
|
||||||
|
+ "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
|
||||||
|
+ "service_id INTEGER NOT NULL, url TEXT NOT NULL, title TEXT NOT NULL, "
|
||||||
|
+ "stream_type TEXT NOT NULL, duration INTEGER NOT NULL, "
|
||||||
|
+ "uploader TEXT NOT NULL, thumbnail_url TEXT, view_count INTEGER, "
|
||||||
|
+ "textual_upload_date TEXT, upload_date INTEGER, "
|
||||||
|
+ "is_upload_date_approximation INTEGER)");
|
||||||
|
|
||||||
|
database.execSQL("INSERT INTO streams_new (uid, service_id, url, title, stream_type, "
|
||||||
|
+ "duration, uploader, thumbnail_url, view_count, textual_upload_date, "
|
||||||
|
+ "upload_date, is_upload_date_approximation) "
|
||||||
|
|
||||||
|
+ "SELECT uid, service_id, url, ifnull(title, ''), "
|
||||||
|
+ "ifnull(stream_type, 'VIDEO_STREAM'), ifnull(duration, 0), "
|
||||||
|
+ "ifnull(uploader, ''), ifnull(thumbnail_url, ''), NULL, NULL, NULL, NULL "
|
||||||
|
|
||||||
|
+ "FROM streams WHERE url IS NOT NULL");
|
||||||
|
|
||||||
|
database.execSQL("DROP TABLE streams");
|
||||||
|
database.execSQL("ALTER TABLE streams_new RENAME TO streams");
|
||||||
|
database.execSQL("CREATE UNIQUE INDEX index_streams_service_id_url "
|
||||||
|
+ "ON streams (service_id, url)");
|
||||||
|
|
||||||
|
// Tables for feed feature
|
||||||
|
database.execSQL("CREATE TABLE IF NOT EXISTS feed "
|
||||||
|
+ "(stream_id INTEGER NOT NULL, subscription_id INTEGER NOT NULL, "
|
||||||
|
+ "PRIMARY KEY(stream_id, subscription_id), "
|
||||||
|
+ "FOREIGN KEY(stream_id) REFERENCES streams(uid) "
|
||||||
|
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, "
|
||||||
|
+ "FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) "
|
||||||
|
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)");
|
||||||
|
database.execSQL("CREATE INDEX index_feed_subscription_id ON feed (subscription_id)");
|
||||||
|
database.execSQL("CREATE TABLE IF NOT EXISTS feed_group "
|
||||||
|
+ "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name TEXT NOT NULL, "
|
||||||
|
+ "icon_id INTEGER NOT NULL, sort_order INTEGER NOT NULL)");
|
||||||
|
database.execSQL("CREATE INDEX index_feed_group_sort_order ON feed_group (sort_order)");
|
||||||
|
database.execSQL("CREATE TABLE IF NOT EXISTS feed_group_subscription_join "
|
||||||
|
+ "(group_id INTEGER NOT NULL, subscription_id INTEGER NOT NULL, "
|
||||||
|
+ "PRIMARY KEY(group_id, subscription_id), "
|
||||||
|
+ "FOREIGN KEY(group_id) REFERENCES feed_group(uid) "
|
||||||
|
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, "
|
||||||
|
+ "FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) "
|
||||||
|
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)");
|
||||||
|
database.execSQL("CREATE INDEX index_feed_group_subscription_join_subscription_id "
|
||||||
|
+ "ON feed_group_subscription_join (subscription_id)");
|
||||||
|
database.execSQL("CREATE TABLE IF NOT EXISTS feed_last_updated "
|
||||||
|
+ "(subscription_id INTEGER NOT NULL, last_updated INTEGER, "
|
||||||
|
+ "PRIMARY KEY(subscription_id), "
|
||||||
|
+ "FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) "
|
||||||
|
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private Migrations() { }
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,152 @@
|
|||||||
|
package org.schabi.newpipe.database.feed.dao
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import androidx.room.Update
|
||||||
|
import io.reactivex.Flowable
|
||||||
|
import java.util.Date
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedEntity
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||||
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
abstract class FeedDAO {
|
||||||
|
@Query("DELETE FROM feed")
|
||||||
|
abstract fun deleteAll(): Int
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
SELECT s.* FROM streams s
|
||||||
|
|
||||||
|
INNER JOIN feed f
|
||||||
|
ON s.uid = f.stream_id
|
||||||
|
|
||||||
|
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
|
||||||
|
|
||||||
|
LIMIT 500
|
||||||
|
""")
|
||||||
|
abstract fun getAllStreams(): Flowable<List<StreamEntity>>
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
SELECT s.* FROM streams s
|
||||||
|
|
||||||
|
INNER JOIN feed f
|
||||||
|
ON s.uid = f.stream_id
|
||||||
|
|
||||||
|
INNER JOIN feed_group_subscription_join fgs
|
||||||
|
ON fgs.subscription_id = f.subscription_id
|
||||||
|
|
||||||
|
INNER JOIN feed_group fg
|
||||||
|
ON fg.uid = fgs.group_id
|
||||||
|
|
||||||
|
WHERE fgs.group_id = :groupId
|
||||||
|
|
||||||
|
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
|
||||||
|
LIMIT 500
|
||||||
|
""")
|
||||||
|
abstract fun getAllStreamsFromGroup(groupId: Long): Flowable<List<StreamEntity>>
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
DELETE FROM feed WHERE
|
||||||
|
|
||||||
|
feed.stream_id IN (
|
||||||
|
SELECT s.uid FROM streams s
|
||||||
|
|
||||||
|
INNER JOIN feed f
|
||||||
|
ON s.uid = f.stream_id
|
||||||
|
|
||||||
|
WHERE s.upload_date < :date
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
abstract fun unlinkStreamsOlderThan(date: Date)
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
DELETE FROM feed
|
||||||
|
|
||||||
|
WHERE feed.subscription_id = :subscriptionId
|
||||||
|
|
||||||
|
AND feed.stream_id IN (
|
||||||
|
SELECT s.uid FROM streams s
|
||||||
|
|
||||||
|
INNER JOIN feed f
|
||||||
|
ON s.uid = f.stream_id
|
||||||
|
|
||||||
|
WHERE s.stream_type = "LIVE_STREAM" OR s.stream_type = "AUDIO_LIVE_STREAM"
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
abstract fun unlinkOldLivestreams(subscriptionId: Long)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
abstract fun insert(feedEntity: FeedEntity)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
abstract fun insertAll(entities: List<FeedEntity>): List<Long>
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
internal abstract fun insertLastUpdated(lastUpdatedEntity: FeedLastUpdatedEntity): Long
|
||||||
|
|
||||||
|
@Update(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
internal abstract fun updateLastUpdated(lastUpdatedEntity: FeedLastUpdatedEntity)
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
open fun setLastUpdatedForSubscription(lastUpdatedEntity: FeedLastUpdatedEntity) {
|
||||||
|
val id = insertLastUpdated(lastUpdatedEntity)
|
||||||
|
|
||||||
|
if (id == -1L) {
|
||||||
|
updateLastUpdated(lastUpdatedEntity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
SELECT MIN(lu.last_updated) FROM feed_last_updated lu
|
||||||
|
|
||||||
|
INNER JOIN feed_group_subscription_join fgs
|
||||||
|
ON fgs.subscription_id = lu.subscription_id AND fgs.group_id = :groupId
|
||||||
|
""")
|
||||||
|
abstract fun oldestSubscriptionUpdate(groupId: Long): Flowable<List<Date>>
|
||||||
|
|
||||||
|
@Query("SELECT MIN(last_updated) FROM feed_last_updated")
|
||||||
|
abstract fun oldestSubscriptionUpdateFromAll(): Flowable<List<Date>>
|
||||||
|
|
||||||
|
@Query("SELECT COUNT(*) FROM feed_last_updated WHERE last_updated IS NULL")
|
||||||
|
abstract fun notLoadedCount(): Flowable<Long>
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
SELECT COUNT(*) FROM subscriptions s
|
||||||
|
|
||||||
|
INNER JOIN feed_group_subscription_join fgs
|
||||||
|
ON s.uid = fgs.subscription_id AND fgs.group_id = :groupId
|
||||||
|
|
||||||
|
LEFT JOIN feed_last_updated lu
|
||||||
|
ON s.uid = lu.subscription_id
|
||||||
|
|
||||||
|
WHERE lu.last_updated IS NULL
|
||||||
|
""")
|
||||||
|
abstract fun notLoadedCountForGroup(groupId: Long): Flowable<Long>
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
SELECT s.* FROM subscriptions s
|
||||||
|
|
||||||
|
LEFT JOIN feed_last_updated lu
|
||||||
|
ON s.uid = lu.subscription_id
|
||||||
|
|
||||||
|
WHERE lu.last_updated IS NULL OR lu.last_updated < :outdatedThreshold
|
||||||
|
""")
|
||||||
|
abstract fun getAllOutdated(outdatedThreshold: Date): Flowable<List<SubscriptionEntity>>
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
SELECT s.* FROM subscriptions s
|
||||||
|
|
||||||
|
INNER JOIN feed_group_subscription_join fgs
|
||||||
|
ON s.uid = fgs.subscription_id AND fgs.group_id = :groupId
|
||||||
|
|
||||||
|
LEFT JOIN feed_last_updated lu
|
||||||
|
ON s.uid = lu.subscription_id
|
||||||
|
|
||||||
|
WHERE lu.last_updated IS NULL OR lu.last_updated < :outdatedThreshold
|
||||||
|
""")
|
||||||
|
abstract fun getAllOutdatedForGroup(groupId: Long, outdatedThreshold: Date): Flowable<List<SubscriptionEntity>>
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
package org.schabi.newpipe.database.feed.dao
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import androidx.room.Update
|
||||||
|
import io.reactivex.Flowable
|
||||||
|
import io.reactivex.Maybe
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
abstract class FeedGroupDAO {
|
||||||
|
|
||||||
|
@Query("SELECT * FROM feed_group ORDER BY sort_order ASC")
|
||||||
|
abstract fun getAll(): Flowable<List<FeedGroupEntity>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM feed_group WHERE uid = :groupId")
|
||||||
|
abstract fun getGroup(groupId: Long): Maybe<FeedGroupEntity>
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
open fun insert(feedGroupEntity: FeedGroupEntity): Long {
|
||||||
|
val nextSortOrder = nextSortOrder()
|
||||||
|
feedGroupEntity.sortOrder = nextSortOrder
|
||||||
|
return insertInternal(feedGroupEntity)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Update(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
abstract fun update(feedGroupEntity: FeedGroupEntity): Int
|
||||||
|
|
||||||
|
@Query("DELETE FROM feed_group")
|
||||||
|
abstract fun deleteAll(): Int
|
||||||
|
|
||||||
|
@Query("DELETE FROM feed_group WHERE uid = :groupId")
|
||||||
|
abstract fun delete(groupId: Long): Int
|
||||||
|
|
||||||
|
@Query("SELECT subscription_id FROM feed_group_subscription_join WHERE group_id = :groupId")
|
||||||
|
abstract fun getSubscriptionIdsFor(groupId: Long): Flowable<List<Long>>
|
||||||
|
|
||||||
|
@Query("DELETE FROM feed_group_subscription_join WHERE group_id = :groupId")
|
||||||
|
abstract fun deleteSubscriptionsFromGroup(groupId: Long): Int
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
abstract fun insertSubscriptionsToGroup(entities: List<FeedGroupSubscriptionEntity>): List<Long>
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
open fun updateSubscriptionsForGroup(groupId: Long, subscriptionIds: List<Long>) {
|
||||||
|
deleteSubscriptionsFromGroup(groupId)
|
||||||
|
insertSubscriptionsToGroup(subscriptionIds.map { FeedGroupSubscriptionEntity(groupId, it) })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
open fun updateOrder(orderMap: Map<Long, Long>) {
|
||||||
|
orderMap.forEach { (groupId, sortOrder) -> updateOrder(groupId, sortOrder) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query("UPDATE feed_group SET sort_order = :sortOrder WHERE uid = :groupId")
|
||||||
|
abstract fun updateOrder(groupId: Long, sortOrder: Long): Int
|
||||||
|
|
||||||
|
@Query("SELECT IFNULL(MAX(sort_order) + 1, 0) FROM feed_group")
|
||||||
|
protected abstract fun nextSortOrder(): Long
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.ABORT)
|
||||||
|
protected abstract fun insertInternal(feedGroupEntity: FeedGroupEntity): Long
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
package org.schabi.newpipe.database.feed.model
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
|
import androidx.room.Index
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedEntity.Companion.FEED_TABLE
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedEntity.Companion.STREAM_ID
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedEntity.Companion.SUBSCRIPTION_ID
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||||
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||||
|
|
||||||
|
@Entity(tableName = FEED_TABLE,
|
||||||
|
primaryKeys = [STREAM_ID, SUBSCRIPTION_ID],
|
||||||
|
indices = [Index(SUBSCRIPTION_ID)],
|
||||||
|
foreignKeys = [
|
||||||
|
ForeignKey(
|
||||||
|
entity = StreamEntity::class,
|
||||||
|
parentColumns = [StreamEntity.STREAM_ID],
|
||||||
|
childColumns = [STREAM_ID],
|
||||||
|
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true),
|
||||||
|
ForeignKey(
|
||||||
|
entity = SubscriptionEntity::class,
|
||||||
|
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
|
||||||
|
childColumns = [SUBSCRIPTION_ID],
|
||||||
|
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
data class FeedEntity(
|
||||||
|
@ColumnInfo(name = STREAM_ID)
|
||||||
|
var streamId: Long,
|
||||||
|
|
||||||
|
@ColumnInfo(name = SUBSCRIPTION_ID)
|
||||||
|
var subscriptionId: Long
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val FEED_TABLE = "feed"
|
||||||
|
|
||||||
|
const val STREAM_ID = "stream_id"
|
||||||
|
const val SUBSCRIPTION_ID = "subscription_id"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
package org.schabi.newpipe.database.feed.model
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Index
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.FEED_GROUP_TABLE
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.SORT_ORDER
|
||||||
|
import org.schabi.newpipe.local.subscription.FeedGroupIcon
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = FEED_GROUP_TABLE,
|
||||||
|
indices = [Index(SORT_ORDER)]
|
||||||
|
)
|
||||||
|
data class FeedGroupEntity(
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
@ColumnInfo(name = ID)
|
||||||
|
val uid: Long,
|
||||||
|
|
||||||
|
@ColumnInfo(name = NAME)
|
||||||
|
var name: String,
|
||||||
|
|
||||||
|
@ColumnInfo(name = ICON)
|
||||||
|
var icon: FeedGroupIcon,
|
||||||
|
|
||||||
|
@ColumnInfo(name = SORT_ORDER)
|
||||||
|
var sortOrder: Long = -1
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
const val FEED_GROUP_TABLE = "feed_group"
|
||||||
|
|
||||||
|
const val ID = "uid"
|
||||||
|
const val NAME = "name"
|
||||||
|
const val ICON = "icon_id"
|
||||||
|
const val SORT_ORDER = "sort_order"
|
||||||
|
|
||||||
|
const val GROUP_ALL_ID = -1L
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
package org.schabi.newpipe.database.feed.model
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
|
import androidx.room.ForeignKey.CASCADE
|
||||||
|
import androidx.room.Index
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity.Companion.FEED_GROUP_SUBSCRIPTION_TABLE
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity.Companion.GROUP_ID
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity.Companion.SUBSCRIPTION_ID
|
||||||
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = FEED_GROUP_SUBSCRIPTION_TABLE,
|
||||||
|
primaryKeys = [GROUP_ID, SUBSCRIPTION_ID],
|
||||||
|
indices = [Index(SUBSCRIPTION_ID)],
|
||||||
|
foreignKeys = [
|
||||||
|
ForeignKey(
|
||||||
|
entity = FeedGroupEntity::class,
|
||||||
|
parentColumns = [FeedGroupEntity.ID],
|
||||||
|
childColumns = [GROUP_ID],
|
||||||
|
onDelete = CASCADE, onUpdate = CASCADE, deferred = true),
|
||||||
|
|
||||||
|
ForeignKey(
|
||||||
|
entity = SubscriptionEntity::class,
|
||||||
|
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
|
||||||
|
childColumns = [SUBSCRIPTION_ID],
|
||||||
|
onDelete = CASCADE, onUpdate = CASCADE, deferred = true)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
data class FeedGroupSubscriptionEntity(
|
||||||
|
@ColumnInfo(name = GROUP_ID)
|
||||||
|
var feedGroupId: Long,
|
||||||
|
|
||||||
|
@ColumnInfo(name = SUBSCRIPTION_ID)
|
||||||
|
var subscriptionId: Long
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val FEED_GROUP_SUBSCRIPTION_TABLE = "feed_group_subscription_join"
|
||||||
|
|
||||||
|
const val GROUP_ID = "group_id"
|
||||||
|
const val SUBSCRIPTION_ID = "subscription_id"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
package org.schabi.newpipe.database.feed.model
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import java.util.Date
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity.Companion.FEED_LAST_UPDATED_TABLE
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity.Companion.SUBSCRIPTION_ID
|
||||||
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = FEED_LAST_UPDATED_TABLE,
|
||||||
|
foreignKeys = [
|
||||||
|
ForeignKey(
|
||||||
|
entity = SubscriptionEntity::class,
|
||||||
|
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
|
||||||
|
childColumns = [SUBSCRIPTION_ID],
|
||||||
|
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
data class FeedLastUpdatedEntity(
|
||||||
|
@PrimaryKey
|
||||||
|
@ColumnInfo(name = SUBSCRIPTION_ID)
|
||||||
|
var subscriptionId: Long,
|
||||||
|
|
||||||
|
@ColumnInfo(name = LAST_UPDATED)
|
||||||
|
var lastUpdated: Date? = null
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val FEED_LAST_UPDATED_TABLE = "feed_last_updated"
|
||||||
|
|
||||||
|
const val SUBSCRIPTION_ID = "subscription_id"
|
||||||
|
const val LAST_UPDATED = "last_updated"
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
package org.schabi.newpipe.database.history.dao;
|
package org.schabi.newpipe.database.history.dao;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.room.Dao;
|
import androidx.room.Dao;
|
||||||
import androidx.room.Query;
|
import androidx.room.Query;
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
||||||
|
|
||||||
@ -18,11 +18,10 @@ import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.TABLE
|
|||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
public interface SearchHistoryDAO extends HistoryDAO<SearchHistoryEntry> {
|
public interface SearchHistoryDAO extends HistoryDAO<SearchHistoryEntry> {
|
||||||
|
|
||||||
String ORDER_BY_CREATION_DATE = " ORDER BY " + CREATION_DATE + " DESC";
|
String ORDER_BY_CREATION_DATE = " ORDER BY " + CREATION_DATE + " DESC";
|
||||||
|
|
||||||
@Query("SELECT * FROM " + TABLE_NAME +
|
@Query("SELECT * FROM " + TABLE_NAME
|
||||||
" WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")")
|
+ " WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")")
|
||||||
@Nullable
|
@Nullable
|
||||||
SearchHistoryEntry getLatestEntry();
|
SearchHistoryEntry getLatestEntry();
|
||||||
|
|
||||||
@ -37,13 +36,16 @@ public interface SearchHistoryDAO extends HistoryDAO<SearchHistoryEntry> {
|
|||||||
@Override
|
@Override
|
||||||
Flowable<List<SearchHistoryEntry>> getAll();
|
Flowable<List<SearchHistoryEntry>> getAll();
|
||||||
|
|
||||||
@Query("SELECT * FROM " + TABLE_NAME + " GROUP BY " + SEARCH + ORDER_BY_CREATION_DATE + " LIMIT :limit")
|
@Query("SELECT * FROM " + TABLE_NAME + " GROUP BY " + SEARCH + ORDER_BY_CREATION_DATE
|
||||||
|
+ " LIMIT :limit")
|
||||||
Flowable<List<SearchHistoryEntry>> getUniqueEntries(int limit);
|
Flowable<List<SearchHistoryEntry>> getUniqueEntries(int limit);
|
||||||
|
|
||||||
@Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE)
|
@Query("SELECT * FROM " + TABLE_NAME
|
||||||
|
+ " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE)
|
||||||
@Override
|
@Override
|
||||||
Flowable<List<SearchHistoryEntry>> listByService(int serviceId);
|
Flowable<List<SearchHistoryEntry>> listByService(int serviceId);
|
||||||
|
|
||||||
@Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SEARCH + " LIKE :query || '%' GROUP BY " + SEARCH + " LIMIT :limit")
|
@Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SEARCH + " LIKE :query || '%'"
|
||||||
|
+ " GROUP BY " + SEARCH + " LIMIT :limit")
|
||||||
Flowable<List<SearchHistoryEntry>> getSimilarEntries(String query, int limit);
|
Flowable<List<SearchHistoryEntry>> getSimilarEntries(String query, int limit);
|
||||||
}
|
}
|
||||||
|
@ -1,32 +1,31 @@
|
|||||||
package org.schabi.newpipe.database.history.dao;
|
package org.schabi.newpipe.database.history.dao;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.room.Dao;
|
import androidx.room.Dao;
|
||||||
import androidx.room.Query;
|
import androidx.room.Query;
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
|
import org.schabi.newpipe.database.history.model.StreamHistoryEntity;
|
||||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntry;
|
import org.schabi.newpipe.database.history.model.StreamHistoryEntry;
|
||||||
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
|
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
|
||||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntity;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import io.reactivex.Flowable;
|
import io.reactivex.Flowable;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID;
|
||||||
|
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE;
|
||||||
|
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE;
|
||||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_REPEAT_COUNT;
|
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_REPEAT_COUNT;
|
||||||
import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_LATEST_DATE;
|
import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_LATEST_DATE;
|
||||||
import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_WATCH_COUNT;
|
import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_WATCH_COUNT;
|
||||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID;
|
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID;
|
||||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
|
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
|
||||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID;
|
|
||||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE;
|
|
||||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE;
|
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
public abstract class StreamHistoryDAO implements HistoryDAO<StreamHistoryEntity> {
|
public abstract class StreamHistoryDAO implements HistoryDAO<StreamHistoryEntity> {
|
||||||
@Query("SELECT * FROM " + STREAM_HISTORY_TABLE +
|
@Query("SELECT * FROM " + STREAM_HISTORY_TABLE
|
||||||
" WHERE " + STREAM_ACCESS_DATE + " = " +
|
+ " WHERE " + STREAM_ACCESS_DATE + " = "
|
||||||
"(SELECT MAX(" + STREAM_ACCESS_DATE + ") FROM " + STREAM_HISTORY_TABLE + ")")
|
+ "(SELECT MAX(" + STREAM_ACCESS_DATE + ") FROM " + STREAM_HISTORY_TABLE + ")")
|
||||||
@Override
|
@Override
|
||||||
@Nullable
|
@Nullable
|
||||||
public abstract StreamHistoryEntity getLatestEntry();
|
public abstract StreamHistoryEntity getLatestEntry();
|
||||||
@ -40,33 +39,40 @@ public abstract class StreamHistoryDAO implements HistoryDAO<StreamHistoryEntity
|
|||||||
public abstract int deleteAll();
|
public abstract int deleteAll();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Flowable<List<StreamHistoryEntity>> listByService(int serviceId) {
|
public Flowable<List<StreamHistoryEntity>> listByService(final int serviceId) {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Query("SELECT * FROM " + STREAM_TABLE +
|
@Query("SELECT * FROM " + STREAM_TABLE
|
||||||
" INNER JOIN " + STREAM_HISTORY_TABLE +
|
+ " INNER JOIN " + STREAM_HISTORY_TABLE
|
||||||
" ON " + STREAM_ID + " = " + JOIN_STREAM_ID +
|
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID
|
||||||
" ORDER BY " + STREAM_ACCESS_DATE + " DESC")
|
+ " ORDER BY " + STREAM_ACCESS_DATE + " DESC")
|
||||||
public abstract Flowable<List<StreamHistoryEntry>> getHistory();
|
public abstract Flowable<List<StreamHistoryEntry>> getHistory();
|
||||||
|
|
||||||
@Query("SELECT * FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID +
|
|
||||||
" = :streamId ORDER BY " + STREAM_ACCESS_DATE + " DESC LIMIT 1")
|
@Query("SELECT * FROM " + STREAM_TABLE
|
||||||
|
+ " INNER JOIN " + STREAM_HISTORY_TABLE
|
||||||
|
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID
|
||||||
|
+ " ORDER BY " + STREAM_ID + " ASC")
|
||||||
|
public abstract Flowable<List<StreamHistoryEntry>> getHistorySortedById();
|
||||||
|
|
||||||
|
@Query("SELECT * FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID
|
||||||
|
+ " = :streamId ORDER BY " + STREAM_ACCESS_DATE + " DESC LIMIT 1")
|
||||||
@Nullable
|
@Nullable
|
||||||
public abstract StreamHistoryEntity getLatestEntry(final long streamId);
|
public abstract StreamHistoryEntity getLatestEntry(long streamId);
|
||||||
|
|
||||||
@Query("DELETE FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
@Query("DELETE FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
||||||
public abstract int deleteStreamHistory(final long streamId);
|
public abstract int deleteStreamHistory(long streamId);
|
||||||
|
|
||||||
@Query("SELECT * FROM " + STREAM_TABLE +
|
@Query("SELECT * FROM " + STREAM_TABLE
|
||||||
|
|
||||||
// Select the latest entry and watch count for each stream id on history table
|
// Select the latest entry and watch count for each stream id on history table
|
||||||
" INNER JOIN " +
|
+ " INNER JOIN "
|
||||||
"(SELECT " + JOIN_STREAM_ID + ", " +
|
+ "(SELECT " + JOIN_STREAM_ID + ", "
|
||||||
" MAX(" + STREAM_ACCESS_DATE + ") AS " + STREAM_LATEST_DATE + ", " +
|
+ " MAX(" + STREAM_ACCESS_DATE + ") AS " + STREAM_LATEST_DATE + ", "
|
||||||
" SUM(" + STREAM_REPEAT_COUNT + ") AS " + STREAM_WATCH_COUNT +
|
+ " SUM(" + STREAM_REPEAT_COUNT + ") AS " + STREAM_WATCH_COUNT
|
||||||
" FROM " + STREAM_HISTORY_TABLE + " GROUP BY " + JOIN_STREAM_ID + ")" +
|
+ " FROM " + STREAM_HISTORY_TABLE + " GROUP BY " + JOIN_STREAM_ID + ")"
|
||||||
|
|
||||||
" ON " + STREAM_ID + " = " + JOIN_STREAM_ID)
|
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID)
|
||||||
public abstract Flowable<List<StreamStatisticsEntry>> getStatistics();
|
public abstract Flowable<List<StreamStatisticsEntry>> getStatistics();
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,6 @@ import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SEARC
|
|||||||
@Entity(tableName = SearchHistoryEntry.TABLE_NAME,
|
@Entity(tableName = SearchHistoryEntry.TABLE_NAME,
|
||||||
indices = {@Index(value = SEARCH)})
|
indices = {@Index(value = SEARCH)})
|
||||||
public class SearchHistoryEntry {
|
public class SearchHistoryEntry {
|
||||||
|
|
||||||
public static final String ID = "id";
|
public static final String ID = "id";
|
||||||
public static final String TABLE_NAME = "search_history";
|
public static final String TABLE_NAME = "search_history";
|
||||||
public static final String SERVICE_ID = "service_id";
|
public static final String SERVICE_ID = "service_id";
|
||||||
@ -33,7 +32,7 @@ public class SearchHistoryEntry {
|
|||||||
@ColumnInfo(name = SEARCH)
|
@ColumnInfo(name = SEARCH)
|
||||||
private String search;
|
private String search;
|
||||||
|
|
||||||
public SearchHistoryEntry(Date creationDate, int serviceId, String search) {
|
public SearchHistoryEntry(final Date creationDate, final int serviceId, final String search) {
|
||||||
this.serviceId = serviceId;
|
this.serviceId = serviceId;
|
||||||
this.creationDate = creationDate;
|
this.creationDate = creationDate;
|
||||||
this.search = search;
|
this.search = search;
|
||||||
@ -43,7 +42,7 @@ public class SearchHistoryEntry {
|
|||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setId(long id) {
|
public void setId(final long id) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,7 +50,7 @@ public class SearchHistoryEntry {
|
|||||||
return creationDate;
|
return creationDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setCreationDate(Date creationDate) {
|
public void setCreationDate(final Date creationDate) {
|
||||||
this.creationDate = creationDate;
|
this.creationDate = creationDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,7 +58,7 @@ public class SearchHistoryEntry {
|
|||||||
return serviceId;
|
return serviceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setServiceId(int serviceId) {
|
public void setServiceId(final int serviceId) {
|
||||||
this.serviceId = serviceId;
|
this.serviceId = serviceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,13 +66,13 @@ public class SearchHistoryEntry {
|
|||||||
return search;
|
return search;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSearch(String search) {
|
public void setSearch(final String search) {
|
||||||
this.search = search;
|
this.search = search;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Ignore
|
@Ignore
|
||||||
public boolean hasEqualValues(SearchHistoryEntry otherEntry) {
|
public boolean hasEqualValues(final SearchHistoryEntry otherEntry) {
|
||||||
return getServiceId() == otherEntry.getServiceId() &&
|
return getServiceId() == otherEntry.getServiceId()
|
||||||
getSearch().equals(otherEntry.getSearch());
|
&& getSearch().equals(otherEntry.getSearch());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,20 @@
|
|||||||
package org.schabi.newpipe.database.history.model;
|
package org.schabi.newpipe.database.history.model;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.room.ColumnInfo;
|
import androidx.room.ColumnInfo;
|
||||||
import androidx.room.Entity;
|
import androidx.room.Entity;
|
||||||
import androidx.room.ForeignKey;
|
import androidx.room.ForeignKey;
|
||||||
import androidx.room.Ignore;
|
import androidx.room.Ignore;
|
||||||
import androidx.room.Index;
|
import androidx.room.Index;
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
import static androidx.room.ForeignKey.CASCADE;
|
import static androidx.room.ForeignKey.CASCADE;
|
||||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE;
|
|
||||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID;
|
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID;
|
||||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE;
|
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE;
|
||||||
|
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE;
|
||||||
|
|
||||||
@Entity(tableName = STREAM_HISTORY_TABLE,
|
@Entity(tableName = STREAM_HISTORY_TABLE,
|
||||||
primaryKeys = {JOIN_STREAM_ID, STREAM_ACCESS_DATE},
|
primaryKeys = {JOIN_STREAM_ID, STREAM_ACCESS_DATE},
|
||||||
@ -27,10 +27,10 @@ import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STRE
|
|||||||
onDelete = CASCADE, onUpdate = CASCADE)
|
onDelete = CASCADE, onUpdate = CASCADE)
|
||||||
})
|
})
|
||||||
public class StreamHistoryEntity {
|
public class StreamHistoryEntity {
|
||||||
final public static String STREAM_HISTORY_TABLE = "stream_history";
|
public static final String STREAM_HISTORY_TABLE = "stream_history";
|
||||||
final public static String JOIN_STREAM_ID = "stream_id";
|
public static final String JOIN_STREAM_ID = "stream_id";
|
||||||
final public static String STREAM_ACCESS_DATE = "access_date";
|
public static final String STREAM_ACCESS_DATE = "access_date";
|
||||||
final public static String STREAM_REPEAT_COUNT = "repeat_count";
|
public static final String STREAM_REPEAT_COUNT = "repeat_count";
|
||||||
|
|
||||||
@ColumnInfo(name = JOIN_STREAM_ID)
|
@ColumnInfo(name = JOIN_STREAM_ID)
|
||||||
private long streamUid;
|
private long streamUid;
|
||||||
@ -42,14 +42,15 @@ public class StreamHistoryEntity {
|
|||||||
@ColumnInfo(name = STREAM_REPEAT_COUNT)
|
@ColumnInfo(name = STREAM_REPEAT_COUNT)
|
||||||
private long repeatCount;
|
private long repeatCount;
|
||||||
|
|
||||||
public StreamHistoryEntity(long streamUid, @NonNull Date accessDate, long repeatCount) {
|
public StreamHistoryEntity(final long streamUid, @NonNull final Date accessDate,
|
||||||
|
final long repeatCount) {
|
||||||
this.streamUid = streamUid;
|
this.streamUid = streamUid;
|
||||||
this.accessDate = accessDate;
|
this.accessDate = accessDate;
|
||||||
this.repeatCount = repeatCount;
|
this.repeatCount = repeatCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Ignore
|
@Ignore
|
||||||
public StreamHistoryEntity(long streamUid, @NonNull Date accessDate) {
|
public StreamHistoryEntity(final long streamUid, @NonNull final Date accessDate) {
|
||||||
this(streamUid, accessDate, 1);
|
this(streamUid, accessDate, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,7 +58,7 @@ public class StreamHistoryEntity {
|
|||||||
return streamUid;
|
return streamUid;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setStreamUid(long streamUid) {
|
public void setStreamUid(final long streamUid) {
|
||||||
this.streamUid = streamUid;
|
this.streamUid = streamUid;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,7 +66,7 @@ public class StreamHistoryEntity {
|
|||||||
return accessDate;
|
return accessDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAccessDate(@NonNull Date accessDate) {
|
public void setAccessDate(@NonNull final Date accessDate) {
|
||||||
this.accessDate = accessDate;
|
this.accessDate = accessDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,7 +74,7 @@ public class StreamHistoryEntity {
|
|||||||
return repeatCount;
|
return repeatCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setRepeatCount(long repeatCount) {
|
public void setRepeatCount(final long repeatCount) {
|
||||||
this.repeatCount = repeatCount;
|
this.repeatCount = repeatCount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,59 +0,0 @@
|
|||||||
package org.schabi.newpipe.database.history.model;
|
|
||||||
|
|
||||||
import androidx.room.ColumnInfo;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
public class StreamHistoryEntry {
|
|
||||||
@ColumnInfo(name = StreamEntity.STREAM_ID)
|
|
||||||
final public long uid;
|
|
||||||
@ColumnInfo(name = StreamEntity.STREAM_SERVICE_ID)
|
|
||||||
final public int serviceId;
|
|
||||||
@ColumnInfo(name = StreamEntity.STREAM_URL)
|
|
||||||
final public String url;
|
|
||||||
@ColumnInfo(name = StreamEntity.STREAM_TITLE)
|
|
||||||
final public String title;
|
|
||||||
@ColumnInfo(name = StreamEntity.STREAM_TYPE)
|
|
||||||
final public StreamType streamType;
|
|
||||||
@ColumnInfo(name = StreamEntity.STREAM_DURATION)
|
|
||||||
final public long duration;
|
|
||||||
@ColumnInfo(name = StreamEntity.STREAM_UPLOADER)
|
|
||||||
final public String uploader;
|
|
||||||
@ColumnInfo(name = StreamEntity.STREAM_THUMBNAIL_URL)
|
|
||||||
final public String thumbnailUrl;
|
|
||||||
@ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID)
|
|
||||||
final public long streamId;
|
|
||||||
@ColumnInfo(name = StreamHistoryEntity.STREAM_ACCESS_DATE)
|
|
||||||
final public Date accessDate;
|
|
||||||
@ColumnInfo(name = StreamHistoryEntity.STREAM_REPEAT_COUNT)
|
|
||||||
final public long repeatCount;
|
|
||||||
|
|
||||||
public StreamHistoryEntry(long uid, int serviceId, String url, String title,
|
|
||||||
StreamType streamType, long duration, String uploader,
|
|
||||||
String thumbnailUrl, long streamId, Date accessDate,
|
|
||||||
long repeatCount) {
|
|
||||||
this.uid = uid;
|
|
||||||
this.serviceId = serviceId;
|
|
||||||
this.url = url;
|
|
||||||
this.title = title;
|
|
||||||
this.streamType = streamType;
|
|
||||||
this.duration = duration;
|
|
||||||
this.uploader = uploader;
|
|
||||||
this.thumbnailUrl = thumbnailUrl;
|
|
||||||
this.streamId = streamId;
|
|
||||||
this.accessDate = accessDate;
|
|
||||||
this.repeatCount = repeatCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public StreamHistoryEntity toStreamHistoryEntity() {
|
|
||||||
return new StreamHistoryEntity(streamId, accessDate, repeatCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean hasEqualValues(StreamHistoryEntry other) {
|
|
||||||
return this.uid == other.uid && streamId == other.streamId &&
|
|
||||||
accessDate.compareTo(other.accessDate) == 0;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,30 @@
|
|||||||
|
package org.schabi.newpipe.database.history.model
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Embedded
|
||||||
|
import java.util.Date
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||||
|
|
||||||
|
data class StreamHistoryEntry(
|
||||||
|
@Embedded
|
||||||
|
val streamEntity: StreamEntity,
|
||||||
|
|
||||||
|
@ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID)
|
||||||
|
val streamId: Long,
|
||||||
|
|
||||||
|
@ColumnInfo(name = StreamHistoryEntity.STREAM_ACCESS_DATE)
|
||||||
|
val accessDate: Date,
|
||||||
|
|
||||||
|
@ColumnInfo(name = StreamHistoryEntity.STREAM_REPEAT_COUNT)
|
||||||
|
val repeatCount: Long
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun toStreamHistoryEntity(): StreamHistoryEntity {
|
||||||
|
return StreamHistoryEntity(streamId, accessDate, repeatCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasEqualValues(other: StreamHistoryEntry): Boolean {
|
||||||
|
return this.streamEntity.uid == other.streamEntity.uid && streamId == other.streamId &&
|
||||||
|
accessDate.compareTo(other.accessDate) == 0
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,33 @@
|
|||||||
package org.schabi.newpipe.database.playlist;
|
package org.schabi.newpipe.database.playlist;
|
||||||
|
|
||||||
import org.schabi.newpipe.database.LocalItem;
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public interface PlaylistLocalItem extends LocalItem {
|
public interface PlaylistLocalItem extends LocalItem {
|
||||||
String getOrderingName();
|
String getOrderingName();
|
||||||
|
|
||||||
|
static List<PlaylistLocalItem> merge(
|
||||||
|
final List<PlaylistMetadataEntry> localPlaylists,
|
||||||
|
final List<PlaylistRemoteEntity> remotePlaylists) {
|
||||||
|
final List<PlaylistLocalItem> items = new ArrayList<>(
|
||||||
|
localPlaylists.size() + remotePlaylists.size());
|
||||||
|
items.addAll(localPlaylists);
|
||||||
|
items.addAll(remotePlaylists);
|
||||||
|
|
||||||
|
Collections.sort(items, (left, right) -> {
|
||||||
|
final String on1 = left.getOrderingName();
|
||||||
|
final String on2 = right.getOrderingName();
|
||||||
|
if (on1 == null) {
|
||||||
|
return on2 == null ? 0 : 1;
|
||||||
|
} else {
|
||||||
|
return on2 == null ? -1 : on1.compareToIgnoreCase(on2);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,18 +7,19 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST
|
|||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
|
||||||
|
|
||||||
public class PlaylistMetadataEntry implements PlaylistLocalItem {
|
public class PlaylistMetadataEntry implements PlaylistLocalItem {
|
||||||
final public static String PLAYLIST_STREAM_COUNT = "streamCount";
|
public static final String PLAYLIST_STREAM_COUNT = "streamCount";
|
||||||
|
|
||||||
@ColumnInfo(name = PLAYLIST_ID)
|
@ColumnInfo(name = PLAYLIST_ID)
|
||||||
final public long uid;
|
public final long uid;
|
||||||
@ColumnInfo(name = PLAYLIST_NAME)
|
@ColumnInfo(name = PLAYLIST_NAME)
|
||||||
final public String name;
|
public final String name;
|
||||||
@ColumnInfo(name = PLAYLIST_THUMBNAIL_URL)
|
@ColumnInfo(name = PLAYLIST_THUMBNAIL_URL)
|
||||||
final public String thumbnailUrl;
|
public final String thumbnailUrl;
|
||||||
@ColumnInfo(name = PLAYLIST_STREAM_COUNT)
|
@ColumnInfo(name = PLAYLIST_STREAM_COUNT)
|
||||||
final public long streamCount;
|
public final long streamCount;
|
||||||
|
|
||||||
public PlaylistMetadataEntry(long uid, String name, String thumbnailUrl, long streamCount) {
|
public PlaylistMetadataEntry(final long uid, final String name, final String thumbnailUrl,
|
||||||
|
final long streamCount) {
|
||||||
this.uid = uid;
|
this.uid = uid;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.thumbnailUrl = thumbnailUrl;
|
this.thumbnailUrl = thumbnailUrl;
|
||||||
|
@ -1,60 +0,0 @@
|
|||||||
package org.schabi.newpipe.database.playlist;
|
|
||||||
|
|
||||||
import androidx.room.ColumnInfo;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.database.LocalItem;
|
|
||||||
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
|
|
||||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
|
||||||
|
|
||||||
public class PlaylistStreamEntry implements LocalItem {
|
|
||||||
@ColumnInfo(name = StreamEntity.STREAM_ID)
|
|
||||||
final public long uid;
|
|
||||||
@ColumnInfo(name = StreamEntity.STREAM_SERVICE_ID)
|
|
||||||
final public int serviceId;
|
|
||||||
@ColumnInfo(name = StreamEntity.STREAM_URL)
|
|
||||||
final public String url;
|
|
||||||
@ColumnInfo(name = StreamEntity.STREAM_TITLE)
|
|
||||||
final public String title;
|
|
||||||
@ColumnInfo(name = StreamEntity.STREAM_TYPE)
|
|
||||||
final public StreamType streamType;
|
|
||||||
@ColumnInfo(name = StreamEntity.STREAM_DURATION)
|
|
||||||
final public long duration;
|
|
||||||
@ColumnInfo(name = StreamEntity.STREAM_UPLOADER)
|
|
||||||
final public String uploader;
|
|
||||||
@ColumnInfo(name = StreamEntity.STREAM_THUMBNAIL_URL)
|
|
||||||
final public String thumbnailUrl;
|
|
||||||
@ColumnInfo(name = PlaylistStreamEntity.JOIN_STREAM_ID)
|
|
||||||
final public long streamId;
|
|
||||||
@ColumnInfo(name = PlaylistStreamEntity.JOIN_INDEX)
|
|
||||||
final public int joinIndex;
|
|
||||||
|
|
||||||
public PlaylistStreamEntry(long uid, int serviceId, String url, String title,
|
|
||||||
StreamType streamType, long duration, String uploader,
|
|
||||||
String thumbnailUrl, long streamId, int joinIndex) {
|
|
||||||
this.uid = uid;
|
|
||||||
this.serviceId = serviceId;
|
|
||||||
this.url = url;
|
|
||||||
this.title = title;
|
|
||||||
this.streamType = streamType;
|
|
||||||
this.duration = duration;
|
|
||||||
this.uploader = uploader;
|
|
||||||
this.thumbnailUrl = thumbnailUrl;
|
|
||||||
this.streamId = streamId;
|
|
||||||
this.joinIndex = joinIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
public StreamInfoItem toStreamInfoItem() throws IllegalArgumentException {
|
|
||||||
StreamInfoItem item = new StreamInfoItem(serviceId, url, title, streamType);
|
|
||||||
item.setThumbnailUrl(thumbnailUrl);
|
|
||||||
item.setUploaderName(uploader);
|
|
||||||
item.setDuration(duration);
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public LocalItemType getLocalItemType() {
|
|
||||||
return LocalItemType.PLAYLIST_STREAM_ITEM;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,34 @@
|
|||||||
|
package org.schabi.newpipe.database.playlist
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Embedded
|
||||||
|
import org.schabi.newpipe.database.LocalItem
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
|
|
||||||
|
class PlaylistStreamEntry(
|
||||||
|
@Embedded
|
||||||
|
val streamEntity: StreamEntity,
|
||||||
|
|
||||||
|
@ColumnInfo(name = PlaylistStreamEntity.JOIN_STREAM_ID)
|
||||||
|
val streamId: Long,
|
||||||
|
|
||||||
|
@ColumnInfo(name = PlaylistStreamEntity.JOIN_INDEX)
|
||||||
|
val joinIndex: Int
|
||||||
|
) : LocalItem {
|
||||||
|
|
||||||
|
@Throws(IllegalArgumentException::class)
|
||||||
|
fun toStreamInfoItem(): StreamInfoItem {
|
||||||
|
val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType)
|
||||||
|
item.duration = streamEntity.duration
|
||||||
|
item.uploaderName = streamEntity.uploader
|
||||||
|
item.thumbnailUrl = streamEntity.thumbnailUrl
|
||||||
|
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLocalItemType(): LocalItem.LocalItemType {
|
||||||
|
return LocalItem.LocalItemType.PLAYLIST_STREAM_ITEM
|
||||||
|
}
|
||||||
|
}
|
@ -24,13 +24,13 @@ public abstract class PlaylistDAO implements BasicDAO<PlaylistEntity> {
|
|||||||
public abstract int deleteAll();
|
public abstract int deleteAll();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Flowable<List<PlaylistEntity>> listByService(int serviceId) {
|
public Flowable<List<PlaylistEntity>> listByService(final int serviceId) {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Query("SELECT * FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId")
|
@Query("SELECT * FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId")
|
||||||
public abstract Flowable<List<PlaylistEntity>> getPlaylist(final long playlistId);
|
public abstract Flowable<List<PlaylistEntity>> getPlaylist(long playlistId);
|
||||||
|
|
||||||
@Query("DELETE FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId")
|
@Query("DELETE FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId")
|
||||||
public abstract int deletePlaylist(final long playlistId);
|
public abstract int deletePlaylist(long playlistId);
|
||||||
}
|
}
|
||||||
|
@ -27,22 +27,21 @@ public abstract class PlaylistRemoteDAO implements BasicDAO<PlaylistRemoteEntity
|
|||||||
public abstract int deleteAll();
|
public abstract int deleteAll();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE +
|
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE
|
||||||
" WHERE " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
+ " WHERE " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
||||||
public abstract Flowable<List<PlaylistRemoteEntity>> listByService(int serviceId);
|
public abstract Flowable<List<PlaylistRemoteEntity>> listByService(int serviceId);
|
||||||
|
|
||||||
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE " +
|
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE "
|
||||||
REMOTE_PLAYLIST_URL + " = :url AND " +
|
+ REMOTE_PLAYLIST_URL + " = :url AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
||||||
REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
|
||||||
public abstract Flowable<List<PlaylistRemoteEntity>> getPlaylist(long serviceId, String url);
|
public abstract Flowable<List<PlaylistRemoteEntity>> getPlaylist(long serviceId, String url);
|
||||||
|
|
||||||
@Query("SELECT " + REMOTE_PLAYLIST_ID + " FROM " + REMOTE_PLAYLIST_TABLE +
|
@Query("SELECT " + REMOTE_PLAYLIST_ID + " FROM " + REMOTE_PLAYLIST_TABLE
|
||||||
" WHERE " +
|
+ " WHERE " + REMOTE_PLAYLIST_URL + " = :url "
|
||||||
REMOTE_PLAYLIST_URL + " = :url AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
+ "AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
||||||
abstract Long getPlaylistIdInternal(long serviceId, String url);
|
abstract Long getPlaylistIdInternal(long serviceId, String url);
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
public long upsert(PlaylistRemoteEntity playlist) {
|
public long upsert(final PlaylistRemoteEntity playlist) {
|
||||||
final Long playlistId = getPlaylistIdInternal(playlist.getServiceId(), playlist.getUrl());
|
final Long playlistId = getPlaylistIdInternal(playlist.getServiceId(), playlist.getUrl());
|
||||||
|
|
||||||
if (playlistId == null) {
|
if (playlistId == null) {
|
||||||
@ -54,7 +53,7 @@ public abstract class PlaylistRemoteDAO implements BasicDAO<PlaylistRemoteEntity
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Query("DELETE FROM " + REMOTE_PLAYLIST_TABLE +
|
@Query("DELETE FROM " + REMOTE_PLAYLIST_TABLE
|
||||||
" WHERE " + REMOTE_PLAYLIST_ID + " = :playlistId")
|
+ " WHERE " + REMOTE_PLAYLIST_ID + " = :playlistId")
|
||||||
public abstract int deletePlaylist(final long playlistId);
|
public abstract int deletePlaylist(long playlistId);
|
||||||
}
|
}
|
||||||
|
@ -14,9 +14,16 @@ import java.util.List;
|
|||||||
import io.reactivex.Flowable;
|
import io.reactivex.Flowable;
|
||||||
|
|
||||||
import static org.schabi.newpipe.database.playlist.PlaylistMetadataEntry.PLAYLIST_STREAM_COUNT;
|
import static org.schabi.newpipe.database.playlist.PlaylistMetadataEntry.PLAYLIST_STREAM_COUNT;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.*;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.*;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
|
||||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.*;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_INDEX;
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_PLAYLIST_ID;
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_STREAM_ID;
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE;
|
||||||
|
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID;
|
||||||
|
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
public abstract class PlaylistStreamDAO implements BasicDAO<PlaylistStreamEntity> {
|
public abstract class PlaylistStreamDAO implements BasicDAO<PlaylistStreamEntity> {
|
||||||
@ -29,40 +36,39 @@ public abstract class PlaylistStreamDAO implements BasicDAO<PlaylistStreamEntity
|
|||||||
public abstract int deleteAll();
|
public abstract int deleteAll();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Flowable<List<PlaylistStreamEntity>> listByService(int serviceId) {
|
public Flowable<List<PlaylistStreamEntity>> listByService(final int serviceId) {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Query("DELETE FROM " + PLAYLIST_STREAM_JOIN_TABLE +
|
@Query("DELETE FROM " + PLAYLIST_STREAM_JOIN_TABLE
|
||||||
" WHERE " + JOIN_PLAYLIST_ID + " = :playlistId")
|
+ " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId")
|
||||||
public abstract void deleteBatch(final long playlistId);
|
public abstract void deleteBatch(long playlistId);
|
||||||
|
|
||||||
@Query("SELECT COALESCE(MAX(" + JOIN_INDEX + "), -1)" +
|
@Query("SELECT COALESCE(MAX(" + JOIN_INDEX + "), -1)"
|
||||||
" FROM " + PLAYLIST_STREAM_JOIN_TABLE +
|
+ " FROM " + PLAYLIST_STREAM_JOIN_TABLE
|
||||||
" WHERE " + JOIN_PLAYLIST_ID + " = :playlistId")
|
+ " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId")
|
||||||
public abstract Flowable<Integer> getMaximumIndexOf(final long playlistId);
|
public abstract Flowable<Integer> getMaximumIndexOf(long playlistId);
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("SELECT * FROM " + STREAM_TABLE + " INNER JOIN " +
|
@Query("SELECT * FROM " + STREAM_TABLE + " INNER JOIN "
|
||||||
// get ids of streams of the given playlist
|
// get ids of streams of the given playlist
|
||||||
"(SELECT " + JOIN_STREAM_ID + "," + JOIN_INDEX +
|
+ "(SELECT " + JOIN_STREAM_ID + "," + JOIN_INDEX
|
||||||
" FROM " + PLAYLIST_STREAM_JOIN_TABLE +
|
+ " FROM " + PLAYLIST_STREAM_JOIN_TABLE
|
||||||
" WHERE " + JOIN_PLAYLIST_ID + " = :playlistId)" +
|
+ " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId)"
|
||||||
|
|
||||||
// then merge with the stream metadata
|
// then merge with the stream metadata
|
||||||
" ON " + STREAM_ID + " = " + JOIN_STREAM_ID +
|
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID
|
||||||
" ORDER BY " + JOIN_INDEX + " ASC")
|
+ " ORDER BY " + JOIN_INDEX + " ASC")
|
||||||
public abstract Flowable<List<PlaylistStreamEntry>> getOrderedStreamsOf(long playlistId);
|
public abstract Flowable<List<PlaylistStreamEntry>> getOrderedStreamsOf(long playlistId);
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", " +
|
@Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", " + PLAYLIST_THUMBNAIL_URL + ", "
|
||||||
PLAYLIST_THUMBNAIL_URL + ", " +
|
+ "COALESCE(COUNT(" + JOIN_PLAYLIST_ID + "), 0) AS " + PLAYLIST_STREAM_COUNT
|
||||||
"COALESCE(COUNT(" + JOIN_PLAYLIST_ID + "), 0) AS " + PLAYLIST_STREAM_COUNT +
|
|
||||||
|
|
||||||
" FROM " + PLAYLIST_TABLE +
|
+ " FROM " + PLAYLIST_TABLE
|
||||||
" LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE +
|
+ " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE
|
||||||
" ON " + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID +
|
+ " ON " + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID
|
||||||
" GROUP BY " + JOIN_PLAYLIST_ID +
|
+ " GROUP BY " + JOIN_PLAYLIST_ID
|
||||||
" ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC")
|
+ " ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC")
|
||||||
public abstract Flowable<List<PlaylistMetadataEntry>> getPlaylistMetadata();
|
public abstract Flowable<List<PlaylistMetadataEntry>> getPlaylistMetadata();
|
||||||
}
|
}
|
||||||
|
@ -11,10 +11,10 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST
|
|||||||
@Entity(tableName = PLAYLIST_TABLE,
|
@Entity(tableName = PLAYLIST_TABLE,
|
||||||
indices = {@Index(value = {PLAYLIST_NAME})})
|
indices = {@Index(value = {PLAYLIST_NAME})})
|
||||||
public class PlaylistEntity {
|
public class PlaylistEntity {
|
||||||
final public static String PLAYLIST_TABLE = "playlists";
|
public static final String PLAYLIST_TABLE = "playlists";
|
||||||
final public static String PLAYLIST_ID = "uid";
|
public static final String PLAYLIST_ID = "uid";
|
||||||
final public static String PLAYLIST_NAME = "name";
|
public static final String PLAYLIST_NAME = "name";
|
||||||
final public static String PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
|
public static final String PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
|
||||||
|
|
||||||
@PrimaryKey(autoGenerate = true)
|
@PrimaryKey(autoGenerate = true)
|
||||||
@ColumnInfo(name = PLAYLIST_ID)
|
@ColumnInfo(name = PLAYLIST_ID)
|
||||||
@ -26,7 +26,7 @@ public class PlaylistEntity {
|
|||||||
@ColumnInfo(name = PLAYLIST_THUMBNAIL_URL)
|
@ColumnInfo(name = PLAYLIST_THUMBNAIL_URL)
|
||||||
private String thumbnailUrl;
|
private String thumbnailUrl;
|
||||||
|
|
||||||
public PlaylistEntity(String name, String thumbnailUrl) {
|
public PlaylistEntity(final String name, final String thumbnailUrl) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.thumbnailUrl = thumbnailUrl;
|
this.thumbnailUrl = thumbnailUrl;
|
||||||
}
|
}
|
||||||
@ -35,7 +35,7 @@ public class PlaylistEntity {
|
|||||||
return uid;
|
return uid;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setUid(long uid) {
|
public void setUid(final long uid) {
|
||||||
this.uid = uid;
|
this.uid = uid;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,7 +43,7 @@ public class PlaylistEntity {
|
|||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setName(String name) {
|
public void setName(final String name) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,7 +51,7 @@ public class PlaylistEntity {
|
|||||||
return thumbnailUrl;
|
return thumbnailUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setThumbnailUrl(String thumbnailUrl) {
|
public void setThumbnailUrl(final String thumbnailUrl) {
|
||||||
this.thumbnailUrl = thumbnailUrl;
|
this.thumbnailUrl = thumbnailUrl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,14 +24,14 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.RE
|
|||||||
@Index(value = {REMOTE_PLAYLIST_SERVICE_ID, REMOTE_PLAYLIST_URL}, unique = true)
|
@Index(value = {REMOTE_PLAYLIST_SERVICE_ID, REMOTE_PLAYLIST_URL}, unique = true)
|
||||||
})
|
})
|
||||||
public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
||||||
final public static String REMOTE_PLAYLIST_TABLE = "remote_playlists";
|
public static final String REMOTE_PLAYLIST_TABLE = "remote_playlists";
|
||||||
final public static String REMOTE_PLAYLIST_ID = "uid";
|
public static final String REMOTE_PLAYLIST_ID = "uid";
|
||||||
final public static String REMOTE_PLAYLIST_SERVICE_ID = "service_id";
|
public static final String REMOTE_PLAYLIST_SERVICE_ID = "service_id";
|
||||||
final public static String REMOTE_PLAYLIST_NAME = "name";
|
public static final String REMOTE_PLAYLIST_NAME = "name";
|
||||||
final public static String REMOTE_PLAYLIST_URL = "url";
|
public static final String REMOTE_PLAYLIST_URL = "url";
|
||||||
final public static String REMOTE_PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
|
public static final String REMOTE_PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
|
||||||
final public static String REMOTE_PLAYLIST_UPLOADER_NAME = "uploader";
|
public static final String REMOTE_PLAYLIST_UPLOADER_NAME = "uploader";
|
||||||
final public static String REMOTE_PLAYLIST_STREAM_COUNT = "stream_count";
|
public static final String REMOTE_PLAYLIST_STREAM_COUNT = "stream_count";
|
||||||
|
|
||||||
@PrimaryKey(autoGenerate = true)
|
@PrimaryKey(autoGenerate = true)
|
||||||
@ColumnInfo(name = REMOTE_PLAYLIST_ID)
|
@ColumnInfo(name = REMOTE_PLAYLIST_ID)
|
||||||
@ -55,8 +55,9 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
|||||||
@ColumnInfo(name = REMOTE_PLAYLIST_STREAM_COUNT)
|
@ColumnInfo(name = REMOTE_PLAYLIST_STREAM_COUNT)
|
||||||
private Long streamCount;
|
private Long streamCount;
|
||||||
|
|
||||||
public PlaylistRemoteEntity(int serviceId, String name, String url, String thumbnailUrl,
|
public PlaylistRemoteEntity(final int serviceId, final String name, final String url,
|
||||||
String uploader, Long streamCount) {
|
final String thumbnailUrl, final String uploader,
|
||||||
|
final Long streamCount) {
|
||||||
this.serviceId = serviceId;
|
this.serviceId = serviceId;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.url = url;
|
this.url = url;
|
||||||
@ -68,7 +69,8 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
|||||||
@Ignore
|
@Ignore
|
||||||
public PlaylistRemoteEntity(final PlaylistInfo info) {
|
public PlaylistRemoteEntity(final PlaylistInfo info) {
|
||||||
this(info.getServiceId(), info.getName(), info.getUrl(),
|
this(info.getServiceId(), info.getName(), info.getUrl(),
|
||||||
info.getThumbnailUrl() == null ? info.getUploaderAvatarUrl() : info.getThumbnailUrl(),
|
info.getThumbnailUrl() == null
|
||||||
|
? info.getUploaderAvatarUrl() : info.getThumbnailUrl(),
|
||||||
info.getUploaderName(), info.getStreamCount());
|
info.getUploaderName(), info.getStreamCount());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,7 +92,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
|||||||
return uid;
|
return uid;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setUid(long uid) {
|
public void setUid(final long uid) {
|
||||||
this.uid = uid;
|
this.uid = uid;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,7 +100,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
|||||||
return serviceId;
|
return serviceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setServiceId(int serviceId) {
|
public void setServiceId(final int serviceId) {
|
||||||
this.serviceId = serviceId;
|
this.serviceId = serviceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,7 +108,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
|||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setName(String name) {
|
public void setName(final String name) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,7 +116,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
|||||||
return thumbnailUrl;
|
return thumbnailUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setThumbnailUrl(String thumbnailUrl) {
|
public void setThumbnailUrl(final String thumbnailUrl) {
|
||||||
this.thumbnailUrl = thumbnailUrl;
|
this.thumbnailUrl = thumbnailUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,7 +124,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
|||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setUrl(String url) {
|
public void setUrl(final String url) {
|
||||||
this.url = url;
|
this.url = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,7 +132,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
|||||||
return uploader;
|
return uploader;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setUploader(String uploader) {
|
public void setUploader(final String uploader) {
|
||||||
this.uploader = uploader;
|
this.uploader = uploader;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,7 +140,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
|||||||
return streamCount;
|
return streamCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setStreamCount(Long streamCount) {
|
public void setStreamCount(final Long streamCount) {
|
||||||
this.streamCount = streamCount;
|
this.streamCount = streamCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,11 +30,10 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PL
|
|||||||
onDelete = CASCADE, onUpdate = CASCADE, deferred = true)
|
onDelete = CASCADE, onUpdate = CASCADE, deferred = true)
|
||||||
})
|
})
|
||||||
public class PlaylistStreamEntity {
|
public class PlaylistStreamEntity {
|
||||||
|
public static final String PLAYLIST_STREAM_JOIN_TABLE = "playlist_stream_join";
|
||||||
final public static String PLAYLIST_STREAM_JOIN_TABLE = "playlist_stream_join";
|
public static final String JOIN_PLAYLIST_ID = "playlist_id";
|
||||||
final public static String JOIN_PLAYLIST_ID = "playlist_id";
|
public static final String JOIN_STREAM_ID = "stream_id";
|
||||||
final public static String JOIN_STREAM_ID = "stream_id";
|
public static final String JOIN_INDEX = "join_index";
|
||||||
final public static String JOIN_INDEX = "join_index";
|
|
||||||
|
|
||||||
@ColumnInfo(name = JOIN_PLAYLIST_ID)
|
@ColumnInfo(name = JOIN_PLAYLIST_ID)
|
||||||
private long playlistUid;
|
private long playlistUid;
|
||||||
@ -55,23 +54,23 @@ public class PlaylistStreamEntity {
|
|||||||
return playlistUid;
|
return playlistUid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setPlaylistUid(final long playlistUid) {
|
||||||
|
this.playlistUid = playlistUid;
|
||||||
|
}
|
||||||
|
|
||||||
public long getStreamUid() {
|
public long getStreamUid() {
|
||||||
return streamUid;
|
return streamUid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setStreamUid(final long streamUid) {
|
||||||
|
this.streamUid = streamUid;
|
||||||
|
}
|
||||||
|
|
||||||
public int getIndex() {
|
public int getIndex() {
|
||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setPlaylistUid(long playlistUid) {
|
public void setIndex(final int index) {
|
||||||
this.playlistUid = playlistUid;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setStreamUid(long streamUid) {
|
|
||||||
this.streamUid = streamUid;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setIndex(int index) {
|
|
||||||
this.index = index;
|
this.index = index;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,69 +0,0 @@
|
|||||||
package org.schabi.newpipe.database.stream;
|
|
||||||
|
|
||||||
import androidx.room.ColumnInfo;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.database.LocalItem;
|
|
||||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntity;
|
|
||||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
public class StreamStatisticsEntry implements LocalItem {
|
|
||||||
final public static String STREAM_LATEST_DATE = "latestAccess";
|
|
||||||
final public static String STREAM_WATCH_COUNT = "watchCount";
|
|
||||||
|
|
||||||
@ColumnInfo(name = StreamEntity.STREAM_ID)
|
|
||||||
final public long uid;
|
|
||||||
@ColumnInfo(name = StreamEntity.STREAM_SERVICE_ID)
|
|
||||||
final public int serviceId;
|
|
||||||
@ColumnInfo(name = StreamEntity.STREAM_URL)
|
|
||||||
final public String url;
|
|
||||||
@ColumnInfo(name = StreamEntity.STREAM_TITLE)
|
|
||||||
final public String title;
|
|
||||||
@ColumnInfo(name = StreamEntity.STREAM_TYPE)
|
|
||||||
final public StreamType streamType;
|
|
||||||
@ColumnInfo(name = StreamEntity.STREAM_DURATION)
|
|
||||||
final public long duration;
|
|
||||||
@ColumnInfo(name = StreamEntity.STREAM_UPLOADER)
|
|
||||||
final public String uploader;
|
|
||||||
@ColumnInfo(name = StreamEntity.STREAM_THUMBNAIL_URL)
|
|
||||||
final public String thumbnailUrl;
|
|
||||||
@ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID)
|
|
||||||
final public long streamId;
|
|
||||||
@ColumnInfo(name = StreamStatisticsEntry.STREAM_LATEST_DATE)
|
|
||||||
final public Date latestAccessDate;
|
|
||||||
@ColumnInfo(name = StreamStatisticsEntry.STREAM_WATCH_COUNT)
|
|
||||||
final public long watchCount;
|
|
||||||
|
|
||||||
public StreamStatisticsEntry(long uid, int serviceId, String url, String title,
|
|
||||||
StreamType streamType, long duration, String uploader,
|
|
||||||
String thumbnailUrl, long streamId, Date latestAccessDate,
|
|
||||||
long watchCount) {
|
|
||||||
this.uid = uid;
|
|
||||||
this.serviceId = serviceId;
|
|
||||||
this.url = url;
|
|
||||||
this.title = title;
|
|
||||||
this.streamType = streamType;
|
|
||||||
this.duration = duration;
|
|
||||||
this.uploader = uploader;
|
|
||||||
this.thumbnailUrl = thumbnailUrl;
|
|
||||||
this.streamId = streamId;
|
|
||||||
this.latestAccessDate = latestAccessDate;
|
|
||||||
this.watchCount = watchCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public StreamInfoItem toStreamInfoItem() {
|
|
||||||
StreamInfoItem item = new StreamInfoItem(serviceId, url, title, streamType);
|
|
||||||
item.setDuration(duration);
|
|
||||||
item.setUploaderName(uploader);
|
|
||||||
item.setThumbnailUrl(thumbnailUrl);
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public LocalItemType getLocalItemType() {
|
|
||||||
return LocalItemType.STATISTIC_STREAM_ITEM;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,42 @@
|
|||||||
|
package org.schabi.newpipe.database.stream
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Embedded
|
||||||
|
import java.util.Date
|
||||||
|
import org.schabi.newpipe.database.LocalItem
|
||||||
|
import org.schabi.newpipe.database.history.model.StreamHistoryEntity
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
|
|
||||||
|
class StreamStatisticsEntry(
|
||||||
|
@Embedded
|
||||||
|
val streamEntity: StreamEntity,
|
||||||
|
|
||||||
|
@ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID)
|
||||||
|
val streamId: Long,
|
||||||
|
|
||||||
|
@ColumnInfo(name = STREAM_LATEST_DATE)
|
||||||
|
val latestAccessDate: Date,
|
||||||
|
|
||||||
|
@ColumnInfo(name = STREAM_WATCH_COUNT)
|
||||||
|
val watchCount: Long
|
||||||
|
) : LocalItem {
|
||||||
|
|
||||||
|
fun toStreamInfoItem(): StreamInfoItem {
|
||||||
|
val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType)
|
||||||
|
item.duration = streamEntity.duration
|
||||||
|
item.uploaderName = streamEntity.uploader
|
||||||
|
item.thumbnailUrl = streamEntity.thumbnailUrl
|
||||||
|
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLocalItemType(): LocalItem.LocalItemType {
|
||||||
|
return LocalItem.LocalItemType.STATISTIC_STREAM_ITEM
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val STREAM_LATEST_DATE = "latestAccess"
|
||||||
|
const val STREAM_WATCH_COUNT = "watchCount"
|
||||||
|
}
|
||||||
|
}
|
@ -1,98 +0,0 @@
|
|||||||
package org.schabi.newpipe.database.stream.dao;
|
|
||||||
|
|
||||||
import androidx.room.Dao;
|
|
||||||
import androidx.room.Insert;
|
|
||||||
import androidx.room.OnConflictStrategy;
|
|
||||||
import androidx.room.Query;
|
|
||||||
import androidx.room.Transaction;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.database.BasicDAO;
|
|
||||||
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
|
|
||||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
|
||||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntity;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import io.reactivex.Flowable;
|
|
||||||
|
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE;
|
|
||||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID;
|
|
||||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_SERVICE_ID;
|
|
||||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
|
|
||||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_URL;
|
|
||||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE;
|
|
||||||
|
|
||||||
@Dao
|
|
||||||
public abstract class StreamDAO implements BasicDAO<StreamEntity> {
|
|
||||||
@Override
|
|
||||||
@Query("SELECT * FROM " + STREAM_TABLE)
|
|
||||||
public abstract Flowable<List<StreamEntity>> getAll();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Query("DELETE FROM " + STREAM_TABLE)
|
|
||||||
public abstract int deleteAll();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Query("SELECT * FROM " + STREAM_TABLE + " WHERE " + STREAM_SERVICE_ID + " = :serviceId")
|
|
||||||
public abstract Flowable<List<StreamEntity>> listByService(int serviceId);
|
|
||||||
|
|
||||||
@Query("SELECT * FROM " + STREAM_TABLE + " WHERE " +
|
|
||||||
STREAM_URL + " = :url AND " +
|
|
||||||
STREAM_SERVICE_ID + " = :serviceId")
|
|
||||||
public abstract Flowable<List<StreamEntity>> getStream(long serviceId, String url);
|
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
|
||||||
abstract void silentInsertAllInternal(final List<StreamEntity> streams);
|
|
||||||
|
|
||||||
@Query("SELECT " + STREAM_ID + " FROM " + STREAM_TABLE + " WHERE " +
|
|
||||||
STREAM_URL + " = :url AND " +
|
|
||||||
STREAM_SERVICE_ID + " = :serviceId")
|
|
||||||
abstract Long getStreamIdInternal(long serviceId, String url);
|
|
||||||
|
|
||||||
@Transaction
|
|
||||||
public long upsert(StreamEntity stream) {
|
|
||||||
final Long streamIdCandidate = getStreamIdInternal(stream.getServiceId(), stream.getUrl());
|
|
||||||
|
|
||||||
if (streamIdCandidate == null) {
|
|
||||||
return insert(stream);
|
|
||||||
} else {
|
|
||||||
stream.setUid(streamIdCandidate);
|
|
||||||
update(stream);
|
|
||||||
return streamIdCandidate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transaction
|
|
||||||
public List<Long> upsertAll(List<StreamEntity> streams) {
|
|
||||||
silentInsertAllInternal(streams);
|
|
||||||
|
|
||||||
final List<Long> streamIds = new ArrayList<>(streams.size());
|
|
||||||
for (StreamEntity stream : streams) {
|
|
||||||
final Long streamId = getStreamIdInternal(stream.getServiceId(), stream.getUrl());
|
|
||||||
if (streamId == null) {
|
|
||||||
throw new IllegalStateException("StreamID cannot be null just after insertion.");
|
|
||||||
}
|
|
||||||
|
|
||||||
streamIds.add(streamId);
|
|
||||||
stream.setUid(streamId);
|
|
||||||
}
|
|
||||||
|
|
||||||
update(streams);
|
|
||||||
return streamIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Query("DELETE FROM " + STREAM_TABLE + " WHERE " + STREAM_ID +
|
|
||||||
" NOT IN " +
|
|
||||||
"(SELECT DISTINCT " + STREAM_ID + " FROM " + STREAM_TABLE +
|
|
||||||
|
|
||||||
" LEFT JOIN " + STREAM_HISTORY_TABLE +
|
|
||||||
" ON " + STREAM_ID + " = " +
|
|
||||||
StreamHistoryEntity.STREAM_HISTORY_TABLE + "." + StreamHistoryEntity.JOIN_STREAM_ID +
|
|
||||||
|
|
||||||
" LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE +
|
|
||||||
" ON " + STREAM_ID + " = " +
|
|
||||||
PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE + "." + PlaylistStreamEntity.JOIN_STREAM_ID +
|
|
||||||
")")
|
|
||||||
public abstract int deleteOrphans();
|
|
||||||
}
|
|
@ -0,0 +1,140 @@
|
|||||||
|
package org.schabi.newpipe.database.stream.dao
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import io.reactivex.Flowable
|
||||||
|
import java.util.Date
|
||||||
|
import org.schabi.newpipe.database.BasicDAO
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamType
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
abstract class StreamDAO : BasicDAO<StreamEntity> {
|
||||||
|
@Query("SELECT * FROM streams")
|
||||||
|
abstract override fun getAll(): Flowable<List<StreamEntity>>
|
||||||
|
|
||||||
|
@Query("DELETE FROM streams")
|
||||||
|
abstract override fun deleteAll(): Int
|
||||||
|
|
||||||
|
@Query("SELECT * FROM streams WHERE service_id = :serviceId")
|
||||||
|
abstract override fun listByService(serviceId: Int): Flowable<List<StreamEntity>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM streams WHERE url = :url AND service_id = :serviceId")
|
||||||
|
abstract fun getStream(serviceId: Long, url: String): Flowable<List<StreamEntity>>
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
internal abstract fun silentInsertInternal(stream: StreamEntity): Long
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
internal abstract fun silentInsertAllInternal(streams: List<StreamEntity>): List<Long>
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
SELECT uid, stream_type, textual_upload_date, upload_date, is_upload_date_approximation, duration
|
||||||
|
FROM streams WHERE url = :url AND service_id = :serviceId
|
||||||
|
""")
|
||||||
|
internal abstract fun getMinimalStreamForCompare(serviceId: Int, url: String): StreamCompareFeed?
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
open fun upsert(newerStream: StreamEntity): Long {
|
||||||
|
val uid = silentInsertInternal(newerStream)
|
||||||
|
|
||||||
|
if (uid != -1L) {
|
||||||
|
newerStream.uid = uid
|
||||||
|
return uid
|
||||||
|
}
|
||||||
|
|
||||||
|
compareAndUpdateStream(newerStream)
|
||||||
|
|
||||||
|
update(newerStream)
|
||||||
|
return newerStream.uid
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
open fun upsertAll(streams: List<StreamEntity>): List<Long> {
|
||||||
|
val insertUidList = silentInsertAllInternal(streams)
|
||||||
|
|
||||||
|
val streamIds = ArrayList<Long>(streams.size)
|
||||||
|
for ((index, uid) in insertUidList.withIndex()) {
|
||||||
|
val newerStream = streams[index]
|
||||||
|
if (uid != -1L) {
|
||||||
|
streamIds.add(uid)
|
||||||
|
newerStream.uid = uid
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
compareAndUpdateStream(newerStream)
|
||||||
|
streamIds.add(newerStream.uid)
|
||||||
|
}
|
||||||
|
|
||||||
|
update(streams)
|
||||||
|
return streamIds
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun compareAndUpdateStream(newerStream: StreamEntity) {
|
||||||
|
val existentMinimalStream = getMinimalStreamForCompare(newerStream.serviceId, newerStream.url)
|
||||||
|
?: throw IllegalStateException("Stream cannot be null just after insertion.")
|
||||||
|
newerStream.uid = existentMinimalStream.uid
|
||||||
|
|
||||||
|
val isNewerStreamLive = newerStream.streamType == AUDIO_LIVE_STREAM || newerStream.streamType == LIVE_STREAM
|
||||||
|
if (!isNewerStreamLive) {
|
||||||
|
|
||||||
|
// Use the existent upload date if the newer stream does not have a better precision
|
||||||
|
// (i.e. is an approximation). This is done to prevent unnecessary changes.
|
||||||
|
val hasBetterPrecision =
|
||||||
|
newerStream.uploadDate != null && newerStream.isUploadDateApproximation != true
|
||||||
|
if (existentMinimalStream.uploadDate != null && !hasBetterPrecision) {
|
||||||
|
newerStream.uploadDate = existentMinimalStream.uploadDate
|
||||||
|
newerStream.textualUploadDate = existentMinimalStream.textualUploadDate
|
||||||
|
newerStream.isUploadDateApproximation = existentMinimalStream.isUploadDateApproximation
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existentMinimalStream.duration > 0 && newerStream.duration < 0) {
|
||||||
|
newerStream.duration = existentMinimalStream.duration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
DELETE FROM streams WHERE
|
||||||
|
|
||||||
|
NOT EXISTS (SELECT 1 FROM stream_history sh
|
||||||
|
WHERE sh.stream_id = streams.uid)
|
||||||
|
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM playlist_stream_join ps
|
||||||
|
WHERE ps.stream_id = streams.uid)
|
||||||
|
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM feed f
|
||||||
|
WHERE f.stream_id = streams.uid)
|
||||||
|
""")
|
||||||
|
abstract fun deleteOrphans(): Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimal entry class used when comparing/updating an existent stream.
|
||||||
|
*/
|
||||||
|
internal data class StreamCompareFeed(
|
||||||
|
@ColumnInfo(name = STREAM_ID)
|
||||||
|
var uid: Long = 0,
|
||||||
|
|
||||||
|
@ColumnInfo(name = StreamEntity.STREAM_TYPE)
|
||||||
|
var streamType: StreamType,
|
||||||
|
|
||||||
|
@ColumnInfo(name = StreamEntity.STREAM_TEXTUAL_UPLOAD_DATE)
|
||||||
|
var textualUploadDate: String? = null,
|
||||||
|
|
||||||
|
@ColumnInfo(name = StreamEntity.STREAM_UPLOAD_DATE)
|
||||||
|
var uploadDate: Date? = null,
|
||||||
|
|
||||||
|
@ColumnInfo(name = StreamEntity.STREAM_IS_UPLOAD_DATE_APPROXIMATION)
|
||||||
|
var isUploadDateApproximation: Boolean? = null,
|
||||||
|
|
||||||
|
@ColumnInfo(name = StreamEntity.STREAM_DURATION)
|
||||||
|
var duration: Long
|
||||||
|
)
|
||||||
|
}
|
@ -27,21 +27,21 @@ public abstract class StreamStateDAO implements BasicDAO<StreamStateEntity> {
|
|||||||
public abstract int deleteAll();
|
public abstract int deleteAll();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Flowable<List<StreamStateEntity>> listByService(int serviceId) {
|
public Flowable<List<StreamStateEntity>> listByService(final int serviceId) {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Query("SELECT * FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
@Query("SELECT * FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
||||||
public abstract Flowable<List<StreamStateEntity>> getState(final long streamId);
|
public abstract Flowable<List<StreamStateEntity>> getState(long streamId);
|
||||||
|
|
||||||
@Query("DELETE FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
@Query("DELETE FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
||||||
public abstract int deleteState(final long streamId);
|
public abstract int deleteState(long streamId);
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
abstract void silentInsertInternal(final StreamStateEntity streamState);
|
abstract void silentInsertInternal(StreamStateEntity streamState);
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
public long upsert(StreamStateEntity stream) {
|
public long upsert(final StreamStateEntity stream) {
|
||||||
silentInsertInternal(stream);
|
silentInsertInternal(stream);
|
||||||
return update(stream);
|
return update(stream);
|
||||||
}
|
}
|
||||||
|
@ -1,153 +0,0 @@
|
|||||||
package org.schabi.newpipe.database.stream.model;
|
|
||||||
|
|
||||||
import androidx.room.ColumnInfo;
|
|
||||||
import androidx.room.Entity;
|
|
||||||
import androidx.room.Ignore;
|
|
||||||
import androidx.room.Index;
|
|
||||||
import androidx.room.PrimaryKey;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
|
||||||
import org.schabi.newpipe.util.Constants;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
|
|
||||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_SERVICE_ID;
|
|
||||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
|
|
||||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_URL;
|
|
||||||
|
|
||||||
@Entity(tableName = STREAM_TABLE,
|
|
||||||
indices = {@Index(value = {STREAM_SERVICE_ID, STREAM_URL}, unique = true)})
|
|
||||||
public class StreamEntity implements Serializable {
|
|
||||||
|
|
||||||
final public static String STREAM_TABLE = "streams";
|
|
||||||
final public static String STREAM_ID = "uid";
|
|
||||||
final public static String STREAM_SERVICE_ID = "service_id";
|
|
||||||
final public static String STREAM_URL = "url";
|
|
||||||
final public static String STREAM_TITLE = "title";
|
|
||||||
final public static String STREAM_TYPE = "stream_type";
|
|
||||||
final public static String STREAM_DURATION = "duration";
|
|
||||||
final public static String STREAM_UPLOADER = "uploader";
|
|
||||||
final public static String STREAM_THUMBNAIL_URL = "thumbnail_url";
|
|
||||||
|
|
||||||
@PrimaryKey(autoGenerate = true)
|
|
||||||
@ColumnInfo(name = STREAM_ID)
|
|
||||||
private long uid = 0;
|
|
||||||
|
|
||||||
@ColumnInfo(name = STREAM_SERVICE_ID)
|
|
||||||
private int serviceId = Constants.NO_SERVICE_ID;
|
|
||||||
|
|
||||||
@ColumnInfo(name = STREAM_URL)
|
|
||||||
private String url;
|
|
||||||
|
|
||||||
@ColumnInfo(name = STREAM_TITLE)
|
|
||||||
private String title;
|
|
||||||
|
|
||||||
@ColumnInfo(name = STREAM_TYPE)
|
|
||||||
private StreamType streamType;
|
|
||||||
|
|
||||||
@ColumnInfo(name = STREAM_DURATION)
|
|
||||||
private Long duration;
|
|
||||||
|
|
||||||
@ColumnInfo(name = STREAM_UPLOADER)
|
|
||||||
private String uploader;
|
|
||||||
|
|
||||||
@ColumnInfo(name = STREAM_THUMBNAIL_URL)
|
|
||||||
private String thumbnailUrl;
|
|
||||||
|
|
||||||
public StreamEntity(final int serviceId, final String title, final String url,
|
|
||||||
final StreamType streamType, final String thumbnailUrl, final String uploader,
|
|
||||||
final long duration) {
|
|
||||||
this.serviceId = serviceId;
|
|
||||||
this.title = title;
|
|
||||||
this.url = url;
|
|
||||||
this.streamType = streamType;
|
|
||||||
this.thumbnailUrl = thumbnailUrl;
|
|
||||||
this.uploader = uploader;
|
|
||||||
this.duration = duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Ignore
|
|
||||||
public StreamEntity(final StreamInfoItem item) {
|
|
||||||
this(item.getServiceId(), item.getName(), item.getUrl(), item.getStreamType(), item.getThumbnailUrl(),
|
|
||||||
item.getUploaderName(), item.getDuration());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Ignore
|
|
||||||
public StreamEntity(final StreamInfo info) {
|
|
||||||
this(info.getServiceId(), info.getName(), info.getUrl(), info.getStreamType(), info.getThumbnailUrl(),
|
|
||||||
info.getUploaderName(), info.getDuration());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Ignore
|
|
||||||
public StreamEntity(final PlayQueueItem item) {
|
|
||||||
this(item.getServiceId(), item.getTitle(), item.getUrl(), item.getStreamType(),
|
|
||||||
item.getThumbnailUrl(), item.getUploader(), item.getDuration());
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getUid() {
|
|
||||||
return uid;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setUid(long uid) {
|
|
||||||
this.uid = uid;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getServiceId() {
|
|
||||||
return serviceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setServiceId(int serviceId) {
|
|
||||||
this.serviceId = serviceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getUrl() {
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setUrl(String url) {
|
|
||||||
this.url = url;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getTitle() {
|
|
||||||
return title;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTitle(String title) {
|
|
||||||
this.title = title;
|
|
||||||
}
|
|
||||||
|
|
||||||
public StreamType getStreamType() {
|
|
||||||
return streamType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setStreamType(StreamType type) {
|
|
||||||
this.streamType = type;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long getDuration() {
|
|
||||||
return duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDuration(Long duration) {
|
|
||||||
this.duration = duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getUploader() {
|
|
||||||
return uploader;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setUploader(String uploader) {
|
|
||||||
this.uploader = uploader;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getThumbnailUrl() {
|
|
||||||
return thumbnailUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setThumbnailUrl(String thumbnailUrl) {
|
|
||||||
this.thumbnailUrl = thumbnailUrl;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,121 @@
|
|||||||
|
package org.schabi.newpipe.database.stream.model
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Ignore
|
||||||
|
import androidx.room.Index
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import java.io.Serializable
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Date
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_SERVICE_ID
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_TABLE
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_URL
|
||||||
|
import org.schabi.newpipe.extractor.localization.DateWrapper
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfo
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamType
|
||||||
|
import org.schabi.newpipe.player.playqueue.PlayQueueItem
|
||||||
|
|
||||||
|
@Entity(tableName = STREAM_TABLE,
|
||||||
|
indices = [
|
||||||
|
Index(value = [STREAM_SERVICE_ID, STREAM_URL], unique = true)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
data class StreamEntity(
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
@ColumnInfo(name = STREAM_ID)
|
||||||
|
var uid: Long = 0,
|
||||||
|
|
||||||
|
@ColumnInfo(name = STREAM_SERVICE_ID)
|
||||||
|
var serviceId: Int,
|
||||||
|
|
||||||
|
@ColumnInfo(name = STREAM_URL)
|
||||||
|
var url: String,
|
||||||
|
|
||||||
|
@ColumnInfo(name = STREAM_TITLE)
|
||||||
|
var title: String,
|
||||||
|
|
||||||
|
@ColumnInfo(name = STREAM_TYPE)
|
||||||
|
var streamType: StreamType,
|
||||||
|
|
||||||
|
@ColumnInfo(name = STREAM_DURATION)
|
||||||
|
var duration: Long,
|
||||||
|
|
||||||
|
@ColumnInfo(name = STREAM_UPLOADER)
|
||||||
|
var uploader: String,
|
||||||
|
|
||||||
|
@ColumnInfo(name = STREAM_THUMBNAIL_URL)
|
||||||
|
var thumbnailUrl: String? = null,
|
||||||
|
|
||||||
|
@ColumnInfo(name = STREAM_VIEWS)
|
||||||
|
var viewCount: Long? = null,
|
||||||
|
|
||||||
|
@ColumnInfo(name = STREAM_TEXTUAL_UPLOAD_DATE)
|
||||||
|
var textualUploadDate: String? = null,
|
||||||
|
|
||||||
|
@ColumnInfo(name = STREAM_UPLOAD_DATE)
|
||||||
|
var uploadDate: Date? = null,
|
||||||
|
|
||||||
|
@ColumnInfo(name = STREAM_IS_UPLOAD_DATE_APPROXIMATION)
|
||||||
|
var isUploadDateApproximation: Boolean? = null
|
||||||
|
) : Serializable {
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
constructor(item: StreamInfoItem) : this(
|
||||||
|
serviceId = item.serviceId, url = item.url, title = item.name,
|
||||||
|
streamType = item.streamType, duration = item.duration, uploader = item.uploaderName,
|
||||||
|
thumbnailUrl = item.thumbnailUrl, viewCount = item.viewCount,
|
||||||
|
textualUploadDate = item.textualUploadDate, uploadDate = item.uploadDate?.date()?.time,
|
||||||
|
isUploadDateApproximation = item.uploadDate?.isApproximation
|
||||||
|
)
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
constructor(info: StreamInfo) : this(
|
||||||
|
serviceId = info.serviceId, url = info.url, title = info.name,
|
||||||
|
streamType = info.streamType, duration = info.duration, uploader = info.uploaderName,
|
||||||
|
thumbnailUrl = info.thumbnailUrl, viewCount = info.viewCount,
|
||||||
|
textualUploadDate = info.textualUploadDate, uploadDate = info.uploadDate?.date()?.time,
|
||||||
|
isUploadDateApproximation = info.uploadDate?.isApproximation
|
||||||
|
)
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
constructor(item: PlayQueueItem) : this(
|
||||||
|
serviceId = item.serviceId, url = item.url, title = item.title,
|
||||||
|
streamType = item.streamType, duration = item.duration, uploader = item.uploader,
|
||||||
|
thumbnailUrl = item.thumbnailUrl
|
||||||
|
)
|
||||||
|
|
||||||
|
fun toStreamInfoItem(): StreamInfoItem {
|
||||||
|
val item = StreamInfoItem(serviceId, url, title, streamType)
|
||||||
|
item.duration = duration
|
||||||
|
item.uploaderName = uploader
|
||||||
|
item.thumbnailUrl = thumbnailUrl
|
||||||
|
|
||||||
|
if (viewCount != null) item.viewCount = viewCount as Long
|
||||||
|
item.textualUploadDate = textualUploadDate
|
||||||
|
item.uploadDate = uploadDate?.let {
|
||||||
|
DateWrapper(Calendar.getInstance().apply { time = it }, isUploadDateApproximation
|
||||||
|
?: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val STREAM_TABLE = "streams"
|
||||||
|
const val STREAM_ID = "uid"
|
||||||
|
const val STREAM_SERVICE_ID = "service_id"
|
||||||
|
const val STREAM_URL = "url"
|
||||||
|
const val STREAM_TITLE = "title"
|
||||||
|
const val STREAM_TYPE = "stream_type"
|
||||||
|
const val STREAM_DURATION = "duration"
|
||||||
|
const val STREAM_UPLOADER = "uploader"
|
||||||
|
const val STREAM_THUMBNAIL_URL = "thumbnail_url"
|
||||||
|
|
||||||
|
const val STREAM_VIEWS = "view_count"
|
||||||
|
const val STREAM_TEXTUAL_UPLOAD_DATE = "textual_upload_date"
|
||||||
|
const val STREAM_UPLOAD_DATE = "upload_date"
|
||||||
|
const val STREAM_IS_UPLOAD_DATE_APPROXIMATION = "is_upload_date_approximation"
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,9 @@
|
|||||||
package org.schabi.newpipe.database.stream.model;
|
package org.schabi.newpipe.database.stream.model;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.room.ColumnInfo;
|
import androidx.room.ColumnInfo;
|
||||||
import androidx.room.Entity;
|
import androidx.room.Entity;
|
||||||
import androidx.room.ForeignKey;
|
import androidx.room.ForeignKey;
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
@ -21,14 +20,17 @@ import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_
|
|||||||
onDelete = CASCADE, onUpdate = CASCADE)
|
onDelete = CASCADE, onUpdate = CASCADE)
|
||||||
})
|
})
|
||||||
public class StreamStateEntity {
|
public class StreamStateEntity {
|
||||||
final public static String STREAM_STATE_TABLE = "stream_state";
|
public static final String STREAM_STATE_TABLE = "stream_state";
|
||||||
final public static String JOIN_STREAM_ID = "stream_id";
|
public static final String JOIN_STREAM_ID = "stream_id";
|
||||||
final public static String STREAM_PROGRESS_TIME = "progress_time";
|
public static final String STREAM_PROGRESS_TIME = "progress_time";
|
||||||
|
|
||||||
|
/**
|
||||||
/** Playback state will not be saved, if playback time less than this threshold */
|
* Playback state will not be saved, if playback time is less than this threshold.
|
||||||
|
*/
|
||||||
private static final int PLAYBACK_SAVE_THRESHOLD_START_SECONDS = 5;
|
private static final int PLAYBACK_SAVE_THRESHOLD_START_SECONDS = 5;
|
||||||
/** Playback state will not be saved, if time left less than this threshold */
|
/**
|
||||||
|
* Playback state will not be saved, if time left is less than this threshold.
|
||||||
|
*/
|
||||||
private static final int PLAYBACK_SAVE_THRESHOLD_END_SECONDS = 10;
|
private static final int PLAYBACK_SAVE_THRESHOLD_END_SECONDS = 10;
|
||||||
|
|
||||||
@ColumnInfo(name = JOIN_STREAM_ID)
|
@ColumnInfo(name = JOIN_STREAM_ID)
|
||||||
@ -37,7 +39,7 @@ public class StreamStateEntity {
|
|||||||
@ColumnInfo(name = STREAM_PROGRESS_TIME)
|
@ColumnInfo(name = STREAM_PROGRESS_TIME)
|
||||||
private long progressTime;
|
private long progressTime;
|
||||||
|
|
||||||
public StreamStateEntity(long streamUid, long progressTime) {
|
public StreamStateEntity(final long streamUid, final long progressTime) {
|
||||||
this.streamUid = streamUid;
|
this.streamUid = streamUid;
|
||||||
this.progressTime = progressTime;
|
this.progressTime = progressTime;
|
||||||
}
|
}
|
||||||
@ -46,7 +48,7 @@ public class StreamStateEntity {
|
|||||||
return streamUid;
|
return streamUid;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setStreamUid(long streamUid) {
|
public void setStreamUid(final long streamUid) {
|
||||||
this.streamUid = streamUid;
|
this.streamUid = streamUid;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,21 +56,23 @@ public class StreamStateEntity {
|
|||||||
return progressTime;
|
return progressTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setProgressTime(long progressTime) {
|
public void setProgressTime(final long progressTime) {
|
||||||
this.progressTime = progressTime;
|
this.progressTime = progressTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isValid(int durationInSeconds) {
|
public boolean isValid(final int durationInSeconds) {
|
||||||
final int seconds = (int) TimeUnit.MILLISECONDS.toSeconds(progressTime);
|
final int seconds = (int) TimeUnit.MILLISECONDS.toSeconds(progressTime);
|
||||||
return seconds > PLAYBACK_SAVE_THRESHOLD_START_SECONDS
|
return seconds > PLAYBACK_SAVE_THRESHOLD_START_SECONDS
|
||||||
&& seconds < durationInSeconds - PLAYBACK_SAVE_THRESHOLD_END_SECONDS;
|
&& seconds < durationInSeconds - PLAYBACK_SAVE_THRESHOLD_END_SECONDS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(@Nullable Object obj) {
|
public boolean equals(@Nullable final Object obj) {
|
||||||
if (obj instanceof StreamStateEntity) {
|
if (obj instanceof StreamStateEntity) {
|
||||||
return ((StreamStateEntity) obj).streamUid == streamUid
|
return ((StreamStateEntity) obj).streamUid == streamUid
|
||||||
&& ((StreamStateEntity) obj).progressTime == progressTime;
|
&& ((StreamStateEntity) obj).progressTime == progressTime;
|
||||||
} else return false;
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,69 +0,0 @@
|
|||||||
package org.schabi.newpipe.database.subscription;
|
|
||||||
|
|
||||||
import androidx.room.Dao;
|
|
||||||
import androidx.room.Insert;
|
|
||||||
import androidx.room.OnConflictStrategy;
|
|
||||||
import androidx.room.Query;
|
|
||||||
import androidx.room.Transaction;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.database.BasicDAO;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import io.reactivex.Flowable;
|
|
||||||
|
|
||||||
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_SERVICE_ID;
|
|
||||||
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_TABLE;
|
|
||||||
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_UID;
|
|
||||||
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_URL;
|
|
||||||
|
|
||||||
@Dao
|
|
||||||
public abstract class SubscriptionDAO implements BasicDAO<SubscriptionEntity> {
|
|
||||||
@Override
|
|
||||||
@Query("SELECT * FROM " + SUBSCRIPTION_TABLE)
|
|
||||||
public abstract Flowable<List<SubscriptionEntity>> getAll();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Query("DELETE FROM " + SUBSCRIPTION_TABLE)
|
|
||||||
public abstract int deleteAll();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Query("SELECT * FROM " + SUBSCRIPTION_TABLE + " WHERE " + SUBSCRIPTION_SERVICE_ID + " = :serviceId")
|
|
||||||
public abstract Flowable<List<SubscriptionEntity>> listByService(int serviceId);
|
|
||||||
|
|
||||||
@Query("SELECT * FROM " + SUBSCRIPTION_TABLE + " WHERE " +
|
|
||||||
SUBSCRIPTION_URL + " LIKE :url AND " +
|
|
||||||
SUBSCRIPTION_SERVICE_ID + " = :serviceId")
|
|
||||||
public abstract Flowable<List<SubscriptionEntity>> getSubscription(int serviceId, String url);
|
|
||||||
|
|
||||||
@Query("SELECT " + SUBSCRIPTION_UID + " FROM " + SUBSCRIPTION_TABLE + " WHERE " +
|
|
||||||
SUBSCRIPTION_URL + " LIKE :url AND " +
|
|
||||||
SUBSCRIPTION_SERVICE_ID + " = :serviceId")
|
|
||||||
abstract Long getSubscriptionIdInternal(int serviceId, String url);
|
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
|
||||||
abstract Long insertInternal(final SubscriptionEntity entities);
|
|
||||||
|
|
||||||
@Transaction
|
|
||||||
public List<SubscriptionEntity> upsertAll(List<SubscriptionEntity> entities) {
|
|
||||||
for (SubscriptionEntity entity : entities) {
|
|
||||||
Long uid = insertInternal(entity);
|
|
||||||
|
|
||||||
if (uid != -1) {
|
|
||||||
entity.setUid(uid);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
uid = getSubscriptionIdInternal(entity.getServiceId(), entity.getUrl());
|
|
||||||
entity.setUid(uid);
|
|
||||||
|
|
||||||
if (uid == -1) {
|
|
||||||
throw new IllegalStateException("Invalid subscription id (-1)");
|
|
||||||
}
|
|
||||||
|
|
||||||
update(entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
return entities;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,103 @@
|
|||||||
|
package org.schabi.newpipe.database.subscription
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import io.reactivex.Flowable
|
||||||
|
import io.reactivex.Maybe
|
||||||
|
import org.schabi.newpipe.database.BasicDAO
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
abstract class SubscriptionDAO : BasicDAO<SubscriptionEntity> {
|
||||||
|
@Query("SELECT COUNT(*) FROM subscriptions")
|
||||||
|
abstract fun rowCount(): Flowable<Long>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM subscriptions WHERE service_id = :serviceId")
|
||||||
|
abstract override fun listByService(serviceId: Int): Flowable<List<SubscriptionEntity>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM subscriptions ORDER BY name COLLATE NOCASE ASC")
|
||||||
|
abstract override fun getAll(): Flowable<List<SubscriptionEntity>>
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
SELECT * FROM subscriptions
|
||||||
|
|
||||||
|
WHERE name LIKE '%' || :filter || '%'
|
||||||
|
|
||||||
|
ORDER BY name COLLATE NOCASE ASC
|
||||||
|
""")
|
||||||
|
abstract fun getSubscriptionsFiltered(filter: String): Flowable<List<SubscriptionEntity>>
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
SELECT * FROM subscriptions s
|
||||||
|
|
||||||
|
LEFT JOIN feed_group_subscription_join fgs
|
||||||
|
ON s.uid = fgs.subscription_id
|
||||||
|
|
||||||
|
WHERE (fgs.subscription_id IS NULL OR fgs.group_id = :currentGroupId)
|
||||||
|
|
||||||
|
ORDER BY name COLLATE NOCASE ASC
|
||||||
|
""")
|
||||||
|
abstract fun getSubscriptionsOnlyUngrouped(
|
||||||
|
currentGroupId: Long
|
||||||
|
): Flowable<List<SubscriptionEntity>>
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
SELECT * FROM subscriptions s
|
||||||
|
|
||||||
|
LEFT JOIN feed_group_subscription_join fgs
|
||||||
|
ON s.uid = fgs.subscription_id
|
||||||
|
|
||||||
|
WHERE (fgs.subscription_id IS NULL OR fgs.group_id = :currentGroupId)
|
||||||
|
AND s.name LIKE '%' || :filter || '%'
|
||||||
|
|
||||||
|
ORDER BY name COLLATE NOCASE ASC
|
||||||
|
""")
|
||||||
|
abstract fun getSubscriptionsOnlyUngroupedFiltered(
|
||||||
|
currentGroupId: Long,
|
||||||
|
filter: String
|
||||||
|
): Flowable<List<SubscriptionEntity>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM subscriptions WHERE url LIKE :url AND service_id = :serviceId")
|
||||||
|
abstract fun getSubscriptionFlowable(serviceId: Int, url: String): Flowable<List<SubscriptionEntity>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM subscriptions WHERE url LIKE :url AND service_id = :serviceId")
|
||||||
|
abstract fun getSubscription(serviceId: Int, url: String): Maybe<SubscriptionEntity>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM subscriptions WHERE uid = :subscriptionId")
|
||||||
|
abstract fun getSubscription(subscriptionId: Long): SubscriptionEntity
|
||||||
|
|
||||||
|
@Query("DELETE FROM subscriptions")
|
||||||
|
abstract override fun deleteAll(): Int
|
||||||
|
|
||||||
|
@Query("DELETE FROM subscriptions WHERE url LIKE :url AND service_id = :serviceId")
|
||||||
|
abstract fun deleteSubscription(serviceId: Int, url: String): Int
|
||||||
|
|
||||||
|
@Query("SELECT uid FROM subscriptions WHERE url LIKE :url AND service_id = :serviceId")
|
||||||
|
internal abstract fun getSubscriptionIdInternal(serviceId: Int, url: String): Long?
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
internal abstract fun silentInsertAllInternal(entities: List<SubscriptionEntity>): List<Long>
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
open fun upsertAll(entities: List<SubscriptionEntity>): List<SubscriptionEntity> {
|
||||||
|
val insertUidList = silentInsertAllInternal(entities)
|
||||||
|
|
||||||
|
insertUidList.forEachIndexed { index: Int, uidFromInsert: Long ->
|
||||||
|
val entity = entities[index]
|
||||||
|
|
||||||
|
if (uidFromInsert != -1L) {
|
||||||
|
entity.uid = uidFromInsert
|
||||||
|
} else {
|
||||||
|
val subscriptionIdFromDb = getSubscriptionIdInternal(entity.serviceId, entity.url)
|
||||||
|
?: throw IllegalStateException("Subscription cannot be null just after insertion.")
|
||||||
|
entity.uid = subscriptionIdFromDb
|
||||||
|
|
||||||
|
update(entity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entities
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,11 @@
|
|||||||
package org.schabi.newpipe.database.subscription;
|
package org.schabi.newpipe.database.subscription;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.room.ColumnInfo;
|
import androidx.room.ColumnInfo;
|
||||||
import androidx.room.Entity;
|
import androidx.room.Entity;
|
||||||
import androidx.room.Ignore;
|
import androidx.room.Ignore;
|
||||||
import androidx.room.Index;
|
import androidx.room.Index;
|
||||||
import androidx.room.PrimaryKey;
|
import androidx.room.PrimaryKey;
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||||
@ -18,15 +18,14 @@ import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCR
|
|||||||
@Entity(tableName = SUBSCRIPTION_TABLE,
|
@Entity(tableName = SUBSCRIPTION_TABLE,
|
||||||
indices = {@Index(value = {SUBSCRIPTION_SERVICE_ID, SUBSCRIPTION_URL}, unique = true)})
|
indices = {@Index(value = {SUBSCRIPTION_SERVICE_ID, SUBSCRIPTION_URL}, unique = true)})
|
||||||
public class SubscriptionEntity {
|
public class SubscriptionEntity {
|
||||||
|
public static final String SUBSCRIPTION_UID = "uid";
|
||||||
final static String SUBSCRIPTION_UID = "uid";
|
public static final String SUBSCRIPTION_TABLE = "subscriptions";
|
||||||
final static String SUBSCRIPTION_TABLE = "subscriptions";
|
public static final String SUBSCRIPTION_SERVICE_ID = "service_id";
|
||||||
final static String SUBSCRIPTION_SERVICE_ID = "service_id";
|
public static final String SUBSCRIPTION_URL = "url";
|
||||||
final static String SUBSCRIPTION_URL = "url";
|
public static final String SUBSCRIPTION_NAME = "name";
|
||||||
final static String SUBSCRIPTION_NAME = "name";
|
public static final String SUBSCRIPTION_AVATAR_URL = "avatar_url";
|
||||||
final static String SUBSCRIPTION_AVATAR_URL = "avatar_url";
|
public static final String SUBSCRIPTION_SUBSCRIBER_COUNT = "subscriber_count";
|
||||||
final static String SUBSCRIPTION_SUBSCRIBER_COUNT = "subscriber_count";
|
public static final String SUBSCRIPTION_DESCRIPTION = "description";
|
||||||
final static String SUBSCRIPTION_DESCRIPTION = "description";
|
|
||||||
|
|
||||||
@PrimaryKey(autoGenerate = true)
|
@PrimaryKey(autoGenerate = true)
|
||||||
private long uid = 0;
|
private long uid = 0;
|
||||||
@ -49,11 +48,21 @@ public class SubscriptionEntity {
|
|||||||
@ColumnInfo(name = SUBSCRIPTION_DESCRIPTION)
|
@ColumnInfo(name = SUBSCRIPTION_DESCRIPTION)
|
||||||
private String description;
|
private String description;
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
public static SubscriptionEntity from(@NonNull final ChannelInfo info) {
|
||||||
|
SubscriptionEntity result = new SubscriptionEntity();
|
||||||
|
result.setServiceId(info.getServiceId());
|
||||||
|
result.setUrl(info.getUrl());
|
||||||
|
result.setData(info.getName(), info.getAvatarUrl(), info.getDescription(),
|
||||||
|
info.getSubscriberCount());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
public long getUid() {
|
public long getUid() {
|
||||||
return uid;
|
return uid;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setUid(long uid) {
|
public void setUid(final long uid) {
|
||||||
this.uid = uid;
|
this.uid = uid;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,7 +70,7 @@ public class SubscriptionEntity {
|
|||||||
return serviceId;
|
return serviceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setServiceId(int serviceId) {
|
public void setServiceId(final int serviceId) {
|
||||||
this.serviceId = serviceId;
|
this.serviceId = serviceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,7 +78,7 @@ public class SubscriptionEntity {
|
|||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setUrl(String url) {
|
public void setUrl(final String url) {
|
||||||
this.url = url;
|
this.url = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,7 +86,7 @@ public class SubscriptionEntity {
|
|||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setName(String name) {
|
public void setName(final String name) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,7 +94,7 @@ public class SubscriptionEntity {
|
|||||||
return avatarUrl;
|
return avatarUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAvatarUrl(String avatarUrl) {
|
public void setAvatarUrl(final String avatarUrl) {
|
||||||
this.avatarUrl = avatarUrl;
|
this.avatarUrl = avatarUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,7 +102,7 @@ public class SubscriptionEntity {
|
|||||||
return subscriberCount;
|
return subscriberCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSubscriberCount(Long subscriberCount) {
|
public void setSubscriberCount(final Long subscriberCount) {
|
||||||
this.subscriberCount = subscriberCount;
|
this.subscriberCount = subscriberCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,19 +110,16 @@ public class SubscriptionEntity {
|
|||||||
return description;
|
return description;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDescription(String description) {
|
public void setDescription(final String description) {
|
||||||
this.description = description;
|
this.description = description;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Ignore
|
@Ignore
|
||||||
public void setData(final String name,
|
public void setData(final String n, final String au, final String d, final Long sc) {
|
||||||
final String avatarUrl,
|
this.setName(n);
|
||||||
final String description,
|
this.setAvatarUrl(au);
|
||||||
final Long subscriberCount) {
|
this.setDescription(d);
|
||||||
this.setName(name);
|
this.setSubscriberCount(sc);
|
||||||
this.setAvatarUrl(avatarUrl);
|
|
||||||
this.setDescription(description);
|
|
||||||
this.setSubscriberCount(subscriberCount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Ignore
|
@Ignore
|
||||||
@ -125,12 +131,54 @@ public class SubscriptionEntity {
|
|||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Ignore
|
|
||||||
public static SubscriptionEntity from(@NonNull ChannelInfo info) {
|
// TODO: Remove these generated methods by migrating this class to a data class from Kotlin.
|
||||||
SubscriptionEntity result = new SubscriptionEntity();
|
@Override
|
||||||
result.setServiceId(info.getServiceId());
|
@SuppressWarnings("EqualsReplaceableByObjectsCall")
|
||||||
result.setUrl(info.getUrl());
|
public boolean equals(final Object o) {
|
||||||
result.setData(info.getName(), info.getAvatarUrl(), info.getDescription(), info.getSubscriberCount());
|
if (this == o) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (o == null || getClass() != o.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final SubscriptionEntity that = (SubscriptionEntity) o;
|
||||||
|
|
||||||
|
if (uid != that.uid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (serviceId != that.serviceId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!url.equals(that.url)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (name != null ? !name.equals(that.name) : that.name != null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (avatarUrl != null ? !avatarUrl.equals(that.avatarUrl) : that.avatarUrl != null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (subscriberCount != null
|
||||||
|
? !subscriberCount.equals(that.subscriberCount)
|
||||||
|
: that.subscriberCount != null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return description != null
|
||||||
|
? description.equals(that.description)
|
||||||
|
: that.description == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int result = (int) (uid ^ (uid >>> 32));
|
||||||
|
result = 31 * result + serviceId;
|
||||||
|
result = 31 * result + url.hashCode();
|
||||||
|
result = 31 * result + (name != null ? name.hashCode() : 0);
|
||||||
|
result = 31 * result + (avatarUrl != null ? avatarUrl.hashCode() : 0);
|
||||||
|
result = 31 * result + (subscriberCount != null ? subscriberCount.hashCode() : 0);
|
||||||
|
result = 31 * result + (description != null ? description.hashCode() : 0);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,32 +3,37 @@ package org.schabi.newpipe.download;
|
|||||||
import android.app.FragmentTransaction;
|
import android.app.FragmentTransaction;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import androidx.appcompat.app.ActionBar;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import androidx.appcompat.widget.Toolbar;
|
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.ViewTreeObserver;
|
import android.view.ViewTreeObserver;
|
||||||
|
|
||||||
|
import androidx.appcompat.app.ActionBar;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.settings.SettingsActivity;
|
import org.schabi.newpipe.util.AndroidTvUtils;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
import org.schabi.newpipe.views.FocusOverlayView;
|
||||||
|
|
||||||
import us.shandian.giga.service.DownloadManagerService;
|
import us.shandian.giga.service.DownloadManagerService;
|
||||||
import us.shandian.giga.ui.fragment.MissionsFragment;
|
import us.shandian.giga.ui.fragment.MissionsFragment;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||||
|
|
||||||
public class DownloadActivity extends AppCompatActivity {
|
public class DownloadActivity extends AppCompatActivity {
|
||||||
|
|
||||||
private static final String MISSIONS_FRAGMENT_TAG = "fragment_tag";
|
private static final String MISSIONS_FRAGMENT_TAG = "fragment_tag";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(final Bundle savedInstanceState) {
|
||||||
// Service
|
// Service
|
||||||
Intent i = new Intent();
|
Intent i = new Intent();
|
||||||
i.setClass(this, DownloadManagerService.class);
|
i.setClass(this, DownloadManagerService.class);
|
||||||
startService(i);
|
startService(i);
|
||||||
|
|
||||||
|
assureCorrectAppLanguage(this);
|
||||||
ThemeHelper.setTheme(this);
|
ThemeHelper.setTheme(this);
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_downloader);
|
setContentView(R.layout.activity_downloader);
|
||||||
@ -43,13 +48,18 @@ public class DownloadActivity extends AppCompatActivity {
|
|||||||
actionBar.setDisplayShowTitleEnabled(true);
|
actionBar.setDisplayShowTitleEnabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
|
getWindow().getDecorView().getViewTreeObserver()
|
||||||
|
.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onGlobalLayout() {
|
public void onGlobalLayout() {
|
||||||
updateFragments();
|
updateFragments();
|
||||||
getWindow().getDecorView().getViewTreeObserver().removeOnGlobalLayoutListener(this);
|
getWindow().getDecorView().getViewTreeObserver().removeOnGlobalLayoutListener(this);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (AndroidTvUtils.isTv(this)) {
|
||||||
|
FocusOverlayView.setupFocusObserver(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateFragments() {
|
private void updateFragments() {
|
||||||
@ -62,7 +72,7 @@ public class DownloadActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onCreateOptionsMenu(Menu menu) {
|
public boolean onCreateOptionsMenu(final Menu menu) {
|
||||||
super.onCreateOptionsMenu(menu);
|
super.onCreateOptionsMenu(menu);
|
||||||
MenuInflater inflater = getMenuInflater();
|
MenuInflater inflater = getMenuInflater();
|
||||||
|
|
||||||
@ -72,17 +82,11 @@ public class DownloadActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
case android.R.id.home: {
|
case android.R.id.home:
|
||||||
onBackPressed();
|
onBackPressed();
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
case R.id.action_settings: {
|
|
||||||
Intent intent = new Intent(this, SettingsActivity.class);
|
|
||||||
startActivity(intent);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
|
@ -11,15 +11,6 @@ import android.os.Bundle;
|
|||||||
import android.os.Environment;
|
import android.os.Environment;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import androidx.annotation.IdRes;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.StringRes;
|
|
||||||
import androidx.fragment.app.DialogFragment;
|
|
||||||
import androidx.documentfile.provider.DocumentFile;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
import androidx.appcompat.view.menu.ActionMenuItemView;
|
|
||||||
import androidx.appcompat.widget.Toolbar;
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.SparseArray;
|
import android.util.SparseArray;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
@ -34,10 +25,21 @@ import android.widget.Spinner;
|
|||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.IdRes;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.appcompat.view.menu.ActionMenuItemView;
|
||||||
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
|
import androidx.fragment.app.DialogFragment;
|
||||||
|
|
||||||
import com.nononsenseapps.filepicker.Utils;
|
import com.nononsenseapps.filepicker.Utils;
|
||||||
|
|
||||||
import org.schabi.newpipe.MainActivity;
|
import org.schabi.newpipe.MainActivity;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.RouterActivity;
|
||||||
import org.schabi.newpipe.extractor.MediaFormat;
|
import org.schabi.newpipe.extractor.MediaFormat;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.localization.Localization;
|
import org.schabi.newpipe.extractor.localization.Localization;
|
||||||
@ -77,25 +79,35 @@ import us.shandian.giga.service.DownloadManagerService;
|
|||||||
import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder;
|
import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder;
|
||||||
import us.shandian.giga.service.MissionState;
|
import us.shandian.giga.service.MissionState;
|
||||||
|
|
||||||
public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener {
|
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||||
|
|
||||||
|
public class DownloadDialog extends DialogFragment
|
||||||
|
implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener {
|
||||||
private static final String TAG = "DialogFragment";
|
private static final String TAG = "DialogFragment";
|
||||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||||
private static final int REQUEST_DOWNLOAD_SAVE_AS = 0x1230;
|
private static final int REQUEST_DOWNLOAD_SAVE_AS = 0x1230;
|
||||||
|
|
||||||
@State
|
@State
|
||||||
protected StreamInfo currentInfo;
|
StreamInfo currentInfo;
|
||||||
@State
|
@State
|
||||||
protected StreamSizeWrapper<AudioStream> wrappedAudioStreams = StreamSizeWrapper.empty();
|
StreamSizeWrapper<AudioStream> wrappedAudioStreams = StreamSizeWrapper.empty();
|
||||||
@State
|
@State
|
||||||
protected StreamSizeWrapper<VideoStream> wrappedVideoStreams = StreamSizeWrapper.empty();
|
StreamSizeWrapper<VideoStream> wrappedVideoStreams = StreamSizeWrapper.empty();
|
||||||
@State
|
@State
|
||||||
protected StreamSizeWrapper<SubtitlesStream> wrappedSubtitleStreams = StreamSizeWrapper.empty();
|
StreamSizeWrapper<SubtitlesStream> wrappedSubtitleStreams = StreamSizeWrapper.empty();
|
||||||
@State
|
@State
|
||||||
protected int selectedVideoIndex = 0;
|
int selectedVideoIndex = 0;
|
||||||
@State
|
@State
|
||||||
protected int selectedAudioIndex = 0;
|
int selectedAudioIndex = 0;
|
||||||
@State
|
@State
|
||||||
protected int selectedSubtitleIndex = 0;
|
int selectedSubtitleIndex = 0;
|
||||||
|
|
||||||
|
private StoredDirectoryHelper mainStorageAudio = null;
|
||||||
|
private StoredDirectoryHelper mainStorageVideo = null;
|
||||||
|
private DownloadManager downloadManager = null;
|
||||||
|
private ActionMenuItemView okButton = null;
|
||||||
|
private Context context;
|
||||||
|
private boolean askForSavePath;
|
||||||
|
|
||||||
private StreamItemAdapter<AudioStream, Stream> audioStreamsAdapter;
|
private StreamItemAdapter<AudioStream, Stream> audioStreamsAdapter;
|
||||||
private StreamItemAdapter<VideoStream, AudioStream> videoStreamsAdapter;
|
private StreamItemAdapter<VideoStream, AudioStream> videoStreamsAdapter;
|
||||||
@ -111,15 +123,16 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||||||
|
|
||||||
private SharedPreferences prefs;
|
private SharedPreferences prefs;
|
||||||
|
|
||||||
public static DownloadDialog newInstance(StreamInfo info) {
|
public static DownloadDialog newInstance(final StreamInfo info) {
|
||||||
DownloadDialog dialog = new DownloadDialog();
|
DownloadDialog dialog = new DownloadDialog();
|
||||||
dialog.setInfo(info);
|
dialog.setInfo(info);
|
||||||
return dialog;
|
return dialog;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static DownloadDialog newInstance(Context context, StreamInfo info) {
|
public static DownloadDialog newInstance(final Context context, final StreamInfo info) {
|
||||||
final ArrayList<VideoStream> streamsList = new ArrayList<>(ListHelper.getSortedStreamVideosList(context,
|
final ArrayList<VideoStream> streamsList = new ArrayList<>(ListHelper
|
||||||
info.getVideoStreams(), info.getVideoOnlyStreams(), false));
|
.getSortedStreamVideosList(context, info.getVideoStreams(),
|
||||||
|
info.getVideoOnlyStreams(), false));
|
||||||
final int selectedStreamIndex = ListHelper.getDefaultResolutionIndex(context, streamsList);
|
final int selectedStreamIndex = ListHelper.getDefaultResolutionIndex(context, streamsList);
|
||||||
|
|
||||||
final DownloadDialog instance = newInstance(info);
|
final DownloadDialog instance = newInstance(info);
|
||||||
@ -131,57 +144,61 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setInfo(StreamInfo info) {
|
private void setInfo(final StreamInfo info) {
|
||||||
this.currentInfo = info;
|
this.currentInfo = info;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAudioStreams(List<AudioStream> audioStreams) {
|
public void setAudioStreams(final List<AudioStream> audioStreams) {
|
||||||
setAudioStreams(new StreamSizeWrapper<>(audioStreams, getContext()));
|
setAudioStreams(new StreamSizeWrapper<>(audioStreams, getContext()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAudioStreams(StreamSizeWrapper<AudioStream> wrappedAudioStreams) {
|
public void setAudioStreams(final StreamSizeWrapper<AudioStream> was) {
|
||||||
this.wrappedAudioStreams = wrappedAudioStreams;
|
this.wrappedAudioStreams = was;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setVideoStreams(List<VideoStream> videoStreams) {
|
public void setVideoStreams(final List<VideoStream> videoStreams) {
|
||||||
setVideoStreams(new StreamSizeWrapper<>(videoStreams, getContext()));
|
setVideoStreams(new StreamSizeWrapper<>(videoStreams, getContext()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setVideoStreams(StreamSizeWrapper<VideoStream> wrappedVideoStreams) {
|
|
||||||
this.wrappedVideoStreams = wrappedVideoStreams;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSubtitleStreams(List<SubtitlesStream> subtitleStreams) {
|
|
||||||
setSubtitleStreams(new StreamSizeWrapper<>(subtitleStreams, getContext()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSubtitleStreams(StreamSizeWrapper<SubtitlesStream> wrappedSubtitleStreams) {
|
|
||||||
this.wrappedSubtitleStreams = wrappedSubtitleStreams;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSelectedVideoStream(int selectedVideoIndex) {
|
|
||||||
this.selectedVideoIndex = selectedVideoIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSelectedAudioStream(int selectedAudioIndex) {
|
|
||||||
this.selectedAudioIndex = selectedAudioIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSelectedSubtitleStream(int selectedSubtitleIndex) {
|
|
||||||
this.selectedSubtitleIndex = selectedSubtitleIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// LifeCycle
|
// LifeCycle
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
public void setVideoStreams(final StreamSizeWrapper<VideoStream> wvs) {
|
||||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
this.wrappedVideoStreams = wvs;
|
||||||
super.onCreate(savedInstanceState);
|
}
|
||||||
if (DEBUG)
|
|
||||||
Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
|
|
||||||
|
|
||||||
if (!PermissionHelper.checkStoragePermissions(getActivity(), PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
|
public void setSubtitleStreams(final List<SubtitlesStream> subtitleStreams) {
|
||||||
|
setSubtitleStreams(new StreamSizeWrapper<>(subtitleStreams, getContext()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSubtitleStreams(
|
||||||
|
final StreamSizeWrapper<SubtitlesStream> wss) {
|
||||||
|
this.wrappedSubtitleStreams = wss;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSelectedVideoStream(final int svi) {
|
||||||
|
this.selectedVideoIndex = svi;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSelectedAudioStream(final int sai) {
|
||||||
|
this.selectedAudioIndex = sai;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSelectedSubtitleStream(final int ssi) {
|
||||||
|
this.selectedSubtitleIndex = ssi;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onCreate() called with: "
|
||||||
|
+ "savedInstanceState = [" + savedInstanceState + "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!PermissionHelper.checkStoragePermissions(getActivity(),
|
||||||
|
PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
|
||||||
getDialog().dismiss();
|
getDialog().dismiss();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -195,17 +212,23 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||||||
List<VideoStream> videoStreams = wrappedVideoStreams.getStreamsList();
|
List<VideoStream> videoStreams = wrappedVideoStreams.getStreamsList();
|
||||||
|
|
||||||
for (int i = 0; i < videoStreams.size(); i++) {
|
for (int i = 0; i < videoStreams.size(); i++) {
|
||||||
if (!videoStreams.get(i).isVideoOnly()) continue;
|
if (!videoStreams.get(i).isVideoOnly()) {
|
||||||
AudioStream audioStream = SecondaryStreamHelper.getAudioStreamFor(wrappedAudioStreams.getStreamsList(), videoStreams.get(i));
|
continue;
|
||||||
|
}
|
||||||
|
AudioStream audioStream = SecondaryStreamHelper
|
||||||
|
.getAudioStreamFor(wrappedAudioStreams.getStreamsList(), videoStreams.get(i));
|
||||||
|
|
||||||
if (audioStream != null) {
|
if (audioStream != null) {
|
||||||
secondaryStreams.append(i, new SecondaryStreamHelper<>(wrappedAudioStreams, audioStream));
|
secondaryStreams
|
||||||
|
.append(i, new SecondaryStreamHelper<>(wrappedAudioStreams, audioStream));
|
||||||
} else if (DEBUG) {
|
} else if (DEBUG) {
|
||||||
Log.w(TAG, "No audio stream candidates for video format " + videoStreams.get(i).getFormat().name());
|
Log.w(TAG, "No audio stream candidates for video format "
|
||||||
|
+ videoStreams.get(i).getFormat().name());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.videoStreamsAdapter = new StreamItemAdapter<>(context, wrappedVideoStreams, secondaryStreams);
|
this.videoStreamsAdapter = new StreamItemAdapter<>(context, wrappedVideoStreams,
|
||||||
|
secondaryStreams);
|
||||||
this.audioStreamsAdapter = new StreamItemAdapter<>(context, wrappedAudioStreams);
|
this.audioStreamsAdapter = new StreamItemAdapter<>(context, wrappedAudioStreams);
|
||||||
this.subtitleStreamsAdapter = new StreamItemAdapter<>(context, wrappedSubtitleStreams);
|
this.subtitleStreamsAdapter = new StreamItemAdapter<>(context, wrappedSubtitleStreams);
|
||||||
|
|
||||||
@ -214,7 +237,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||||||
|
|
||||||
context.bindService(intent, new ServiceConnection() {
|
context.bindService(intent, new ServiceConnection() {
|
||||||
@Override
|
@Override
|
||||||
public void onServiceConnected(ComponentName cname, IBinder service) {
|
public void onServiceConnected(final ComponentName cname, final IBinder service) {
|
||||||
DownloadManagerBinder mgr = (DownloadManagerBinder) service;
|
DownloadManagerBinder mgr = (DownloadManagerBinder) service;
|
||||||
|
|
||||||
mainStorageAudio = mgr.getMainStorageAudio();
|
mainStorageAudio = mgr.getMainStorageAudio();
|
||||||
@ -228,25 +251,34 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onServiceDisconnected(ComponentName name) {
|
public void onServiceDisconnected(final ComponentName name) {
|
||||||
// nothing to do
|
// nothing to do
|
||||||
}
|
}
|
||||||
}, Context.BIND_AUTO_CREATE);
|
}, Context.BIND_AUTO_CREATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Inits
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container,
|
||||||
if (DEBUG)
|
final Bundle savedInstanceState) {
|
||||||
Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]");
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onCreateView() called with: "
|
||||||
|
+ "inflater = [" + inflater + "], container = [" + container + "], "
|
||||||
|
+ "savedInstanceState = [" + savedInstanceState + "]");
|
||||||
|
}
|
||||||
return inflater.inflate(R.layout.download_dialog, container);
|
return inflater.inflate(R.layout.download_dialog, container);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
|
||||||
super.onViewCreated(view, savedInstanceState);
|
super.onViewCreated(view, savedInstanceState);
|
||||||
nameEditText = view.findViewById(R.id.file_name);
|
nameEditText = view.findViewById(R.id.file_name);
|
||||||
nameEditText.setText(FilenameUtils.createFilename(getContext(), currentInfo.getName()));
|
nameEditText.setText(FilenameUtils.createFilename(getContext(), currentInfo.getName()));
|
||||||
selectedAudioIndex = ListHelper.getDefaultAudioFormat(getContext(), currentInfo.getAudioStreams());
|
selectedAudioIndex = ListHelper
|
||||||
|
.getDefaultAudioFormat(getContext(), currentInfo.getAudioStreams());
|
||||||
|
|
||||||
selectedSubtitleIndex = getSubtitleIndexBy(subtitleStreamsAdapter.getAll());
|
selectedSubtitleIndex = getSubtitleIndexBy(subtitleStreamsAdapter.getAll());
|
||||||
|
|
||||||
@ -268,21 +300,20 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||||||
threadsCountTextView.setText(String.valueOf(threads));
|
threadsCountTextView.setText(String.valueOf(threads));
|
||||||
threadsSeekBar.setProgress(threads - 1);
|
threadsSeekBar.setProgress(threads - 1);
|
||||||
threadsSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
threadsSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onProgressChanged(SeekBar seekbar, int progress, boolean fromUser) {
|
public void onProgressChanged(final SeekBar seekbar, final int progress,
|
||||||
progress++;
|
final boolean fromUser) {
|
||||||
prefs.edit().putInt(getString(R.string.default_download_threads), progress).apply();
|
final int newProgress = progress + 1;
|
||||||
threadsCountTextView.setText(String.valueOf(progress));
|
prefs.edit().putInt(getString(R.string.default_download_threads), newProgress)
|
||||||
|
.apply();
|
||||||
|
threadsCountTextView.setText(String.valueOf(newProgress));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStartTrackingTouch(SeekBar p1) {
|
public void onStartTrackingTouch(final SeekBar p1) { }
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStopTrackingTouch(SeekBar p1) {
|
public void onStopTrackingTouch(final SeekBar p1) { }
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
fetchStreamsSize();
|
fetchStreamsSize();
|
||||||
@ -291,17 +322,20 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||||||
private void fetchStreamsSize() {
|
private void fetchStreamsSize() {
|
||||||
disposables.clear();
|
disposables.clear();
|
||||||
|
|
||||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedVideoStreams).subscribe(result -> {
|
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedVideoStreams)
|
||||||
|
.subscribe(result -> {
|
||||||
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.video_button) {
|
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.video_button) {
|
||||||
setupVideoSpinner();
|
setupVideoSpinner();
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedAudioStreams).subscribe(result -> {
|
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedAudioStreams)
|
||||||
|
.subscribe(result -> {
|
||||||
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.audio_button) {
|
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.audio_button) {
|
||||||
setupAudioSpinner();
|
setupAudioSpinner();
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedSubtitleStreams).subscribe(result -> {
|
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedSubtitleStreams)
|
||||||
|
.subscribe(result -> {
|
||||||
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.subtitle_button) {
|
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.subtitle_button) {
|
||||||
setupSubtitleSpinner();
|
setupSubtitleSpinner();
|
||||||
}
|
}
|
||||||
@ -314,14 +348,22 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||||||
disposables.clear();
|
disposables.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Radio group Video&Audio options - Listener
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
Icepick.saveInstanceState(this, outState);
|
Icepick.saveInstanceState(this, outState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Streams Spinner Listener
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
|
||||||
super.onActivityResult(requestCode, resultCode, data);
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
|
|
||||||
if (requestCode == REQUEST_DOWNLOAD_SAVE_AS && resultCode == Activity.RESULT_OK) {
|
if (requestCode == REQUEST_DOWNLOAD_SAVE_AS && resultCode == Activity.RESULT_OK) {
|
||||||
@ -332,7 +374,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||||||
|
|
||||||
if (FilePickerActivityHelper.isOwnFileUri(context, data.getData())) {
|
if (FilePickerActivityHelper.isOwnFileUri(context, data.getData())) {
|
||||||
File file = Utils.getFileForUri(data.getData());
|
File file = Utils.getFileForUri(data.getData());
|
||||||
checkSelectedDownload(null, Uri.fromFile(file), file.getName(), StoredFileHelper.DEFAULT_MIME);
|
checkSelectedDownload(null, Uri.fromFile(file), file.getName(),
|
||||||
|
StoredFileHelper.DEFAULT_MIME);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -343,39 +386,46 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||||||
}
|
}
|
||||||
|
|
||||||
// check if the selected file was previously used
|
// check if the selected file was previously used
|
||||||
checkSelectedDownload(null, data.getData(), docFile.getName(), docFile.getType());
|
checkSelectedDownload(null, data.getData(), docFile.getName(),
|
||||||
|
docFile.getType());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
private void initToolbar(final Toolbar toolbar) {
|
||||||
// Inits
|
if (DEBUG) {
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
Log.d(TAG, "initToolbar() called with: toolbar = [" + toolbar + "]");
|
||||||
|
}
|
||||||
private void initToolbar(Toolbar toolbar) {
|
|
||||||
if (DEBUG) Log.d(TAG, "initToolbar() called with: toolbar = [" + toolbar + "]");
|
|
||||||
|
|
||||||
boolean isLight = ThemeHelper.isLightThemeSelected(getActivity());
|
|
||||||
|
|
||||||
toolbar.setTitle(R.string.download_dialog_title);
|
toolbar.setTitle(R.string.download_dialog_title);
|
||||||
toolbar.setNavigationIcon(isLight ? R.drawable.ic_arrow_back_black_24dp : R.drawable.ic_arrow_back_white_24dp);
|
toolbar.setNavigationIcon(
|
||||||
|
ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_arrow_back));
|
||||||
toolbar.inflateMenu(R.menu.dialog_url);
|
toolbar.inflateMenu(R.menu.dialog_url);
|
||||||
toolbar.setNavigationOnClickListener(v -> getDialog().dismiss());
|
toolbar.setNavigationOnClickListener(v -> requireDialog().dismiss());
|
||||||
toolbar.setNavigationContentDescription(R.string.cancel);
|
toolbar.setNavigationContentDescription(R.string.cancel);
|
||||||
|
|
||||||
okButton = toolbar.findViewById(R.id.okay);
|
okButton = toolbar.findViewById(R.id.okay);
|
||||||
okButton.setEnabled(false);// disable until the download service connection is done
|
okButton.setEnabled(false); // disable until the download service connection is done
|
||||||
|
|
||||||
toolbar.setOnMenuItemClickListener(item -> {
|
toolbar.setOnMenuItemClickListener(item -> {
|
||||||
if (item.getItemId() == R.id.okay) {
|
if (item.getItemId() == R.id.okay) {
|
||||||
prepareSelectedDownload();
|
prepareSelectedDownload();
|
||||||
|
if (getActivity() instanceof RouterActivity) {
|
||||||
|
getActivity().finish();
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Utils
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
private void setupAudioSpinner() {
|
private void setupAudioSpinner() {
|
||||||
if (getContext() == null) return;
|
if (getContext() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
streamsSpinner.setAdapter(audioStreamsAdapter);
|
streamsSpinner.setAdapter(audioStreamsAdapter);
|
||||||
streamsSpinner.setSelection(selectedAudioIndex);
|
streamsSpinner.setSelection(selectedAudioIndex);
|
||||||
@ -383,7 +433,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void setupVideoSpinner() {
|
private void setupVideoSpinner() {
|
||||||
if (getContext() == null) return;
|
if (getContext() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
streamsSpinner.setAdapter(videoStreamsAdapter);
|
streamsSpinner.setAdapter(videoStreamsAdapter);
|
||||||
streamsSpinner.setSelection(selectedVideoIndex);
|
streamsSpinner.setSelection(selectedVideoIndex);
|
||||||
@ -391,21 +443,21 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void setupSubtitleSpinner() {
|
private void setupSubtitleSpinner() {
|
||||||
if (getContext() == null) return;
|
if (getContext() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
streamsSpinner.setAdapter(subtitleStreamsAdapter);
|
streamsSpinner.setAdapter(subtitleStreamsAdapter);
|
||||||
streamsSpinner.setSelection(selectedSubtitleIndex);
|
streamsSpinner.setSelection(selectedSubtitleIndex);
|
||||||
setRadioButtonsState(true);
|
setRadioButtonsState(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Radio group Video&Audio options - Listener
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCheckedChanged(RadioGroup group, @IdRes int checkedId) {
|
public void onCheckedChanged(final RadioGroup group, @IdRes final int checkedId) {
|
||||||
if (DEBUG)
|
if (DEBUG) {
|
||||||
Log.d(TAG, "onCheckedChanged() called with: group = [" + group + "], checkedId = [" + checkedId + "]");
|
Log.d(TAG, "onCheckedChanged() called with: "
|
||||||
|
+ "group = [" + group + "], checkedId = [" + checkedId + "]");
|
||||||
|
}
|
||||||
boolean flag = true;
|
boolean flag = true;
|
||||||
|
|
||||||
switch (checkedId) {
|
switch (checkedId) {
|
||||||
@ -424,14 +476,14 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||||||
threadsSeekBar.setEnabled(flag);
|
threadsSeekBar.setEnabled(flag);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Streams Spinner Listener
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
public void onItemSelected(final AdapterView<?> parent, final View view,
|
||||||
if (DEBUG)
|
final int position, final long id) {
|
||||||
Log.d(TAG, "onItemSelected() called with: parent = [" + parent + "], view = [" + view + "], position = [" + position + "], id = [" + id + "]");
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onItemSelected() called with: "
|
||||||
|
+ "parent = [" + parent + "], view = [" + view + "], "
|
||||||
|
+ "position = [" + position + "], id = [" + id + "]");
|
||||||
|
}
|
||||||
switch (radioStreamsGroup.getCheckedRadioButtonId()) {
|
switch (radioStreamsGroup.getCheckedRadioButtonId()) {
|
||||||
case R.id.audio_button:
|
case R.id.audio_button:
|
||||||
selectedAudioIndex = position;
|
selectedAudioIndex = position;
|
||||||
@ -446,13 +498,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onNothingSelected(AdapterView<?> parent) {
|
public void onNothingSelected(final AdapterView<?> parent) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Utils
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
protected void setupDownloadOptions() {
|
protected void setupDownloadOptions() {
|
||||||
setRadioButtonsState(false);
|
setRadioButtonsState(false);
|
||||||
|
|
||||||
@ -477,30 +525,36 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||||||
subtitleButton.setChecked(true);
|
subtitleButton.setChecked(true);
|
||||||
setupSubtitleSpinner();
|
setupSubtitleSpinner();
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(getContext(), R.string.no_streams_available_download, Toast.LENGTH_SHORT).show();
|
Toast.makeText(getContext(), R.string.no_streams_available_download,
|
||||||
|
Toast.LENGTH_SHORT).show();
|
||||||
getDialog().dismiss();
|
getDialog().dismiss();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setRadioButtonsState(boolean enabled) {
|
private void setRadioButtonsState(final boolean enabled) {
|
||||||
radioStreamsGroup.findViewById(R.id.audio_button).setEnabled(enabled);
|
radioStreamsGroup.findViewById(R.id.audio_button).setEnabled(enabled);
|
||||||
radioStreamsGroup.findViewById(R.id.video_button).setEnabled(enabled);
|
radioStreamsGroup.findViewById(R.id.video_button).setEnabled(enabled);
|
||||||
radioStreamsGroup.findViewById(R.id.subtitle_button).setEnabled(enabled);
|
radioStreamsGroup.findViewById(R.id.subtitle_button).setEnabled(enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getSubtitleIndexBy(List<SubtitlesStream> streams) {
|
private int getSubtitleIndexBy(final List<SubtitlesStream> streams) {
|
||||||
final Localization preferredLocalization = NewPipe.getPreferredLocalization();
|
final Localization preferredLocalization = NewPipe.getPreferredLocalization();
|
||||||
|
|
||||||
int candidate = 0;
|
int candidate = 0;
|
||||||
for (int i = 0; i < streams.size(); i++) {
|
for (int i = 0; i < streams.size(); i++) {
|
||||||
final Locale streamLocale = streams.get(i).getLocale();
|
final Locale streamLocale = streams.get(i).getLocale();
|
||||||
|
|
||||||
final boolean languageEquals = streamLocale.getLanguage() != null && preferredLocalization.getLanguageCode() != null &&
|
final boolean languageEquals = streamLocale.getLanguage() != null
|
||||||
streamLocale.getLanguage().equals(new Locale(preferredLocalization.getLanguageCode()).getLanguage());
|
&& preferredLocalization.getLanguageCode() != null
|
||||||
final boolean countryEquals = streamLocale.getCountry() != null && streamLocale.getCountry().equals(preferredLocalization.getCountryCode());
|
&& streamLocale.getLanguage()
|
||||||
|
.equals(new Locale(preferredLocalization.getLanguageCode()).getLanguage());
|
||||||
|
final boolean countryEquals = streamLocale.getCountry() != null
|
||||||
|
&& streamLocale.getCountry().equals(preferredLocalization.getCountryCode());
|
||||||
|
|
||||||
if (languageEquals) {
|
if (languageEquals) {
|
||||||
if (countryEquals) return i;
|
if (countryEquals) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
candidate = i;
|
candidate = i;
|
||||||
}
|
}
|
||||||
@ -509,35 +563,30 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||||||
return candidate;
|
return candidate;
|
||||||
}
|
}
|
||||||
|
|
||||||
StoredDirectoryHelper mainStorageAudio = null;
|
|
||||||
StoredDirectoryHelper mainStorageVideo = null;
|
|
||||||
DownloadManager downloadManager = null;
|
|
||||||
ActionMenuItemView okButton = null;
|
|
||||||
Context context;
|
|
||||||
boolean askForSavePath;
|
|
||||||
|
|
||||||
private String getNameEditText() {
|
private String getNameEditText() {
|
||||||
String str = nameEditText.getText().toString().trim();
|
String str = nameEditText.getText().toString().trim();
|
||||||
|
|
||||||
return FilenameUtils.createFilename(context, str.isEmpty() ? currentInfo.getName() : str);
|
return FilenameUtils.createFilename(context, str.isEmpty() ? currentInfo.getName() : str);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showFailedDialog(@StringRes int msg) {
|
private void showFailedDialog(@StringRes final int msg) {
|
||||||
|
assureCorrectAppLanguage(getContext());
|
||||||
new AlertDialog.Builder(context)
|
new AlertDialog.Builder(context)
|
||||||
.setTitle(R.string.general_error)
|
.setTitle(R.string.general_error)
|
||||||
.setMessage(msg)
|
.setMessage(msg)
|
||||||
.setNegativeButton(android.R.string.ok, null)
|
.setNegativeButton(getString(R.string.finish), null)
|
||||||
.create()
|
.create()
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showErrorActivity(Exception e) {
|
private void showErrorActivity(final Exception e) {
|
||||||
ErrorActivity.reportError(
|
ErrorActivity.reportError(
|
||||||
context,
|
context,
|
||||||
Collections.singletonList(e),
|
Collections.singletonList(e),
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "-", "-", R.string.general_error)
|
ErrorActivity.ErrorInfo
|
||||||
|
.make(UserAction.SOMETHING_ELSE, "-", "-", R.string.general_error)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -555,8 +604,16 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||||||
case R.id.audio_button:
|
case R.id.audio_button:
|
||||||
mainStorage = mainStorageAudio;
|
mainStorage = mainStorageAudio;
|
||||||
format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
|
format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
|
||||||
mime = format.mimeType;
|
switch (format) {
|
||||||
filename += format.suffix;
|
case WEBMA_OPUS:
|
||||||
|
mime = "audio/ogg";
|
||||||
|
filename += "opus";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
mime = format.mimeType;
|
||||||
|
filename += format.suffix;
|
||||||
|
break;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case R.id.video_button:
|
case R.id.video_button:
|
||||||
mainStorage = mainStorageVideo;
|
mainStorage = mainStorageVideo;
|
||||||
@ -565,7 +622,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||||||
filename += format.suffix;
|
filename += format.suffix;
|
||||||
break;
|
break;
|
||||||
case R.id.subtitle_button:
|
case R.id.subtitle_button:
|
||||||
mainStorage = mainStorageVideo;// subtitle & video files go together
|
mainStorage = mainStorageVideo; // subtitle & video files go together
|
||||||
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
|
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
|
||||||
mime = format.mimeType;
|
mime = format.mimeType;
|
||||||
filename += format == MediaFormat.TTML ? MediaFormat.SRT.suffix : format.suffix;
|
filename += format == MediaFormat.TTML ? MediaFormat.SRT.suffix : format.suffix;
|
||||||
@ -580,23 +637,25 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||||||
// * save path not defined (via download settings)
|
// * save path not defined (via download settings)
|
||||||
// * the user checked the "ask where to download" option
|
// * the user checked the "ask where to download" option
|
||||||
|
|
||||||
if (!askForSavePath)
|
if (!askForSavePath) {
|
||||||
Toast.makeText(context, getString(R.string.no_available_dir), Toast.LENGTH_LONG).show();
|
Toast.makeText(context, getString(R.string.no_available_dir),
|
||||||
|
Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
|
||||||
if (NewPipeSettings.useStorageAccessFramework(context)) {
|
if (NewPipeSettings.useStorageAccessFramework(context)) {
|
||||||
StoredFileHelper.requestSafWithFileCreation(this, REQUEST_DOWNLOAD_SAVE_AS, filename, mime);
|
StoredFileHelper.requestSafWithFileCreation(this, REQUEST_DOWNLOAD_SAVE_AS,
|
||||||
|
filename, mime);
|
||||||
} else {
|
} else {
|
||||||
File initialSavePath;
|
File initialSavePath;
|
||||||
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.audio_button)
|
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.audio_button) {
|
||||||
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MUSIC);
|
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MUSIC);
|
||||||
else
|
} else {
|
||||||
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MOVIES);
|
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MOVIES);
|
||||||
|
}
|
||||||
|
|
||||||
initialSavePath = new File(initialSavePath, filename);
|
initialSavePath = new File(initialSavePath, filename);
|
||||||
startActivityForResult(
|
startActivityForResult(FilePickerActivityHelper.chooseFileToSave(context,
|
||||||
FilePickerActivityHelper.chooseFileToSave(context, initialSavePath.getAbsolutePath()),
|
initialSavePath.getAbsolutePath()), REQUEST_DOWNLOAD_SAVE_AS);
|
||||||
REQUEST_DOWNLOAD_SAVE_AS
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@ -606,7 +665,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||||||
checkSelectedDownload(mainStorage, mainStorage.findFile(filename), filename, mime);
|
checkSelectedDownload(mainStorage, mainStorage.findFile(filename), filename, mime);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkSelectedDownload(StoredDirectoryHelper mainStorage, Uri targetFile, String filename, String mime) {
|
private void checkSelectedDownload(final StoredDirectoryHelper mainStorage,
|
||||||
|
final Uri targetFile, final String filename,
|
||||||
|
final String mime) {
|
||||||
StoredFileHelper storage;
|
StoredFileHelper storage;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -615,10 +676,12 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||||||
storage = new StoredFileHelper(context, null, targetFile, "");
|
storage = new StoredFileHelper(context, null, targetFile, "");
|
||||||
} else if (targetFile == null) {
|
} else if (targetFile == null) {
|
||||||
// the file does not exist, but it is probably used in a pending download
|
// the file does not exist, but it is probably used in a pending download
|
||||||
storage = new StoredFileHelper(mainStorage.getUri(), filename, mime, mainStorage.getTag());
|
storage = new StoredFileHelper(mainStorage.getUri(), filename, mime,
|
||||||
|
mainStorage.getTag());
|
||||||
} else {
|
} else {
|
||||||
// the target filename is already use, attempt to use it
|
// the target filename is already use, attempt to use it
|
||||||
storage = new StoredFileHelper(context, mainStorage.getUri(), targetFile, mainStorage.getTag());
|
storage = new StoredFileHelper(context, mainStorage.getUri(), targetFile,
|
||||||
|
mainStorage.getTag());
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
showErrorActivity(e);
|
showErrorActivity(e);
|
||||||
@ -722,24 +785,28 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
// try take (or steal) the file
|
// try take (or steal) the file
|
||||||
storageNew = new StoredFileHelper(context, mainStorage.getUri(), targetFile, mainStorage.getTag());
|
storageNew = new StoredFileHelper(context, mainStorage.getUri(),
|
||||||
|
targetFile, mainStorage.getTag());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.e(TAG, "Failed to take (or steal) the file in " + targetFile.toString());
|
Log.e(TAG, "Failed to take (or steal) the file in "
|
||||||
|
+ targetFile.toString());
|
||||||
storageNew = null;
|
storageNew = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (storageNew != null && storageNew.canWrite())
|
if (storageNew != null && storageNew.canWrite()) {
|
||||||
continueSelectedDownload(storageNew);
|
continueSelectedDownload(storageNew);
|
||||||
else
|
} else {
|
||||||
showFailedDialog(R.string.error_file_creation);
|
showFailedDialog(R.string.error_file_creation);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case PendingRunning:
|
case PendingRunning:
|
||||||
storageNew = mainStorage.createUniqueFile(filename, mime);
|
storageNew = mainStorage.createUniqueFile(filename, mime);
|
||||||
if (storageNew == null)
|
if (storageNew == null) {
|
||||||
showFailedDialog(R.string.error_file_creation);
|
showFailedDialog(R.string.error_file_creation);
|
||||||
else
|
} else {
|
||||||
continueSelectedDownload(storageNew);
|
continueSelectedDownload(storageNew);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -747,7 +814,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||||||
askDialog.create().show();
|
askDialog.create().show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void continueSelectedDownload(@NonNull StoredFileHelper storage) {
|
private void continueSelectedDownload(@NonNull final StoredFileHelper storage) {
|
||||||
if (!storage.canWrite()) {
|
if (!storage.canWrite()) {
|
||||||
showFailedDialog(R.string.permission_denied);
|
showFailedDialog(R.string.permission_denied);
|
||||||
return;
|
return;
|
||||||
@ -755,7 +822,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||||||
|
|
||||||
// check if the selected file has to be overwritten, by simply checking its length
|
// check if the selected file has to be overwritten, by simply checking its length
|
||||||
try {
|
try {
|
||||||
if (storage.length() > 0) storage.truncate();
|
if (storage.length() > 0) {
|
||||||
|
storage.truncate();
|
||||||
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.e(TAG, "failed to truncate the file: " + storage.getUri().toString(), e);
|
Log.e(TAG, "failed to truncate the file: " + storage.getUri().toString(), e);
|
||||||
showFailedDialog(R.string.overwrite_failed);
|
showFailedDialog(R.string.overwrite_failed);
|
||||||
@ -795,13 +864,15 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||||||
if (secondary != null) {
|
if (secondary != null) {
|
||||||
secondaryStream = secondary.getStream();
|
secondaryStream = secondary.getStream();
|
||||||
|
|
||||||
if (selectedStream.getFormat() == MediaFormat.MPEG_4)
|
if (selectedStream.getFormat() == MediaFormat.MPEG_4) {
|
||||||
psName = Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER;
|
psName = Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER;
|
||||||
else
|
} else {
|
||||||
psName = Postprocessing.ALGORITHM_WEBM_MUXER;
|
psName = Postprocessing.ALGORITHM_WEBM_MUXER;
|
||||||
|
}
|
||||||
|
|
||||||
psArgs = null;
|
psArgs = null;
|
||||||
long videoSize = wrappedVideoStreams.getSizeInBytes((VideoStream) selectedStream);
|
long videoSize = wrappedVideoStreams
|
||||||
|
.getSizeInBytes((VideoStream) selectedStream);
|
||||||
|
|
||||||
// set nearLength, only, if both sizes are fetched or known. This probably
|
// set nearLength, only, if both sizes are fetched or known. This probably
|
||||||
// does not work on slow networks but is later updated in the downloader
|
// does not work on slow networks but is later updated in the downloader
|
||||||
@ -811,7 +882,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case R.id.subtitle_button:
|
case R.id.subtitle_button:
|
||||||
threads = 1;// use unique thread for subtitles due small file size
|
threads = 1; // use unique thread for subtitles due small file size
|
||||||
kind = 's';
|
kind = 's';
|
||||||
selectedStream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex);
|
selectedStream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex);
|
||||||
|
|
||||||
@ -819,8 +890,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||||||
psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
|
psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
|
||||||
psArgs = new String[]{
|
psArgs = new String[]{
|
||||||
selectedStream.getFormat().getSuffix(),
|
selectedStream.getFormat().getSuffix(),
|
||||||
"false",// ignore empty frames
|
"false" // ignore empty frames
|
||||||
"false",// detect youtube duplicate lines
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -839,14 +909,12 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||||||
urls = new String[]{
|
urls = new String[]{
|
||||||
selectedStream.getUrl(), secondaryStream.getUrl()
|
selectedStream.getUrl(), secondaryStream.getUrl()
|
||||||
};
|
};
|
||||||
recoveryInfo = new MissionRecoveryInfo[]{
|
recoveryInfo = new MissionRecoveryInfo[]{new MissionRecoveryInfo(selectedStream),
|
||||||
new MissionRecoveryInfo(selectedStream), new MissionRecoveryInfo(secondaryStream)
|
new MissionRecoveryInfo(secondaryStream)};
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DownloadManagerService.startMission(
|
DownloadManagerService.startMission(context, urls, storage, kind, threads,
|
||||||
context, urls, storage, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength, recoveryInfo
|
currentInfo.getUrl(), psName, psArgs, nearLength, recoveryInfo);
|
||||||
);
|
|
||||||
|
|
||||||
dismiss();
|
dismiss();
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
package org.schabi.newpipe.fragments;
|
package org.schabi.newpipe.fragments;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates that the current fragment can handle back presses
|
* Indicates that the current fragment can handle back presses.
|
||||||
*/
|
*/
|
||||||
public interface BackPressable {
|
public interface BackPressable {
|
||||||
/**
|
/**
|
||||||
* A back press was delegated to this fragment
|
* A back press was delegated to this fragment.
|
||||||
*
|
*
|
||||||
* @return if the back press was handled
|
* @return if the back press was handled
|
||||||
*/
|
*/
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
package org.schabi.newpipe.fragments;
|
package org.schabi.newpipe.fragments;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.StringRes;
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
@ -11,19 +10,23 @@ import android.widget.ProgressBar;
|
|||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
|
|
||||||
import com.jakewharton.rxbinding2.view.RxView;
|
import com.jakewharton.rxbinding2.view.RxView;
|
||||||
|
|
||||||
import org.schabi.newpipe.BaseFragment;
|
import org.schabi.newpipe.BaseFragment;
|
||||||
import org.schabi.newpipe.MainActivity;
|
import org.schabi.newpipe.MainActivity;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.ReCaptchaActivity;
|
import org.schabi.newpipe.ReCaptchaActivity;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||||
import org.schabi.newpipe.report.ErrorActivity;
|
import org.schabi.newpipe.report.ErrorActivity;
|
||||||
import org.schabi.newpipe.report.UserAction;
|
import org.schabi.newpipe.report.UserAction;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExceptionUtils;
|
||||||
import org.schabi.newpipe.util.InfoCache;
|
import org.schabi.newpipe.util.InfoCache;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@ -35,22 +38,21 @@ import io.reactivex.android.schedulers.AndroidSchedulers;
|
|||||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||||
|
|
||||||
public abstract class BaseStateFragment<I> extends BaseFragment implements ViewContract<I> {
|
public abstract class BaseStateFragment<I> extends BaseFragment implements ViewContract<I> {
|
||||||
|
|
||||||
@State
|
@State
|
||||||
protected AtomicBoolean wasLoading = new AtomicBoolean();
|
protected AtomicBoolean wasLoading = new AtomicBoolean();
|
||||||
protected AtomicBoolean isLoading = new AtomicBoolean();
|
protected AtomicBoolean isLoading = new AtomicBoolean();
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
protected View emptyStateView;
|
private View emptyStateView;
|
||||||
@Nullable
|
@Nullable
|
||||||
protected ProgressBar loadingProgressBar;
|
private ProgressBar loadingProgressBar;
|
||||||
|
|
||||||
protected View errorPanelRoot;
|
protected View errorPanelRoot;
|
||||||
protected Button errorButtonRetry;
|
private Button errorButtonRetry;
|
||||||
protected TextView errorTextView;
|
private TextView errorTextView;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onViewCreated(View rootView, Bundle savedInstanceState) {
|
public void onViewCreated(final View rootView, final Bundle savedInstanceState) {
|
||||||
super.onViewCreated(rootView, savedInstanceState);
|
super.onViewCreated(rootView, savedInstanceState);
|
||||||
doInitialLoadLogic();
|
doInitialLoadLogic();
|
||||||
}
|
}
|
||||||
@ -61,14 +63,12 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
|||||||
wasLoading.set(isLoading.get());
|
wasLoading.set(isLoading.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Init
|
// Init
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||||
super.initViews(rootView, savedInstanceState);
|
super.initViews(rootView, savedInstanceState);
|
||||||
|
|
||||||
emptyStateView = rootView.findViewById(R.id.empty_state_view);
|
emptyStateView = rootView.findViewById(R.id.empty_state_view);
|
||||||
@ -104,8 +104,10 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
|||||||
startLoading(true);
|
startLoading(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void startLoading(boolean forceLoad) {
|
protected void startLoading(final boolean forceLoad) {
|
||||||
if (DEBUG) Log.d(TAG, "startLoading() called with: forceLoad = [" + forceLoad + "]");
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "startLoading() called with: forceLoad = [" + forceLoad + "]");
|
||||||
|
}
|
||||||
showLoading();
|
showLoading();
|
||||||
isLoading.set(true);
|
isLoading.set(true);
|
||||||
}
|
}
|
||||||
@ -116,42 +118,62 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void showLoading() {
|
public void showLoading() {
|
||||||
if (emptyStateView != null) animateView(emptyStateView, false, 150);
|
if (emptyStateView != null) {
|
||||||
if (loadingProgressBar != null) animateView(loadingProgressBar, true, 400);
|
animateView(emptyStateView, false, 150);
|
||||||
|
}
|
||||||
|
if (loadingProgressBar != null) {
|
||||||
|
animateView(loadingProgressBar, true, 400);
|
||||||
|
}
|
||||||
animateView(errorPanelRoot, false, 150);
|
animateView(errorPanelRoot, false, 150);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void hideLoading() {
|
public void hideLoading() {
|
||||||
if (emptyStateView != null) animateView(emptyStateView, false, 150);
|
if (emptyStateView != null) {
|
||||||
if (loadingProgressBar != null) animateView(loadingProgressBar, false, 0);
|
animateView(emptyStateView, false, 150);
|
||||||
|
}
|
||||||
|
if (loadingProgressBar != null) {
|
||||||
|
animateView(loadingProgressBar, false, 0);
|
||||||
|
}
|
||||||
animateView(errorPanelRoot, false, 150);
|
animateView(errorPanelRoot, false, 150);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void showEmptyState() {
|
public void showEmptyState() {
|
||||||
isLoading.set(false);
|
isLoading.set(false);
|
||||||
if (emptyStateView != null) animateView(emptyStateView, true, 200);
|
if (emptyStateView != null) {
|
||||||
if (loadingProgressBar != null) animateView(loadingProgressBar, false, 0);
|
animateView(emptyStateView, true, 200);
|
||||||
|
}
|
||||||
|
if (loadingProgressBar != null) {
|
||||||
|
animateView(loadingProgressBar, false, 0);
|
||||||
|
}
|
||||||
animateView(errorPanelRoot, false, 150);
|
animateView(errorPanelRoot, false, 150);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void showError(String message, boolean showRetryButton) {
|
public void showError(final String message, final boolean showRetryButton) {
|
||||||
if (DEBUG) Log.d(TAG, "showError() called with: message = [" + message + "], showRetryButton = [" + showRetryButton + "]");
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "showError() called with: "
|
||||||
|
+ "message = [" + message + "], showRetryButton = [" + showRetryButton + "]");
|
||||||
|
}
|
||||||
isLoading.set(false);
|
isLoading.set(false);
|
||||||
InfoCache.getInstance().clearCache();
|
InfoCache.getInstance().clearCache();
|
||||||
hideLoading();
|
hideLoading();
|
||||||
|
|
||||||
errorTextView.setText(message);
|
errorTextView.setText(message);
|
||||||
if (showRetryButton) animateView(errorButtonRetry, true, 600);
|
if (showRetryButton) {
|
||||||
else animateView(errorButtonRetry, false, 0);
|
animateView(errorButtonRetry, true, 600);
|
||||||
|
} else {
|
||||||
|
animateView(errorButtonRetry, false, 0);
|
||||||
|
}
|
||||||
animateView(errorPanelRoot, true, 300);
|
animateView(errorPanelRoot, true, 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleResult(I result) {
|
public void handleResult(final I result) {
|
||||||
if (DEBUG) Log.d(TAG, "handleResult() called with: result = [" + result + "]");
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "handleResult() called with: result = [" + result + "]");
|
||||||
|
}
|
||||||
hideLoading();
|
hideLoading();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,37 +182,52 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default implementation handles some general exceptions
|
* Default implementation handles some general exceptions.
|
||||||
*
|
*
|
||||||
* @return if the exception was handled
|
* @param exception The exception that should be handled
|
||||||
|
* @return If the exception was handled
|
||||||
*/
|
*/
|
||||||
protected boolean onError(Throwable exception) {
|
protected boolean onError(final Throwable exception) {
|
||||||
if (DEBUG) Log.d(TAG, "onError() called with: exception = [" + exception + "]");
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onError() called with: exception = [" + exception + "]");
|
||||||
|
}
|
||||||
isLoading.set(false);
|
isLoading.set(false);
|
||||||
|
|
||||||
if (isDetached() || isRemoving()) {
|
if (isDetached() || isRemoving()) {
|
||||||
if (DEBUG) Log.w(TAG, "onError() is detached or removing = [" + exception + "]");
|
if (DEBUG) {
|
||||||
|
Log.w(TAG, "onError() is detached or removing = [" + exception + "]");
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ExtractorHelper.isInterruptedCaused(exception)) {
|
if (ExceptionUtils.isInterruptedCaused(exception)) {
|
||||||
if (DEBUG) Log.w(TAG, "onError() isInterruptedCaused! = [" + exception + "]");
|
if (DEBUG) {
|
||||||
|
Log.w(TAG, "onError() isInterruptedCaused! = [" + exception + "]");
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exception instanceof ReCaptchaException) {
|
if (exception instanceof ReCaptchaException) {
|
||||||
onReCaptchaException((ReCaptchaException) exception);
|
onReCaptchaException((ReCaptchaException) exception);
|
||||||
return true;
|
return true;
|
||||||
} else if (exception instanceof IOException) {
|
} else if (exception instanceof ContentNotAvailableException) {
|
||||||
|
showError(getString(R.string.content_not_available), false);
|
||||||
|
return true;
|
||||||
|
} else if (ExceptionUtils.isNetworkRelated(exception)) {
|
||||||
showError(getString(R.string.network_error), true);
|
showError(getString(R.string.network_error), true);
|
||||||
return true;
|
return true;
|
||||||
|
} else if (exception instanceof ContentNotSupportedException) {
|
||||||
|
showError(getString(R.string.content_not_supported), false);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onReCaptchaException(ReCaptchaException exception) {
|
public void onReCaptchaException(final ReCaptchaException exception) {
|
||||||
if (DEBUG) Log.d(TAG, "onReCaptchaException() called");
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onReCaptchaException() called");
|
||||||
|
}
|
||||||
Toast.makeText(activity, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show();
|
Toast.makeText(activity, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show();
|
||||||
// Starting ReCaptcha Challenge Activity
|
// Starting ReCaptcha Challenge Activity
|
||||||
Intent intent = new Intent(activity, ReCaptchaActivity.class);
|
Intent intent = new Intent(activity, ReCaptchaActivity.class);
|
||||||
@ -200,33 +237,58 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
|||||||
showError(getString(R.string.recaptcha_request_toast), false);
|
showError(getString(R.string.recaptcha_request_toast), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onUnrecoverableError(Throwable exception, UserAction userAction, String serviceName, String request, @StringRes int errorId) {
|
public void onUnrecoverableError(final Throwable exception, final UserAction userAction,
|
||||||
onUnrecoverableError(Collections.singletonList(exception), userAction, serviceName, request, errorId);
|
final String serviceName, final String request,
|
||||||
|
@StringRes final int errorId) {
|
||||||
|
onUnrecoverableError(Collections.singletonList(exception), userAction, serviceName,
|
||||||
|
request, errorId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onUnrecoverableError(List<Throwable> exception, UserAction userAction, String serviceName, String request, @StringRes int errorId) {
|
public void onUnrecoverableError(final List<Throwable> exception, final UserAction userAction,
|
||||||
if (DEBUG) Log.d(TAG, "onUnrecoverableError() called with: exception = [" + exception + "]");
|
final String serviceName, final String request,
|
||||||
|
@StringRes final int errorId) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onUnrecoverableError() called with: exception = [" + exception + "]");
|
||||||
|
}
|
||||||
|
|
||||||
if (serviceName == null) serviceName = "none";
|
ErrorActivity.reportError(getContext(), exception, MainActivity.class, null,
|
||||||
if (request == null) request = "none";
|
ErrorActivity.ErrorInfo.make(userAction, serviceName == null ? "none" : serviceName,
|
||||||
|
request == null ? "none" : request, errorId));
|
||||||
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) {
|
public void showSnackBarError(final Throwable exception, final UserAction userAction,
|
||||||
showSnackBarError(Collections.singletonList(exception), userAction, serviceName, request, errorId);
|
final String serviceName, final String request,
|
||||||
|
@StringRes final int errorId) {
|
||||||
|
showSnackBarError(Collections.singletonList(exception), userAction, serviceName, request,
|
||||||
|
errorId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show a SnackBar and only call ErrorActivity#reportError IF we a find a valid view (otherwise the error screen appears)
|
* Show a SnackBar and only call
|
||||||
|
* {@link ErrorActivity#reportError(Context, List, Class, View, ErrorActivity.ErrorInfo)}
|
||||||
|
* IF we a find a valid view (otherwise the error screen appears).
|
||||||
|
*
|
||||||
|
* @param exception List of the exceptions to show
|
||||||
|
* @param userAction The user action that caused the exception
|
||||||
|
* @param serviceName The service where the exception happened
|
||||||
|
* @param request The page that was requested
|
||||||
|
* @param errorId The ID of the error
|
||||||
*/
|
*/
|
||||||
public void showSnackBarError(List<Throwable> exception, UserAction userAction, String serviceName, String request, @StringRes int errorId) {
|
public void showSnackBarError(final List<Throwable> exception, final UserAction userAction,
|
||||||
|
final String serviceName, final String request,
|
||||||
|
@StringRes final int errorId) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "showSnackBarError() called with: exception = [" + exception + "], userAction = [" + userAction + "], request = [" + request + "], errorId = [" + errorId + "]");
|
Log.d(TAG, "showSnackBarError() called with: "
|
||||||
|
+ "exception = [" + exception + "], userAction = [" + userAction + "], "
|
||||||
|
+ "request = [" + request + "], errorId = [" + errorId + "]");
|
||||||
}
|
}
|
||||||
View rootView = activity != null ? activity.findViewById(android.R.id.content) : null;
|
View rootView = activity != null ? activity.findViewById(android.R.id.content) : null;
|
||||||
if (rootView == null && getView() != null) rootView = getView();
|
if (rootView == null && getView() != null) {
|
||||||
if (rootView == null) return;
|
rootView = getView();
|
||||||
|
}
|
||||||
|
if (rootView == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ErrorActivity.reportError(getContext(), exception, MainActivity.class, rootView,
|
ErrorActivity.reportError(getContext(), exception, MainActivity.class, rootView,
|
||||||
ErrorActivity.ErrorInfo.make(userAction, serviceName, request, errorId));
|
ErrorActivity.ErrorInfo.make(userAction, serviceName, request, errorId));
|
||||||
|
@ -1,24 +1,26 @@
|
|||||||
package org.schabi.newpipe.fragments;
|
package org.schabi.newpipe.fragments;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import org.schabi.newpipe.BaseFragment;
|
import org.schabi.newpipe.BaseFragment;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
|
||||||
public class BlankFragment extends BaseFragment {
|
public class BlankFragment extends BaseFragment {
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
|
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
|
||||||
|
final Bundle savedInstanceState) {
|
||||||
setTitle("NewPipe");
|
setTitle("NewPipe");
|
||||||
return inflater.inflate(R.layout.fragment_blank, container, false);
|
return inflater.inflate(R.layout.fragment_blank, container, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setUserVisibleHint(boolean isVisibleToUser) {
|
public void setUserVisibleHint(final boolean isVisibleToUser) {
|
||||||
super.setUserVisibleHint(isVisibleToUser);
|
super.setUserVisibleHint(isVisibleToUser);
|
||||||
setTitle("NewPipe");
|
setTitle("NewPipe");
|
||||||
// leave this inline. Will make it harder for copy cats.
|
// leave this inline. Will make it harder for copy cats.
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
package org.schabi.newpipe.fragments;
|
package org.schabi.newpipe.fragments;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import org.schabi.newpipe.BaseFragment;
|
import org.schabi.newpipe.BaseFragment;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
|
||||||
public class EmptyFragment extends BaseFragment {
|
public class EmptyFragment extends BaseFragment {
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
|
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
|
||||||
|
final Bundle savedInstanceState) {
|
||||||
return inflater.inflate(R.layout.fragment_empty, container, false);
|
return inflater.inflate(R.layout.fragment_empty, container, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
package org.schabi.newpipe.fragments;
|
package org.schabi.newpipe.fragments;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.res.ColorStateList;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.preference.PreferenceManager;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
@ -16,7 +18,7 @@ import androidx.appcompat.app.ActionBar;
|
|||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.fragment.app.FragmentStatePagerAdapter;
|
import androidx.fragment.app.FragmentStatePagerAdapterMenuWorkaround;
|
||||||
import androidx.viewpager.widget.ViewPager;
|
import androidx.viewpager.widget.ViewPager;
|
||||||
|
|
||||||
import com.google.android.material.tabs.TabLayout;
|
import com.google.android.material.tabs.TabLayout;
|
||||||
@ -30,6 +32,8 @@ import org.schabi.newpipe.settings.tabs.Tab;
|
|||||||
import org.schabi.newpipe.settings.tabs.TabsManager;
|
import org.schabi.newpipe.settings.tabs.TabsManager;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
import org.schabi.newpipe.views.ScrollableTabLayout;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -37,26 +41,29 @@ import java.util.List;
|
|||||||
public class MainFragment extends BaseFragment implements TabLayout.OnTabSelectedListener {
|
public class MainFragment extends BaseFragment implements TabLayout.OnTabSelectedListener {
|
||||||
private ViewPager viewPager;
|
private ViewPager viewPager;
|
||||||
private SelectedTabsPagerAdapter pagerAdapter;
|
private SelectedTabsPagerAdapter pagerAdapter;
|
||||||
private TabLayout tabLayout;
|
private ScrollableTabLayout tabLayout;
|
||||||
|
|
||||||
private List<Tab> tabsList = new ArrayList<>();
|
private List<Tab> tabsList = new ArrayList<>();
|
||||||
private TabsManager tabsManager;
|
private TabsManager tabsManager;
|
||||||
|
|
||||||
private boolean hasTabsChanged = false;
|
private boolean hasTabsChanged = false;
|
||||||
|
|
||||||
|
private boolean previousYoutubeRestrictedModeEnabled;
|
||||||
|
private String youtubeRestrictedModeEnabledKey;
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Fragment's LifeCycle
|
// Fragment's LifeCycle
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(final Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setHasOptionsMenu(true);
|
setHasOptionsMenu(true);
|
||||||
|
|
||||||
tabsManager = TabsManager.getManager(activity);
|
tabsManager = TabsManager.getManager(activity);
|
||||||
tabsManager.setSavedTabsListener(() -> {
|
tabsManager.setSavedTabsListener(() -> {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "TabsManager.SavedTabsChangeListener: onTabsChanged called, isResumed = " + isResumed());
|
Log.d(TAG, "TabsManager.SavedTabsChangeListener: "
|
||||||
|
+ "onTabsChanged called, isResumed = " + isResumed());
|
||||||
}
|
}
|
||||||
if (isResumed()) {
|
if (isResumed()) {
|
||||||
setupTabs();
|
setupTabs();
|
||||||
@ -64,20 +71,29 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
|||||||
hasTabsChanged = true;
|
hasTabsChanged = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
youtubeRestrictedModeEnabledKey = getString(R.string.youtube_restricted_mode_enabled);
|
||||||
|
previousYoutubeRestrictedModeEnabled =
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(getContext())
|
||||||
|
.getBoolean(youtubeRestrictedModeEnabledKey, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||||
|
@Nullable final ViewGroup container,
|
||||||
|
@Nullable final Bundle savedInstanceState) {
|
||||||
return inflater.inflate(R.layout.fragment_main, container, false);
|
return inflater.inflate(R.layout.fragment_main, container, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||||
super.initViews(rootView, savedInstanceState);
|
super.initViews(rootView, savedInstanceState);
|
||||||
|
|
||||||
tabLayout = rootView.findViewById(R.id.main_tab_layout);
|
tabLayout = rootView.findViewById(R.id.main_tab_layout);
|
||||||
viewPager = rootView.findViewById(R.id.pager);
|
viewPager = rootView.findViewById(R.id.pager);
|
||||||
|
|
||||||
|
tabLayout.setTabIconTint(ColorStateList.valueOf(
|
||||||
|
ThemeHelper.resolveColorFromAttr(requireContext(), R.attr.colorAccent)));
|
||||||
tabLayout.setupWithViewPager(viewPager);
|
tabLayout.setupWithViewPager(viewPager);
|
||||||
tabLayout.addOnTabSelectedListener(this);
|
tabLayout.addOnTabSelectedListener(this);
|
||||||
|
|
||||||
@ -88,14 +104,24 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
|||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
|
||||||
if (hasTabsChanged) setupTabs();
|
boolean youtubeRestrictedModeEnabled =
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(getContext())
|
||||||
|
.getBoolean(youtubeRestrictedModeEnabledKey, false);
|
||||||
|
if (previousYoutubeRestrictedModeEnabled != youtubeRestrictedModeEnabled) {
|
||||||
|
previousYoutubeRestrictedModeEnabled = youtubeRestrictedModeEnabled;
|
||||||
|
setupTabs();
|
||||||
|
} else if (hasTabsChanged) {
|
||||||
|
setupTabs();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
tabsManager.unsetSavedTabsListener();
|
tabsManager.unsetSavedTabsListener();
|
||||||
if (viewPager != null) viewPager.setAdapter(null);
|
if (viewPager != null) {
|
||||||
|
viewPager.setAdapter(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
@ -103,9 +129,12 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
|
||||||
super.onCreateOptionsMenu(menu, inflater);
|
super.onCreateOptionsMenu(menu, inflater);
|
||||||
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]");
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onCreateOptionsMenu() called with: "
|
||||||
|
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||||
|
}
|
||||||
inflater.inflate(R.menu.main_fragment_menu, menu);
|
inflater.inflate(R.menu.main_fragment_menu, menu);
|
||||||
|
|
||||||
ActionBar supportActionBar = activity.getSupportActionBar();
|
ActionBar supportActionBar = activity.getSupportActionBar();
|
||||||
@ -115,7 +144,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
case R.id.action_search:
|
case R.id.action_search:
|
||||||
try {
|
try {
|
||||||
@ -135,16 +164,18 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
|||||||
// Tabs
|
// Tabs
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
public void setupTabs() {
|
private void setupTabs() {
|
||||||
tabsList.clear();
|
tabsList.clear();
|
||||||
tabsList.addAll(tabsManager.getTabs());
|
tabsList.addAll(tabsManager.getTabs());
|
||||||
|
|
||||||
if (pagerAdapter == null || !pagerAdapter.sameTabs(tabsList)) {
|
if (pagerAdapter == null || !pagerAdapter.sameTabs(tabsList)) {
|
||||||
pagerAdapter = new SelectedTabsPagerAdapter(requireContext(), getChildFragmentManager(), tabsList);
|
pagerAdapter = new SelectedTabsPagerAdapter(requireContext(),
|
||||||
|
getChildFragmentManager(), tabsList);
|
||||||
}
|
}
|
||||||
// Clear previous tabs/fragments and set new adapter
|
|
||||||
viewPager.setAdapter(pagerAdapter);
|
viewPager.setAdapter(null);
|
||||||
viewPager.setOffscreenPageLimit(tabsList.size());
|
viewPager.setOffscreenPageLimit(tabsList.size());
|
||||||
|
viewPager.setAdapter(pagerAdapter);
|
||||||
|
|
||||||
updateTabsIconAndDescription();
|
updateTabsIconAndDescription();
|
||||||
updateTitleForTab(viewPager.getCurrentItem());
|
updateTitleForTab(viewPager.getCurrentItem());
|
||||||
@ -163,38 +194,45 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateTitleForTab(int tabPosition) {
|
private void updateTitleForTab(final int tabPosition) {
|
||||||
setTitle(tabsList.get(tabPosition).getTabName(requireContext()));
|
setTitle(tabsList.get(tabPosition).getTabName(requireContext()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTabSelected(TabLayout.Tab selectedTab) {
|
public void onTabSelected(final TabLayout.Tab selectedTab) {
|
||||||
if (DEBUG) Log.d(TAG, "onTabSelected() called with: selectedTab = [" + selectedTab + "]");
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onTabSelected() called with: selectedTab = [" + selectedTab + "]");
|
||||||
|
}
|
||||||
updateTitleForTab(selectedTab.getPosition());
|
updateTitleForTab(selectedTab.getPosition());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTabUnselected(TabLayout.Tab tab) {
|
public void onTabUnselected(final TabLayout.Tab tab) { }
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTabReselected(TabLayout.Tab tab) {
|
public void onTabReselected(final TabLayout.Tab tab) {
|
||||||
if (DEBUG) Log.d(TAG, "onTabReselected() called with: tab = [" + tab + "]");
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onTabReselected() called with: tab = [" + tab + "]");
|
||||||
|
}
|
||||||
updateTitleForTab(tab.getPosition());
|
updateTitleForTab(tab.getPosition());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class SelectedTabsPagerAdapter extends FragmentStatePagerAdapter {
|
private static final class SelectedTabsPagerAdapter
|
||||||
|
extends FragmentStatePagerAdapterMenuWorkaround {
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final List<Tab> internalTabsList;
|
private final List<Tab> internalTabsList;
|
||||||
|
|
||||||
private SelectedTabsPagerAdapter(Context context, FragmentManager fragmentManager, List<Tab> tabsList) {
|
private SelectedTabsPagerAdapter(final Context context,
|
||||||
|
final FragmentManager fragmentManager,
|
||||||
|
final List<Tab> tabsList) {
|
||||||
super(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
|
super(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.internalTabsList = new ArrayList<>(tabsList);
|
this.internalTabsList = new ArrayList<>(tabsList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public Fragment getItem(int position) {
|
public Fragment getItem(final int position) {
|
||||||
final Tab tab = internalTabsList.get(position);
|
final Tab tab = internalTabsList.get(position);
|
||||||
|
|
||||||
Throwable throwable = null;
|
Throwable throwable = null;
|
||||||
@ -206,8 +244,8 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (throwable != null) {
|
if (throwable != null) {
|
||||||
ErrorActivity.reportError(context, throwable, null, null,
|
ErrorActivity.reportError(context, throwable, null, null, ErrorActivity.ErrorInfo
|
||||||
ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash));
|
.make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash));
|
||||||
return new BlankFragment();
|
return new BlankFragment();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -219,7 +257,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getItemPosition(Object object) {
|
public int getItemPosition(final Object object) {
|
||||||
// Causes adapter to reload all Fragments when
|
// Causes adapter to reload all Fragments when
|
||||||
// notifyDataSetChanged is called
|
// notifyDataSetChanged is called
|
||||||
return POSITION_NONE;
|
return POSITION_NONE;
|
||||||
@ -230,7 +268,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
|||||||
return internalTabsList.size();
|
return internalTabsList.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean sameTabs(List<Tab> tabsToCompare) {
|
public boolean sameTabs(final List<Tab> tabsToCompare) {
|
||||||
return internalTabsList.equals(tabsToCompare);
|
return internalTabsList.equals(tabsToCompare);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,12 +9,13 @@ import androidx.recyclerview.widget.StaggeredGridLayoutManager;
|
|||||||
* if the view is scrolled below the last item.
|
* if the view is scrolled below the last item.
|
||||||
*/
|
*/
|
||||||
public abstract class OnScrollBelowItemsListener extends RecyclerView.OnScrollListener {
|
public abstract class OnScrollBelowItemsListener extends RecyclerView.OnScrollListener {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
|
public void onScrolled(final RecyclerView recyclerView, final int dx, final int dy) {
|
||||||
super.onScrolled(recyclerView, dx, dy);
|
super.onScrolled(recyclerView, dx, dy);
|
||||||
if (dy > 0) {
|
if (dy > 0) {
|
||||||
int pastVisibleItems = 0, visibleItemCount, totalItemCount;
|
int pastVisibleItems = 0;
|
||||||
|
int visibleItemCount;
|
||||||
|
int totalItemCount;
|
||||||
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
|
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
|
||||||
|
|
||||||
visibleItemCount = layoutManager.getChildCount();
|
visibleItemCount = layoutManager.getChildCount();
|
||||||
@ -22,10 +23,14 @@ public abstract class OnScrollBelowItemsListener extends RecyclerView.OnScrollLi
|
|||||||
|
|
||||||
// Already covers the GridLayoutManager case
|
// Already covers the GridLayoutManager case
|
||||||
if (layoutManager instanceof LinearLayoutManager) {
|
if (layoutManager instanceof LinearLayoutManager) {
|
||||||
pastVisibleItems = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
|
pastVisibleItems = ((LinearLayoutManager) layoutManager)
|
||||||
|
.findFirstVisibleItemPosition();
|
||||||
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
|
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
|
||||||
int[] positions = ((StaggeredGridLayoutManager) layoutManager).findFirstVisibleItemPositions(null);
|
int[] positions = ((StaggeredGridLayoutManager) layoutManager)
|
||||||
if (positions != null && positions.length > 0) pastVisibleItems = positions[0];
|
.findFirstVisibleItemPositions(null);
|
||||||
|
if (positions != null && positions.length > 0) {
|
||||||
|
pastVisibleItems = positions[0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((visibleItemCount + pastVisibleItems) >= totalItemCount) {
|
if ((visibleItemCount + pastVisibleItems) >= totalItemCount) {
|
||||||
|
@ -2,8 +2,11 @@ package org.schabi.newpipe.fragments;
|
|||||||
|
|
||||||
public interface ViewContract<I> {
|
public interface ViewContract<I> {
|
||||||
void showLoading();
|
void showLoading();
|
||||||
|
|
||||||
void hideLoading();
|
void hideLoading();
|
||||||
|
|
||||||
void showEmptyState();
|
void showEmptyState();
|
||||||
|
|
||||||
void showError(String message, boolean showRetryButton);
|
void showError(String message, boolean showRetryButton);
|
||||||
|
|
||||||
void handleResult(I result);
|
void handleResult(I result);
|
||||||
|
@ -6,8 +6,8 @@ import java.io.Serializable;
|
|||||||
|
|
||||||
class StackItem implements Serializable {
|
class StackItem implements Serializable {
|
||||||
private final int serviceId;
|
private final int serviceId;
|
||||||
private String title;
|
|
||||||
private String url;
|
private String url;
|
||||||
|
private String title;
|
||||||
private PlayQueue playQueue;
|
private PlayQueue playQueue;
|
||||||
|
|
||||||
StackItem(final int serviceId, final String url, final String title, final PlayQueue playQueue) {
|
StackItem(final int serviceId, final String url, final String title, final PlayQueue playQueue) {
|
||||||
@ -17,10 +17,6 @@ class StackItem implements Serializable {
|
|||||||
this.playQueue = playQueue;
|
this.playQueue = playQueue;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setTitle(String title) {
|
|
||||||
this.title = title;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setUrl(String url) {
|
public void setUrl(String url) {
|
||||||
this.url = url;
|
this.url = url;
|
||||||
}
|
}
|
||||||
@ -37,6 +33,10 @@ class StackItem implements Serializable {
|
|||||||
return title;
|
return title;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setTitle(final String title) {
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
public String getUrl() {
|
public String getUrl() {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
@ -1,27 +1,27 @@
|
|||||||
package org.schabi.newpipe.fragments.detail;
|
package org.schabi.newpipe.fragments.detail;
|
||||||
|
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.fragment.app.FragmentPagerAdapter;
|
import androidx.fragment.app.FragmentPagerAdapter;
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class TabAdaptor extends FragmentPagerAdapter {
|
public class TabAdaptor extends FragmentPagerAdapter {
|
||||||
|
|
||||||
private final List<Fragment> mFragmentList = new ArrayList<>();
|
private final List<Fragment> mFragmentList = new ArrayList<>();
|
||||||
private final List<String> mFragmentTitleList = new ArrayList<>();
|
private final List<String> mFragmentTitleList = new ArrayList<>();
|
||||||
private final FragmentManager fragmentManager;
|
private final FragmentManager fragmentManager;
|
||||||
|
|
||||||
public TabAdaptor(FragmentManager fm) {
|
public TabAdaptor(final FragmentManager fm) {
|
||||||
super(fm);
|
super(fm);
|
||||||
this.fragmentManager = fm;
|
this.fragmentManager = fm;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Fragment getItem(int position) {
|
public Fragment getItem(final int position) {
|
||||||
return mFragmentList.get(position);
|
return mFragmentList.get(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ public class TabAdaptor extends FragmentPagerAdapter {
|
|||||||
return mFragmentList.size();
|
return mFragmentList.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addFragment(Fragment fragment, String title) {
|
public void addFragment(final Fragment fragment, final String title) {
|
||||||
mFragmentList.add(fragment);
|
mFragmentList.add(fragment);
|
||||||
mFragmentTitleList.add(title);
|
mFragmentTitleList.add(title);
|
||||||
}
|
}
|
||||||
@ -40,46 +40,49 @@ public class TabAdaptor extends FragmentPagerAdapter {
|
|||||||
mFragmentTitleList.clear();
|
mFragmentTitleList.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeItem(int position){
|
public void removeItem(final int position) {
|
||||||
mFragmentList.remove(position == 0 ? 0 : position - 1);
|
mFragmentList.remove(position == 0 ? 0 : position - 1);
|
||||||
mFragmentTitleList.remove(position == 0 ? 0 : position - 1);
|
mFragmentTitleList.remove(position == 0 ? 0 : position - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateItem(int position, Fragment fragment){
|
public void updateItem(final int position, final Fragment fragment) {
|
||||||
mFragmentList.set(position, fragment);
|
mFragmentList.set(position, fragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateItem(String title, Fragment fragment){
|
public void updateItem(final String title, final Fragment fragment) {
|
||||||
int index = mFragmentTitleList.indexOf(title);
|
int index = mFragmentTitleList.indexOf(title);
|
||||||
if(index != -1){
|
if (index != -1) {
|
||||||
updateItem(index, fragment);
|
updateItem(index, fragment);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getItemPosition(Object object) {
|
public int getItemPosition(final Object object) {
|
||||||
if (mFragmentList.contains(object)) return mFragmentList.indexOf(object);
|
if (mFragmentList.contains(object)) {
|
||||||
else return POSITION_NONE;
|
return mFragmentList.indexOf(object);
|
||||||
|
} else {
|
||||||
|
return POSITION_NONE;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getItemPositionByTitle(String title) {
|
public int getItemPositionByTitle(final String title) {
|
||||||
return mFragmentTitleList.indexOf(title);
|
return mFragmentTitleList.indexOf(title);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public String getItemTitle(int position) {
|
public String getItemTitle(final int position) {
|
||||||
if (position < 0 || position >= mFragmentTitleList.size()) {
|
if (position < 0 || position >= mFragmentTitleList.size()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return mFragmentTitleList.get(position);
|
return mFragmentTitleList.get(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void notifyDataSetUpdate(){
|
public void notifyDataSetUpdate() {
|
||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void destroyItem(ViewGroup container, int position, Object object) {
|
public void destroyItem(final ViewGroup container, final int position, final Object object) {
|
||||||
fragmentManager.beginTransaction().remove((Fragment) object).commitNowAllowingStateLoss();
|
fragmentManager.beginTransaction().remove((Fragment) object).commitNowAllowingStateLoss();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import android.app.Activity;
|
|||||||
import android.content.*;
|
import android.content.*;
|
||||||
import android.content.pm.ActivityInfo;
|
import android.content.pm.ActivityInfo;
|
||||||
import android.database.ContentObserver;
|
import android.database.ContentObserver;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
@ -31,11 +32,13 @@ import androidx.viewpager.widget.ViewPager;
|
|||||||
import android.text.Html;
|
import android.text.Html;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.text.method.LinkMovementMethod;
|
|
||||||
import android.text.util.Linkify;
|
import android.text.util.Linkify;
|
||||||
import android.util.DisplayMetrics;
|
import android.util.DisplayMetrics;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.appcompat.content.res.AppCompatResources;
|
||||||
|
|
||||||
import com.nostra13.universalimageloader.core.assist.FailReason;
|
import com.nostra13.universalimageloader.core.assist.FailReason;
|
||||||
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
|
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
|
||||||
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
|
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
|
||||||
@ -46,11 +49,10 @@ import org.schabi.newpipe.download.DownloadDialog;
|
|||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.ServiceList;
|
import org.schabi.newpipe.extractor.ServiceList;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
|
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
|
||||||
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor;
|
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor;
|
||||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
|
import org.schabi.newpipe.extractor.stream.Description;
|
||||||
import org.schabi.newpipe.extractor.stream.Stream;
|
import org.schabi.newpipe.extractor.stream.Stream;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
@ -63,20 +65,34 @@ import org.schabi.newpipe.fragments.list.videos.RelatedVideosFragment;
|
|||||||
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
|
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.player.*;
|
import org.schabi.newpipe.player.*;
|
||||||
|
import org.schabi.newpipe.player.event.OnKeyDownListener;
|
||||||
import org.schabi.newpipe.player.event.PlayerEventListener;
|
import org.schabi.newpipe.player.event.PlayerEventListener;
|
||||||
import org.schabi.newpipe.player.event.PlayerServiceEventListener;
|
import org.schabi.newpipe.player.event.PlayerServiceEventListener;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||||
import org.schabi.newpipe.player.playqueue.*;
|
import org.schabi.newpipe.player.playqueue.*;
|
||||||
import org.schabi.newpipe.report.ErrorActivity;
|
import org.schabi.newpipe.report.ErrorActivity;
|
||||||
import org.schabi.newpipe.report.UserAction;
|
import org.schabi.newpipe.report.UserAction;
|
||||||
import org.schabi.newpipe.util.*;
|
import org.schabi.newpipe.util.AndroidTvUtils;
|
||||||
|
import org.schabi.newpipe.util.Constants;
|
||||||
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
|
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||||
|
import org.schabi.newpipe.util.InfoCache;
|
||||||
|
import org.schabi.newpipe.util.ListHelper;
|
||||||
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
import org.schabi.newpipe.util.PermissionHelper;
|
||||||
|
import org.schabi.newpipe.util.ShareUtils;
|
||||||
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||||
|
import org.schabi.newpipe.views.LargeTextMovementMethod;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import icepick.State;
|
import icepick.State;
|
||||||
|
import io.noties.markwon.Markwon;
|
||||||
|
import io.noties.markwon.linkify.LinkifyPlugin;
|
||||||
import io.reactivex.Single;
|
import io.reactivex.Single;
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.disposables.CompositeDisposable;
|
import io.reactivex.disposables.CompositeDisposable;
|
||||||
@ -86,6 +102,7 @@ import io.reactivex.schedulers.Schedulers;
|
|||||||
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS;
|
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS;
|
||||||
import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired;
|
import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired;
|
||||||
import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET;
|
import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET;
|
||||||
|
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
|
||||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||||
|
|
||||||
public class VideoDetailFragment
|
public class VideoDetailFragment
|
||||||
@ -95,7 +112,8 @@ public class VideoDetailFragment
|
|||||||
View.OnClickListener,
|
View.OnClickListener,
|
||||||
View.OnLongClickListener,
|
View.OnLongClickListener,
|
||||||
PlayerEventListener,
|
PlayerEventListener,
|
||||||
PlayerServiceEventListener {
|
PlayerServiceEventListener,
|
||||||
|
OnKeyDownListener {
|
||||||
public static final String AUTO_PLAY = "auto_play";
|
public static final String AUTO_PLAY = "auto_play";
|
||||||
|
|
||||||
private static final int RELATED_STREAMS_UPDATE_FLAG = 0x1;
|
private static final int RELATED_STREAMS_UPDATE_FLAG = 0x1;
|
||||||
@ -112,6 +130,9 @@ public class VideoDetailFragment
|
|||||||
private static final String RELATED_TAB_TAG = "NEXT VIDEO";
|
private static final String RELATED_TAB_TAG = "NEXT VIDEO";
|
||||||
private static final String EMPTY_TAB_TAG = "EMPTY TAB";
|
private static final String EMPTY_TAB_TAG = "EMPTY TAB";
|
||||||
|
|
||||||
|
private static final String INFO_KEY = "info_key";
|
||||||
|
private static final String STACK_KEY = "stack_key";
|
||||||
|
|
||||||
private boolean showRelatedStreams;
|
private boolean showRelatedStreams;
|
||||||
private boolean showComments;
|
private boolean showComments;
|
||||||
private String selectedTabTag;
|
private String selectedTabTag;
|
||||||
@ -174,6 +195,8 @@ public class VideoDetailFragment
|
|||||||
private View uploaderRootLayout;
|
private View uploaderRootLayout;
|
||||||
private TextView uploaderTextView;
|
private TextView uploaderTextView;
|
||||||
private ImageView uploaderThumb;
|
private ImageView uploaderThumb;
|
||||||
|
private TextView subChannelTextView;
|
||||||
|
private ImageView subChannelThumb;
|
||||||
|
|
||||||
private TextView thumbsUpTextView;
|
private TextView thumbsUpTextView;
|
||||||
private ImageView thumbsUpImageView;
|
private ImageView thumbsUpImageView;
|
||||||
@ -300,13 +323,13 @@ public class VideoDetailFragment
|
|||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Fragment's Lifecycle
|
// Fragment's Lifecycle
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void
|
public void onCreate(final Bundle savedInstanceState) {
|
||||||
onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
showRelatedStreams = PreferenceManager.getDefaultSharedPreferences(activity)
|
showRelatedStreams = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||||
@ -336,19 +359,22 @@ public class VideoDetailFragment
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container,
|
||||||
|
final Bundle savedInstanceState) {
|
||||||
return inflater.inflate(R.layout.fragment_video_detail, container, false);
|
return inflater.inflate(R.layout.fragment_video_detail, container, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPause() {
|
public void onPause() {
|
||||||
super.onPause();
|
super.onPause();
|
||||||
if (currentWorker != null) currentWorker.dispose();
|
if (currentWorker != null) {
|
||||||
|
currentWorker.dispose();
|
||||||
|
}
|
||||||
setupBrightness(true);
|
setupBrightness(true);
|
||||||
PreferenceManager.getDefaultSharedPreferences(getContext())
|
PreferenceManager.getDefaultSharedPreferences(getContext())
|
||||||
.edit()
|
.edit()
|
||||||
.putString(getString(R.string.stream_info_selected_tab_key), pageAdapter.getItemTitle(viewPager.getCurrentItem()))
|
.putString(getString(R.string.stream_info_selected_tab_key),
|
||||||
|
pageAdapter.getItemTitle(viewPager.getCurrentItem()))
|
||||||
.apply();
|
.apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -395,8 +421,12 @@ public class VideoDetailFragment
|
|||||||
activity.unregisterReceiver(broadcastReceiver);
|
activity.unregisterReceiver(broadcastReceiver);
|
||||||
activity.getContentResolver().unregisterContentObserver(settingsContentObserver);
|
activity.getContentResolver().unregisterContentObserver(settingsContentObserver);
|
||||||
|
|
||||||
if (positionSubscriber != null) positionSubscriber.dispose();
|
if (positionSubscriber != null) {
|
||||||
if (currentWorker != null) currentWorker.dispose();
|
positionSubscriber.dispose();
|
||||||
|
}
|
||||||
|
if (currentWorker != null) {
|
||||||
|
currentWorker.dispose();
|
||||||
|
}
|
||||||
disposables.clear();
|
disposables.clear();
|
||||||
positionSubscriber = null;
|
positionSubscriber = null;
|
||||||
currentWorker = null;
|
currentWorker = null;
|
||||||
@ -404,13 +434,16 @@ public class VideoDetailFragment
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
|
||||||
super.onActivityResult(requestCode, resultCode, data);
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
switch (requestCode) {
|
switch (requestCode) {
|
||||||
case ReCaptchaActivity.RECAPTCHA_REQUEST:
|
case ReCaptchaActivity.RECAPTCHA_REQUEST:
|
||||||
if (resultCode == Activity.RESULT_OK) {
|
if (resultCode == Activity.RESULT_OK) {
|
||||||
NavigationHelper.openVideoDetailFragment(getFragmentManager(), serviceId, url, name);
|
NavigationHelper
|
||||||
} else Log.e(TAG, "ReCaptcha failed");
|
.openVideoDetailFragment(getFragmentManager(), serviceId, url, name);
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "ReCaptcha failed");
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
Log.e(TAG, "Request code from activity not supported [" + requestCode + "]");
|
Log.e(TAG, "Request code from activity not supported [" + requestCode + "]");
|
||||||
@ -419,7 +452,8 @@ public class VideoDetailFragment
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences,
|
||||||
|
final String key) {
|
||||||
if (key.equals(getString(R.string.show_next_video_key))) {
|
if (key.equals(getString(R.string.show_next_video_key))) {
|
||||||
showRelatedStreams = sharedPreferences.getBoolean(key, true);
|
showRelatedStreams = sharedPreferences.getBoolean(key, true);
|
||||||
updateFlags |= RELATED_STREAMS_UPDATE_FLAG;
|
updateFlags |= RELATED_STREAMS_UPDATE_FLAG;
|
||||||
@ -433,11 +467,8 @@ public class VideoDetailFragment
|
|||||||
// State Saving
|
// State Saving
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
private static final String INFO_KEY = "info_key";
|
|
||||||
private static final String STACK_KEY = "stack_key";
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSaveInstanceState(Bundle outState) {
|
public void onSaveInstanceState(final Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
|
|
||||||
// Check if the next video label and video is visible,
|
// Check if the next video label and video is visible,
|
||||||
@ -453,7 +484,7 @@ public class VideoDetailFragment
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onRestoreInstanceState(@NonNull Bundle savedState) {
|
protected void onRestoreInstanceState(@NonNull final Bundle savedState) {
|
||||||
super.onRestoreInstanceState(savedState);
|
super.onRestoreInstanceState(savedState);
|
||||||
|
|
||||||
Serializable serializable = savedState.getSerializable(INFO_KEY);
|
Serializable serializable = savedState.getSerializable(INFO_KEY);
|
||||||
@ -476,8 +507,10 @@ public class VideoDetailFragment
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(final View v) {
|
||||||
if (isLoading.get() || currentInfo == null) return;
|
if (isLoading.get() || currentInfo == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (v.getId()) {
|
switch (v.getId()) {
|
||||||
case R.id.detail_controls_background:
|
case R.id.detail_controls_background:
|
||||||
@ -499,7 +532,18 @@ public class VideoDetailFragment
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case R.id.detail_uploader_root_layout:
|
case R.id.detail_uploader_root_layout:
|
||||||
openChannel();
|
if (TextUtils.isEmpty(currentInfo.getSubChannelUrl())) {
|
||||||
|
if (!TextUtils.isEmpty(currentInfo.getUploaderUrl())) {
|
||||||
|
openChannel(currentInfo.getUploaderUrl(), currentInfo.getUploaderName());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.i(TAG, "Can't open sub-channel because we got no channel URL");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
openChannel(currentInfo.getSubChannelUrl(),
|
||||||
|
currentInfo.getSubChannelName());
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case R.id.detail_thumbnail_root_layout:
|
case R.id.detail_thumbnail_root_layout:
|
||||||
openVideoPlayer();
|
openVideoPlayer();
|
||||||
@ -527,9 +571,23 @@ public class VideoDetailFragment
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void openChannel(final String subChannelUrl, final String subChannelName) {
|
||||||
|
try {
|
||||||
|
NavigationHelper.openChannelFragment(
|
||||||
|
getFragmentManager(),
|
||||||
|
currentInfo.getServiceId(),
|
||||||
|
subChannelUrl,
|
||||||
|
subChannelName);
|
||||||
|
} catch (Exception e) {
|
||||||
|
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onLongClick(View v) {
|
public boolean onLongClick(final View v) {
|
||||||
if (isLoading.get() || currentInfo == null) return false;
|
if (isLoading.get() || currentInfo == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
switch (v.getId()) {
|
switch (v.getId()) {
|
||||||
case R.id.detail_controls_background:
|
case R.id.detail_controls_background:
|
||||||
@ -543,7 +601,19 @@ public class VideoDetailFragment
|
|||||||
break;
|
break;
|
||||||
case R.id.overlay_thumbnail:
|
case R.id.overlay_thumbnail:
|
||||||
case R.id.overlay_metadata_layout:
|
case R.id.overlay_metadata_layout:
|
||||||
openChannel();
|
if (currentInfo != null)
|
||||||
|
openChannel(currentInfo.getUploaderUrl(), currentInfo.getUploaderName());
|
||||||
|
break;
|
||||||
|
case R.id.detail_uploader_root_layout:
|
||||||
|
if (TextUtils.isEmpty(currentInfo.getSubChannelUrl())) {
|
||||||
|
Log.w(TAG,
|
||||||
|
"Can't open parent channel because we got no parent channel URL");
|
||||||
|
} else {
|
||||||
|
openChannel(currentInfo.getUploaderUrl(), currentInfo.getUploaderName());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case R.id.detail_title_root_layout:
|
||||||
|
ShareUtils.copyToClipboard(getContext(), videoTitleTextView.getText().toString());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -554,11 +624,16 @@ public class VideoDetailFragment
|
|||||||
if (videoDescriptionRootLayout.getVisibility() == View.VISIBLE) {
|
if (videoDescriptionRootLayout.getVisibility() == View.VISIBLE) {
|
||||||
videoTitleTextView.setMaxLines(1);
|
videoTitleTextView.setMaxLines(1);
|
||||||
videoDescriptionRootLayout.setVisibility(View.GONE);
|
videoDescriptionRootLayout.setVisibility(View.GONE);
|
||||||
videoTitleToggleArrow.setImageResource(R.drawable.arrow_down);
|
videoDescriptionView.setFocusable(false);
|
||||||
|
videoTitleToggleArrow.setImageResource(
|
||||||
|
ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_expand_more));
|
||||||
} else {
|
} else {
|
||||||
videoTitleTextView.setMaxLines(10);
|
videoTitleTextView.setMaxLines(10);
|
||||||
videoDescriptionRootLayout.setVisibility(View.VISIBLE);
|
videoDescriptionRootLayout.setVisibility(View.VISIBLE);
|
||||||
videoTitleToggleArrow.setImageResource(R.drawable.arrow_up);
|
videoDescriptionView.setFocusable(true);
|
||||||
|
videoDescriptionView.setMovementMethod(new LargeTextMovementMethod());
|
||||||
|
videoTitleToggleArrow.setImageResource(
|
||||||
|
ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_expand_less));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -567,7 +642,7 @@ public class VideoDetailFragment
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||||
super.initViews(rootView, savedInstanceState);
|
super.initViews(rootView, savedInstanceState);
|
||||||
thumbnailBackgroundButton = rootView.findViewById(R.id.detail_thumbnail_root_layout);
|
thumbnailBackgroundButton = rootView.findViewById(R.id.detail_thumbnail_root_layout);
|
||||||
thumbnailImageView = rootView.findViewById(R.id.detail_thumbnail_image_view);
|
thumbnailImageView = rootView.findViewById(R.id.detail_thumbnail_image_view);
|
||||||
@ -592,8 +667,6 @@ public class VideoDetailFragment
|
|||||||
videoDescriptionRootLayout = rootView.findViewById(R.id.detail_description_root_layout);
|
videoDescriptionRootLayout = rootView.findViewById(R.id.detail_description_root_layout);
|
||||||
videoUploadDateView = rootView.findViewById(R.id.detail_upload_date_view);
|
videoUploadDateView = rootView.findViewById(R.id.detail_upload_date_view);
|
||||||
videoDescriptionView = rootView.findViewById(R.id.detail_description_view);
|
videoDescriptionView = rootView.findViewById(R.id.detail_description_view);
|
||||||
videoDescriptionView.setMovementMethod(LinkMovementMethod.getInstance());
|
|
||||||
videoDescriptionView.setAutoLinkMask(Linkify.WEB_URLS);
|
|
||||||
|
|
||||||
thumbsUpTextView = rootView.findViewById(R.id.detail_thumbs_up_count_view);
|
thumbsUpTextView = rootView.findViewById(R.id.detail_thumbs_up_count_view);
|
||||||
thumbsUpImageView = rootView.findViewById(R.id.detail_thumbs_up_img_view);
|
thumbsUpImageView = rootView.findViewById(R.id.detail_thumbs_up_img_view);
|
||||||
@ -604,6 +677,8 @@ public class VideoDetailFragment
|
|||||||
uploaderRootLayout = rootView.findViewById(R.id.detail_uploader_root_layout);
|
uploaderRootLayout = rootView.findViewById(R.id.detail_uploader_root_layout);
|
||||||
uploaderTextView = rootView.findViewById(R.id.detail_uploader_text_view);
|
uploaderTextView = rootView.findViewById(R.id.detail_uploader_text_view);
|
||||||
uploaderThumb = rootView.findViewById(R.id.detail_uploader_thumbnail_view);
|
uploaderThumb = rootView.findViewById(R.id.detail_uploader_thumbnail_view);
|
||||||
|
subChannelTextView = rootView.findViewById(R.id.detail_sub_channel_text_view);
|
||||||
|
subChannelThumb = rootView.findViewById(R.id.detail_sub_channel_thumbnail_view);
|
||||||
|
|
||||||
overlay = rootView.findViewById(R.id.overlay_layout);
|
overlay = rootView.findViewById(R.id.overlay_layout);
|
||||||
overlayMetadata = rootView.findViewById(R.id.overlay_metadata_layout);
|
overlayMetadata = rootView.findViewById(R.id.overlay_metadata_layout);
|
||||||
@ -624,14 +699,28 @@ public class VideoDetailFragment
|
|||||||
relatedStreamsLayout = rootView.findViewById(R.id.relatedStreamsLayout);
|
relatedStreamsLayout = rootView.findViewById(R.id.relatedStreamsLayout);
|
||||||
|
|
||||||
setHeightThumbnail();
|
setHeightThumbnail();
|
||||||
|
|
||||||
|
thumbnailBackgroundButton.requestFocus();
|
||||||
|
|
||||||
|
if (AndroidTvUtils.isTv(getContext())) {
|
||||||
|
// remove ripple effects from detail controls
|
||||||
|
final int transparent = getResources().getColor(R.color.transparent_background_color);
|
||||||
|
detailControlsAddToPlaylist.setBackgroundColor(transparent);
|
||||||
|
detailControlsBackground.setBackgroundColor(transparent);
|
||||||
|
detailControlsPopup.setBackgroundColor(transparent);
|
||||||
|
detailControlsDownload.setBackgroundColor(transparent);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initListeners() {
|
protected void initListeners() {
|
||||||
super.initListeners();
|
super.initListeners();
|
||||||
|
|
||||||
videoTitleRoot.setOnClickListener(this);
|
videoTitleRoot.setOnLongClickListener(this);
|
||||||
uploaderRootLayout.setOnClickListener(this);
|
uploaderRootLayout.setOnClickListener(this);
|
||||||
|
uploaderRootLayout.setOnLongClickListener(this);
|
||||||
|
videoTitleRoot.setOnClickListener(this);
|
||||||
thumbnailBackgroundButton.setOnClickListener(this);
|
thumbnailBackgroundButton.setOnClickListener(this);
|
||||||
detailControlsBackground.setOnClickListener(this);
|
detailControlsBackground.setOnClickListener(this);
|
||||||
detailControlsPopup.setOnClickListener(this);
|
detailControlsPopup.setOnClickListener(this);
|
||||||
@ -674,25 +763,31 @@ public class VideoDetailFragment
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initThumbnailViews(@NonNull StreamInfo info) {
|
private void initThumbnailViews(@NonNull final StreamInfo info) {
|
||||||
thumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark);
|
thumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark);
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(info.getThumbnailUrl())) {
|
if (!TextUtils.isEmpty(info.getThumbnailUrl())) {
|
||||||
final String infoServiceName = NewPipe.getNameOfService(info.getServiceId());
|
final String infoServiceName = NewPipe.getNameOfService(info.getServiceId());
|
||||||
final ImageLoadingListener loadingListener = new SimpleImageLoadingListener() {
|
final ImageLoadingListener onFailListener = new SimpleImageLoadingListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
|
public void onLoadingFailed(final String imageUri, final View view,
|
||||||
|
final FailReason failReason) {
|
||||||
showSnackBarError(failReason.getCause(), UserAction.LOAD_IMAGE,
|
showSnackBarError(failReason.getCause(), UserAction.LOAD_IMAGE,
|
||||||
infoServiceName, imageUri, R.string.could_not_load_thumbnails);
|
infoServiceName, imageUri, R.string.could_not_load_thumbnails);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
imageLoader.displayImage(info.getThumbnailUrl(), thumbnailImageView,
|
IMAGE_LOADER.displayImage(info.getThumbnailUrl(), thumbnailImageView,
|
||||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, loadingListener);
|
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, onFailListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TextUtils.isEmpty(info.getSubChannelAvatarUrl())) {
|
||||||
|
IMAGE_LOADER.displayImage(info.getSubChannelAvatarUrl(), subChannelThumb,
|
||||||
|
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(info.getUploaderAvatarUrl())) {
|
if (!TextUtils.isEmpty(info.getUploaderAvatarUrl())) {
|
||||||
imageLoader.displayImage(info.getUploaderAvatarUrl(), uploaderThumb,
|
IMAGE_LOADER.displayImage(info.getUploaderAvatarUrl(), uploaderThumb,
|
||||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -707,20 +802,16 @@ public class VideoDetailFragment
|
|||||||
*/
|
*/
|
||||||
protected final LinkedList<StackItem> stack = new LinkedList<>();
|
protected final LinkedList<StackItem> stack = new LinkedList<>();
|
||||||
|
|
||||||
/*public void setTitleToUrl(int serviceId, String videoUrl, String name) {
|
@Override
|
||||||
if (name != null && !name.isEmpty()) {
|
public boolean onKeyDown(final int keyCode) {
|
||||||
for (StackItem stackItem : stack) {
|
return player != null && player.onKeyDown(keyCode);
|
||||||
if (stack.peek().getServiceId() == serviceId
|
}
|
||||||
&& stackItem.getUrl().equals(videoUrl)) {
|
|
||||||
stackItem.setTitle(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onBackPressed() {
|
public boolean onBackPressed() {
|
||||||
if (DEBUG) Log.d(TAG, "onBackPressed() called");
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onBackPressed() called");
|
||||||
|
}
|
||||||
|
|
||||||
// If we are in fullscreen mode just exit from it via first back press
|
// If we are in fullscreen mode just exit from it via first back press
|
||||||
if (player != null && player.isFullscreen()) {
|
if (player != null && player.isFullscreen()) {
|
||||||
@ -782,23 +873,28 @@ public class VideoDetailFragment
|
|||||||
protected void doInitialLoadLogic() {
|
protected void doInitialLoadLogic() {
|
||||||
if (wasCleared()) return;
|
if (wasCleared()) return;
|
||||||
|
|
||||||
if (currentInfo == null) prepareAndLoadInfo();
|
if (currentInfo == null) {
|
||||||
else prepareAndHandleInfo(currentInfo, false);
|
prepareAndLoadInfo();
|
||||||
|
} else {
|
||||||
|
prepareAndHandleInfo(currentInfo, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void selectAndLoadVideo(final int serviceId, final String videoUrl, final String name, final PlayQueue playQueue) {
|
public void selectAndLoadVideo(final int sid, final String videoUrl, final String title, final PlayQueue playQueue) {
|
||||||
// Situation when user switches from players to main player. All needed data is here, we can start watching
|
// Situation when user switches from players to main player. All needed data is here, we can start watching
|
||||||
if (this.playQueue != null && this.playQueue.equals(playQueue)) {
|
if (this.playQueue != null && this.playQueue.equals(playQueue)) {
|
||||||
openVideoPlayer();
|
openVideoPlayer();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setInitialData(serviceId, videoUrl, name, playQueue);
|
setInitialData(sid, videoUrl, title, playQueue);
|
||||||
startLoading(false, true);
|
startLoading(false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void prepareAndHandleInfo(final StreamInfo info, final boolean scrollToTop) {
|
private void prepareAndHandleInfo(final StreamInfo info, final boolean scrollToTop) {
|
||||||
if (DEBUG) Log.d(TAG, "prepareAndHandleInfo() called with: info = ["
|
if (DEBUG) {
|
||||||
+ info + "], scrollToTop = [" + scrollToTop + "]");
|
Log.d(TAG, "prepareAndHandleInfo() called with: "
|
||||||
|
+ "info = [" + info + "], scrollToTop = [" + scrollToTop + "]");
|
||||||
|
}
|
||||||
|
|
||||||
showLoading();
|
showLoading();
|
||||||
initTabs();
|
initTabs();
|
||||||
@ -820,7 +916,11 @@ public class VideoDetailFragment
|
|||||||
|
|
||||||
initTabs();
|
initTabs();
|
||||||
currentInfo = null;
|
currentInfo = null;
|
||||||
if (currentWorker != null) currentWorker.dispose();
|
if (currentWorker != null) {
|
||||||
|
currentWorker.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
|
||||||
|
|
||||||
runWorker(forceLoad, stack.isEmpty());
|
runWorker(forceLoad, stack.isEmpty());
|
||||||
}
|
}
|
||||||
@ -836,20 +936,27 @@ public class VideoDetailFragment
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void runWorker(final boolean forceLoad, final boolean addToBackStack) {
|
private void runWorker(final boolean forceLoad, final boolean addToBackStack) {
|
||||||
|
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
|
||||||
currentWorker = ExtractorHelper.getStreamInfo(serviceId, url, forceLoad)
|
currentWorker = ExtractorHelper.getStreamInfo(serviceId, url, forceLoad)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe((@NonNull StreamInfo result) -> {
|
.subscribe((@NonNull final StreamInfo result) -> {
|
||||||
isLoading.set(false);
|
isLoading.set(false);
|
||||||
hideMainPlayer();
|
hideMainPlayer();
|
||||||
handleResult(result);
|
if (result.getAgeLimit() != NO_AGE_LIMIT && !prefs.getBoolean(
|
||||||
showContent();
|
getString(R.string.show_age_restricted_content), false)) {
|
||||||
if (addToBackStack) {
|
hideAgeRestrictedContent();
|
||||||
if (playQueue == null) playQueue = new SinglePlayQueue(result);
|
} else {
|
||||||
stack.push(new StackItem(serviceId, url, name, playQueue));
|
currentInfo = result;
|
||||||
|
handleResult(result);
|
||||||
|
showContent();
|
||||||
|
if (addToBackStack) {
|
||||||
|
if (playQueue == null) playQueue = new SinglePlayQueue(result);
|
||||||
|
stack.push(new StackItem(serviceId, url, name, playQueue));
|
||||||
|
}
|
||||||
|
if (isAutoplayEnabled()) openVideoPlayer();
|
||||||
}
|
}
|
||||||
if (isAutoplayEnabled()) openVideoPlayer();
|
}, (@NonNull final Throwable throwable) -> {
|
||||||
}, (@NonNull Throwable throwable) -> {
|
|
||||||
isLoading.set(false);
|
isLoading.set(false);
|
||||||
onError(throwable);
|
onError(throwable);
|
||||||
});
|
});
|
||||||
@ -879,8 +986,10 @@ public class VideoDetailFragment
|
|||||||
if (pageAdapter.getCount() < 2) {
|
if (pageAdapter.getCount() < 2) {
|
||||||
tabLayout.setVisibility(View.GONE);
|
tabLayout.setVisibility(View.GONE);
|
||||||
} else {
|
} else {
|
||||||
final int position = pageAdapter.getItemPositionByTitle(selectedTabTag);
|
int position = pageAdapter.getItemPositionByTitle(selectedTabTag);
|
||||||
if (position != -1) viewPager.setCurrentItem(position);
|
if (position != -1) {
|
||||||
|
viewPager.setCurrentItem(position);
|
||||||
|
}
|
||||||
tabLayout.setVisibility(View.VISIBLE);
|
tabLayout.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1001,22 +1110,6 @@ public class VideoDetailFragment
|
|||||||
return queue;
|
return queue;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openChannel() {
|
|
||||||
if (TextUtils.isEmpty(currentInfo.getUploaderUrl())) {
|
|
||||||
Log.w(TAG, "Can't open channel because we got no channel URL");
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
NavigationHelper.openChannelFragment(
|
|
||||||
getFragmentManager(),
|
|
||||||
currentInfo.getServiceId(),
|
|
||||||
currentInfo.getUploaderUrl(),
|
|
||||||
currentInfo.getUploaderName());
|
|
||||||
} catch (Exception e) {
|
|
||||||
ErrorActivity.reportUiError(activity, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Utils
|
// Utils
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
@ -1029,12 +1122,12 @@ public class VideoDetailFragment
|
|||||||
@NonNull final StreamInfo info,
|
@NonNull final StreamInfo info,
|
||||||
@NonNull final Stream selectedStream) {
|
@NonNull final Stream selectedStream) {
|
||||||
NavigationHelper.playOnExternalPlayer(context, currentInfo.getName(),
|
NavigationHelper.playOnExternalPlayer(context, currentInfo.getName(),
|
||||||
currentInfo.getUploaderName(), selectedStream);
|
currentInfo.getSubChannelName(), selectedStream);
|
||||||
|
|
||||||
final HistoryRecordManager recordManager = new HistoryRecordManager(requireContext());
|
final HistoryRecordManager recordManager = new HistoryRecordManager(requireContext());
|
||||||
disposables.add(recordManager.onViewed(info).onErrorComplete()
|
disposables.add(recordManager.onViewed(info).onErrorComplete()
|
||||||
.subscribe(
|
.subscribe(
|
||||||
ignored -> {/* successful */},
|
ignored -> { /* successful */ },
|
||||||
error -> Log.e(TAG, "Register view failure: ", error)
|
error -> Log.e(TAG, "Register view failure: ", error)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@ -1101,28 +1194,42 @@ public class VideoDetailFragment
|
|||||||
return sortedVideoStreams != null ? sortedVideoStreams.get(selectedVideoStreamIndex) : null;
|
return sortedVideoStreams != null ? sortedVideoStreams.get(selectedVideoStreamIndex) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void prepareDescription(final String descriptionHtml) {
|
private void prepareDescription(final Description description) {
|
||||||
if (TextUtils.isEmpty(descriptionHtml)) {
|
if (TextUtils.isEmpty(description.getContent())
|
||||||
|
|| description == Description.emptyDescription) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
disposables.add(Single.just(descriptionHtml)
|
if (description.getType() == Description.HTML) {
|
||||||
.map((@io.reactivex.annotations.NonNull String description) -> {
|
disposables.add(Single.just(description.getContent())
|
||||||
final Spanned parsedDescription;
|
.map((@io.reactivex.annotations.NonNull String descriptionText) -> {
|
||||||
if (Build.VERSION.SDK_INT >= 24) {
|
Spanned parsedDescription;
|
||||||
parsedDescription = Html.fromHtml(description, 0);
|
if (Build.VERSION.SDK_INT >= 24) {
|
||||||
} else {
|
parsedDescription = Html.fromHtml(descriptionText, 0);
|
||||||
//noinspection deprecation
|
} else {
|
||||||
parsedDescription = Html.fromHtml(description);
|
//noinspection deprecation
|
||||||
}
|
parsedDescription = Html.fromHtml(descriptionText);
|
||||||
return parsedDescription;
|
}
|
||||||
})
|
return parsedDescription;
|
||||||
.subscribeOn(Schedulers.computation())
|
})
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.subscribeOn(Schedulers.computation())
|
||||||
.subscribe((@io.reactivex.annotations.NonNull Spanned spanned) -> {
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
videoDescriptionView.setText(spanned);
|
.subscribe((@io.reactivex.annotations.NonNull Spanned spanned) -> {
|
||||||
videoDescriptionView.setVisibility(View.VISIBLE);
|
videoDescriptionView.setText(spanned);
|
||||||
}));
|
videoDescriptionView.setVisibility(View.VISIBLE);
|
||||||
|
}));
|
||||||
|
} else if (description.getType() == Description.MARKDOWN) {
|
||||||
|
final Markwon markwon = Markwon.builder(getContext())
|
||||||
|
.usePlugin(LinkifyPlugin.create())
|
||||||
|
.build();
|
||||||
|
markwon.setMarkdown(videoDescriptionView, description.getContent());
|
||||||
|
videoDescriptionView.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
//== Description.PLAIN_TEXT
|
||||||
|
videoDescriptionView.setAutoLinkMask(Linkify.WEB_URLS);
|
||||||
|
videoDescriptionView.setText(description.getContent(), TextView.BufferType.SPANNABLE);
|
||||||
|
videoDescriptionView.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1154,27 +1261,31 @@ public class VideoDetailFragment
|
|||||||
contentRootLayoutHiding.setVisibility(View.VISIBLE);
|
contentRootLayoutHiding.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void setInitialData(final int serviceId, final String url, final String name, final PlayQueue playQueue) {
|
protected void setInitialData(final int sid, final String u, final String name, final PlayQueue playQueue) {
|
||||||
this.serviceId = serviceId;
|
this.serviceId = sid;
|
||||||
this.url = url;
|
this.url = u;
|
||||||
this.name = !TextUtils.isEmpty(name) ? name : "";
|
this.name = !TextUtils.isEmpty(name) ? name : "";
|
||||||
this.playQueue = playQueue;
|
this.playQueue = playQueue;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setErrorImage(final int imageResource) {
|
private void setErrorImage(final int imageResource) {
|
||||||
if (thumbnailImageView == null || activity == null) return;
|
if (thumbnailImageView == null || activity == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
thumbnailImageView.setImageDrawable(ContextCompat.getDrawable(activity, imageResource));
|
thumbnailImageView.setImageDrawable(
|
||||||
|
AppCompatResources.getDrawable(requireContext(), imageResource));
|
||||||
animateView(thumbnailImageView, false, 0, 0,
|
animateView(thumbnailImageView, false, 0, 0,
|
||||||
() -> animateView(thumbnailImageView, true, 500));
|
() -> animateView(thumbnailImageView, true, 500));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void showError(String message, boolean showRetryButton) {
|
public void showError(final String message, final boolean showRetryButton) {
|
||||||
showError(message, showRetryButton, R.drawable.not_available_monkey);
|
showError(message, showRetryButton, R.drawable.not_available_monkey);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void showError(String message, boolean showRetryButton, @DrawableRes int imageError) {
|
protected void showError(final String message, final boolean showRetryButton,
|
||||||
|
@DrawableRes final int imageError) {
|
||||||
super.showError(message, showRetryButton);
|
super.showError(message, showRetryButton);
|
||||||
setErrorImage(imageError);
|
setErrorImage(imageError);
|
||||||
}
|
}
|
||||||
@ -1236,7 +1347,6 @@ public class VideoDetailFragment
|
|||||||
animateView(videoTitleTextView, true, 0);
|
animateView(videoTitleTextView, true, 0);
|
||||||
|
|
||||||
videoDescriptionRootLayout.setVisibility(View.GONE);
|
videoDescriptionRootLayout.setVisibility(View.GONE);
|
||||||
videoTitleToggleArrow.setImageResource(R.drawable.arrow_down);
|
|
||||||
videoTitleToggleArrow.setVisibility(View.GONE);
|
videoTitleToggleArrow.setVisibility(View.GONE);
|
||||||
videoTitleRoot.setClickable(false);
|
videoTitleRoot.setClickable(false);
|
||||||
|
|
||||||
@ -1248,14 +1358,14 @@ public class VideoDetailFragment
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
imageLoader.cancelDisplayTask(thumbnailImageView);
|
IMAGE_LOADER.cancelDisplayTask(thumbnailImageView);
|
||||||
imageLoader.cancelDisplayTask(uploaderThumb);
|
IMAGE_LOADER.cancelDisplayTask(subChannelThumb);
|
||||||
thumbnailImageView.setImageBitmap(null);
|
thumbnailImageView.setImageBitmap(null);
|
||||||
uploaderThumb.setImageBitmap(null);
|
subChannelThumb.setImageBitmap(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleResult(@NonNull StreamInfo info) {
|
public void handleResult(@NonNull final StreamInfo info) {
|
||||||
super.handleResult(info);
|
super.handleResult(info);
|
||||||
|
|
||||||
currentInfo = info;
|
currentInfo = info;
|
||||||
@ -1263,11 +1373,13 @@ public class VideoDetailFragment
|
|||||||
|
|
||||||
if (showRelatedStreams) {
|
if (showRelatedStreams) {
|
||||||
if (null == relatedStreamsLayout) { //phone
|
if (null == relatedStreamsLayout) { //phone
|
||||||
pageAdapter.updateItem(RELATED_TAB_TAG, RelatedVideosFragment.getInstance(info));
|
pageAdapter.updateItem(RELATED_TAB_TAG,
|
||||||
|
RelatedVideosFragment.getInstance(info));
|
||||||
pageAdapter.notifyDataSetUpdate();
|
pageAdapter.notifyDataSetUpdate();
|
||||||
} else { //tablet
|
} else { //tablet
|
||||||
getChildFragmentManager().beginTransaction()
|
getChildFragmentManager().beginTransaction()
|
||||||
.replace(R.id.relatedStreamsLayout, RelatedVideosFragment.getInstance(info))
|
.replace(R.id.relatedStreamsLayout,
|
||||||
|
RelatedVideosFragment.getInstance(info))
|
||||||
.commitNow();
|
.commitNow();
|
||||||
relatedStreamsLayout.setVisibility(player != null && player.isFullscreen() ? View.GONE : View.VISIBLE);
|
relatedStreamsLayout.setVisibility(player != null && player.isFullscreen() ? View.GONE : View.VISIBLE);
|
||||||
}
|
}
|
||||||
@ -1276,22 +1388,28 @@ public class VideoDetailFragment
|
|||||||
animateView(thumbnailPlayButton, true, 200);
|
animateView(thumbnailPlayButton, true, 200);
|
||||||
videoTitleTextView.setText(name);
|
videoTitleTextView.setText(name);
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(info.getUploaderName())) {
|
if (!TextUtils.isEmpty(info.getSubChannelName())) {
|
||||||
uploaderTextView.setText(info.getUploaderName());
|
displayBothUploaderAndSubChannel(info);
|
||||||
uploaderTextView.setVisibility(View.VISIBLE);
|
} else if (!TextUtils.isEmpty(info.getUploaderName())) {
|
||||||
uploaderTextView.setSelected(true);
|
displayUploaderAsSubChannel(info);
|
||||||
} else {
|
} else {
|
||||||
uploaderTextView.setVisibility(View.GONE);
|
uploaderTextView.setVisibility(View.GONE);
|
||||||
|
uploaderThumb.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
uploaderThumb.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.buddy));
|
|
||||||
|
Drawable buddyDrawable = AppCompatResources.getDrawable(activity, R.drawable.buddy);
|
||||||
|
subChannelThumb.setImageDrawable(buddyDrawable);
|
||||||
|
uploaderThumb.setImageDrawable(buddyDrawable);
|
||||||
|
|
||||||
if (info.getViewCount() >= 0) {
|
if (info.getViewCount() >= 0) {
|
||||||
if (info.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) {
|
if (info.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) {
|
||||||
videoCountView.setText(Localization.listeningCount(activity, info.getViewCount()));
|
videoCountView.setText(Localization.listeningCount(activity, info.getViewCount()));
|
||||||
} else if (info.getStreamType().equals(StreamType.LIVE_STREAM)) {
|
} else if (info.getStreamType().equals(StreamType.LIVE_STREAM)) {
|
||||||
videoCountView.setText(Localization.watchingCount(activity, info.getViewCount()));
|
videoCountView.setText(Localization
|
||||||
|
.localizeWatchingCount(activity, info.getViewCount()));
|
||||||
} else {
|
} else {
|
||||||
videoCountView.setText(Localization.localizeViewCount(activity, info.getViewCount()));
|
videoCountView.setText(Localization
|
||||||
|
.localizeViewCount(activity, info.getViewCount()));
|
||||||
}
|
}
|
||||||
videoCountView.setVisibility(View.VISIBLE);
|
videoCountView.setVisibility(View.VISIBLE);
|
||||||
} else {
|
} else {
|
||||||
@ -1307,7 +1425,8 @@ public class VideoDetailFragment
|
|||||||
thumbsDisabledTextView.setVisibility(View.VISIBLE);
|
thumbsDisabledTextView.setVisibility(View.VISIBLE);
|
||||||
} else {
|
} else {
|
||||||
if (info.getDislikeCount() >= 0) {
|
if (info.getDislikeCount() >= 0) {
|
||||||
thumbsDownTextView.setText(Localization.shortCount(activity, info.getDislikeCount()));
|
thumbsDownTextView.setText(Localization
|
||||||
|
.shortCount(activity, info.getDislikeCount()));
|
||||||
thumbsDownTextView.setVisibility(View.VISIBLE);
|
thumbsDownTextView.setVisibility(View.VISIBLE);
|
||||||
thumbsDownImageView.setVisibility(View.VISIBLE);
|
thumbsDownImageView.setVisibility(View.VISIBLE);
|
||||||
} else {
|
} else {
|
||||||
@ -1342,12 +1461,14 @@ public class VideoDetailFragment
|
|||||||
|
|
||||||
videoDescriptionView.setVisibility(View.GONE);
|
videoDescriptionView.setVisibility(View.GONE);
|
||||||
videoTitleRoot.setClickable(true);
|
videoTitleRoot.setClickable(true);
|
||||||
|
videoTitleToggleArrow.setImageResource(
|
||||||
|
ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_expand_more));
|
||||||
videoTitleToggleArrow.setVisibility(View.VISIBLE);
|
videoTitleToggleArrow.setVisibility(View.VISIBLE);
|
||||||
videoTitleToggleArrow.setImageResource(R.drawable.arrow_down);
|
|
||||||
videoDescriptionRootLayout.setVisibility(View.GONE);
|
videoDescriptionRootLayout.setVisibility(View.GONE);
|
||||||
|
|
||||||
if (info.getUploadDate() != null) {
|
if (info.getUploadDate() != null) {
|
||||||
videoUploadDateView.setText(Localization.localizeUploadDate(activity, info.getUploadDate().date().getTime()));
|
videoUploadDateView.setText(Localization
|
||||||
|
.localizeUploadDate(activity, info.getUploadDate().date().getTime()));
|
||||||
videoUploadDateView.setVisibility(View.VISIBLE);
|
videoUploadDateView.setVisibility(View.VISIBLE);
|
||||||
} else {
|
} else {
|
||||||
videoUploadDateView.setText(null);
|
videoUploadDateView.setText(null);
|
||||||
@ -1382,16 +1503,54 @@ public class VideoDetailFragment
|
|||||||
detailControlsDownload.setVisibility(View.GONE);
|
detailControlsDownload.setVisibility(View.GONE);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
if (info.getAudioStreams().isEmpty()) detailControlsBackground.setVisibility(View.GONE);
|
if (info.getAudioStreams().isEmpty()) {
|
||||||
if (!info.getVideoStreams().isEmpty()
|
detailControlsBackground.setVisibility(View.GONE);
|
||||||
|| !info.getVideoOnlyStreams().isEmpty()) break;
|
}
|
||||||
|
if (!info.getVideoStreams().isEmpty() || !info.getVideoOnlyStreams().isEmpty()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
detailControlsPopup.setVisibility(View.GONE);
|
detailControlsPopup.setVisibility(View.GONE);
|
||||||
thumbnailPlayButton.setImageResource(R.drawable.ic_headset_white_24dp);
|
thumbnailPlayButton.setImageResource(R.drawable.ic_headset_shadow);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void hideAgeRestrictedContent() {
|
||||||
|
showError(getString(R.string.restricted_video), false);
|
||||||
|
|
||||||
|
if (relatedStreamsLayout != null) { // tablet
|
||||||
|
relatedStreamsLayout.setVisibility(View.INVISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
viewPager.setVisibility(View.GONE);
|
||||||
|
tabLayout.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void displayUploaderAsSubChannel(final StreamInfo info) {
|
||||||
|
subChannelTextView.setText(info.getUploaderName());
|
||||||
|
subChannelTextView.setVisibility(View.VISIBLE);
|
||||||
|
subChannelTextView.setSelected(true);
|
||||||
|
uploaderTextView.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void displayBothUploaderAndSubChannel(final StreamInfo info) {
|
||||||
|
subChannelTextView.setText(info.getSubChannelName());
|
||||||
|
subChannelTextView.setVisibility(View.VISIBLE);
|
||||||
|
subChannelTextView.setSelected(true);
|
||||||
|
|
||||||
|
subChannelThumb.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
|
if (!TextUtils.isEmpty(info.getUploaderName())) {
|
||||||
|
uploaderTextView.setText(
|
||||||
|
String.format(getString(R.string.video_detail_by), info.getUploaderName()));
|
||||||
|
uploaderTextView.setVisibility(View.VISIBLE);
|
||||||
|
uploaderTextView.setSelected(true);
|
||||||
|
} else {
|
||||||
|
uploaderTextView.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public void openDownloadDialog() {
|
public void openDownloadDialog() {
|
||||||
try {
|
try {
|
||||||
@ -1423,24 +1582,20 @@ public class VideoDetailFragment
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean onError(Throwable exception) {
|
protected boolean onError(final Throwable exception) {
|
||||||
if (super.onError(exception)) return true;
|
if (super.onError(exception)) {
|
||||||
|
return true;
|
||||||
else if (exception instanceof ContentNotAvailableException) {
|
|
||||||
showError(getString(R.string.content_not_available), false);
|
|
||||||
} else {
|
|
||||||
final int errorId = exception instanceof YoutubeStreamExtractor.DecryptException
|
|
||||||
? R.string.youtube_signature_decryption_error
|
|
||||||
: exception instanceof ParsingException
|
|
||||||
? R.string.parsing_error
|
|
||||||
: R.string.general_error;
|
|
||||||
onUnrecoverableError(exception,
|
|
||||||
UserAction.REQUESTED_STREAM,
|
|
||||||
NewPipe.getNameOfService(serviceId),
|
|
||||||
url,
|
|
||||||
errorId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int errorId = exception instanceof YoutubeStreamExtractor.DecryptException
|
||||||
|
? R.string.youtube_signature_decryption_error
|
||||||
|
: exception instanceof ExtractionException
|
||||||
|
? R.string.parsing_error
|
||||||
|
: R.string.general_error;
|
||||||
|
|
||||||
|
onUnrecoverableError(exception, UserAction.REQUESTED_STREAM,
|
||||||
|
NewPipe.getNameOfService(serviceId), url, errorId);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1449,9 +1604,9 @@ public class VideoDetailFragment
|
|||||||
positionSubscriber.dispose();
|
positionSubscriber.dispose();
|
||||||
}
|
}
|
||||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
|
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
|
||||||
final boolean playbackResumeEnabled =
|
final boolean playbackResumeEnabled = prefs
|
||||||
prefs.getBoolean(activity.getString(R.string.enable_watch_history_key), true)
|
.getBoolean(activity.getString(R.string.enable_watch_history_key), true)
|
||||||
&& prefs.getBoolean(activity.getString(R.string.enable_playback_resume_key), true);
|
&& prefs.getBoolean(activity.getString(R.string.enable_playback_resume_key), true);
|
||||||
final boolean showPlaybackPosition = prefs.getBoolean(
|
final boolean showPlaybackPosition = prefs.getBoolean(
|
||||||
activity.getString(R.string.enable_playback_state_lists_key), true);
|
activity.getString(R.string.enable_playback_state_lists_key), true);
|
||||||
if (!playbackResumeEnabled) {
|
if (!playbackResumeEnabled) {
|
||||||
@ -1459,6 +1614,12 @@ public class VideoDetailFragment
|
|||||||
|| playQueue.getItem().getRecoveryPosition() == RECOVERY_UNSET || !showPlaybackPosition) {
|
|| playQueue.getItem().getRecoveryPosition() == RECOVERY_UNSET || !showPlaybackPosition) {
|
||||||
positionView.setVisibility(View.INVISIBLE);
|
positionView.setVisibility(View.INVISIBLE);
|
||||||
detailPositionView.setVisibility(View.GONE);
|
detailPositionView.setVisibility(View.GONE);
|
||||||
|
// TODO: Remove this check when separation of concerns is done.
|
||||||
|
// (live streams weren't getting updated because they are mixed)
|
||||||
|
if (!info.getStreamType().equals(StreamType.LIVE_STREAM)
|
||||||
|
&& !info.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Show saved position from backStack if user allows it
|
// Show saved position from backStack if user allows it
|
||||||
showPlaybackProgress(playQueue.getItem().getRecoveryPosition(),
|
showPlaybackProgress(playQueue.getItem().getRecoveryPosition(),
|
||||||
@ -1466,9 +1627,12 @@ public class VideoDetailFragment
|
|||||||
animateView(positionView, true, 500);
|
animateView(positionView, true, 500);
|
||||||
animateView(detailPositionView, true, 500);
|
animateView(detailPositionView, true, 500);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final HistoryRecordManager recordManager = new HistoryRecordManager(requireContext());
|
final HistoryRecordManager recordManager = new HistoryRecordManager(requireContext());
|
||||||
|
|
||||||
|
// TODO: Separate concerns when updating database data.
|
||||||
|
// (move the updating part to when the loading happens)
|
||||||
positionSubscriber = recordManager.loadStreamState(info)
|
positionSubscriber = recordManager.loadStreamState(info)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.onErrorComplete()
|
.onErrorComplete()
|
||||||
@ -1478,7 +1642,9 @@ public class VideoDetailFragment
|
|||||||
animateView(positionView, true, 500);
|
animateView(positionView, true, 500);
|
||||||
animateView(detailPositionView, true, 500);
|
animateView(detailPositionView, true, 500);
|
||||||
}, e -> {
|
}, e -> {
|
||||||
if (DEBUG) e.printStackTrace();
|
if (DEBUG) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
}, () -> {
|
}, () -> {
|
||||||
positionView.setVisibility(View.GONE);
|
positionView.setVisibility(View.GONE);
|
||||||
detailPositionView.setVisibility(View.GONE);
|
detailPositionView.setVisibility(View.GONE);
|
||||||
@ -1489,7 +1655,7 @@ public class VideoDetailFragment
|
|||||||
final int progressSeconds = (int) TimeUnit.MILLISECONDS.toSeconds(progress);
|
final int progressSeconds = (int) TimeUnit.MILLISECONDS.toSeconds(progress);
|
||||||
final int durationSeconds = (int) TimeUnit.MILLISECONDS.toSeconds(duration);
|
final int durationSeconds = (int) TimeUnit.MILLISECONDS.toSeconds(duration);
|
||||||
positionView.setMax(durationSeconds);
|
positionView.setMax(durationSeconds);
|
||||||
positionView.setProgress(progressSeconds);
|
positionView.setProgressAnimated(progressSeconds);
|
||||||
detailPositionView.setText(Localization.getDurationString(progressSeconds));
|
detailPositionView.setText(Localization.getDurationString(progressSeconds));
|
||||||
if (positionView.getVisibility() != View.VISIBLE) {
|
if (positionView.getVisibility() != View.VISIBLE) {
|
||||||
animateView(positionView, true, 100);
|
animateView(positionView, true, 100);
|
||||||
@ -1862,12 +2028,12 @@ public class VideoDetailFragment
|
|||||||
overlayChannelTextView.setText(TextUtils.isEmpty(uploader) ? "" : uploader);
|
overlayChannelTextView.setText(TextUtils.isEmpty(uploader) ? "" : uploader);
|
||||||
overlayThumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark);
|
overlayThumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark);
|
||||||
if (!TextUtils.isEmpty(thumbnailUrl))
|
if (!TextUtils.isEmpty(thumbnailUrl))
|
||||||
imageLoader.displayImage(thumbnailUrl, overlayThumbnailImageView,
|
IMAGE_LOADER.displayImage(thumbnailUrl, overlayThumbnailImageView,
|
||||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, null);
|
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setOverlayPlayPauseImage() {
|
private void setOverlayPlayPauseImage() {
|
||||||
final int attr = player != null && player.getPlayer().getPlayWhenReady() ? R.attr.pause : R.attr.play;
|
final int attr = player != null && player.getPlayer().getPlayWhenReady() ? R.attr.ic_pause : R.attr.ic_play_arrow;
|
||||||
overlayPlayPauseButton.setImageResource(ThemeHelper.resolveResourceIdFromAttr(activity, attr));
|
overlayPlayPauseButton.setImageResource(ThemeHelper.resolveResourceIdFromAttr(activity, attr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,17 +7,17 @@ import android.content.res.Configuration;
|
|||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.appcompat.app.ActionBar;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import androidx.recyclerview.widget.GridLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.ActionBar;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||||
@ -34,13 +34,21 @@ import org.schabi.newpipe.util.NavigationHelper;
|
|||||||
import org.schabi.newpipe.util.OnClickGesture;
|
import org.schabi.newpipe.util.OnClickGesture;
|
||||||
import org.schabi.newpipe.util.StateSaver;
|
import org.schabi.newpipe.util.StateSaver;
|
||||||
import org.schabi.newpipe.util.StreamDialogEntry;
|
import org.schabi.newpipe.util.StreamDialogEntry;
|
||||||
|
import org.schabi.newpipe.views.SuperScrollLayoutManager;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
|
|
||||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||||
|
|
||||||
public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implements ListViewContract<I, N>, StateSaver.WriteRead, SharedPreferences.OnSharedPreferenceChangeListener {
|
public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||||
|
implements ListViewContract<I, N>, StateSaver.WriteRead,
|
||||||
|
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
|
private static final int LIST_MODE_UPDATE_FLAG = 0x32;
|
||||||
|
protected StateSaver.SavedState savedState;
|
||||||
|
|
||||||
|
private boolean useDefaultStateSaving = true;
|
||||||
|
private int updateFlags = 0;
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Views
|
// Views
|
||||||
@ -48,18 +56,19 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
|||||||
|
|
||||||
protected InfoListAdapter infoListAdapter;
|
protected InfoListAdapter infoListAdapter;
|
||||||
protected RecyclerView itemsList;
|
protected RecyclerView itemsList;
|
||||||
private int updateFlags = 0;
|
private int focusedPosition = -1;
|
||||||
|
|
||||||
private static final int LIST_MODE_UPDATE_FLAG = 0x32;
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// LifeCycle
|
// LifeCycle
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAttach(Context context) {
|
public void onAttach(final Context context) {
|
||||||
super.onAttach(context);
|
super.onAttach(context);
|
||||||
infoListAdapter = new InfoListAdapter(activity);
|
|
||||||
|
if (infoListAdapter == null) {
|
||||||
|
infoListAdapter = new InfoListAdapter(activity);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -68,7 +77,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(final Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setHasOptionsMenu(true);
|
setHasOptionsMenu(true);
|
||||||
PreferenceManager.getDefaultSharedPreferences(activity)
|
PreferenceManager.getDefaultSharedPreferences(activity)
|
||||||
@ -78,7 +87,9 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
|||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
StateSaver.onDestroy(savedState);
|
if (useDefaultStateSaving) {
|
||||||
|
StateSaver.onDestroy(savedState);
|
||||||
|
}
|
||||||
PreferenceManager.getDefaultSharedPreferences(activity)
|
PreferenceManager.getDefaultSharedPreferences(activity)
|
||||||
.unregisterOnSharedPreferenceChangeListener(this);
|
.unregisterOnSharedPreferenceChangeListener(this);
|
||||||
}
|
}
|
||||||
@ -90,8 +101,9 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
|||||||
if (updateFlags != 0) {
|
if (updateFlags != 0) {
|
||||||
if ((updateFlags & LIST_MODE_UPDATE_FLAG) != 0) {
|
if ((updateFlags & LIST_MODE_UPDATE_FLAG) != 0) {
|
||||||
final boolean useGrid = isGridLayout();
|
final boolean useGrid = isGridLayout();
|
||||||
itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager());
|
itemsList.setLayoutManager(useGrid
|
||||||
infoListAdapter.setGridItemVariants(useGrid);
|
? getGridLayoutManager() : getListLayoutManager());
|
||||||
|
infoListAdapter.setUseGridVariant(useGrid);
|
||||||
infoListAdapter.notifyDataSetChanged();
|
infoListAdapter.notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
updateFlags = 0;
|
updateFlags = 0;
|
||||||
@ -102,7 +114,15 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
|||||||
// State Saving
|
// State Saving
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
protected StateSaver.SavedState savedState;
|
/**
|
||||||
|
* If the default implementation of {@link StateSaver.WriteRead} should be used.
|
||||||
|
*
|
||||||
|
* @see StateSaver
|
||||||
|
* @param useDefaultStateSaving Whether the default implementation should be used
|
||||||
|
*/
|
||||||
|
public void setUseDefaultStateSaving(final boolean useDefaultStateSaving) {
|
||||||
|
this.useDefaultStateSaving = useDefaultStateSaving;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String generateSuffix() {
|
public String generateSuffix() {
|
||||||
@ -110,28 +130,81 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
|||||||
return "." + infoListAdapter.getItemsList().size() + ".list";
|
return "." + infoListAdapter.getItemsList().size() + ".list";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int getFocusedPosition() {
|
||||||
|
try {
|
||||||
|
final View focusedItem = itemsList.getFocusedChild();
|
||||||
|
final RecyclerView.ViewHolder itemHolder =
|
||||||
|
itemsList.findContainingViewHolder(focusedItem);
|
||||||
|
return itemHolder.getAdapterPosition();
|
||||||
|
} catch (NullPointerException e) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void writeTo(Queue<Object> objectsToSave) {
|
public void writeTo(final Queue<Object> objectsToSave) {
|
||||||
|
if (!useDefaultStateSaving) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
objectsToSave.add(infoListAdapter.getItemsList());
|
objectsToSave.add(infoListAdapter.getItemsList());
|
||||||
|
objectsToSave.add(getFocusedPosition());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception {
|
public void readFrom(@NonNull final Queue<Object> savedObjects) throws Exception {
|
||||||
|
if (!useDefaultStateSaving) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
infoListAdapter.getItemsList().clear();
|
infoListAdapter.getItemsList().clear();
|
||||||
infoListAdapter.getItemsList().addAll((List<InfoItem>) savedObjects.poll());
|
infoListAdapter.getItemsList().addAll((List<InfoItem>) savedObjects.poll());
|
||||||
|
restoreFocus((Integer) savedObjects.poll());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void restoreFocus(final Integer position) {
|
||||||
|
if (position == null || position < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsList.post(() -> {
|
||||||
|
RecyclerView.ViewHolder focusedHolder =
|
||||||
|
itemsList.findViewHolderForAdapterPosition(position);
|
||||||
|
|
||||||
|
if (focusedHolder != null) {
|
||||||
|
focusedHolder.itemView.requestFocus();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSaveInstanceState(Bundle bundle) {
|
public void onSaveInstanceState(final Bundle bundle) {
|
||||||
super.onSaveInstanceState(bundle);
|
super.onSaveInstanceState(bundle);
|
||||||
savedState = StateSaver.tryToSave(activity.isChangingConfigurations(), savedState, bundle, this);
|
if (useDefaultStateSaving) {
|
||||||
|
savedState = StateSaver
|
||||||
|
.tryToSave(activity.isChangingConfigurations(), savedState, bundle, this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onRestoreInstanceState(@NonNull Bundle bundle) {
|
protected void onRestoreInstanceState(@NonNull final Bundle bundle) {
|
||||||
super.onRestoreInstanceState(bundle);
|
super.onRestoreInstanceState(bundle);
|
||||||
savedState = StateSaver.tryToRestore(bundle, this);
|
if (useDefaultStateSaving) {
|
||||||
|
savedState = StateSaver.tryToRestore(bundle, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStop() {
|
||||||
|
focusedPosition = getFocusedPosition();
|
||||||
|
super.onStop();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStart() {
|
||||||
|
super.onStart();
|
||||||
|
restoreFocus(focusedPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
@ -147,36 +220,39 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected RecyclerView.LayoutManager getListLayoutManager() {
|
protected RecyclerView.LayoutManager getListLayoutManager() {
|
||||||
return new LinearLayoutManager(activity);
|
return new SuperScrollLayoutManager(activity);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected RecyclerView.LayoutManager getGridLayoutManager() {
|
protected RecyclerView.LayoutManager getGridLayoutManager() {
|
||||||
final Resources resources = activity.getResources();
|
final Resources resources = activity.getResources();
|
||||||
int width = resources.getDimensionPixelSize(R.dimen.video_item_grid_thumbnail_image_width);
|
int width = resources.getDimensionPixelSize(R.dimen.video_item_grid_thumbnail_image_width);
|
||||||
width += (24 * resources.getDisplayMetrics().density);
|
width += (24 * resources.getDisplayMetrics().density);
|
||||||
final int spanCount = (int) Math.floor(resources.getDisplayMetrics().widthPixels / (double)width);
|
final int spanCount = (int) Math.floor(resources.getDisplayMetrics().widthPixels
|
||||||
|
/ (double) width);
|
||||||
final GridLayoutManager lm = new GridLayoutManager(activity, spanCount);
|
final GridLayoutManager lm = new GridLayoutManager(activity, spanCount);
|
||||||
lm.setSpanSizeLookup(infoListAdapter.getSpanSizeLookup(spanCount));
|
lm.setSpanSizeLookup(infoListAdapter.getSpanSizeLookup(spanCount));
|
||||||
return lm;
|
return lm;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||||
super.initViews(rootView, savedInstanceState);
|
super.initViews(rootView, savedInstanceState);
|
||||||
|
|
||||||
final boolean useGrid = isGridLayout();
|
final boolean useGrid = isGridLayout();
|
||||||
itemsList = rootView.findViewById(R.id.items_list);
|
itemsList = rootView.findViewById(R.id.items_list);
|
||||||
itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager());
|
itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager());
|
||||||
|
|
||||||
infoListAdapter.setGridItemVariants(useGrid);
|
infoListAdapter.setUseGridVariant(useGrid);
|
||||||
infoListAdapter.setFooter(getListFooter());
|
infoListAdapter.setFooter(getListFooter());
|
||||||
infoListAdapter.setHeader(getListHeader());
|
infoListAdapter.setHeader(getListHeader());
|
||||||
|
|
||||||
itemsList.setAdapter(infoListAdapter);
|
itemsList.setAdapter(infoListAdapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void onItemSelected(InfoItem selectedItem) {
|
protected void onItemSelected(final InfoItem selectedItem) {
|
||||||
if (DEBUG) Log.d(TAG, "onItemSelected() called with: selectedItem = [" + selectedItem + "]");
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onItemSelected() called with: selectedItem = [" + selectedItem + "]");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -184,19 +260,19 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
|||||||
super.initListeners();
|
super.initListeners();
|
||||||
infoListAdapter.setOnStreamSelectedListener(new OnClickGesture<StreamInfoItem>() {
|
infoListAdapter.setOnStreamSelectedListener(new OnClickGesture<StreamInfoItem>() {
|
||||||
@Override
|
@Override
|
||||||
public void selected(StreamInfoItem selectedItem) {
|
public void selected(final StreamInfoItem selectedItem) {
|
||||||
onStreamSelected(selectedItem);
|
onStreamSelected(selectedItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void held(StreamInfoItem selectedItem) {
|
public void held(final StreamInfoItem selectedItem) {
|
||||||
showStreamDialog(selectedItem);
|
showStreamDialog(selectedItem);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
infoListAdapter.setOnChannelSelectedListener(new OnClickGesture<ChannelInfoItem>() {
|
infoListAdapter.setOnChannelSelectedListener(new OnClickGesture<ChannelInfoItem>() {
|
||||||
@Override
|
@Override
|
||||||
public void selected(ChannelInfoItem selectedItem) {
|
public void selected(final ChannelInfoItem selectedItem) {
|
||||||
try {
|
try {
|
||||||
onItemSelected(selectedItem);
|
onItemSelected(selectedItem);
|
||||||
NavigationHelper.openChannelFragment(getFM(),
|
NavigationHelper.openChannelFragment(getFM(),
|
||||||
@ -211,7 +287,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
|||||||
|
|
||||||
infoListAdapter.setOnPlaylistSelectedListener(new OnClickGesture<PlaylistInfoItem>() {
|
infoListAdapter.setOnPlaylistSelectedListener(new OnClickGesture<PlaylistInfoItem>() {
|
||||||
@Override
|
@Override
|
||||||
public void selected(PlaylistInfoItem selectedItem) {
|
public void selected(final PlaylistInfoItem selectedItem) {
|
||||||
try {
|
try {
|
||||||
onItemSelected(selectedItem);
|
onItemSelected(selectedItem);
|
||||||
NavigationHelper.openPlaylistFragment(getFM(),
|
NavigationHelper.openPlaylistFragment(getFM(),
|
||||||
@ -226,7 +302,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
|||||||
|
|
||||||
infoListAdapter.setOnCommentsSelectedListener(new OnClickGesture<CommentsInfoItem>() {
|
infoListAdapter.setOnCommentsSelectedListener(new OnClickGesture<CommentsInfoItem>() {
|
||||||
@Override
|
@Override
|
||||||
public void selected(CommentsInfoItem selectedItem) {
|
public void selected(final CommentsInfoItem selectedItem) {
|
||||||
onItemSelected(selectedItem);
|
onItemSelected(selectedItem);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -234,13 +310,13 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
|||||||
itemsList.clearOnScrollListeners();
|
itemsList.clearOnScrollListeners();
|
||||||
itemsList.addOnScrollListener(new OnScrollBelowItemsListener() {
|
itemsList.addOnScrollListener(new OnScrollBelowItemsListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onScrolledDown(RecyclerView recyclerView) {
|
public void onScrolledDown(final RecyclerView recyclerView) {
|
||||||
onScrollToBottom();
|
onScrollToBottom();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onStreamSelected(StreamInfoItem selectedItem) {
|
private void onStreamSelected(final StreamInfoItem selectedItem) {
|
||||||
onItemSelected(selectedItem);
|
onItemSelected(selectedItem);
|
||||||
NavigationHelper.openVideoDetailFragment(getFM(),
|
NavigationHelper.openVideoDetailFragment(getFM(),
|
||||||
selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName());
|
selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName());
|
||||||
@ -253,12 +329,12 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
protected void showStreamDialog(final StreamInfoItem item) {
|
protected void showStreamDialog(final StreamInfoItem item) {
|
||||||
final Context context = getContext();
|
final Context context = getContext();
|
||||||
final Activity activity = getActivity();
|
final Activity activity = getActivity();
|
||||||
if (context == null || context.getResources() == null || activity == null) return;
|
if (context == null || context.getResources() == null || activity == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (item.getStreamType() == StreamType.AUDIO_STREAM) {
|
if (item.getStreamType() == StreamType.AUDIO_STREAM) {
|
||||||
StreamDialogEntry.setEnabledEntries(
|
StreamDialogEntry.setEnabledEntries(
|
||||||
@ -276,8 +352,8 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
|||||||
StreamDialogEntry.share);
|
StreamDialogEntry.share);
|
||||||
}
|
}
|
||||||
|
|
||||||
new InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context), (dialog, which) ->
|
new InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context),
|
||||||
StreamDialogEntry.clickOn(which, this, item)).show();
|
(dialog, which) -> StreamDialogEntry.clickOn(which, this, item)).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
@ -285,8 +361,11 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
|
||||||
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]");
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onCreateOptionsMenu() called with: "
|
||||||
|
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||||
|
}
|
||||||
super.onCreateOptionsMenu(menu, inflater);
|
super.onCreateOptionsMenu(menu, inflater);
|
||||||
ActionBar supportActionBar = activity.getSupportActionBar();
|
ActionBar supportActionBar = activity.getSupportActionBar();
|
||||||
if (supportActionBar != null) {
|
if (supportActionBar != null) {
|
||||||
@ -324,7 +403,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void showError(String message, boolean showRetryButton) {
|
public void showError(final String message, final boolean showRetryButton) {
|
||||||
super.showError(message, showRetryButton);
|
super.showError(message, showRetryButton);
|
||||||
showListFooter(false);
|
showListFooter(false);
|
||||||
animateView(itemsList, false, 200);
|
animateView(itemsList, false, 200);
|
||||||
@ -346,25 +425,28 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleNextItems(N result) {
|
public void handleNextItems(final N result) {
|
||||||
isLoading.set(false);
|
isLoading.set(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences,
|
||||||
|
final String key) {
|
||||||
if (key.equals(getString(R.string.list_view_mode_key))) {
|
if (key.equals(getString(R.string.list_view_mode_key))) {
|
||||||
updateFlags |= LIST_MODE_UPDATE_FLAG;
|
updateFlags |= LIST_MODE_UPDATE_FLAG;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected boolean isGridLayout() {
|
protected boolean isGridLayout() {
|
||||||
final String list_mode = PreferenceManager.getDefaultSharedPreferences(activity).getString(getString(R.string.list_view_mode_key), getString(R.string.list_view_mode_value));
|
final String listMode = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||||
if ("auto".equals(list_mode)) {
|
.getString(getString(R.string.list_view_mode_key),
|
||||||
|
getString(R.string.list_view_mode_value));
|
||||||
|
if ("auto".equals(listMode)) {
|
||||||
final Configuration configuration = getResources().getConfiguration();
|
final Configuration configuration = getResources().getConfiguration();
|
||||||
return configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
return configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||||
&& configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE);
|
&& configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE);
|
||||||
} else {
|
} else {
|
||||||
return "grid".equals(list_mode);
|
return "grid".equals(listMode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,9 @@ import androidx.annotation.NonNull;
|
|||||||
|
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
import org.schabi.newpipe.extractor.ListInfo;
|
import org.schabi.newpipe.extractor.ListInfo;
|
||||||
|
import org.schabi.newpipe.extractor.Page;
|
||||||
import org.schabi.newpipe.util.Constants;
|
import org.schabi.newpipe.util.Constants;
|
||||||
|
import org.schabi.newpipe.views.NewPipeRecyclerView;
|
||||||
|
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
|
|
||||||
@ -21,7 +23,6 @@ import io.reactivex.schedulers.Schedulers;
|
|||||||
|
|
||||||
public abstract class BaseListInfoFragment<I extends ListInfo>
|
public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||||
extends BaseListFragment<I, ListExtractor.InfoItemsPage> {
|
extends BaseListFragment<I, ListExtractor.InfoItemsPage> {
|
||||||
|
|
||||||
@State
|
@State
|
||||||
protected int serviceId = Constants.NO_SERVICE_ID;
|
protected int serviceId = Constants.NO_SERVICE_ID;
|
||||||
@State
|
@State
|
||||||
@ -30,11 +31,11 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
|||||||
protected String url;
|
protected String url;
|
||||||
|
|
||||||
protected I currentInfo;
|
protected I currentInfo;
|
||||||
protected String currentNextPageUrl;
|
protected Page currentNextPage;
|
||||||
protected Disposable currentWorker;
|
protected Disposable currentWorker;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||||
super.initViews(rootView, savedInstanceState);
|
super.initViews(rootView, savedInstanceState);
|
||||||
setTitle(name);
|
setTitle(name);
|
||||||
showListFooter(hasMoreItems());
|
showListFooter(hasMoreItems());
|
||||||
@ -43,7 +44,9 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
|||||||
@Override
|
@Override
|
||||||
public void onPause() {
|
public void onPause() {
|
||||||
super.onPause();
|
super.onPause();
|
||||||
if (currentWorker != null) currentWorker.dispose();
|
if (currentWorker != null) {
|
||||||
|
currentWorker.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -73,18 +76,18 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void writeTo(Queue<Object> objectsToSave) {
|
public void writeTo(final Queue<Object> objectsToSave) {
|
||||||
super.writeTo(objectsToSave);
|
super.writeTo(objectsToSave);
|
||||||
objectsToSave.add(currentInfo);
|
objectsToSave.add(currentInfo);
|
||||||
objectsToSave.add(currentNextPageUrl);
|
objectsToSave.add(currentNextPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception {
|
public void readFrom(@NonNull final Queue<Object> savedObjects) throws Exception {
|
||||||
super.readFrom(savedObjects);
|
super.readFrom(savedObjects);
|
||||||
currentInfo = (I) savedObjects.poll();
|
currentInfo = (I) savedObjects.poll();
|
||||||
currentNextPageUrl = (String) savedObjects.poll();
|
currentNextPage = (Page) savedObjects.poll();
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
@ -92,10 +95,14 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
protected void doInitialLoadLogic() {
|
protected void doInitialLoadLogic() {
|
||||||
if (DEBUG) Log.d(TAG, "doInitialLoadLogic() called");
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "doInitialLoadLogic() called");
|
||||||
|
}
|
||||||
if (currentInfo == null) {
|
if (currentInfo == null) {
|
||||||
startLoading(false);
|
startLoading(false);
|
||||||
} else handleResult(currentInfo);
|
} else {
|
||||||
|
handleResult(currentInfo);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -103,43 +110,56 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
|||||||
* You can use the default implementations from {@link org.schabi.newpipe.util.ExtractorHelper}.
|
* 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
|
* @param forceLoad allow or disallow the result to come from the cache
|
||||||
|
* @return Rx {@link Single} containing the {@link ListInfo}
|
||||||
*/
|
*/
|
||||||
protected abstract Single<I> loadResult(boolean forceLoad);
|
protected abstract Single<I> loadResult(boolean forceLoad);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void startLoading(boolean forceLoad) {
|
public void startLoading(final boolean forceLoad) {
|
||||||
super.startLoading(forceLoad);
|
super.startLoading(forceLoad);
|
||||||
|
|
||||||
showListFooter(false);
|
showListFooter(false);
|
||||||
infoListAdapter.clearStreamItemList();
|
infoListAdapter.clearStreamItemList();
|
||||||
|
|
||||||
currentInfo = null;
|
currentInfo = null;
|
||||||
if (currentWorker != null) currentWorker.dispose();
|
if (currentWorker != null) {
|
||||||
|
currentWorker.dispose();
|
||||||
|
}
|
||||||
currentWorker = loadResult(forceLoad)
|
currentWorker = loadResult(forceLoad)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe((@NonNull I result) -> {
|
.subscribe((@NonNull I result) -> {
|
||||||
isLoading.set(false);
|
isLoading.set(false);
|
||||||
currentInfo = result;
|
currentInfo = result;
|
||||||
currentNextPageUrl = result.getNextPageUrl();
|
currentNextPage = result.getNextPage();
|
||||||
handleResult(result);
|
handleResult(result);
|
||||||
}, (@NonNull Throwable throwable) -> onError(throwable));
|
}, (@NonNull Throwable throwable) -> onError(throwable));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implement the logic to load more items<br/>
|
* Implement the logic to load more items.
|
||||||
* You can use the default implementations from {@link org.schabi.newpipe.util.ExtractorHelper}
|
* <p>You can use the default implementations
|
||||||
|
* from {@link org.schabi.newpipe.util.ExtractorHelper}.</p>
|
||||||
|
*
|
||||||
|
* @return Rx {@link Single} containing the {@link ListExtractor.InfoItemsPage}
|
||||||
*/
|
*/
|
||||||
protected abstract Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic();
|
protected abstract Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic();
|
||||||
|
|
||||||
protected void loadMoreItems() {
|
protected void loadMoreItems() {
|
||||||
isLoading.set(true);
|
isLoading.set(true);
|
||||||
|
|
||||||
if (currentWorker != null) currentWorker.dispose();
|
if (currentWorker != null) {
|
||||||
|
currentWorker.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
forbidDownwardFocusScroll();
|
||||||
|
|
||||||
currentWorker = loadMoreItemsLogic()
|
currentWorker = loadMoreItemsLogic()
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe((@io.reactivex.annotations.NonNull ListExtractor.InfoItemsPage InfoItemsPage) -> {
|
.doFinally(this::allowDownwardFocusScroll)
|
||||||
|
.subscribe((@io.reactivex.annotations.NonNull
|
||||||
|
ListExtractor.InfoItemsPage InfoItemsPage) -> {
|
||||||
isLoading.set(false);
|
isLoading.set(false);
|
||||||
handleNextItems(InfoItemsPage);
|
handleNextItems(InfoItemsPage);
|
||||||
}, (@io.reactivex.annotations.NonNull Throwable throwable) -> {
|
}, (@io.reactivex.annotations.NonNull Throwable throwable) -> {
|
||||||
@ -148,10 +168,22 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void forbidDownwardFocusScroll() {
|
||||||
|
if (itemsList instanceof NewPipeRecyclerView) {
|
||||||
|
((NewPipeRecyclerView) itemsList).setFocusScrollAllowed(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void allowDownwardFocusScroll() {
|
||||||
|
if (itemsList instanceof NewPipeRecyclerView) {
|
||||||
|
((NewPipeRecyclerView) itemsList).setFocusScrollAllowed(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleNextItems(ListExtractor.InfoItemsPage result) {
|
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
|
||||||
super.handleNextItems(result);
|
super.handleNextItems(result);
|
||||||
currentNextPageUrl = result.getNextPageUrl();
|
currentNextPage = result.getNextPage();
|
||||||
infoListAdapter.addInfoItemList(result.getItems());
|
infoListAdapter.addInfoItemList(result.getItems());
|
||||||
|
|
||||||
showListFooter(hasMoreItems());
|
showListFooter(hasMoreItems());
|
||||||
@ -159,7 +191,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean hasMoreItems() {
|
protected boolean hasMoreItems() {
|
||||||
return !TextUtils.isEmpty(currentNextPageUrl);
|
return Page.isValid(currentNextPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
@ -167,7 +199,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleResult(@NonNull I result) {
|
public void handleResult(@NonNull final I result) {
|
||||||
super.handleResult(result);
|
super.handleResult(result);
|
||||||
|
|
||||||
name = result.getName();
|
name = result.getName();
|
||||||
@ -188,9 +220,9 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
|||||||
// Utils
|
// Utils
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
protected void setInitialData(int serviceId, String url, String name) {
|
protected void setInitialData(final int sid, final String u, final String title) {
|
||||||
this.serviceId = serviceId;
|
this.serviceId = sid;
|
||||||
this.url = url;
|
this.url = u;
|
||||||
this.name = !TextUtils.isEmpty(name) ? name : "";
|
this.name = !TextUtils.isEmpty(title) ? title : "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,12 +4,9 @@ import android.content.Context;
|
|||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
import androidx.appcompat.app.ActionBar;
|
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.util.TypedValue;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
@ -21,6 +18,12 @@ import android.widget.ImageView;
|
|||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.appcompat.app.ActionBar;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
import com.jakewharton.rxbinding2.view.RxView;
|
import com.jakewharton.rxbinding2.view.RxView;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
@ -29,13 +32,14 @@ import org.schabi.newpipe.extractor.InfoItem;
|
|||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
|
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||||
import org.schabi.newpipe.local.subscription.SubscriptionService;
|
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
||||||
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
|
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
|
import org.schabi.newpipe.report.ErrorActivity;
|
||||||
import org.schabi.newpipe.report.UserAction;
|
import org.schabi.newpipe.report.UserAction;
|
||||||
import org.schabi.newpipe.util.AnimationUtils;
|
import org.schabi.newpipe.util.AnimationUtils;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
@ -43,8 +47,10 @@ import org.schabi.newpipe.util.ImageDisplayConstants;
|
|||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.ShareUtils;
|
import org.schabi.newpipe.util.ShareUtils;
|
||||||
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
@ -62,42 +68,43 @@ import static org.schabi.newpipe.util.AnimationUtils.animateBackgroundColor;
|
|||||||
import static org.schabi.newpipe.util.AnimationUtils.animateTextColor;
|
import static org.schabi.newpipe.util.AnimationUtils.animateTextColor;
|
||||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||||
|
|
||||||
public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||||
|
implements View.OnClickListener {
|
||||||
|
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
|
||||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||||
private Disposable subscribeButtonMonitor;
|
private Disposable subscribeButtonMonitor;
|
||||||
private SubscriptionService subscriptionService;
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Views
|
// Views
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
private SubscriptionManager subscriptionManager;
|
||||||
private View headerRootLayout;
|
private View headerRootLayout;
|
||||||
private ImageView headerChannelBanner;
|
private ImageView headerChannelBanner;
|
||||||
private ImageView headerAvatarView;
|
private ImageView headerAvatarView;
|
||||||
private TextView headerTitleView;
|
private TextView headerTitleView;
|
||||||
|
private ImageView headerSubChannelAvatarView;
|
||||||
|
private TextView headerSubChannelTitleView;
|
||||||
private TextView headerSubscribersTextView;
|
private TextView headerSubscribersTextView;
|
||||||
private Button headerSubscribeButton;
|
private Button headerSubscribeButton;
|
||||||
private View playlistCtrl;
|
private View playlistCtrl;
|
||||||
|
|
||||||
private LinearLayout headerPlayAllButton;
|
private LinearLayout headerPlayAllButton;
|
||||||
private LinearLayout headerPopupButton;
|
private LinearLayout headerPopupButton;
|
||||||
private LinearLayout headerBackgroundButton;
|
private LinearLayout headerBackgroundButton;
|
||||||
|
|
||||||
private MenuItem menuRssButton;
|
private MenuItem menuRssButton;
|
||||||
|
private TextView contentNotSupportedTextView;
|
||||||
|
private TextView kaomojiTextView;
|
||||||
|
private TextView noVideosTextView;
|
||||||
|
|
||||||
public static ChannelFragment getInstance(int serviceId, String url, String name) {
|
public static ChannelFragment getInstance(final int serviceId, final String url,
|
||||||
|
final String name) {
|
||||||
ChannelFragment instance = new ChannelFragment();
|
ChannelFragment instance = new ChannelFragment();
|
||||||
instance.setInitialData(serviceId, url, name);
|
instance.setInitialData(serviceId, url, name);
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// LifeCycle
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setUserVisibleHint(boolean isVisibleToUser) {
|
public void setUserVisibleHint(final boolean isVisibleToUser) {
|
||||||
super.setUserVisibleHint(isVisibleToUser);
|
super.setUserVisibleHint(isVisibleToUser);
|
||||||
if (activity != null
|
if (activity != null
|
||||||
&& useAsFrontPage
|
&& useAsFrontPage
|
||||||
@ -106,22 +113,40 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// LifeCycle
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAttach(Context context) {
|
public void onAttach(final Context context) {
|
||||||
super.onAttach(context);
|
super.onAttach(context);
|
||||||
subscriptionService = SubscriptionService.getInstance(activity);
|
subscriptionManager = new SubscriptionManager(activity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||||
|
@Nullable final ViewGroup container,
|
||||||
|
@Nullable final Bundle savedInstanceState) {
|
||||||
return inflater.inflate(R.layout.fragment_channel, container, false);
|
return inflater.inflate(R.layout.fragment_channel, container, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(final View rootView, final Bundle savedInstanceState) {
|
||||||
|
super.onViewCreated(rootView, savedInstanceState);
|
||||||
|
contentNotSupportedTextView = rootView.findViewById(R.id.error_content_not_supported);
|
||||||
|
kaomojiTextView = rootView.findViewById(R.id.channel_kaomoji);
|
||||||
|
noVideosTextView = rootView.findViewById(R.id.channel_no_videos);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
if (disposables != null) disposables.clear();
|
if (disposables != null) {
|
||||||
if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose();
|
disposables.clear();
|
||||||
|
}
|
||||||
|
if (subscribeButtonMonitor != null) {
|
||||||
|
subscribeButtonMonitor.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
@ -129,14 +154,18 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
protected View getListHeader() {
|
protected View getListHeader() {
|
||||||
headerRootLayout = activity.getLayoutInflater().inflate(R.layout.channel_header, itemsList, false);
|
headerRootLayout = activity.getLayoutInflater()
|
||||||
|
.inflate(R.layout.channel_header, itemsList, false);
|
||||||
headerChannelBanner = headerRootLayout.findViewById(R.id.channel_banner_image);
|
headerChannelBanner = headerRootLayout.findViewById(R.id.channel_banner_image);
|
||||||
headerAvatarView = headerRootLayout.findViewById(R.id.channel_avatar_view);
|
headerAvatarView = headerRootLayout.findViewById(R.id.channel_avatar_view);
|
||||||
headerTitleView = headerRootLayout.findViewById(R.id.channel_title_view);
|
headerTitleView = headerRootLayout.findViewById(R.id.channel_title_view);
|
||||||
headerSubscribersTextView = headerRootLayout.findViewById(R.id.channel_subscriber_view);
|
headerSubscribersTextView = headerRootLayout.findViewById(R.id.channel_subscriber_view);
|
||||||
headerSubscribeButton = headerRootLayout.findViewById(R.id.channel_subscribe_button);
|
headerSubscribeButton = headerRootLayout.findViewById(R.id.channel_subscribe_button);
|
||||||
playlistCtrl = headerRootLayout.findViewById(R.id.playlist_control);
|
playlistCtrl = headerRootLayout.findViewById(R.id.playlist_control);
|
||||||
|
headerSubChannelAvatarView =
|
||||||
|
headerRootLayout.findViewById(R.id.sub_channel_avatar_view);
|
||||||
|
headerSubChannelTitleView =
|
||||||
|
headerRootLayout.findViewById(R.id.sub_channel_title_view);
|
||||||
|
|
||||||
headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_all_button);
|
headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_all_button);
|
||||||
headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button);
|
headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button);
|
||||||
@ -145,12 +174,20 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
|||||||
return headerRootLayout;
|
return headerRootLayout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initListeners() {
|
||||||
|
super.initListeners();
|
||||||
|
|
||||||
|
headerSubChannelTitleView.setOnClickListener(this);
|
||||||
|
headerSubChannelAvatarView.setOnClickListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Menu
|
// Menu
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
|
||||||
super.onCreateOptionsMenu(menu, inflater);
|
super.onCreateOptionsMenu(menu, inflater);
|
||||||
ActionBar supportActionBar = activity.getSupportActionBar();
|
ActionBar supportActionBar = activity.getSupportActionBar();
|
||||||
if (useAsFrontPage && supportActionBar != null) {
|
if (useAsFrontPage && supportActionBar != null) {
|
||||||
@ -158,8 +195,10 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
|||||||
} else {
|
} else {
|
||||||
inflater.inflate(R.menu.menu_channel, menu);
|
inflater.inflate(R.menu.menu_channel, menu);
|
||||||
|
|
||||||
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu +
|
if (DEBUG) {
|
||||||
"], inflater = [" + inflater + "]");
|
Log.d(TAG, "onCreateOptionsMenu() called with: "
|
||||||
|
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||||
|
}
|
||||||
menuRssButton = menu.findItem(R.id.menu_item_rss);
|
menuRssButton = menu.findItem(R.id.menu_item_rss);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -173,19 +212,22 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
|
case R.id.action_settings:
|
||||||
|
NavigationHelper.openSettings(requireContext());
|
||||||
|
break;
|
||||||
case R.id.menu_item_rss:
|
case R.id.menu_item_rss:
|
||||||
openRssFeed();
|
openRssFeed();
|
||||||
break;
|
break;
|
||||||
case R.id.menu_item_openInBrowser:
|
case R.id.menu_item_openInBrowser:
|
||||||
if (currentInfo != null) {
|
if (currentInfo != null) {
|
||||||
ShareUtils.openUrlInBrowser(this.getContext(), currentInfo.getOriginalUrl());
|
ShareUtils.openUrlInBrowser(requireContext(), currentInfo.getOriginalUrl());
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case R.id.menu_item_share:
|
case R.id.menu_item_share:
|
||||||
if (currentInfo != null) {
|
if (currentInfo != null) {
|
||||||
ShareUtils.shareUrl(this.getContext(), name, currentInfo.getOriginalUrl());
|
ShareUtils.shareUrl(requireContext(), name, currentInfo.getOriginalUrl());
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@ -198,19 +240,17 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
|||||||
// Channel Subscription
|
// Channel Subscription
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
|
|
||||||
|
|
||||||
private void monitorSubscription(final ChannelInfo info) {
|
private void monitorSubscription(final ChannelInfo info) {
|
||||||
final Consumer<Throwable> onError = (Throwable throwable) -> {
|
final Consumer<Throwable> onError = (Throwable throwable) -> {
|
||||||
animateView(headerSubscribeButton, false, 100);
|
animateView(headerSubscribeButton, false, 100);
|
||||||
showSnackBarError(throwable, UserAction.SUBSCRIPTION,
|
showSnackBarError(throwable, UserAction.SUBSCRIPTION,
|
||||||
NewPipe.getNameOfService(currentInfo.getServiceId()),
|
NewPipe.getNameOfService(currentInfo.getServiceId()),
|
||||||
"Get subscription status",
|
"Get subscription status", 0);
|
||||||
0);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
final Observable<List<SubscriptionEntity>> observable = subscriptionService.subscriptionTable()
|
final Observable<List<SubscriptionEntity>> observable = subscriptionManager
|
||||||
.getSubscription(info.getServiceId(), info.getUrl())
|
.subscriptionTable()
|
||||||
|
.getSubscriptionFlowable(info.getServiceId(), info.getUrl())
|
||||||
.toObservable();
|
.toObservable();
|
||||||
|
|
||||||
disposables.add(observable
|
disposables.add(observable
|
||||||
@ -218,34 +258,40 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
|||||||
.subscribe(getSubscribeUpdateMonitor(info), onError));
|
.subscribe(getSubscribeUpdateMonitor(info), onError));
|
||||||
|
|
||||||
disposables.add(observable
|
disposables.add(observable
|
||||||
// Some updates are very rapid (when calling the updateSubscription(info), for example)
|
// Some updates are very rapid
|
||||||
// so only update the UI for the latest emission ("sync" the subscribe button's state)
|
// (for example when calling the updateSubscription(info))
|
||||||
|
// so only update the UI for the latest emission
|
||||||
|
// ("sync" the subscribe button's state)
|
||||||
.debounce(100, TimeUnit.MILLISECONDS)
|
.debounce(100, TimeUnit.MILLISECONDS)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe((List<SubscriptionEntity> subscriptionEntities) ->
|
.subscribe((List<SubscriptionEntity> subscriptionEntities) ->
|
||||||
updateSubscribeButton(!subscriptionEntities.isEmpty())
|
updateSubscribeButton(!subscriptionEntities.isEmpty()), onError));
|
||||||
, onError));
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription) {
|
private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription,
|
||||||
|
final ChannelInfo info) {
|
||||||
return (@NonNull Object o) -> {
|
return (@NonNull Object o) -> {
|
||||||
subscriptionService.subscriptionTable().insert(subscription);
|
subscriptionManager.insertSubscription(subscription, info);
|
||||||
return o;
|
return o;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private Function<Object, Object> mapOnUnsubscribe(final SubscriptionEntity subscription) {
|
private Function<Object, Object> mapOnUnsubscribe(final SubscriptionEntity subscription) {
|
||||||
return (@NonNull Object o) -> {
|
return (@NonNull Object o) -> {
|
||||||
subscriptionService.subscriptionTable().delete(subscription);
|
subscriptionManager.deleteSubscription(subscription);
|
||||||
return o;
|
return o;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateSubscription(final ChannelInfo info) {
|
private void updateSubscription(final ChannelInfo info) {
|
||||||
if (DEBUG) Log.d(TAG, "updateSubscription() called with: info = [" + info + "]");
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "updateSubscription() called with: info = [" + info + "]");
|
||||||
|
}
|
||||||
final Action onComplete = () -> {
|
final Action onComplete = () -> {
|
||||||
if (DEBUG) Log.d(TAG, "Updated subscription: " + info.getUrl());
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "Updated subscription: " + info.getUrl());
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
final Consumer<Throwable> onError = (@NonNull Throwable throwable) ->
|
final Consumer<Throwable> onError = (@NonNull Throwable throwable) ->
|
||||||
@ -255,15 +301,18 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
|||||||
"Updating Subscription for " + info.getUrl(),
|
"Updating Subscription for " + info.getUrl(),
|
||||||
R.string.subscription_update_failed);
|
R.string.subscription_update_failed);
|
||||||
|
|
||||||
disposables.add(subscriptionService.updateChannelInfo(info)
|
disposables.add(subscriptionManager.updateChannelInfo(info)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(onComplete, onError));
|
.subscribe(onComplete, onError));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Disposable monitorSubscribeButton(final Button subscribeButton, final Function<Object, Object> action) {
|
private Disposable monitorSubscribeButton(final Button subscribeButton,
|
||||||
|
final Function<Object, Object> action) {
|
||||||
final Consumer<Object> onNext = (@NonNull Object o) -> {
|
final Consumer<Object> onNext = (@NonNull Object o) -> {
|
||||||
if (DEBUG) Log.d(TAG, "Changed subscription status to this channel!");
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "Changed subscription status to this channel!");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
final Consumer<Throwable> onError = (@NonNull Throwable throwable) ->
|
final Consumer<Throwable> onError = (@NonNull Throwable throwable) ->
|
||||||
@ -284,12 +333,18 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
|||||||
|
|
||||||
private Consumer<List<SubscriptionEntity>> getSubscribeUpdateMonitor(final ChannelInfo info) {
|
private Consumer<List<SubscriptionEntity>> getSubscribeUpdateMonitor(final ChannelInfo info) {
|
||||||
return (List<SubscriptionEntity> subscriptionEntities) -> {
|
return (List<SubscriptionEntity> subscriptionEntities) -> {
|
||||||
if (DEBUG)
|
if (DEBUG) {
|
||||||
Log.d(TAG, "subscriptionService.subscriptionTable.doOnNext() called with: subscriptionEntities = [" + subscriptionEntities + "]");
|
Log.d(TAG, "subscriptionManager.subscriptionTable.doOnNext() called with: "
|
||||||
if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose();
|
+ "subscriptionEntities = [" + subscriptionEntities + "]");
|
||||||
|
}
|
||||||
|
if (subscribeButtonMonitor != null) {
|
||||||
|
subscribeButtonMonitor.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
if (subscriptionEntities.isEmpty()) {
|
if (subscriptionEntities.isEmpty()) {
|
||||||
if (DEBUG) Log.d(TAG, "No subscription to this channel!");
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "No subscription to this channel!");
|
||||||
|
}
|
||||||
SubscriptionEntity channel = new SubscriptionEntity();
|
SubscriptionEntity channel = new SubscriptionEntity();
|
||||||
channel.setServiceId(info.getServiceId());
|
channel.setServiceId(info.getServiceId());
|
||||||
channel.setUrl(info.getUrl());
|
channel.setUrl(info.getUrl());
|
||||||
@ -297,34 +352,45 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
|||||||
info.getAvatarUrl(),
|
info.getAvatarUrl(),
|
||||||
info.getDescription(),
|
info.getDescription(),
|
||||||
info.getSubscriberCount());
|
info.getSubscriberCount());
|
||||||
subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnSubscribe(channel));
|
subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton,
|
||||||
|
mapOnSubscribe(channel, info));
|
||||||
} else {
|
} else {
|
||||||
if (DEBUG) Log.d(TAG, "Found subscription to this channel!");
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "Found subscription to this channel!");
|
||||||
|
}
|
||||||
final SubscriptionEntity subscription = subscriptionEntities.get(0);
|
final SubscriptionEntity subscription = subscriptionEntities.get(0);
|
||||||
subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnUnsubscribe(subscription));
|
subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton,
|
||||||
|
mapOnUnsubscribe(subscription));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateSubscribeButton(boolean isSubscribed) {
|
private void updateSubscribeButton(final boolean isSubscribed) {
|
||||||
if (DEBUG) Log.d(TAG, "updateSubscribeButton() called with: isSubscribed = [" + isSubscribed + "]");
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "updateSubscribeButton() called with: "
|
||||||
|
+ "isSubscribed = [" + isSubscribed + "]");
|
||||||
|
}
|
||||||
|
|
||||||
boolean isButtonVisible = headerSubscribeButton.getVisibility() == View.VISIBLE;
|
boolean isButtonVisible = headerSubscribeButton.getVisibility() == View.VISIBLE;
|
||||||
int backgroundDuration = isButtonVisible ? 300 : 0;
|
int backgroundDuration = isButtonVisible ? 300 : 0;
|
||||||
int textDuration = isButtonVisible ? 200 : 0;
|
int textDuration = isButtonVisible ? 200 : 0;
|
||||||
|
|
||||||
int subscribeBackground = ContextCompat.getColor(activity, R.color.subscribe_background_color);
|
int subscribeBackground = ThemeHelper
|
||||||
|
.resolveColorFromAttr(activity, R.attr.colorPrimary);
|
||||||
int subscribeText = ContextCompat.getColor(activity, R.color.subscribe_text_color);
|
int subscribeText = ContextCompat.getColor(activity, R.color.subscribe_text_color);
|
||||||
int subscribedBackground = ContextCompat.getColor(activity, R.color.subscribed_background_color);
|
int subscribedBackground = ContextCompat
|
||||||
|
.getColor(activity, R.color.subscribed_background_color);
|
||||||
int subscribedText = ContextCompat.getColor(activity, R.color.subscribed_text_color);
|
int subscribedText = ContextCompat.getColor(activity, R.color.subscribed_text_color);
|
||||||
|
|
||||||
if (!isSubscribed) {
|
if (!isSubscribed) {
|
||||||
headerSubscribeButton.setText(R.string.subscribe_button_title);
|
headerSubscribeButton.setText(R.string.subscribe_button_title);
|
||||||
animateBackgroundColor(headerSubscribeButton, backgroundDuration, subscribedBackground, subscribeBackground);
|
animateBackgroundColor(headerSubscribeButton, backgroundDuration, subscribedBackground,
|
||||||
|
subscribeBackground);
|
||||||
animateTextColor(headerSubscribeButton, textDuration, subscribedText, subscribeText);
|
animateTextColor(headerSubscribeButton, textDuration, subscribedText, subscribeText);
|
||||||
} else {
|
} else {
|
||||||
headerSubscribeButton.setText(R.string.subscribed_button_title);
|
headerSubscribeButton.setText(R.string.subscribed_button_title);
|
||||||
animateBackgroundColor(headerSubscribeButton, backgroundDuration, subscribeBackground, subscribedBackground);
|
animateBackgroundColor(headerSubscribeButton, backgroundDuration, subscribeBackground,
|
||||||
|
subscribedBackground);
|
||||||
animateTextColor(headerSubscribeButton, textDuration, subscribeText, subscribedText);
|
animateTextColor(headerSubscribeButton, textDuration, subscribeText, subscribedText);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -337,14 +403,42 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
|
protected Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
|
||||||
return ExtractorHelper.getMoreChannelItems(serviceId, url, currentNextPageUrl);
|
return ExtractorHelper.getMoreChannelItems(serviceId, url, currentNextPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Single<ChannelInfo> loadResult(boolean forceLoad) {
|
protected Single<ChannelInfo> loadResult(final boolean forceLoad) {
|
||||||
return ExtractorHelper.getChannelInfo(serviceId, url, forceLoad);
|
return ExtractorHelper.getChannelInfo(serviceId, url, forceLoad);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// OnClick
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(final View v) {
|
||||||
|
if (isLoading.get() || currentInfo == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (v.getId()) {
|
||||||
|
case R.id.sub_channel_avatar_view:
|
||||||
|
case R.id.sub_channel_title_view:
|
||||||
|
if (!TextUtils.isEmpty(currentInfo.getParentChannelUrl())) {
|
||||||
|
try {
|
||||||
|
NavigationHelper.openChannelFragment(getFragmentManager(),
|
||||||
|
currentInfo.getServiceId(), currentInfo.getParentChannelUrl(),
|
||||||
|
currentInfo.getParentChannelName());
|
||||||
|
} catch (Exception e) {
|
||||||
|
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
|
||||||
|
}
|
||||||
|
} else if (DEBUG) {
|
||||||
|
Log.i(TAG, "Can't open parent channel because we got no channel URL");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Contract
|
// Contract
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
@ -353,47 +447,100 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
|||||||
public void showLoading() {
|
public void showLoading() {
|
||||||
super.showLoading();
|
super.showLoading();
|
||||||
|
|
||||||
imageLoader.cancelDisplayTask(headerChannelBanner);
|
IMAGE_LOADER.cancelDisplayTask(headerChannelBanner);
|
||||||
imageLoader.cancelDisplayTask(headerAvatarView);
|
IMAGE_LOADER.cancelDisplayTask(headerAvatarView);
|
||||||
|
IMAGE_LOADER.cancelDisplayTask(headerSubChannelAvatarView);
|
||||||
animateView(headerSubscribeButton, false, 100);
|
animateView(headerSubscribeButton, false, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleResult(@NonNull ChannelInfo result) {
|
public void handleResult(@NonNull final ChannelInfo result) {
|
||||||
super.handleResult(result);
|
super.handleResult(result);
|
||||||
|
|
||||||
headerRootLayout.setVisibility(View.VISIBLE);
|
headerRootLayout.setVisibility(View.VISIBLE);
|
||||||
imageLoader.displayImage(result.getBannerUrl(), headerChannelBanner,
|
IMAGE_LOADER.displayImage(result.getBannerUrl(), headerChannelBanner,
|
||||||
ImageDisplayConstants.DISPLAY_BANNER_OPTIONS);
|
ImageDisplayConstants.DISPLAY_BANNER_OPTIONS);
|
||||||
imageLoader.displayImage(result.getAvatarUrl(), headerAvatarView,
|
IMAGE_LOADER.displayImage(result.getAvatarUrl(), headerAvatarView,
|
||||||
|
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||||
|
IMAGE_LOADER.displayImage(result.getParentChannelAvatarUrl(), headerSubChannelAvatarView,
|
||||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||||
|
|
||||||
headerSubscribersTextView.setVisibility(View.VISIBLE);
|
headerSubscribersTextView.setVisibility(View.VISIBLE);
|
||||||
if (result.getSubscriberCount() >= 0) {
|
if (result.getSubscriberCount() >= 0) {
|
||||||
headerSubscribersTextView.setText(Localization.localizeSubscribersCount(activity, result.getSubscriberCount()));
|
headerSubscribersTextView.setText(Localization
|
||||||
|
.shortSubscriberCount(activity, result.getSubscriberCount()));
|
||||||
} else {
|
} else {
|
||||||
headerSubscribersTextView.setText(R.string.subscribers_count_not_available);
|
headerSubscribersTextView.setText(R.string.subscribers_count_not_available);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (menuRssButton != null) menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl()));
|
if (!TextUtils.isEmpty(currentInfo.getParentChannelName())) {
|
||||||
|
headerSubChannelTitleView.setText(String.format(
|
||||||
|
getString(R.string.channel_created_by),
|
||||||
|
currentInfo.getParentChannelName())
|
||||||
|
);
|
||||||
|
headerSubChannelTitleView.setVisibility(View.VISIBLE);
|
||||||
|
headerSubChannelAvatarView.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
headerSubChannelTitleView.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (menuRssButton != null) {
|
||||||
|
menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl()));
|
||||||
|
}
|
||||||
|
|
||||||
playlistCtrl.setVisibility(View.VISIBLE);
|
playlistCtrl.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
if (!result.getErrors().isEmpty()) {
|
List<Throwable> errors = new ArrayList<>(result.getErrors());
|
||||||
showSnackBarError(result.getErrors(), UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
|
if (!errors.isEmpty()) {
|
||||||
|
|
||||||
|
// handling ContentNotSupportedException not to show the error but an appropriate string
|
||||||
|
// so that crashes won't be sent uselessly and the user will understand what happened
|
||||||
|
for (Iterator<Throwable> it = errors.iterator(); it.hasNext();) {
|
||||||
|
Throwable throwable = it.next();
|
||||||
|
if (throwable instanceof ContentNotSupportedException) {
|
||||||
|
showContentNotSupported();
|
||||||
|
it.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
showSnackBarError(errors, UserAction.REQUESTED_CHANNEL,
|
||||||
|
NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (disposables != null) disposables.clear();
|
if (disposables != null) {
|
||||||
if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose();
|
disposables.clear();
|
||||||
|
}
|
||||||
|
if (subscribeButtonMonitor != null) {
|
||||||
|
subscribeButtonMonitor.dispose();
|
||||||
|
}
|
||||||
updateSubscription(result);
|
updateSubscription(result);
|
||||||
monitorSubscription(result);
|
monitorSubscription(result);
|
||||||
|
|
||||||
headerPlayAllButton.setOnClickListener(
|
headerPlayAllButton.setOnClickListener(view -> NavigationHelper
|
||||||
view -> NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), true));
|
.playOnMainPlayer(activity, getPlayQueue(), true));
|
||||||
headerPopupButton.setOnClickListener(
|
headerPopupButton.setOnClickListener(view -> NavigationHelper
|
||||||
view -> NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
|
.playOnPopupPlayer(activity, getPlayQueue(), false));
|
||||||
headerBackgroundButton.setOnClickListener(
|
headerBackgroundButton.setOnClickListener(view -> NavigationHelper
|
||||||
view -> NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
|
.playOnBackgroundPlayer(activity, getPlayQueue(), false));
|
||||||
|
|
||||||
|
headerPopupButton.setOnLongClickListener(view -> {
|
||||||
|
NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue(), true);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
headerBackgroundButton.setOnLongClickListener(view -> {
|
||||||
|
NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue(), true);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showContentNotSupported() {
|
||||||
|
contentNotSupportedTextView.setVisibility(View.VISIBLE);
|
||||||
|
kaomojiTextView.setText("(︶︹︺)");
|
||||||
|
kaomojiTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 45f);
|
||||||
|
noVideosTextView.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private PlayQueue getPlayQueue() {
|
private PlayQueue getPlayQueue() {
|
||||||
@ -407,17 +554,12 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
|||||||
streamItems.add((StreamInfoItem) i);
|
streamItems.add((StreamInfoItem) i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new ChannelPlayQueue(
|
return new ChannelPlayQueue(currentInfo.getServiceId(), currentInfo.getUrl(),
|
||||||
currentInfo.getServiceId(),
|
currentInfo.getNextPage(), streamItems, index);
|
||||||
currentInfo.getUrl(),
|
|
||||||
currentInfo.getNextPageUrl(),
|
|
||||||
streamItems,
|
|
||||||
index
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleNextItems(ListExtractor.InfoItemsPage result) {
|
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
|
||||||
super.handleNextItems(result);
|
super.handleNextItems(result);
|
||||||
|
|
||||||
if (!result.getErrors().isEmpty()) {
|
if (!result.getErrors().isEmpty()) {
|
||||||
@ -434,19 +576,17 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean onError(Throwable exception) {
|
protected boolean onError(final Throwable exception) {
|
||||||
if (super.onError(exception)) return true;
|
if (super.onError(exception)) {
|
||||||
|
return true;
|
||||||
if (exception instanceof ContentNotAvailableException) {
|
|
||||||
showError(getString(R.string.content_not_available), false);
|
|
||||||
} else {
|
|
||||||
int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error;
|
|
||||||
onUnrecoverableError(exception,
|
|
||||||
UserAction.REQUESTED_CHANNEL,
|
|
||||||
NewPipe.getNameOfService(serviceId),
|
|
||||||
url,
|
|
||||||
errorId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -455,8 +595,10 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setTitle(String title) {
|
public void setTitle(final String title) {
|
||||||
super.setTitle(title);
|
super.setTitle(title);
|
||||||
if (!useAsFrontPage) headerTitleView.setText(title);
|
if (!useAsFrontPage) {
|
||||||
|
headerTitleView.setText(title);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,14 +2,15 @@ package org.schabi.newpipe.fragments.list.comments;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
@ -23,17 +24,12 @@ import io.reactivex.Single;
|
|||||||
import io.reactivex.disposables.CompositeDisposable;
|
import io.reactivex.disposables.CompositeDisposable;
|
||||||
|
|
||||||
public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
|
public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
|
||||||
|
|
||||||
private CompositeDisposable disposables = new CompositeDisposable();
|
private CompositeDisposable disposables = new CompositeDisposable();
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Views
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private boolean mIsVisibleToUser = false;
|
private boolean mIsVisibleToUser = false;
|
||||||
|
|
||||||
public static CommentsFragment getInstance(int serviceId, String url, String name) {
|
public static CommentsFragment getInstance(final int serviceId, final String url,
|
||||||
|
final String name) {
|
||||||
CommentsFragment instance = new CommentsFragment();
|
CommentsFragment instance = new CommentsFragment();
|
||||||
instance.setInitialData(serviceId, url, name);
|
instance.setInitialData(serviceId, url, name);
|
||||||
return instance;
|
return instance;
|
||||||
@ -44,39 +40,42 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setUserVisibleHint(boolean isVisibleToUser) {
|
public void setUserVisibleHint(final boolean isVisibleToUser) {
|
||||||
super.setUserVisibleHint(isVisibleToUser);
|
super.setUserVisibleHint(isVisibleToUser);
|
||||||
mIsVisibleToUser = isVisibleToUser;
|
mIsVisibleToUser = isVisibleToUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAttach(Context context) {
|
public void onAttach(final Context context) {
|
||||||
super.onAttach(context);
|
super.onAttach(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||||
|
@Nullable final ViewGroup container,
|
||||||
|
@Nullable final Bundle savedInstanceState) {
|
||||||
return inflater.inflate(R.layout.fragment_comments, container, false);
|
return inflater.inflate(R.layout.fragment_comments, container, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
if (disposables != null) disposables.clear();
|
if (disposables != null) {
|
||||||
|
disposables.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Load and handle
|
// Load and handle
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
|
protected Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
|
||||||
return ExtractorHelper.getMoreCommentItems(serviceId, currentInfo, currentNextPageUrl);
|
return ExtractorHelper.getMoreCommentItems(serviceId, currentInfo, currentNextPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Single<CommentsInfo> loadResult(boolean forceLoad) {
|
protected Single<CommentsInfo> loadResult(final boolean forceLoad) {
|
||||||
return ExtractorHelper.getCommentsInfo(serviceId, url, forceLoad);
|
return ExtractorHelper.getCommentsInfo(serviceId, url, forceLoad);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,27 +89,28 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleResult(@NonNull CommentsInfo result) {
|
public void handleResult(@NonNull final CommentsInfo result) {
|
||||||
super.handleResult(result);
|
super.handleResult(result);
|
||||||
|
|
||||||
AnimationUtils.slideUp(getView(),120, 150, 0.06f);
|
AnimationUtils.slideUp(getView(), 120, 150, 0.06f);
|
||||||
|
|
||||||
if (!result.getErrors().isEmpty()) {
|
if (!result.getErrors().isEmpty()) {
|
||||||
showSnackBarError(result.getErrors(), UserAction.REQUESTED_COMMENTS, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
|
showSnackBarError(result.getErrors(), UserAction.REQUESTED_COMMENTS,
|
||||||
|
NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (disposables != null) disposables.clear();
|
if (disposables != null) {
|
||||||
|
disposables.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleNextItems(ListExtractor.InfoItemsPage result) {
|
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
|
||||||
super.handleNextItems(result);
|
super.handleNextItems(result);
|
||||||
|
|
||||||
if (!result.getErrors().isEmpty()) {
|
if (!result.getErrors().isEmpty()) {
|
||||||
showSnackBarError(result.getErrors(),
|
showSnackBarError(result.getErrors(), UserAction.REQUESTED_COMMENTS,
|
||||||
UserAction.REQUESTED_COMMENTS,
|
NewPipe.getNameOfService(serviceId), "Get next page of: " + url,
|
||||||
NewPipe.getNameOfService(serviceId),
|
|
||||||
"Get next page of: " + url,
|
|
||||||
R.string.general_error);
|
R.string.general_error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -120,11 +120,14 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean onError(Throwable exception) {
|
protected boolean onError(final Throwable exception) {
|
||||||
if (super.onError(exception)) return true;
|
if (super.onError(exception)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
hideLoading();
|
hideLoading();
|
||||||
showSnackBarError(exception, UserAction.REQUESTED_COMMENTS, NewPipe.getNameOfService(serviceId), url, R.string.error_unable_to_load_comments);
|
showSnackBarError(exception, UserAction.REQUESTED_COMMENTS,
|
||||||
|
NewPipe.getNameOfService(serviceId), url, R.string.error_unable_to_load_comments);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,14 +136,10 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setTitle(String title) {
|
public void setTitle(final String title) { }
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { }
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean isGridLayout() {
|
protected boolean isGridLayout() {
|
||||||
|
@ -10,9 +10,8 @@ import org.schabi.newpipe.util.KioskTranslator;
|
|||||||
import org.schabi.newpipe.util.ServiceHelper;
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
|
|
||||||
public class DefaultKioskFragment extends KioskFragment {
|
public class DefaultKioskFragment extends KioskFragment {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(final Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
if (serviceId < 0) {
|
if (serviceId < 0) {
|
||||||
@ -25,7 +24,9 @@ public class DefaultKioskFragment extends KioskFragment {
|
|||||||
super.onResume();
|
super.onResume();
|
||||||
|
|
||||||
if (serviceId != ServiceHelper.getSelectedServiceId(requireContext())) {
|
if (serviceId != ServiceHelper.getSelectedServiceId(requireContext())) {
|
||||||
if (currentWorker != null) currentWorker.dispose();
|
if (currentWorker != null) {
|
||||||
|
currentWorker.dispose();
|
||||||
|
}
|
||||||
updateSelectedDefaultKiosk();
|
updateSelectedDefaultKiosk();
|
||||||
reloadContent();
|
reloadContent();
|
||||||
}
|
}
|
||||||
@ -43,9 +44,10 @@ public class DefaultKioskFragment extends KioskFragment {
|
|||||||
name = kioskTranslatedName;
|
name = kioskTranslatedName;
|
||||||
|
|
||||||
currentInfo = null;
|
currentInfo = null;
|
||||||
currentNextPageUrl = null;
|
currentNextPage = null;
|
||||||
} catch (ExtractionException e) {
|
} catch (ExtractionException e) {
|
||||||
onUnrecoverableError(e, UserAction.REQUESTED_KIOSK, "none", "Loading default kiosk from selected service", 0);
|
onUnrecoverableError(e, UserAction.REQUESTED_KIOSK, "none",
|
||||||
|
"Loading default kiosk from selected service", 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,16 @@
|
|||||||
package org.schabi.newpipe.fragments.list.kiosk;
|
package org.schabi.newpipe.fragments.list.kiosk;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.ActionBar;
|
|
||||||
|
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.appcompat.app.ActionBar;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
@ -33,45 +32,45 @@ import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by Christian Schabesberger on 23.09.17.
|
* Created by Christian Schabesberger on 23.09.17.
|
||||||
*
|
* <p>
|
||||||
* Copyright (C) Christian Schabesberger 2017 <chris.schabesberger@mailbox.org>
|
* Copyright (C) Christian Schabesberger 2017 <chris.schabesberger@mailbox.org>
|
||||||
* KioskFragment.java is part of NewPipe.
|
* KioskFragment.java is part of NewPipe.
|
||||||
*
|
* </p>
|
||||||
|
* <p>
|
||||||
* NewPipe is free software: you can redistribute it and/or modify
|
* NewPipe is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* (at your option) any later version.
|
* (at your option) any later version.
|
||||||
*
|
* </p>
|
||||||
|
* <p>
|
||||||
* NewPipe is distributed in the hope that it will be useful,
|
* NewPipe is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* GNU General Public License for more details.
|
* GNU General Public License for more details.
|
||||||
*
|
* </p>
|
||||||
|
* <p>
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
* </p>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
||||||
|
|
||||||
@State
|
@State
|
||||||
protected String kioskId = "";
|
String kioskId = "";
|
||||||
protected String kioskTranslatedName;
|
String kioskTranslatedName;
|
||||||
@State
|
@State
|
||||||
protected ContentCountry contentCountry;
|
ContentCountry contentCountry;
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Views
|
// Views
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
public static KioskFragment getInstance(int serviceId)
|
public static KioskFragment getInstance(final int serviceId) throws ExtractionException {
|
||||||
throws ExtractionException {
|
|
||||||
return getInstance(serviceId, NewPipe.getService(serviceId)
|
return getInstance(serviceId, NewPipe.getService(serviceId)
|
||||||
.getKioskList()
|
.getKioskList().getDefaultKioskId());
|
||||||
.getDefaultKioskId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static KioskFragment getInstance(int serviceId, String kioskId)
|
public static KioskFragment getInstance(final int serviceId, final String kioskId)
|
||||||
throws ExtractionException {
|
throws ExtractionException {
|
||||||
KioskFragment instance = new KioskFragment();
|
KioskFragment instance = new KioskFragment();
|
||||||
StreamingService service = NewPipe.getService(serviceId);
|
StreamingService service = NewPipe.getService(serviceId);
|
||||||
@ -88,7 +87,7 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(final Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
kioskTranslatedName = KioskTranslator.getTranslatedKioskName(kioskId, activity);
|
kioskTranslatedName = KioskTranslator.getTranslatedKioskName(kioskId, activity);
|
||||||
@ -97,9 +96,9 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setUserVisibleHint(boolean isVisibleToUser) {
|
public void setUserVisibleHint(final boolean isVisibleToUser) {
|
||||||
super.setUserVisibleHint(isVisibleToUser);
|
super.setUserVisibleHint(isVisibleToUser);
|
||||||
if(useAsFrontPage && isVisibleToUser && activity != null) {
|
if (useAsFrontPage && isVisibleToUser && activity != null) {
|
||||||
try {
|
try {
|
||||||
setTitle(kioskTranslatedName);
|
setTitle(kioskTranslatedName);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@ -111,7 +110,9 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||||
|
@Nullable final ViewGroup container,
|
||||||
|
@Nullable final Bundle savedInstanceState) {
|
||||||
return inflater.inflate(R.layout.fragment_kiosk, container, false);
|
return inflater.inflate(R.layout.fragment_kiosk, container, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,7 +130,7 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
|
||||||
super.onCreateOptionsMenu(menu, inflater);
|
super.onCreateOptionsMenu(menu, inflater);
|
||||||
ActionBar supportActionBar = activity.getSupportActionBar();
|
ActionBar supportActionBar = activity.getSupportActionBar();
|
||||||
if (supportActionBar != null && useAsFrontPage) {
|
if (supportActionBar != null && useAsFrontPage) {
|
||||||
@ -142,18 +143,14 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Single<KioskInfo> loadResult(boolean forceReload) {
|
public Single<KioskInfo> loadResult(final boolean forceReload) {
|
||||||
contentCountry = Localization.getPreferredContentCountry(requireContext());
|
contentCountry = Localization.getPreferredContentCountry(requireContext());
|
||||||
return ExtractorHelper.getKioskInfo(serviceId,
|
return ExtractorHelper.getKioskInfo(serviceId, url, forceReload);
|
||||||
url,
|
|
||||||
forceReload);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
|
public Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
|
||||||
return ExtractorHelper.getMoreKioskItems(serviceId,
|
return ExtractorHelper.getMoreKioskItems(serviceId, url, currentNextPage);
|
||||||
url,
|
|
||||||
currentNextPageUrl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
@ -181,13 +178,13 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleNextItems(ListExtractor.InfoItemsPage result) {
|
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
|
||||||
super.handleNextItems(result);
|
super.handleNextItems(result);
|
||||||
|
|
||||||
if (!result.getErrors().isEmpty()) {
|
if (!result.getErrors().isEmpty()) {
|
||||||
showSnackBarError(result.getErrors(),
|
showSnackBarError(result.getErrors(),
|
||||||
UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(serviceId)
|
UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(serviceId),
|
||||||
, "Get next page of: " + url, 0);
|
"Get next page of: " + url, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,9 +3,6 @@ package org.schabi.newpipe.fragments.list.playlist;
|
|||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
@ -17,6 +14,10 @@ import android.view.ViewGroup;
|
|||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
import org.reactivestreams.Subscriber;
|
import org.reactivestreams.Subscriber;
|
||||||
import org.reactivestreams.Subscription;
|
import org.reactivestreams.Subscription;
|
||||||
import org.schabi.newpipe.NewPipeDatabase;
|
import org.schabi.newpipe.NewPipeDatabase;
|
||||||
@ -38,6 +39,7 @@ import org.schabi.newpipe.report.ErrorActivity;
|
|||||||
import org.schabi.newpipe.report.UserAction;
|
import org.schabi.newpipe.report.UserAction;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||||
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.ShareUtils;
|
import org.schabi.newpipe.util.ShareUtils;
|
||||||
import org.schabi.newpipe.util.StreamDialogEntry;
|
import org.schabi.newpipe.util.StreamDialogEntry;
|
||||||
@ -57,7 +59,6 @@ import io.reactivex.disposables.Disposables;
|
|||||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||||
|
|
||||||
public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||||
|
|
||||||
private CompositeDisposable disposables;
|
private CompositeDisposable disposables;
|
||||||
private Subscription bookmarkReactor;
|
private Subscription bookmarkReactor;
|
||||||
private AtomicBoolean isBookmarkButtonReady;
|
private AtomicBoolean isBookmarkButtonReady;
|
||||||
@ -82,7 +83,8 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
|||||||
|
|
||||||
private MenuItem playlistBookmarkButton;
|
private MenuItem playlistBookmarkButton;
|
||||||
|
|
||||||
public static PlaylistFragment getInstance(int serviceId, String url, String name) {
|
public static PlaylistFragment getInstance(final int serviceId, final String url,
|
||||||
|
final String name) {
|
||||||
PlaylistFragment instance = new PlaylistFragment();
|
PlaylistFragment instance = new PlaylistFragment();
|
||||||
instance.setInitialData(serviceId, url, name);
|
instance.setInitialData(serviceId, url, name);
|
||||||
return instance;
|
return instance;
|
||||||
@ -93,17 +95,18 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(final Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
disposables = new CompositeDisposable();
|
disposables = new CompositeDisposable();
|
||||||
isBookmarkButtonReady = new AtomicBoolean(false);
|
isBookmarkButtonReady = new AtomicBoolean(false);
|
||||||
remotePlaylistManager = new RemotePlaylistManager(NewPipeDatabase.getInstance(
|
remotePlaylistManager = new RemotePlaylistManager(NewPipeDatabase
|
||||||
requireContext()));
|
.getInstance(requireContext()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
|
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||||
@Nullable Bundle savedInstanceState) {
|
@Nullable final ViewGroup container,
|
||||||
|
@Nullable final Bundle savedInstanceState) {
|
||||||
return inflater.inflate(R.layout.fragment_playlist, container, false);
|
return inflater.inflate(R.layout.fragment_playlist, container, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,7 +115,8 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
protected View getListHeader() {
|
protected View getListHeader() {
|
||||||
headerRootLayout = activity.getLayoutInflater().inflate(R.layout.playlist_header, itemsList, false);
|
headerRootLayout = activity.getLayoutInflater()
|
||||||
|
.inflate(R.layout.playlist_header, itemsList, false);
|
||||||
headerTitleView = headerRootLayout.findViewById(R.id.playlist_title_view);
|
headerTitleView = headerRootLayout.findViewById(R.id.playlist_title_view);
|
||||||
headerUploaderLayout = headerRootLayout.findViewById(R.id.uploader_layout);
|
headerUploaderLayout = headerRootLayout.findViewById(R.id.uploader_layout);
|
||||||
headerUploaderName = headerRootLayout.findViewById(R.id.uploader_name);
|
headerUploaderName = headerRootLayout.findViewById(R.id.uploader_name);
|
||||||
@ -129,21 +133,23 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||||
super.initViews(rootView, savedInstanceState);
|
super.initViews(rootView, savedInstanceState);
|
||||||
|
|
||||||
infoListAdapter.useMiniItemVariants(true);
|
infoListAdapter.setUseMiniVariant(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private PlayQueue getPlayQueueStartingAt(StreamInfoItem infoItem) {
|
private PlayQueue getPlayQueueStartingAt(final StreamInfoItem infoItem) {
|
||||||
return getPlayQueue(Math.max(infoListAdapter.getItemsList().indexOf(infoItem), 0));
|
return getPlayQueue(Math.max(infoListAdapter.getItemsList().indexOf(infoItem), 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void showStreamDialog(StreamInfoItem item) {
|
protected void showStreamDialog(final StreamInfoItem item) {
|
||||||
final Context context = getContext();
|
final Context context = getContext();
|
||||||
final Activity activity = getActivity();
|
final Activity activity = getActivity();
|
||||||
if (context == null || context.getResources() == null || activity == null) return;
|
if (context == null || context.getResources() == null || activity == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (item.getStreamType() == StreamType.AUDIO_STREAM) {
|
if (item.getStreamType() == StreamType.AUDIO_STREAM) {
|
||||||
StreamDialogEntry.setEnabledEntries(
|
StreamDialogEntry.setEnabledEntries(
|
||||||
@ -160,21 +166,25 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
|||||||
StreamDialogEntry.append_playlist,
|
StreamDialogEntry.append_playlist,
|
||||||
StreamDialogEntry.share);
|
StreamDialogEntry.share);
|
||||||
|
|
||||||
StreamDialogEntry.start_here_on_popup.setCustomAction(
|
StreamDialogEntry.start_here_on_popup.setCustomAction((fragment, infoItem) ->
|
||||||
(fragment, infoItem) -> NavigationHelper.playOnPopupPlayer(context, getPlayQueueStartingAt(infoItem), true));
|
NavigationHelper.playOnPopupPlayer(context,
|
||||||
|
getPlayQueueStartingAt(infoItem), true));
|
||||||
}
|
}
|
||||||
|
|
||||||
StreamDialogEntry.start_here_on_background.setCustomAction(
|
StreamDialogEntry.start_here_on_background.setCustomAction((fragment, infoItem) ->
|
||||||
(fragment, infoItem) -> NavigationHelper.playOnBackgroundPlayer(context, getPlayQueueStartingAt(infoItem), true));
|
NavigationHelper.playOnBackgroundPlayer(context,
|
||||||
|
getPlayQueueStartingAt(infoItem), true));
|
||||||
|
|
||||||
new InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context), (dialog, which) ->
|
new InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context),
|
||||||
StreamDialogEntry.clickOn(which, this, item)).show();
|
(dialog, which) -> StreamDialogEntry.clickOn(which, this, item)).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
|
||||||
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu +
|
if (DEBUG) {
|
||||||
"], inflater = [" + inflater + "]");
|
Log.d(TAG, "onCreateOptionsMenu() called with: "
|
||||||
|
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||||
|
}
|
||||||
super.onCreateOptionsMenu(menu, inflater);
|
super.onCreateOptionsMenu(menu, inflater);
|
||||||
inflater.inflate(R.menu.menu_playlist, menu);
|
inflater.inflate(R.menu.menu_playlist, menu);
|
||||||
|
|
||||||
@ -185,10 +195,16 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
|||||||
@Override
|
@Override
|
||||||
public void onDestroyView() {
|
public void onDestroyView() {
|
||||||
super.onDestroyView();
|
super.onDestroyView();
|
||||||
if (isBookmarkButtonReady != null) isBookmarkButtonReady.set(false);
|
if (isBookmarkButtonReady != null) {
|
||||||
|
isBookmarkButtonReady.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
if (disposables != null) disposables.clear();
|
if (disposables != null) {
|
||||||
if (bookmarkReactor != null) bookmarkReactor.cancel();
|
disposables.clear();
|
||||||
|
}
|
||||||
|
if (bookmarkReactor != null) {
|
||||||
|
bookmarkReactor.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
bookmarkReactor = null;
|
bookmarkReactor = null;
|
||||||
}
|
}
|
||||||
@ -197,7 +213,9 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
|||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
|
|
||||||
if (disposables != null) disposables.dispose();
|
if (disposables != null) {
|
||||||
|
disposables.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
disposables = null;
|
disposables = null;
|
||||||
remotePlaylistManager = null;
|
remotePlaylistManager = null;
|
||||||
@ -211,22 +229,25 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
|
protected Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
|
||||||
return ExtractorHelper.getMorePlaylistItems(serviceId, url, currentNextPageUrl);
|
return ExtractorHelper.getMorePlaylistItems(serviceId, url, currentNextPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Single<PlaylistInfo> loadResult(boolean forceLoad) {
|
protected Single<PlaylistInfo> loadResult(final boolean forceLoad) {
|
||||||
return ExtractorHelper.getPlaylistInfo(serviceId, url, forceLoad);
|
return ExtractorHelper.getPlaylistInfo(serviceId, url, forceLoad);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
|
case R.id.action_settings:
|
||||||
|
NavigationHelper.openSettings(requireContext());
|
||||||
|
break;
|
||||||
case R.id.menu_item_openInBrowser:
|
case R.id.menu_item_openInBrowser:
|
||||||
ShareUtils.openUrlInBrowser(this.getContext(), url);
|
ShareUtils.openUrlInBrowser(requireContext(), url);
|
||||||
break;
|
break;
|
||||||
case R.id.menu_item_share:
|
case R.id.menu_item_share:
|
||||||
ShareUtils.shareUrl(this.getContext(), name, url);
|
ShareUtils.shareUrl(requireContext(), name, url);
|
||||||
break;
|
break;
|
||||||
case R.id.menu_item_bookmark:
|
case R.id.menu_item_bookmark:
|
||||||
onBookmarkClicked();
|
onBookmarkClicked();
|
||||||
@ -248,7 +269,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
|||||||
animateView(headerRootLayout, false, 200);
|
animateView(headerRootLayout, false, 200);
|
||||||
animateView(itemsList, false, 100);
|
animateView(itemsList, false, 100);
|
||||||
|
|
||||||
imageLoader.cancelDisplayTask(headerUploaderAvatar);
|
IMAGE_LOADER.cancelDisplayTask(headerUploaderAvatar);
|
||||||
animateView(headerUploaderLayout, false, 200);
|
animateView(headerUploaderLayout, false, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -259,7 +280,8 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
|||||||
animateView(headerRootLayout, true, 100);
|
animateView(headerRootLayout, true, 100);
|
||||||
animateView(headerUploaderLayout, true, 300);
|
animateView(headerUploaderLayout, true, 300);
|
||||||
headerUploaderLayout.setOnClickListener(null);
|
headerUploaderLayout.setOnClickListener(null);
|
||||||
if (!TextUtils.isEmpty(result.getUploaderName())) { // If we have an uploader : Put them into the ui
|
// If we have an uploader put them into the UI
|
||||||
|
if (!TextUtils.isEmpty(result.getUploaderName())) {
|
||||||
headerUploaderName.setText(result.getUploaderName());
|
headerUploaderName.setText(result.getUploaderName());
|
||||||
if (!TextUtils.isEmpty(result.getUploaderUrl())) {
|
if (!TextUtils.isEmpty(result.getUploaderUrl())) {
|
||||||
headerUploaderLayout.setOnClickListener(v -> {
|
headerUploaderLayout.setOnClickListener(v -> {
|
||||||
@ -273,19 +295,20 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else { // Else : say we have no uploader
|
} else { // Otherwise say we have no uploader
|
||||||
headerUploaderName.setText(R.string.playlist_no_uploader);
|
headerUploaderName.setText(R.string.playlist_no_uploader);
|
||||||
}
|
}
|
||||||
|
|
||||||
playlistCtrl.setVisibility(View.VISIBLE);
|
playlistCtrl.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
imageLoader.displayImage(result.getUploaderAvatarUrl(), headerUploaderAvatar,
|
IMAGE_LOADER.displayImage(result.getUploaderAvatarUrl(), headerUploaderAvatar,
|
||||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||||
headerStreamCount.setText(getResources().getQuantityString(R.plurals.videos,
|
headerStreamCount.setText(Localization
|
||||||
(int) result.getStreamCount(), (int) result.getStreamCount()));
|
.localizeStreamCount(getContext(), result.getStreamCount()));
|
||||||
|
|
||||||
if (!result.getErrors().isEmpty()) {
|
if (!result.getErrors().isEmpty()) {
|
||||||
showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
|
showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST,
|
||||||
|
NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
remotePlaylistManager.getPlaylist(result)
|
remotePlaylistManager.getPlaylist(result)
|
||||||
@ -318,27 +341,27 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
|||||||
|
|
||||||
private PlayQueue getPlayQueue(final int index) {
|
private PlayQueue getPlayQueue(final int index) {
|
||||||
final List<StreamInfoItem> infoItems = new ArrayList<>();
|
final List<StreamInfoItem> infoItems = new ArrayList<>();
|
||||||
for(InfoItem i : infoListAdapter.getItemsList()) {
|
for (InfoItem i : infoListAdapter.getItemsList()) {
|
||||||
if(i instanceof StreamInfoItem) {
|
if (i instanceof StreamInfoItem) {
|
||||||
infoItems.add((StreamInfoItem) i);
|
infoItems.add((StreamInfoItem) i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new PlaylistPlayQueue(
|
return new PlaylistPlayQueue(
|
||||||
currentInfo.getServiceId(),
|
currentInfo.getServiceId(),
|
||||||
currentInfo.getUrl(),
|
currentInfo.getUrl(),
|
||||||
currentInfo.getNextPageUrl(),
|
currentInfo.getNextPage(),
|
||||||
infoItems,
|
infoItems,
|
||||||
index
|
index
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleNextItems(ListExtractor.InfoItemsPage result) {
|
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
|
||||||
super.handleNextItems(result);
|
super.handleNextItems(result);
|
||||||
|
|
||||||
if (!result.getErrors().isEmpty()) {
|
if (!result.getErrors().isEmpty()) {
|
||||||
showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(serviceId)
|
showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST,
|
||||||
, "Get next page of: " + url, 0);
|
NewPipe.getNameOfService(serviceId), "Get next page of: " + url, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -347,15 +370,15 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean onError(Throwable exception) {
|
protected boolean onError(final Throwable exception) {
|
||||||
if (super.onError(exception)) return true;
|
if (super.onError(exception)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error;
|
int errorId = exception instanceof ExtractionException
|
||||||
onUnrecoverableError(exception,
|
? R.string.parsing_error : R.string.general_error;
|
||||||
UserAction.REQUESTED_PLAYLIST,
|
onUnrecoverableError(exception, UserAction.REQUESTED_PLAYLIST,
|
||||||
NewPipe.getNameOfService(serviceId),
|
NewPipe.getNameOfService(serviceId), url, errorId);
|
||||||
url,
|
|
||||||
errorId);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -363,13 +386,18 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
|||||||
// Utils
|
// Utils
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
private Flowable<Integer> getUpdateProcessor(@NonNull List<PlaylistRemoteEntity> playlists,
|
private Flowable<Integer> getUpdateProcessor(
|
||||||
@NonNull PlaylistInfo result) {
|
@NonNull final List<PlaylistRemoteEntity> playlists,
|
||||||
|
@NonNull final PlaylistInfo result) {
|
||||||
final Flowable<Integer> noItemToUpdate = Flowable.just(/*noItemToUpdate=*/-1);
|
final Flowable<Integer> noItemToUpdate = Flowable.just(/*noItemToUpdate=*/-1);
|
||||||
if (playlists.isEmpty()) return noItemToUpdate;
|
if (playlists.isEmpty()) {
|
||||||
|
return noItemToUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
final PlaylistRemoteEntity playlistEntity = playlists.get(0);
|
final PlaylistRemoteEntity playlistRemoteEntity = playlists.get(0);
|
||||||
if (playlistEntity.isIdenticalTo(result)) return noItemToUpdate;
|
if (playlistRemoteEntity.isIdenticalTo(result)) {
|
||||||
|
return noItemToUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
return remotePlaylistManager.onUpdate(playlists.get(0).getUid(), result).toFlowable();
|
return remotePlaylistManager.onUpdate(playlists.get(0).getUid(), result).toFlowable();
|
||||||
}
|
}
|
||||||
@ -377,56 +405,59 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
|||||||
private Subscriber<List<PlaylistRemoteEntity>> getPlaylistBookmarkSubscriber() {
|
private Subscriber<List<PlaylistRemoteEntity>> getPlaylistBookmarkSubscriber() {
|
||||||
return new Subscriber<List<PlaylistRemoteEntity>>() {
|
return new Subscriber<List<PlaylistRemoteEntity>>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSubscribe(Subscription s) {
|
public void onSubscribe(final Subscription s) {
|
||||||
if (bookmarkReactor != null) bookmarkReactor.cancel();
|
if (bookmarkReactor != null) {
|
||||||
|
bookmarkReactor.cancel();
|
||||||
|
}
|
||||||
bookmarkReactor = s;
|
bookmarkReactor = s;
|
||||||
bookmarkReactor.request(1);
|
bookmarkReactor.request(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onNext(List<PlaylistRemoteEntity> playlist) {
|
public void onNext(final List<PlaylistRemoteEntity> playlist) {
|
||||||
playlistEntity = playlist.isEmpty() ? null : playlist.get(0);
|
playlistEntity = playlist.isEmpty() ? null : playlist.get(0);
|
||||||
|
|
||||||
updateBookmarkButtons();
|
updateBookmarkButtons();
|
||||||
isBookmarkButtonReady.set(true);
|
isBookmarkButtonReady.set(true);
|
||||||
|
|
||||||
if (bookmarkReactor != null) bookmarkReactor.request(1);
|
if (bookmarkReactor != null) {
|
||||||
|
bookmarkReactor.request(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(Throwable t) {
|
public void onError(final Throwable t) {
|
||||||
PlaylistFragment.this.onError(t);
|
PlaylistFragment.this.onError(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onComplete() {
|
public void onComplete() { }
|
||||||
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setTitle(String title) {
|
public void setTitle(final String title) {
|
||||||
super.setTitle(title);
|
super.setTitle(title);
|
||||||
headerTitleView.setText(title);
|
headerTitleView.setText(title);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onBookmarkClicked() {
|
private void onBookmarkClicked() {
|
||||||
if (isBookmarkButtonReady == null || !isBookmarkButtonReady.get() ||
|
if (isBookmarkButtonReady == null || !isBookmarkButtonReady.get()
|
||||||
remotePlaylistManager == null)
|
|| remotePlaylistManager == null) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final Disposable action;
|
final Disposable action;
|
||||||
|
|
||||||
if (currentInfo != null && playlistEntity == null) {
|
if (currentInfo != null && playlistEntity == null) {
|
||||||
action = remotePlaylistManager.onBookmark(currentInfo)
|
action = remotePlaylistManager.onBookmark(currentInfo)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(ignored -> {/* Do nothing */}, this::onError);
|
.subscribe(ignored -> { /* Do nothing */ }, this::onError);
|
||||||
} else if (playlistEntity != null) {
|
} else if (playlistEntity != null) {
|
||||||
action = remotePlaylistManager.deletePlaylist(playlistEntity.getUid())
|
action = remotePlaylistManager.deletePlaylist(playlistEntity.getUid())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.doFinally(() -> playlistEntity = null)
|
.doFinally(() -> playlistEntity = null)
|
||||||
.subscribe(ignored -> {/* Do nothing */}, this::onError);
|
.subscribe(ignored -> { /* Do nothing */ }, this::onError);
|
||||||
} else {
|
} else {
|
||||||
action = Disposables.empty();
|
action = Disposables.empty();
|
||||||
}
|
}
|
||||||
@ -435,13 +466,15 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateBookmarkButtons() {
|
private void updateBookmarkButtons() {
|
||||||
if (playlistBookmarkButton == null || activity == null) return;
|
if (playlistBookmarkButton == null || activity == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final int iconAttr = playlistEntity == null ?
|
final int iconAttr = playlistEntity == null
|
||||||
R.attr.ic_playlist_add : R.attr.ic_playlist_check;
|
? R.attr.ic_playlist_add : R.attr.ic_playlist_check;
|
||||||
|
|
||||||
final int titleRes = playlistEntity == null ?
|
final int titleRes = playlistEntity == null
|
||||||
R.string.bookmark_playlist : R.string.unbookmark_playlist;
|
? R.string.bookmark_playlist : R.string.unbookmark_playlist;
|
||||||
|
|
||||||
playlistBookmarkButton.setIcon(ThemeHelper.resolveResourceIdFromAttr(activity, iconAttr));
|
playlistBookmarkButton.setIcon(ThemeHelper.resolveResourceIdFromAttr(activity, iconAttr));
|
||||||
playlistBookmarkButton.setTitle(titleRes);
|
playlistBookmarkButton.setTitle(titleRes);
|
||||||
|
@ -6,14 +6,8 @@ import android.content.Intent;
|
|||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.ActionBar;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import androidx.appcompat.widget.TooltipCompat;
|
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
|
||||||
import android.text.Editable;
|
import android.text.Editable;
|
||||||
|
import android.text.Html;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.text.TextWatcher;
|
import android.text.TextWatcher;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
@ -30,31 +24,37 @@ import android.view.inputmethod.InputMethodManager;
|
|||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.appcompat.app.ActionBar;
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.appcompat.widget.TooltipCompat;
|
||||||
|
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.ReCaptchaActivity;
|
import org.schabi.newpipe.ReCaptchaActivity;
|
||||||
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
|
import org.schabi.newpipe.extractor.Page;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
import org.schabi.newpipe.extractor.search.SearchExtractor;
|
import org.schabi.newpipe.extractor.search.SearchExtractor;
|
||||||
import org.schabi.newpipe.extractor.search.SearchInfo;
|
import org.schabi.newpipe.extractor.search.SearchInfo;
|
||||||
import org.schabi.newpipe.util.FireTvUtils;
|
|
||||||
import org.schabi.newpipe.fragments.BackPressable;
|
import org.schabi.newpipe.fragments.BackPressable;
|
||||||
import org.schabi.newpipe.fragments.list.BaseListFragment;
|
import org.schabi.newpipe.fragments.list.BaseListFragment;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.report.ErrorActivity;
|
import org.schabi.newpipe.report.ErrorActivity;
|
||||||
import org.schabi.newpipe.report.UserAction;
|
import org.schabi.newpipe.report.UserAction;
|
||||||
|
import org.schabi.newpipe.util.AndroidTvUtils;
|
||||||
import org.schabi.newpipe.util.AnimationUtils;
|
import org.schabi.newpipe.util.AnimationUtils;
|
||||||
import org.schabi.newpipe.util.Constants;
|
import org.schabi.newpipe.util.Constants;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InterruptedIOException;
|
|
||||||
import java.net.SocketException;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@ -73,14 +73,13 @@ import io.reactivex.disposables.Disposable;
|
|||||||
import io.reactivex.schedulers.Schedulers;
|
import io.reactivex.schedulers.Schedulers;
|
||||||
import io.reactivex.subjects.PublishSubject;
|
import io.reactivex.subjects.PublishSubject;
|
||||||
|
|
||||||
|
import static android.text.Html.escapeHtml;
|
||||||
import static androidx.recyclerview.widget.ItemTouchHelper.Callback.makeMovementFlags;
|
import static androidx.recyclerview.widget.ItemTouchHelper.Callback.makeMovementFlags;
|
||||||
import static java.util.Arrays.asList;
|
import static java.util.Arrays.asList;
|
||||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||||
|
|
||||||
public class SearchFragment
|
public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.InfoItemsPage>
|
||||||
extends BaseListFragment<SearchInfo, ListExtractor.InfoItemsPage>
|
|
||||||
implements BackPressable {
|
implements BackPressable {
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Search
|
// Search
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
@ -92,44 +91,51 @@ public class SearchFragment
|
|||||||
private static final int THRESHOLD_NETWORK_SUGGESTION = 1;
|
private static final int THRESHOLD_NETWORK_SUGGESTION = 1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How much time have to pass without emitting a item (i.e. the user stop typing) to fetch/show the suggestions, in milliseconds.
|
* How much time have to pass without emitting a item (i.e. the user stop typing)
|
||||||
|
* to fetch/show the suggestions, in milliseconds.
|
||||||
*/
|
*/
|
||||||
private static final int SUGGESTIONS_DEBOUNCE = 120; //ms
|
private static final int SUGGESTIONS_DEBOUNCE = 120; //ms
|
||||||
|
private final PublishSubject<String> suggestionPublisher = PublishSubject.create();
|
||||||
|
|
||||||
@State
|
@State
|
||||||
protected int filterItemCheckedId = -1;
|
int filterItemCheckedId = -1;
|
||||||
|
|
||||||
@State
|
@State
|
||||||
protected int serviceId = Constants.NO_SERVICE_ID;
|
protected int serviceId = Constants.NO_SERVICE_ID;
|
||||||
|
|
||||||
// this three represet the current search query
|
// these three represents the current search query
|
||||||
@State
|
@State
|
||||||
protected String searchString;
|
String searchString;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* No content filter should add like contentfilter = all
|
* No content filter should add like contentFilter = all
|
||||||
* be aware of this when implementing an extractor.
|
* be aware of this when implementing an extractor.
|
||||||
*/
|
*/
|
||||||
@State
|
@State
|
||||||
protected String[] contentFilter = new String[0];
|
String[] contentFilter = new String[0];
|
||||||
@State
|
|
||||||
protected String sortFilter;
|
|
||||||
|
|
||||||
// these represtent the last search
|
|
||||||
@State
|
|
||||||
protected String lastSearchedString;
|
|
||||||
|
|
||||||
@State
|
@State
|
||||||
protected boolean wasSearchFocused = false;
|
String sortFilter;
|
||||||
|
|
||||||
|
// these represents the last search
|
||||||
|
@State
|
||||||
|
String lastSearchedString;
|
||||||
|
|
||||||
|
@State
|
||||||
|
String searchSuggestion;
|
||||||
|
|
||||||
|
@State
|
||||||
|
boolean isCorrectedSearch;
|
||||||
|
|
||||||
|
@State
|
||||||
|
boolean wasSearchFocused = false;
|
||||||
|
|
||||||
private Map<Integer, String> menuItemToFilterName;
|
private Map<Integer, String> menuItemToFilterName;
|
||||||
private StreamingService service;
|
private StreamingService service;
|
||||||
private String currentPageUrl;
|
private Page nextPage;
|
||||||
private String nextPageUrl;
|
|
||||||
private String contentCountry;
|
private String contentCountry;
|
||||||
private boolean isSuggestionsEnabled = true;
|
private boolean isSuggestionsEnabled = true;
|
||||||
|
|
||||||
private final PublishSubject<String> suggestionPublisher = PublishSubject.create();
|
|
||||||
private Disposable searchDisposable;
|
private Disposable searchDisposable;
|
||||||
private Disposable suggestionDisposable;
|
private Disposable suggestionDisposable;
|
||||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||||
@ -145,12 +151,16 @@ public class SearchFragment
|
|||||||
private EditText searchEditText;
|
private EditText searchEditText;
|
||||||
private View searchClear;
|
private View searchClear;
|
||||||
|
|
||||||
|
private TextView correctSuggestion;
|
||||||
|
|
||||||
private View suggestionsPanel;
|
private View suggestionsPanel;
|
||||||
private RecyclerView suggestionsRecyclerView;
|
private RecyclerView suggestionsRecyclerView;
|
||||||
|
|
||||||
/*////////////////////////////////////////////////////////////////////////*/
|
/*////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
public static SearchFragment getInstance(int serviceId, String searchString) {
|
private TextWatcher textWatcher;
|
||||||
|
|
||||||
|
public static SearchFragment getInstance(final int serviceId, final String searchString) {
|
||||||
SearchFragment searchFragment = new SearchFragment();
|
SearchFragment searchFragment = new SearchFragment();
|
||||||
searchFragment.setQuery(serviceId, searchString, new String[0], "");
|
searchFragment.setQuery(serviceId, searchString, new String[0], "");
|
||||||
|
|
||||||
@ -173,33 +183,37 @@ public class SearchFragment
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAttach(Context context) {
|
public void onAttach(final Context context) {
|
||||||
super.onAttach(context);
|
super.onAttach(context);
|
||||||
|
|
||||||
suggestionListAdapter = new SuggestionListAdapter(activity);
|
suggestionListAdapter = new SuggestionListAdapter(activity);
|
||||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
|
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
|
||||||
boolean isSearchHistoryEnabled = preferences.getBoolean(getString(R.string.enable_search_history_key), true);
|
boolean isSearchHistoryEnabled = preferences
|
||||||
|
.getBoolean(getString(R.string.enable_search_history_key), true);
|
||||||
suggestionListAdapter.setShowSuggestionHistory(isSearchHistoryEnabled);
|
suggestionListAdapter.setShowSuggestionHistory(isSearchHistoryEnabled);
|
||||||
|
|
||||||
historyRecordManager = new HistoryRecordManager(context);
|
historyRecordManager = new HistoryRecordManager(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(final Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
|
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
|
||||||
isSuggestionsEnabled = preferences.getBoolean(getString(R.string.show_search_suggestions_key), true);
|
isSuggestionsEnabled = preferences
|
||||||
contentCountry = preferences.getString(getString(R.string.content_country_key), getString(R.string.default_country_value));
|
.getBoolean(getString(R.string.show_search_suggestions_key), true);
|
||||||
|
contentCountry = preferences.getString(getString(R.string.content_country_key),
|
||||||
|
getString(R.string.default_localization_key));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
|
||||||
|
@Nullable final Bundle savedInstanceState) {
|
||||||
return inflater.inflate(R.layout.fragment_search, container, false);
|
return inflater.inflate(R.layout.fragment_search, container, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onViewCreated(View rootView, Bundle savedInstanceState) {
|
public void onViewCreated(final View rootView, final Bundle savedInstanceState) {
|
||||||
super.onViewCreated(rootView, savedInstanceState);
|
super.onViewCreated(rootView, savedInstanceState);
|
||||||
showSearchOnStart();
|
showSearchOnStart();
|
||||||
initSearchListeners();
|
initSearchListeners();
|
||||||
@ -211,15 +225,23 @@ public class SearchFragment
|
|||||||
|
|
||||||
wasSearchFocused = searchEditText.hasFocus();
|
wasSearchFocused = searchEditText.hasFocus();
|
||||||
|
|
||||||
if (searchDisposable != null) searchDisposable.dispose();
|
if (searchDisposable != null) {
|
||||||
if (suggestionDisposable != null) suggestionDisposable.dispose();
|
searchDisposable.dispose();
|
||||||
if (disposables != null) disposables.clear();
|
}
|
||||||
|
if (suggestionDisposable != null) {
|
||||||
|
suggestionDisposable.dispose();
|
||||||
|
}
|
||||||
|
if (disposables != null) {
|
||||||
|
disposables.clear();
|
||||||
|
}
|
||||||
hideKeyboardSearch();
|
hideKeyboardSearch();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
if (DEBUG) Log.d(TAG, "onResume() called");
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onResume() called");
|
||||||
|
}
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -245,7 +267,11 @@ public class SearchFragment
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (suggestionDisposable == null || suggestionDisposable.isDisposed()) initSuggestionObserver();
|
handleSearchSuggestion();
|
||||||
|
|
||||||
|
if (suggestionDisposable == null || suggestionDisposable.isDisposed()) {
|
||||||
|
initSuggestionObserver();
|
||||||
|
}
|
||||||
|
|
||||||
if (TextUtils.isEmpty(searchString) || wasSearchFocused) {
|
if (TextUtils.isEmpty(searchString) || wasSearchFocused) {
|
||||||
showKeyboardSearch();
|
showKeyboardSearch();
|
||||||
@ -259,7 +285,9 @@ public class SearchFragment
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroyView() {
|
public void onDestroyView() {
|
||||||
if (DEBUG) Log.d(TAG, "onDestroyView() called");
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onDestroyView() called");
|
||||||
|
}
|
||||||
unsetSearchListeners();
|
unsetSearchListeners();
|
||||||
super.onDestroyView();
|
super.onDestroyView();
|
||||||
}
|
}
|
||||||
@ -267,19 +295,27 @@ public class SearchFragment
|
|||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
if (searchDisposable != null) searchDisposable.dispose();
|
if (searchDisposable != null) {
|
||||||
if (suggestionDisposable != null) suggestionDisposable.dispose();
|
searchDisposable.dispose();
|
||||||
if (disposables != null) disposables.clear();
|
}
|
||||||
|
if (suggestionDisposable != null) {
|
||||||
|
suggestionDisposable.dispose();
|
||||||
|
}
|
||||||
|
if (disposables != null) {
|
||||||
|
disposables.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
|
||||||
switch (requestCode) {
|
switch (requestCode) {
|
||||||
case ReCaptchaActivity.RECAPTCHA_REQUEST:
|
case ReCaptchaActivity.RECAPTCHA_REQUEST:
|
||||||
if (resultCode == Activity.RESULT_OK
|
if (resultCode == Activity.RESULT_OK
|
||||||
&& !TextUtils.isEmpty(searchString)) {
|
&& !TextUtils.isEmpty(searchString)) {
|
||||||
search(searchString, contentFilter, sortFilter);
|
search(searchString, contentFilter, sortFilter);
|
||||||
} else Log.e(TAG, "ReCaptcha failed");
|
} else {
|
||||||
|
Log.e(TAG, "ReCaptcha failed");
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@ -293,25 +329,27 @@ public class SearchFragment
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||||
super.initViews(rootView, savedInstanceState);
|
super.initViews(rootView, savedInstanceState);
|
||||||
suggestionsPanel = rootView.findViewById(R.id.suggestions_panel);
|
suggestionsPanel = rootView.findViewById(R.id.suggestions_panel);
|
||||||
suggestionsRecyclerView = rootView.findViewById(R.id.suggestions_list);
|
suggestionsRecyclerView = rootView.findViewById(R.id.suggestions_list);
|
||||||
suggestionsRecyclerView.setAdapter(suggestionListAdapter);
|
suggestionsRecyclerView.setAdapter(suggestionListAdapter);
|
||||||
new ItemTouchHelper(new ItemTouchHelper.Callback() {
|
new ItemTouchHelper(new ItemTouchHelper.Callback() {
|
||||||
@Override
|
@Override
|
||||||
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
|
public int getMovementFlags(@NonNull final RecyclerView recyclerView,
|
||||||
|
@NonNull final RecyclerView.ViewHolder viewHolder) {
|
||||||
return getSuggestionMovementFlags(recyclerView, viewHolder);
|
return getSuggestionMovementFlags(recyclerView, viewHolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder,
|
public boolean onMove(@NonNull final RecyclerView recyclerView,
|
||||||
@NonNull RecyclerView.ViewHolder viewHolder1) {
|
@NonNull final RecyclerView.ViewHolder viewHolder,
|
||||||
|
@NonNull final RecyclerView.ViewHolder viewHolder1) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int i) {
|
public void onSwiped(@NonNull final RecyclerView.ViewHolder viewHolder, final int i) {
|
||||||
onSuggestionItemSwiped(viewHolder, i);
|
onSuggestionItemSwiped(viewHolder, i);
|
||||||
}
|
}
|
||||||
}).attachToRecyclerView(suggestionsRecyclerView);
|
}).attachToRecyclerView(suggestionsRecyclerView);
|
||||||
@ -319,6 +357,8 @@ public class SearchFragment
|
|||||||
searchToolbarContainer = activity.findViewById(R.id.toolbar_search_container);
|
searchToolbarContainer = activity.findViewById(R.id.toolbar_search_container);
|
||||||
searchEditText = searchToolbarContainer.findViewById(R.id.toolbar_search_edit_text);
|
searchEditText = searchToolbarContainer.findViewById(R.id.toolbar_search_edit_text);
|
||||||
searchClear = searchToolbarContainer.findViewById(R.id.toolbar_search_clear);
|
searchClear = searchToolbarContainer.findViewById(R.id.toolbar_search_clear);
|
||||||
|
|
||||||
|
correctSuggestion = rootView.findViewById(R.id.correct_suggestion);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
@ -326,21 +366,19 @@ public class SearchFragment
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void writeTo(Queue<Object> objectsToSave) {
|
public void writeTo(final Queue<Object> objectsToSave) {
|
||||||
super.writeTo(objectsToSave);
|
super.writeTo(objectsToSave);
|
||||||
objectsToSave.add(currentPageUrl);
|
objectsToSave.add(nextPage);
|
||||||
objectsToSave.add(nextPageUrl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception {
|
public void readFrom(@NonNull final Queue<Object> savedObjects) throws Exception {
|
||||||
super.readFrom(savedObjects);
|
super.readFrom(savedObjects);
|
||||||
currentPageUrl = (String) savedObjects.poll();
|
nextPage = (Page) savedObjects.poll();
|
||||||
nextPageUrl = (String) savedObjects.poll();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSaveInstanceState(Bundle bundle) {
|
public void onSaveInstanceState(final Bundle bundle) {
|
||||||
searchString = searchEditText != null
|
searchString = searchEditText != null
|
||||||
? searchEditText.getText().toString()
|
? searchEditText.getText().toString()
|
||||||
: searchString;
|
: searchString;
|
||||||
@ -372,7 +410,7 @@ public class SearchFragment
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
|
||||||
super.onCreateOptionsMenu(menu, inflater);
|
super.onCreateOptionsMenu(menu, inflater);
|
||||||
|
|
||||||
ActionBar supportActionBar = activity.getSupportActionBar();
|
ActionBar supportActionBar = activity.getSupportActionBar();
|
||||||
@ -386,13 +424,20 @@ public class SearchFragment
|
|||||||
int itemId = 0;
|
int itemId = 0;
|
||||||
boolean isFirstItem = true;
|
boolean isFirstItem = true;
|
||||||
final Context c = getContext();
|
final Context c = getContext();
|
||||||
for(String filter : service.getSearchQHFactory().getAvailableContentFilter()) {
|
for (String filter : service.getSearchQHFactory().getAvailableContentFilter()) {
|
||||||
|
if (filter.equals("music_songs")) {
|
||||||
|
MenuItem musicItem = menu.add(2,
|
||||||
|
itemId++,
|
||||||
|
0,
|
||||||
|
"YouTube Music");
|
||||||
|
musicItem.setEnabled(false);
|
||||||
|
}
|
||||||
menuItemToFilterName.put(itemId, filter);
|
menuItemToFilterName.put(itemId, filter);
|
||||||
MenuItem item = menu.add(1,
|
MenuItem item = menu.add(1,
|
||||||
itemId++,
|
itemId++,
|
||||||
0,
|
0,
|
||||||
ServiceHelper.getTranslatedFilterString(filter, c));
|
ServiceHelper.getTranslatedFilterString(filter, c));
|
||||||
if(isFirstItem) {
|
if (isFirstItem) {
|
||||||
item.setChecked(true);
|
item.setChecked(true);
|
||||||
isFirstItem = false;
|
isFirstItem = false;
|
||||||
}
|
}
|
||||||
@ -403,19 +448,20 @@ public class SearchFragment
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||||
|
List<String> cf = new ArrayList<>(1);
|
||||||
List<String> contentFilter = new ArrayList<>(1);
|
cf.add(menuItemToFilterName.get(item.getItemId()));
|
||||||
contentFilter.add(menuItemToFilterName.get(item.getItemId()));
|
changeContentFilter(item, cf);
|
||||||
changeContentFilter(item, contentFilter);
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void restoreFilterChecked(Menu menu, int itemId) {
|
private void restoreFilterChecked(final Menu menu, final int itemId) {
|
||||||
if (itemId != -1) {
|
if (itemId != -1) {
|
||||||
MenuItem item = menu.findItem(itemId);
|
MenuItem item = menu.findItem(itemId);
|
||||||
if (item == null) return;
|
if (item == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
item.setChecked(true);
|
item.setChecked(true);
|
||||||
}
|
}
|
||||||
@ -425,13 +471,13 @@ public class SearchFragment
|
|||||||
// Search
|
// Search
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
private TextWatcher textWatcher;
|
|
||||||
|
|
||||||
private void showSearchOnStart() {
|
private void showSearchOnStart() {
|
||||||
if (DEBUG) Log.d(TAG, "showSearchOnStart() called, searchQuery → "
|
if (DEBUG) {
|
||||||
+ searchString
|
Log.d(TAG, "showSearchOnStart() called, searchQuery → "
|
||||||
+ ", lastSearchedQuery → "
|
+ searchString
|
||||||
+ lastSearchedString);
|
+ ", lastSearchedQuery → "
|
||||||
|
+ lastSearchedString);
|
||||||
|
}
|
||||||
searchEditText.setText(searchString);
|
searchEditText.setText(searchString);
|
||||||
|
|
||||||
if (TextUtils.isEmpty(searchString) || TextUtils.isEmpty(searchEditText.getText())) {
|
if (TextUtils.isEmpty(searchString) || TextUtils.isEmpty(searchEditText.getText())) {
|
||||||
@ -451,14 +497,20 @@ public class SearchFragment
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void initSearchListeners() {
|
private void initSearchListeners() {
|
||||||
if (DEBUG) Log.d(TAG, "initSearchListeners() called");
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "initSearchListeners() called");
|
||||||
|
}
|
||||||
searchClear.setOnClickListener(v -> {
|
searchClear.setOnClickListener(v -> {
|
||||||
if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]");
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onClick() called with: v = [" + v + "]");
|
||||||
|
}
|
||||||
if (TextUtils.isEmpty(searchEditText.getText())) {
|
if (TextUtils.isEmpty(searchEditText.getText())) {
|
||||||
NavigationHelper.gotoMainFragment(getFragmentManager());
|
NavigationHelper.gotoMainFragment(getFragmentManager());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
correctSuggestion.setVisibility(View.GONE);
|
||||||
|
|
||||||
searchEditText.setText("");
|
searchEditText.setText("");
|
||||||
suggestionListAdapter.setItems(new ArrayList<>());
|
suggestionListAdapter.setItems(new ArrayList<>());
|
||||||
showKeyboardSearch();
|
showKeyboardSearch();
|
||||||
@ -467,53 +519,65 @@ public class SearchFragment
|
|||||||
TooltipCompat.setTooltipText(searchClear, getString(R.string.clear));
|
TooltipCompat.setTooltipText(searchClear, getString(R.string.clear));
|
||||||
|
|
||||||
searchEditText.setOnClickListener(v -> {
|
searchEditText.setOnClickListener(v -> {
|
||||||
if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]");
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onClick() called with: v = [" + v + "]");
|
||||||
|
}
|
||||||
if (isSuggestionsEnabled && errorPanelRoot.getVisibility() != View.VISIBLE) {
|
if (isSuggestionsEnabled && errorPanelRoot.getVisibility() != View.VISIBLE) {
|
||||||
showSuggestionsPanel();
|
showSuggestionsPanel();
|
||||||
}
|
}
|
||||||
if(FireTvUtils.isFireTv()){
|
if (AndroidTvUtils.isTv(getContext())) {
|
||||||
showKeyboardSearch();
|
showKeyboardSearch();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
searchEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> {
|
searchEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> {
|
||||||
if (DEBUG) Log.d(TAG, "onFocusChange() called with: v = [" + v + "], hasFocus = [" + hasFocus + "]");
|
if (DEBUG) {
|
||||||
if (isSuggestionsEnabled && hasFocus && errorPanelRoot.getVisibility() != View.VISIBLE) {
|
Log.d(TAG, "onFocusChange() called with: "
|
||||||
|
+ "v = [" + v + "], hasFocus = [" + hasFocus + "]");
|
||||||
|
}
|
||||||
|
if (isSuggestionsEnabled && hasFocus
|
||||||
|
&& errorPanelRoot.getVisibility() != View.VISIBLE) {
|
||||||
showSuggestionsPanel();
|
showSuggestionsPanel();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
suggestionListAdapter.setListener(new SuggestionListAdapter.OnSuggestionItemSelected() {
|
suggestionListAdapter.setListener(new SuggestionListAdapter.OnSuggestionItemSelected() {
|
||||||
@Override
|
@Override
|
||||||
public void onSuggestionItemSelected(SuggestionItem item) {
|
public void onSuggestionItemSelected(final SuggestionItem item) {
|
||||||
search(item.query, new String[0], "");
|
search(item.query, new String[0], "");
|
||||||
searchEditText.setText(item.query);
|
searchEditText.setText(item.query);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSuggestionItemInserted(SuggestionItem item) {
|
public void onSuggestionItemInserted(final SuggestionItem item) {
|
||||||
searchEditText.setText(item.query);
|
searchEditText.setText(item.query);
|
||||||
searchEditText.setSelection(searchEditText.getText().length());
|
searchEditText.setSelection(searchEditText.getText().length());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSuggestionItemLongClick(SuggestionItem item) {
|
public void onSuggestionItemLongClick(final SuggestionItem item) {
|
||||||
if (item.fromHistory) showDeleteSuggestionDialog(item);
|
if (item.fromHistory) {
|
||||||
|
showDeleteSuggestionDialog(item);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (textWatcher != null) searchEditText.removeTextChangedListener(textWatcher);
|
if (textWatcher != null) {
|
||||||
|
searchEditText.removeTextChangedListener(textWatcher);
|
||||||
|
}
|
||||||
textWatcher = new TextWatcher() {
|
textWatcher = new TextWatcher() {
|
||||||
@Override
|
@Override
|
||||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
public void beforeTextChanged(final CharSequence s, final int start,
|
||||||
|
final int count, final int after) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
public void onTextChanged(final CharSequence s, final int start,
|
||||||
|
final int before, final int count) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void afterTextChanged(Editable s) {
|
public void afterTextChanged(final Editable s) {
|
||||||
String newText = searchEditText.getText().toString();
|
String newText = searchEditText.getText().toString();
|
||||||
suggestionPublisher.onNext(newText);
|
suggestionPublisher.onNext(newText);
|
||||||
}
|
}
|
||||||
@ -522,48 +586,62 @@ public class SearchFragment
|
|||||||
searchEditText.setOnEditorActionListener(
|
searchEditText.setOnEditorActionListener(
|
||||||
(TextView v, int actionId, KeyEvent event) -> {
|
(TextView v, int actionId, KeyEvent event) -> {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "onEditorAction() called with: v = [" + v + "], actionId = [" + actionId + "], event = [" + event + "]");
|
Log.d(TAG, "onEditorAction() called with: v = [" + v + "], "
|
||||||
|
+ "actionId = [" + actionId + "], event = [" + event + "]");
|
||||||
}
|
}
|
||||||
if(actionId == EditorInfo.IME_ACTION_PREVIOUS){
|
if (actionId == EditorInfo.IME_ACTION_PREVIOUS) {
|
||||||
hideKeyboardSearch();
|
hideKeyboardSearch();
|
||||||
} else if (event != null
|
} else if (event != null
|
||||||
&& (event.getKeyCode() == KeyEvent.KEYCODE_ENTER
|
&& (event.getKeyCode() == KeyEvent.KEYCODE_ENTER
|
||||||
|| event.getAction() == EditorInfo.IME_ACTION_SEARCH)) {
|
|| event.getAction() == EditorInfo.IME_ACTION_SEARCH)) {
|
||||||
search(searchEditText.getText().toString(), new String[0], "");
|
search(searchEditText.getText().toString(), new String[0], "");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (suggestionDisposable == null || suggestionDisposable.isDisposed())
|
if (suggestionDisposable == null || suggestionDisposable.isDisposed()) {
|
||||||
initSuggestionObserver();
|
initSuggestionObserver();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void unsetSearchListeners() {
|
private void unsetSearchListeners() {
|
||||||
if (DEBUG) Log.d(TAG, "unsetSearchListeners() called");
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "unsetSearchListeners() called");
|
||||||
|
}
|
||||||
searchClear.setOnClickListener(null);
|
searchClear.setOnClickListener(null);
|
||||||
searchClear.setOnLongClickListener(null);
|
searchClear.setOnLongClickListener(null);
|
||||||
searchEditText.setOnClickListener(null);
|
searchEditText.setOnClickListener(null);
|
||||||
searchEditText.setOnFocusChangeListener(null);
|
searchEditText.setOnFocusChangeListener(null);
|
||||||
searchEditText.setOnEditorActionListener(null);
|
searchEditText.setOnEditorActionListener(null);
|
||||||
|
|
||||||
if (textWatcher != null) searchEditText.removeTextChangedListener(textWatcher);
|
if (textWatcher != null) {
|
||||||
|
searchEditText.removeTextChangedListener(textWatcher);
|
||||||
|
}
|
||||||
textWatcher = null;
|
textWatcher = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showSuggestionsPanel() {
|
private void showSuggestionsPanel() {
|
||||||
if (DEBUG) Log.d(TAG, "showSuggestionsPanel() called");
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "showSuggestionsPanel() called");
|
||||||
|
}
|
||||||
animateView(suggestionsPanel, AnimationUtils.Type.LIGHT_SLIDE_AND_ALPHA, true, 200);
|
animateView(suggestionsPanel, AnimationUtils.Type.LIGHT_SLIDE_AND_ALPHA, true, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void hideSuggestionsPanel() {
|
private void hideSuggestionsPanel() {
|
||||||
if (DEBUG) Log.d(TAG, "hideSuggestionsPanel() called");
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "hideSuggestionsPanel() called");
|
||||||
|
}
|
||||||
animateView(suggestionsPanel, AnimationUtils.Type.LIGHT_SLIDE_AND_ALPHA, false, 200);
|
animateView(suggestionsPanel, AnimationUtils.Type.LIGHT_SLIDE_AND_ALPHA, false, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showKeyboardSearch() {
|
private void showKeyboardSearch() {
|
||||||
if (DEBUG) Log.d(TAG, "showKeyboardSearch() called");
|
if (DEBUG) {
|
||||||
if (searchEditText == null) return;
|
Log.d(TAG, "showKeyboardSearch() called");
|
||||||
|
}
|
||||||
|
if (searchEditText == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (searchEditText.requestFocus()) {
|
if (searchEditText.requestFocus()) {
|
||||||
InputMethodManager imm = (InputMethodManager) activity.getSystemService(
|
InputMethodManager imm = (InputMethodManager) activity.getSystemService(
|
||||||
@ -573,19 +651,26 @@ public class SearchFragment
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void hideKeyboardSearch() {
|
private void hideKeyboardSearch() {
|
||||||
if (DEBUG) Log.d(TAG, "hideKeyboardSearch() called");
|
if (DEBUG) {
|
||||||
if (searchEditText == null) return;
|
Log.d(TAG, "hideKeyboardSearch() called");
|
||||||
|
}
|
||||||
|
if (searchEditText == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
InputMethodManager imm = (InputMethodManager) activity.getSystemService(
|
InputMethodManager imm = (InputMethodManager) activity
|
||||||
Context.INPUT_METHOD_SERVICE);
|
.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||||
imm.hideSoftInputFromWindow(searchEditText.getWindowToken(), InputMethodManager.RESULT_UNCHANGED_SHOWN);
|
imm.hideSoftInputFromWindow(searchEditText.getWindowToken(),
|
||||||
|
InputMethodManager.RESULT_UNCHANGED_SHOWN);
|
||||||
|
|
||||||
searchEditText.clearFocus();
|
searchEditText.clearFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showDeleteSuggestionDialog(final SuggestionItem item) {
|
private void showDeleteSuggestionDialog(final SuggestionItem item) {
|
||||||
if (activity == null || historyRecordManager == null || suggestionPublisher == null ||
|
if (activity == null || historyRecordManager == null || suggestionPublisher == null
|
||||||
searchEditText == null || disposables == null) return;
|
|| searchEditText == null || disposables == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
final String query = item.query;
|
final String query = item.query;
|
||||||
new AlertDialog.Builder(activity)
|
new AlertDialog.Builder(activity)
|
||||||
.setTitle(query)
|
.setTitle(query)
|
||||||
@ -619,20 +704,20 @@ public class SearchFragment
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void giveSearchEditTextFocus() {
|
|
||||||
showKeyboardSearch();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initSuggestionObserver() {
|
private void initSuggestionObserver() {
|
||||||
if (DEBUG) Log.d(TAG, "initSuggestionObserver() called");
|
if (DEBUG) {
|
||||||
if (suggestionDisposable != null) suggestionDisposable.dispose();
|
Log.d(TAG, "initSuggestionObserver() called");
|
||||||
|
}
|
||||||
|
if (suggestionDisposable != null) {
|
||||||
|
suggestionDisposable.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
final Observable<String> observable = suggestionPublisher
|
final Observable<String> observable = suggestionPublisher
|
||||||
.debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS)
|
.debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS)
|
||||||
.startWith(searchString != null
|
.startWith(searchString != null
|
||||||
? searchString
|
? searchString
|
||||||
: "")
|
: "")
|
||||||
.filter(searchString -> isSuggestionsEnabled);
|
.filter(ss -> isSuggestionsEnabled);
|
||||||
|
|
||||||
suggestionDisposable = observable
|
suggestionDisposable = observable
|
||||||
.switchMap(query -> {
|
.switchMap(query -> {
|
||||||
@ -641,13 +726,15 @@ public class SearchFragment
|
|||||||
final Observable<List<SuggestionItem>> local = flowable.toObservable()
|
final Observable<List<SuggestionItem>> local = flowable.toObservable()
|
||||||
.map(searchHistoryEntries -> {
|
.map(searchHistoryEntries -> {
|
||||||
List<SuggestionItem> result = new ArrayList<>();
|
List<SuggestionItem> result = new ArrayList<>();
|
||||||
for (SearchHistoryEntry entry : searchHistoryEntries)
|
for (SearchHistoryEntry entry : searchHistoryEntries) {
|
||||||
result.add(new SuggestionItem(true, entry.getSearch()));
|
result.add(new SuggestionItem(true, entry.getSearch()));
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (query.length() < THRESHOLD_NETWORK_SUGGESTION) {
|
if (query.length() < THRESHOLD_NETWORK_SUGGESTION) {
|
||||||
// Only pass through if the query length is equal or greater than THRESHOLD_NETWORK_SUGGESTION
|
// Only pass through if the query length
|
||||||
|
// is equal or greater than THRESHOLD_NETWORK_SUGGESTION
|
||||||
return local.materialize();
|
return local.materialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -664,7 +751,9 @@ public class SearchFragment
|
|||||||
|
|
||||||
return Observable.zip(local, network, (localResult, networkResult) -> {
|
return Observable.zip(local, network, (localResult, networkResult) -> {
|
||||||
List<SuggestionItem> result = new ArrayList<>();
|
List<SuggestionItem> result = new ArrayList<>();
|
||||||
if (localResult.size() > 0) result.addAll(localResult);
|
if (localResult.size() > 0) {
|
||||||
|
result.addAll(localResult);
|
||||||
|
}
|
||||||
|
|
||||||
// Remove duplicates
|
// Remove duplicates
|
||||||
final Iterator<SuggestionItem> iterator = networkResult.iterator();
|
final Iterator<SuggestionItem> iterator = networkResult.iterator();
|
||||||
@ -678,7 +767,9 @@ public class SearchFragment
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (networkResult.size() > 0) result.addAll(networkResult);
|
if (networkResult.size() > 0) {
|
||||||
|
result.addAll(networkResult);
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}).materialize();
|
}).materialize();
|
||||||
})
|
})
|
||||||
@ -688,12 +779,7 @@ public class SearchFragment
|
|||||||
if (listNotification.isOnNext()) {
|
if (listNotification.isOnNext()) {
|
||||||
handleSuggestions(listNotification.getValue());
|
handleSuggestions(listNotification.getValue());
|
||||||
} else if (listNotification.isOnError()) {
|
} else if (listNotification.isOnError()) {
|
||||||
Throwable error = listNotification.getError();
|
onSuggestionError(listNotification.getError());
|
||||||
if (!ExtractorHelper.hasAssignableCauseThrowable(error,
|
|
||||||
IOException.class, SocketException.class,
|
|
||||||
InterruptedException.class, InterruptedIOException.class)) {
|
|
||||||
onSuggestionError(error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -703,17 +789,21 @@ public class SearchFragment
|
|||||||
// no-op
|
// no-op
|
||||||
}
|
}
|
||||||
|
|
||||||
private void search(final String searchString, String[] contentFilter, String sortFilter) {
|
private void search(final String ss, final String[] cf, final String sf) {
|
||||||
if (DEBUG) Log.d(TAG, "search() called with: query = [" + searchString + "]");
|
if (DEBUG) {
|
||||||
if (searchString.isEmpty()) return;
|
Log.d(TAG, "search() called with: query = [" + ss + "]");
|
||||||
|
}
|
||||||
|
if (ss.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final StreamingService service = NewPipe.getServiceByUrl(searchString);
|
final StreamingService streamingService = NewPipe.getServiceByUrl(ss);
|
||||||
if (service != null) {
|
if (streamingService != null) {
|
||||||
showLoading();
|
showLoading();
|
||||||
disposables.add(Observable
|
disposables.add(Observable
|
||||||
.fromCallable(() ->
|
.fromCallable(() ->
|
||||||
NavigationHelper.getIntentByLink(activity, service, searchString))
|
NavigationHelper.getIntentByLink(activity, streamingService, ss))
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(intent -> {
|
.subscribe(intent -> {
|
||||||
@ -723,36 +813,41 @@ public class SearchFragment
|
|||||||
showError(getString(R.string.url_not_supported_toast), false)));
|
showError(getString(R.string.url_not_supported_toast), false)));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception ignored) {
|
||||||
// Exception occurred, it's not a url
|
// Exception occurred, it's not a url
|
||||||
}
|
}
|
||||||
|
|
||||||
lastSearchedString = this.searchString;
|
lastSearchedString = this.searchString;
|
||||||
this.searchString = searchString;
|
this.searchString = ss;
|
||||||
infoListAdapter.clearStreamItemList();
|
infoListAdapter.clearStreamItemList();
|
||||||
hideSuggestionsPanel();
|
hideSuggestionsPanel();
|
||||||
hideKeyboardSearch();
|
hideKeyboardSearch();
|
||||||
|
|
||||||
historyRecordManager.onSearched(serviceId, searchString)
|
historyRecordManager.onSearched(serviceId, ss)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(
|
.subscribe(
|
||||||
ignored -> {},
|
ignored -> {
|
||||||
|
},
|
||||||
error -> showSnackBarError(error, UserAction.SEARCHED,
|
error -> showSnackBarError(error, UserAction.SEARCHED,
|
||||||
NewPipe.getNameOfService(serviceId), searchString, 0)
|
NewPipe.getNameOfService(serviceId), ss, 0)
|
||||||
);
|
);
|
||||||
suggestionPublisher.onNext(searchString);
|
suggestionPublisher.onNext(ss);
|
||||||
startLoading(false);
|
startLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void startLoading(boolean forceLoad) {
|
public void startLoading(final boolean forceLoad) {
|
||||||
super.startLoading(forceLoad);
|
super.startLoading(forceLoad);
|
||||||
if (disposables != null) disposables.clear();
|
if (disposables != null) {
|
||||||
if (searchDisposable != null) searchDisposable.dispose();
|
disposables.clear();
|
||||||
|
}
|
||||||
|
if (searchDisposable != null) {
|
||||||
|
searchDisposable.dispose();
|
||||||
|
}
|
||||||
searchDisposable = ExtractorHelper.searchFor(serviceId,
|
searchDisposable = ExtractorHelper.searchFor(serviceId,
|
||||||
searchString,
|
searchString,
|
||||||
Arrays.asList(contentFilter),
|
Arrays.asList(contentFilter),
|
||||||
sortFilter)
|
sortFilter)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.doOnEvent((searchResult, throwable) -> isLoading.set(false))
|
.doOnEvent((searchResult, throwable) -> isLoading.set(false))
|
||||||
@ -762,16 +857,20 @@ public class SearchFragment
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void loadMoreItems() {
|
protected void loadMoreItems() {
|
||||||
if(nextPageUrl == null || nextPageUrl.isEmpty()) return;
|
if (!Page.isValid(nextPage)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
isLoading.set(true);
|
isLoading.set(true);
|
||||||
showListFooter(true);
|
showListFooter(true);
|
||||||
if (searchDisposable != null) searchDisposable.dispose();
|
if (searchDisposable != null) {
|
||||||
|
searchDisposable.dispose();
|
||||||
|
}
|
||||||
searchDisposable = ExtractorHelper.getMoreSearchItems(
|
searchDisposable = ExtractorHelper.getMoreSearchItems(
|
||||||
serviceId,
|
serviceId,
|
||||||
searchString,
|
searchString,
|
||||||
asList(contentFilter),
|
asList(contentFilter),
|
||||||
sortFilter,
|
sortFilter,
|
||||||
nextPageUrl)
|
nextPage)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.doOnEvent((nextItemsResult, throwable) -> isLoading.set(false))
|
.doOnEvent((nextItemsResult, throwable) -> isLoading.set(false))
|
||||||
@ -785,7 +884,7 @@ public class SearchFragment
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onItemSelected(InfoItem selectedItem) {
|
protected void onItemSelected(final InfoItem selectedItem) {
|
||||||
super.onItemSelected(selectedItem);
|
super.onItemSelected(selectedItem);
|
||||||
hideKeyboardSearch();
|
hideKeyboardSearch();
|
||||||
}
|
}
|
||||||
@ -794,22 +893,22 @@ public class SearchFragment
|
|||||||
// Utils
|
// Utils
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
private void changeContentFilter(MenuItem item, List<String> contentFilter) {
|
private void changeContentFilter(final MenuItem item, final List<String> cf) {
|
||||||
this.filterItemCheckedId = item.getItemId();
|
this.filterItemCheckedId = item.getItemId();
|
||||||
item.setChecked(true);
|
item.setChecked(true);
|
||||||
|
|
||||||
this.contentFilter = new String[] {contentFilter.get(0)};
|
this.contentFilter = new String[]{cf.get(0)};
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(searchString)) {
|
if (!TextUtils.isEmpty(searchString)) {
|
||||||
search(searchString, this.contentFilter, sortFilter);
|
search(searchString, this.contentFilter, sortFilter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setQuery(int serviceId, String searchString, String[] contentfilter, String sortFilter) {
|
private void setQuery(final int sid, final String ss, final String[] cf, final String sf) {
|
||||||
this.serviceId = serviceId;
|
this.serviceId = sid;
|
||||||
this.searchString = searchString;
|
this.searchString = searchString;
|
||||||
this.contentFilter = contentfilter;
|
this.contentFilter = cf;
|
||||||
this.sortFilter = sortFilter;
|
this.sortFilter = sf;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
@ -817,7 +916,9 @@ public class SearchFragment
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
public void handleSuggestions(@NonNull final List<SuggestionItem> suggestions) {
|
public void handleSuggestions(@NonNull final List<SuggestionItem> suggestions) {
|
||||||
if (DEBUG) Log.d(TAG, "handleSuggestions() called with: suggestions = [" + suggestions + "]");
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "handleSuggestions() called with: suggestions = [" + suggestions + "]");
|
||||||
|
}
|
||||||
suggestionsRecyclerView.smoothScrollToPosition(0);
|
suggestionsRecyclerView.smoothScrollToPosition(0);
|
||||||
suggestionsRecyclerView.post(() -> suggestionListAdapter.setItems(suggestions));
|
suggestionsRecyclerView.post(() -> suggestionListAdapter.setItems(suggestions));
|
||||||
|
|
||||||
@ -826,9 +927,13 @@ public class SearchFragment
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onSuggestionError(Throwable exception) {
|
public void onSuggestionError(final Throwable exception) {
|
||||||
if (DEBUG) Log.d(TAG, "onSuggestionError() called with: exception = [" + exception + "]");
|
if (DEBUG) {
|
||||||
if (super.onError(exception)) return;
|
Log.d(TAG, "onSuggestionError() called with: exception = [" + exception + "]");
|
||||||
|
}
|
||||||
|
if (super.onError(exception)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
int errorId = exception instanceof ParsingException
|
int errorId = exception instanceof ParsingException
|
||||||
? R.string.parsing_error
|
? R.string.parsing_error
|
||||||
@ -848,7 +953,7 @@ public class SearchFragment
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void showError(String message, boolean showRetryButton) {
|
public void showError(final String message, final boolean showRetryButton) {
|
||||||
super.showError(message, showRetryButton);
|
super.showError(message, showRetryButton);
|
||||||
hideSuggestionsPanel();
|
hideSuggestionsPanel();
|
||||||
hideKeyboardSearch();
|
hideKeyboardSearch();
|
||||||
@ -859,18 +964,22 @@ public class SearchFragment
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleResult(@NonNull SearchInfo result) {
|
public void handleResult(@NonNull final SearchInfo result) {
|
||||||
final List<Throwable> exceptions = result.getErrors();
|
final List<Throwable> exceptions = result.getErrors();
|
||||||
if (!exceptions.isEmpty()
|
if (!exceptions.isEmpty()
|
||||||
&& !(exceptions.size() == 1
|
&& !(exceptions.size() == 1
|
||||||
&& exceptions.get(0) instanceof SearchExtractor.NothingFoundException)){
|
&& exceptions.get(0) instanceof SearchExtractor.NothingFoundException)) {
|
||||||
showSnackBarError(result.getErrors(), UserAction.SEARCHED,
|
showSnackBarError(result.getErrors(), UserAction.SEARCHED,
|
||||||
NewPipe.getNameOfService(serviceId), searchString, 0);
|
NewPipe.getNameOfService(serviceId), searchString, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
searchSuggestion = result.getSearchSuggestion();
|
||||||
|
isCorrectedSearch = result.isCorrectedSearch();
|
||||||
|
|
||||||
|
handleSearchSuggestion();
|
||||||
|
|
||||||
lastSearchedString = searchString;
|
lastSearchedString = searchString;
|
||||||
nextPageUrl = result.getNextPageUrl();
|
nextPage = result.getNextPage();
|
||||||
currentPageUrl = result.getUrl();
|
|
||||||
|
|
||||||
if (infoListAdapter.getItemsList().size() == 0) {
|
if (infoListAdapter.getItemsList().size() == 0) {
|
||||||
if (!result.getRelatedItems().isEmpty()) {
|
if (!result.getRelatedItems().isEmpty()) {
|
||||||
@ -885,24 +994,58 @@ public class SearchFragment
|
|||||||
super.handleResult(result);
|
super.handleResult(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void handleSearchSuggestion() {
|
||||||
|
if (TextUtils.isEmpty(searchSuggestion)) {
|
||||||
|
correctSuggestion.setVisibility(View.GONE);
|
||||||
|
} else {
|
||||||
|
final String helperText = getString(isCorrectedSearch
|
||||||
|
? R.string.search_showing_result_for
|
||||||
|
: R.string.did_you_mean);
|
||||||
|
|
||||||
|
final String highlightedSearchSuggestion =
|
||||||
|
"<b><i>" + escapeHtml(searchSuggestion) + "</i></b>";
|
||||||
|
correctSuggestion.setText(
|
||||||
|
Html.fromHtml(String.format(helperText, highlightedSearchSuggestion)));
|
||||||
|
|
||||||
|
|
||||||
|
correctSuggestion.setOnClickListener(v -> {
|
||||||
|
correctSuggestion.setVisibility(View.GONE);
|
||||||
|
search(searchSuggestion, contentFilter, sortFilter);
|
||||||
|
searchEditText.setText(searchSuggestion);
|
||||||
|
});
|
||||||
|
|
||||||
|
correctSuggestion.setOnLongClickListener(v -> {
|
||||||
|
searchEditText.setText(searchSuggestion);
|
||||||
|
searchEditText.setSelection(searchSuggestion.length());
|
||||||
|
showKeyboardSearch();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
correctSuggestion.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleNextItems(ListExtractor.InfoItemsPage result) {
|
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
|
||||||
showListFooter(false);
|
showListFooter(false);
|
||||||
currentPageUrl = result.getNextPageUrl();
|
|
||||||
infoListAdapter.addInfoItemList(result.getItems());
|
infoListAdapter.addInfoItemList(result.getItems());
|
||||||
nextPageUrl = result.getNextPageUrl();
|
nextPage = result.getNextPage();
|
||||||
|
|
||||||
if (!result.getErrors().isEmpty()) {
|
if (!result.getErrors().isEmpty()) {
|
||||||
showSnackBarError(result.getErrors(), UserAction.SEARCHED,
|
showSnackBarError(result.getErrors(), UserAction.SEARCHED,
|
||||||
NewPipe.getNameOfService(serviceId)
|
NewPipe.getNameOfService(serviceId),
|
||||||
, "\"" + searchString + "\" → page: " + nextPageUrl, 0);
|
"\"" + searchString + "\" → pageUrl: " + nextPage.getUrl() + ", "
|
||||||
|
+ "pageIds: " + nextPage.getIds() + ", "
|
||||||
|
+ "pageCookies: " + nextPage.getCookies(), 0);
|
||||||
}
|
}
|
||||||
super.handleNextItems(result);
|
super.handleNextItems(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean onError(Throwable exception) {
|
protected boolean onError(final Throwable exception) {
|
||||||
if (super.onError(exception)) return true;
|
if (super.onError(exception)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (exception instanceof SearchExtractor.NothingFoundException) {
|
if (exception instanceof SearchExtractor.NothingFoundException) {
|
||||||
infoListAdapter.clearStreamItemList();
|
infoListAdapter.clearStreamItemList();
|
||||||
@ -922,13 +1065,20 @@ public class SearchFragment
|
|||||||
// Suggestion item touch helper
|
// Suggestion item touch helper
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
public int getSuggestionMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
|
public int getSuggestionMovementFlags(@NonNull final RecyclerView recyclerView,
|
||||||
|
@NonNull final RecyclerView.ViewHolder viewHolder) {
|
||||||
final int position = viewHolder.getAdapterPosition();
|
final int position = viewHolder.getAdapterPosition();
|
||||||
|
if (position == RecyclerView.NO_POSITION) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
final SuggestionItem item = suggestionListAdapter.getItem(position);
|
final SuggestionItem item = suggestionListAdapter.getItem(position);
|
||||||
return item.fromHistory ? makeMovementFlags(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) : 0;
|
return item.fromHistory ? makeMovementFlags(0,
|
||||||
|
ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onSuggestionItemSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int i) {
|
public void onSuggestionItemSwiped(@NonNull final RecyclerView.ViewHolder viewHolder,
|
||||||
|
final int i) {
|
||||||
final int position = viewHolder.getAdapterPosition();
|
final int position = viewHolder.getAdapterPosition();
|
||||||
final String query = suggestionListAdapter.getItem(position).query;
|
final String query = suggestionListAdapter.getItem(position).query;
|
||||||
final Disposable onDelete = historyRecordManager.deleteSearchHistory(query)
|
final Disposable onDelete = historyRecordManager.deleteSearchHistory(query)
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
package org.schabi.newpipe.fragments.list.search;
|
package org.schabi.newpipe.fragments.list.search;
|
||||||
|
|
||||||
public class SuggestionItem {
|
public class SuggestionItem {
|
||||||
public final boolean fromHistory;
|
final boolean fromHistory;
|
||||||
public final String query;
|
public final String query;
|
||||||
|
|
||||||
public SuggestionItem(boolean fromHistory, String query) {
|
public SuggestionItem(final boolean fromHistory, final String query) {
|
||||||
this.fromHistory = fromHistory;
|
this.fromHistory = fromHistory;
|
||||||
this.query = query;
|
this.query = query;
|
||||||
}
|
}
|
||||||
|
@ -2,36 +2,32 @@ package org.schabi.newpipe.fragments.list.search;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.TypedArray;
|
import android.content.res.TypedArray;
|
||||||
import androidx.annotation.AttrRes;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.AttrRes;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class SuggestionListAdapter extends RecyclerView.Adapter<SuggestionListAdapter.SuggestionItemHolder> {
|
public class SuggestionListAdapter
|
||||||
|
extends RecyclerView.Adapter<SuggestionListAdapter.SuggestionItemHolder> {
|
||||||
private final ArrayList<SuggestionItem> items = new ArrayList<>();
|
private final ArrayList<SuggestionItem> items = new ArrayList<>();
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private OnSuggestionItemSelected listener;
|
private OnSuggestionItemSelected listener;
|
||||||
private boolean showSuggestionHistory = true;
|
private boolean showSuggestionHistory = true;
|
||||||
|
|
||||||
public interface OnSuggestionItemSelected {
|
public SuggestionListAdapter(final Context context) {
|
||||||
void onSuggestionItemSelected(SuggestionItem item);
|
|
||||||
void onSuggestionItemInserted(SuggestionItem item);
|
|
||||||
void onSuggestionItemLongClick(SuggestionItem item);
|
|
||||||
}
|
|
||||||
|
|
||||||
public SuggestionListAdapter(Context context) {
|
|
||||||
this.context = context;
|
this.context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setItems(List<SuggestionItem> items) {
|
public void setItems(final List<SuggestionItem> items) {
|
||||||
this.items.clear();
|
this.items.clear();
|
||||||
if (showSuggestionHistory) {
|
if (showSuggestionHistory) {
|
||||||
this.items.addAll(items);
|
this.items.addAll(items);
|
||||||
@ -46,36 +42,43 @@ public class SuggestionListAdapter extends RecyclerView.Adapter<SuggestionListAd
|
|||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setListener(OnSuggestionItemSelected listener) {
|
public void setListener(final OnSuggestionItemSelected listener) {
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setShowSuggestionHistory(boolean v) {
|
public void setShowSuggestionHistory(final boolean v) {
|
||||||
showSuggestionHistory = v;
|
showSuggestionHistory = v;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SuggestionItemHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
public SuggestionItemHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
|
||||||
return new SuggestionItemHolder(LayoutInflater.from(context).inflate(R.layout.item_search_suggestion, parent, false));
|
return new SuggestionItemHolder(LayoutInflater.from(context)
|
||||||
|
.inflate(R.layout.item_search_suggestion, parent, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(SuggestionItemHolder holder, int position) {
|
public void onBindViewHolder(final SuggestionItemHolder holder, final int position) {
|
||||||
final SuggestionItem currentItem = getItem(position);
|
final SuggestionItem currentItem = getItem(position);
|
||||||
holder.updateFrom(currentItem);
|
holder.updateFrom(currentItem);
|
||||||
holder.queryView.setOnClickListener(v -> {
|
holder.queryView.setOnClickListener(v -> {
|
||||||
if (listener != null) listener.onSuggestionItemSelected(currentItem);
|
if (listener != null) {
|
||||||
|
listener.onSuggestionItemSelected(currentItem);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
holder.queryView.setOnLongClickListener(v -> {
|
holder.queryView.setOnLongClickListener(v -> {
|
||||||
if (listener != null) listener.onSuggestionItemLongClick(currentItem);
|
if (listener != null) {
|
||||||
return true;
|
listener.onSuggestionItemLongClick(currentItem);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
});
|
});
|
||||||
holder.insertView.setOnClickListener(v -> {
|
holder.insertView.setOnClickListener(v -> {
|
||||||
if (listener != null) listener.onSuggestionItemInserted(currentItem);
|
if (listener != null) {
|
||||||
|
listener.onSuggestionItemInserted(currentItem);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
SuggestionItem getItem(int position) {
|
SuggestionItem getItem(final int position) {
|
||||||
return items.get(position);
|
return items.get(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,7 +91,15 @@ public class SuggestionListAdapter extends RecyclerView.Adapter<SuggestionListAd
|
|||||||
return getItemCount() == 0;
|
return getItemCount() == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class SuggestionItemHolder extends RecyclerView.ViewHolder {
|
public interface OnSuggestionItemSelected {
|
||||||
|
void onSuggestionItemSelected(SuggestionItem item);
|
||||||
|
|
||||||
|
void onSuggestionItemInserted(SuggestionItem item);
|
||||||
|
|
||||||
|
void onSuggestionItemLongClick(SuggestionItem item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class SuggestionItemHolder extends RecyclerView.ViewHolder {
|
||||||
private final TextView itemSuggestionQuery;
|
private final TextView itemSuggestionQuery;
|
||||||
private final ImageView suggestionIcon;
|
private final ImageView suggestionIcon;
|
||||||
private final View queryView;
|
private final View queryView;
|
||||||
@ -98,7 +109,7 @@ public class SuggestionListAdapter extends RecyclerView.Adapter<SuggestionListAd
|
|||||||
private final int historyResId;
|
private final int historyResId;
|
||||||
private final int searchResId;
|
private final int searchResId;
|
||||||
|
|
||||||
private SuggestionItemHolder(View rootView) {
|
private SuggestionItemHolder(final View rootView) {
|
||||||
super(rootView);
|
super(rootView);
|
||||||
suggestionIcon = rootView.findViewById(R.id.item_suggestion_icon);
|
suggestionIcon = rootView.findViewById(R.id.item_suggestion_icon);
|
||||||
itemSuggestionQuery = rootView.findViewById(R.id.item_suggestion_query);
|
itemSuggestionQuery = rootView.findViewById(R.id.item_suggestion_query);
|
||||||
@ -106,20 +117,21 @@ public class SuggestionListAdapter extends RecyclerView.Adapter<SuggestionListAd
|
|||||||
queryView = rootView.findViewById(R.id.suggestion_search);
|
queryView = rootView.findViewById(R.id.suggestion_search);
|
||||||
insertView = rootView.findViewById(R.id.suggestion_insert);
|
insertView = rootView.findViewById(R.id.suggestion_insert);
|
||||||
|
|
||||||
historyResId = resolveResourceIdFromAttr(rootView.getContext(), R.attr.history);
|
historyResId = resolveResourceIdFromAttr(rootView.getContext(), R.attr.ic_history);
|
||||||
searchResId = resolveResourceIdFromAttr(rootView.getContext(), R.attr.search);
|
searchResId = resolveResourceIdFromAttr(rootView.getContext(), R.attr.ic_search);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateFrom(SuggestionItem item) {
|
private static int resolveResourceIdFromAttr(final Context context,
|
||||||
suggestionIcon.setImageResource(item.fromHistory ? historyResId : searchResId);
|
@AttrRes final int attr) {
|
||||||
itemSuggestionQuery.setText(item.query);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int resolveResourceIdFromAttr(Context context, @AttrRes int attr) {
|
|
||||||
TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{attr});
|
TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{attr});
|
||||||
int attributeResourceId = a.getResourceId(0, 0);
|
int attributeResourceId = a.getResourceId(0, 0);
|
||||||
a.recycle();
|
a.recycle();
|
||||||
return attributeResourceId;
|
return attributeResourceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateFrom(final SuggestionItem item) {
|
||||||
|
suggestionIcon.setImageResource(item.fromHistory ? historyResId : searchResId);
|
||||||
|
itemSuggestionQuery.setText(item.query);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,6 @@ import android.content.Context;
|
|||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
@ -14,6 +12,9 @@ import android.view.ViewGroup;
|
|||||||
import android.widget.CompoundButton;
|
import android.widget.CompoundButton;
|
||||||
import android.widget.Switch;
|
import android.widget.Switch;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
@ -28,53 +29,61 @@ import java.io.Serializable;
|
|||||||
import io.reactivex.Single;
|
import io.reactivex.Single;
|
||||||
import io.reactivex.disposables.CompositeDisposable;
|
import io.reactivex.disposables.CompositeDisposable;
|
||||||
|
|
||||||
public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInfo> implements SharedPreferences.OnSharedPreferenceChangeListener{
|
public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInfo>
|
||||||
|
implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
|
private static final String INFO_KEY = "related_info_key";
|
||||||
private CompositeDisposable disposables = new CompositeDisposable();
|
private CompositeDisposable disposables = new CompositeDisposable();
|
||||||
private RelatedStreamInfo relatedStreamInfo;
|
private RelatedStreamInfo relatedStreamInfo;
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Views
|
// Views
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
private View headerRootLayout;
|
private View headerRootLayout;
|
||||||
private Switch aSwitch;
|
private Switch aSwitch;
|
||||||
|
|
||||||
private boolean mIsVisibleToUser = false;
|
private boolean mIsVisibleToUser = false;
|
||||||
|
|
||||||
public static RelatedVideosFragment getInstance(StreamInfo info) {
|
public static RelatedVideosFragment getInstance(final StreamInfo info) {
|
||||||
RelatedVideosFragment instance = new RelatedVideosFragment();
|
RelatedVideosFragment instance = new RelatedVideosFragment();
|
||||||
instance.setInitialData(info);
|
instance.setInitialData(info);
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setUserVisibleHint(final boolean isVisibleToUser) {
|
||||||
|
super.setUserVisibleHint(isVisibleToUser);
|
||||||
|
mIsVisibleToUser = isVisibleToUser;
|
||||||
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// LifeCycle
|
// LifeCycle
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setUserVisibleHint(boolean isVisibleToUser) {
|
public void onAttach(final Context context) {
|
||||||
super.setUserVisibleHint(isVisibleToUser);
|
|
||||||
mIsVisibleToUser = isVisibleToUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAttach(Context context) {
|
|
||||||
super.onAttach(context);
|
super.onAttach(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||||
|
@Nullable final ViewGroup container,
|
||||||
|
@Nullable final Bundle savedInstanceState) {
|
||||||
return inflater.inflate(R.layout.fragment_related_streams, container, false);
|
return inflater.inflate(R.layout.fragment_related_streams, container, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
if (disposables != null) disposables.clear();
|
if (disposables != null) {
|
||||||
|
disposables.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected View getListHeader(){
|
protected View getListHeader() {
|
||||||
if(relatedStreamInfo != null && relatedStreamInfo.getNextStream() != null){
|
if (relatedStreamInfo != null && relatedStreamInfo.getNextStream() != null) {
|
||||||
headerRootLayout = activity.getLayoutInflater().inflate(R.layout.related_streams_header, itemsList, false);
|
headerRootLayout = activity.getLayoutInflater()
|
||||||
|
.inflate(R.layout.related_streams_header, itemsList, false);
|
||||||
aSwitch = headerRootLayout.findViewById(R.id.autoplay_switch);
|
aSwitch = headerRootLayout.findViewById(R.id.autoplay_switch);
|
||||||
|
|
||||||
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getContext());
|
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||||
@ -82,14 +91,14 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
|
|||||||
aSwitch.setChecked(autoplay);
|
aSwitch.setChecked(autoplay);
|
||||||
aSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
aSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
|
public void onCheckedChanged(final CompoundButton compoundButton,
|
||||||
SharedPreferences.Editor prefEdit = PreferenceManager.getDefaultSharedPreferences(getContext()).edit();
|
final boolean b) {
|
||||||
prefEdit.putBoolean(getString(R.string.auto_queue_key), b);
|
PreferenceManager.getDefaultSharedPreferences(getContext()).edit()
|
||||||
prefEdit.apply();
|
.putBoolean(getString(R.string.auto_queue_key), b).apply();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return headerRootLayout;
|
return headerRootLayout;
|
||||||
}else{
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -99,38 +108,44 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
|
|||||||
return Single.fromCallable(() -> ListExtractor.InfoItemsPage.emptyPage());
|
return Single.fromCallable(() -> ListExtractor.InfoItemsPage.emptyPage());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Single<RelatedStreamInfo> loadResult(boolean forceLoad) {
|
|
||||||
return Single.fromCallable(() -> relatedStreamInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Contract
|
// Contract
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Single<RelatedStreamInfo> loadResult(final boolean forceLoad) {
|
||||||
|
return Single.fromCallable(() -> relatedStreamInfo);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void showLoading() {
|
public void showLoading() {
|
||||||
super.showLoading();
|
super.showLoading();
|
||||||
if(null != headerRootLayout) headerRootLayout.setVisibility(View.INVISIBLE);
|
if (headerRootLayout != null) {
|
||||||
|
headerRootLayout.setVisibility(View.INVISIBLE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleResult(@NonNull RelatedStreamInfo result) {
|
public void handleResult(@NonNull final RelatedStreamInfo result) {
|
||||||
|
|
||||||
super.handleResult(result);
|
super.handleResult(result);
|
||||||
|
|
||||||
if(null != headerRootLayout) headerRootLayout.setVisibility(View.VISIBLE);
|
if (headerRootLayout != null) {
|
||||||
AnimationUtils.slideUp(getView(),120, 96, 0.06f);
|
headerRootLayout.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
AnimationUtils.slideUp(getView(), 120, 96, 0.06f);
|
||||||
|
|
||||||
if (!result.getErrors().isEmpty()) {
|
if (!result.getErrors().isEmpty()) {
|
||||||
showSnackBarError(result.getErrors(), UserAction.REQUESTED_STREAM, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
|
showSnackBarError(result.getErrors(), UserAction.REQUESTED_STREAM,
|
||||||
|
NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (disposables != null) disposables.clear();
|
if (disposables != null) {
|
||||||
|
disposables.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleNextItems(ListExtractor.InfoItemsPage result) {
|
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
|
||||||
super.handleNextItems(result);
|
super.handleNextItems(result);
|
||||||
|
|
||||||
if (!result.getErrors().isEmpty()) {
|
if (!result.getErrors().isEmpty()) {
|
||||||
@ -147,11 +162,14 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean onError(Throwable exception) {
|
protected boolean onError(final Throwable exception) {
|
||||||
if (super.onError(exception)) return true;
|
if (super.onError(exception)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
hideLoading();
|
hideLoading();
|
||||||
showSnackBarError(exception, UserAction.REQUESTED_STREAM, NewPipe.getNameOfService(serviceId), url, R.string.general_error);
|
showSnackBarError(exception, UserAction.REQUESTED_STREAM,
|
||||||
|
NewPipe.getNameOfService(serviceId), url, R.string.general_error);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,45 +178,47 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setTitle(String title) {
|
public void setTitle(final String title) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setInitialData(StreamInfo info) {
|
private void setInitialData(final StreamInfo info) {
|
||||||
super.setInitialData(info.getServiceId(), info.getUrl(), info.getName());
|
super.setInitialData(info.getServiceId(), info.getUrl(), info.getName());
|
||||||
if(this.relatedStreamInfo == null) this.relatedStreamInfo = RelatedStreamInfo.getInfo(info);
|
if (this.relatedStreamInfo == null) {
|
||||||
|
this.relatedStreamInfo = RelatedStreamInfo.getInfo(info);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static final String INFO_KEY = "related_info_key";
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSaveInstanceState(Bundle outState) {
|
public void onSaveInstanceState(final Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
outState.putSerializable(INFO_KEY, relatedStreamInfo);
|
outState.putSerializable(INFO_KEY, relatedStreamInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onRestoreInstanceState(@NonNull Bundle savedState) {
|
protected void onRestoreInstanceState(@NonNull final Bundle savedState) {
|
||||||
super.onRestoreInstanceState(savedState);
|
super.onRestoreInstanceState(savedState);
|
||||||
if (savedState != null) {
|
if (savedState != null) {
|
||||||
Serializable serializable = savedState.getSerializable(INFO_KEY);
|
Serializable serializable = savedState.getSerializable(INFO_KEY);
|
||||||
if(serializable instanceof RelatedStreamInfo){
|
if (serializable instanceof RelatedStreamInfo) {
|
||||||
this.relatedStreamInfo = (RelatedStreamInfo) serializable;
|
this.relatedStreamInfo = (RelatedStreamInfo) serializable;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) {
|
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences,
|
||||||
|
final String s) {
|
||||||
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getContext());
|
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||||
Boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
|
boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
|
||||||
if(null != aSwitch) aSwitch.setChecked(autoplay);
|
if (null != aSwitch) {
|
||||||
|
aSwitch.setChecked(autoplay);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
package org.schabi.newpipe.info_list;
|
package org.schabi.newpipe.info_list;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
@ -29,24 +30,26 @@ import org.schabi.newpipe.util.OnClickGesture;
|
|||||||
* <p>
|
* <p>
|
||||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||||
* InfoItemBuilder.java is part of NewPipe.
|
* InfoItemBuilder.java is part of NewPipe.
|
||||||
|
* </p>
|
||||||
* <p>
|
* <p>
|
||||||
* NewPipe is free software: you can redistribute it and/or modify
|
* NewPipe is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* (at your option) any later version.
|
* (at your option) any later version.
|
||||||
|
* </p>
|
||||||
* <p>
|
* <p>
|
||||||
* NewPipe is distributed in the hope that it will be useful,
|
* NewPipe is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* GNU General Public License for more details.
|
* GNU General Public License for more details.
|
||||||
|
* </p>
|
||||||
* <p>
|
* <p>
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
* </p>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public class InfoItemBuilder {
|
public class InfoItemBuilder {
|
||||||
private static final String TAG = InfoItemBuilder.class.toString();
|
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final ImageLoader imageLoader = ImageLoader.getInstance();
|
private final ImageLoader imageLoader = ImageLoader.getInstance();
|
||||||
|
|
||||||
@ -55,31 +58,39 @@ public class InfoItemBuilder {
|
|||||||
private OnClickGesture<PlaylistInfoItem> onPlaylistSelectedListener;
|
private OnClickGesture<PlaylistInfoItem> onPlaylistSelectedListener;
|
||||||
private OnClickGesture<CommentsInfoItem> onCommentsSelectedListener;
|
private OnClickGesture<CommentsInfoItem> onCommentsSelectedListener;
|
||||||
|
|
||||||
public InfoItemBuilder(Context context) {
|
public InfoItemBuilder(final Context context) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem, final HistoryRecordManager historyRecordManager) {
|
public View buildView(@NonNull final ViewGroup parent, @NonNull final InfoItem infoItem,
|
||||||
|
final HistoryRecordManager historyRecordManager) {
|
||||||
return buildView(parent, infoItem, historyRecordManager, false);
|
return buildView(parent, infoItem, historyRecordManager, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem,
|
public View buildView(@NonNull final ViewGroup parent, @NonNull final InfoItem infoItem,
|
||||||
final HistoryRecordManager historyRecordManager, boolean useMiniVariant) {
|
final HistoryRecordManager historyRecordManager,
|
||||||
|
final boolean useMiniVariant) {
|
||||||
InfoItemHolder holder = holderFromInfoType(parent, infoItem.getInfoType(), useMiniVariant);
|
InfoItemHolder holder = holderFromInfoType(parent, infoItem.getInfoType(), useMiniVariant);
|
||||||
holder.updateFromItem(infoItem, historyRecordManager);
|
holder.updateFromItem(infoItem, historyRecordManager);
|
||||||
return holder.itemView;
|
return holder.itemView;
|
||||||
}
|
}
|
||||||
|
|
||||||
private InfoItemHolder holderFromInfoType(@NonNull ViewGroup parent, @NonNull InfoItem.InfoType infoType, boolean useMiniVariant) {
|
private InfoItemHolder holderFromInfoType(@NonNull final ViewGroup parent,
|
||||||
|
@NonNull final InfoItem.InfoType infoType,
|
||||||
|
final boolean useMiniVariant) {
|
||||||
switch (infoType) {
|
switch (infoType) {
|
||||||
case STREAM:
|
case STREAM:
|
||||||
return useMiniVariant ? new StreamMiniInfoItemHolder(this, parent) : new StreamInfoItemHolder(this, parent);
|
return useMiniVariant ? new StreamMiniInfoItemHolder(this, parent)
|
||||||
|
: new StreamInfoItemHolder(this, parent);
|
||||||
case CHANNEL:
|
case CHANNEL:
|
||||||
return useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent) : new ChannelInfoItemHolder(this, parent);
|
return useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent)
|
||||||
|
: new ChannelInfoItemHolder(this, parent);
|
||||||
case PLAYLIST:
|
case PLAYLIST:
|
||||||
return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent) : new PlaylistInfoItemHolder(this, parent);
|
return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent)
|
||||||
|
: new PlaylistInfoItemHolder(this, parent);
|
||||||
case COMMENT:
|
case COMMENT:
|
||||||
return useMiniVariant ? new CommentsMiniInfoItemHolder(this, parent) : new CommentsInfoItemHolder(this, parent);
|
return useMiniVariant ? new CommentsMiniInfoItemHolder(this, parent)
|
||||||
|
: new CommentsInfoItemHolder(this, parent);
|
||||||
default:
|
default:
|
||||||
throw new RuntimeException("InfoType not expected = " + infoType.name());
|
throw new RuntimeException("InfoType not expected = " + infoType.name());
|
||||||
}
|
}
|
||||||
@ -97,7 +108,7 @@ public class InfoItemBuilder {
|
|||||||
return onStreamSelectedListener;
|
return onStreamSelectedListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setOnStreamSelectedListener(OnClickGesture<StreamInfoItem> listener) {
|
public void setOnStreamSelectedListener(final OnClickGesture<StreamInfoItem> listener) {
|
||||||
this.onStreamSelectedListener = listener;
|
this.onStreamSelectedListener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,7 +116,7 @@ public class InfoItemBuilder {
|
|||||||
return onChannelSelectedListener;
|
return onChannelSelectedListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setOnChannelSelectedListener(OnClickGesture<ChannelInfoItem> listener) {
|
public void setOnChannelSelectedListener(final OnClickGesture<ChannelInfoItem> listener) {
|
||||||
this.onChannelSelectedListener = listener;
|
this.onChannelSelectedListener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,7 +124,7 @@ public class InfoItemBuilder {
|
|||||||
return onPlaylistSelectedListener;
|
return onPlaylistSelectedListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setOnPlaylistSelectedListener(OnClickGesture<PlaylistInfoItem> listener) {
|
public void setOnPlaylistSelectedListener(final OnClickGesture<PlaylistInfoItem> listener) {
|
||||||
this.onPlaylistSelectedListener = listener;
|
this.onPlaylistSelectedListener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,8 +132,8 @@ public class InfoItemBuilder {
|
|||||||
return onCommentsSelectedListener;
|
return onCommentsSelectedListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setOnCommentsSelectedListener(OnClickGesture<CommentsInfoItem> onCommentsSelectedListener) {
|
public void setOnCommentsSelectedListener(
|
||||||
|
final OnClickGesture<CommentsInfoItem> onCommentsSelectedListener) {
|
||||||
this.onCommentsSelectedListener = onCommentsSelectedListener;
|
this.onCommentsSelectedListener = onCommentsSelectedListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user