merged dev

This commit is contained in:
Ritvik Saraf 2018-12-25 19:31:25 +05:30
commit d61797fa3a
193 changed files with 13000 additions and 3424 deletions

View File

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

View File

@ -5,13 +5,13 @@ android:
components:
# The BuildTools version used by NewPipe
- tools
- build-tools-27.0.3
- build-tools-28.0.3
# The SDK version used to compile NewPipe
- android-27
- android-28
before_install:
- yes | sdkmanager "platforms;android-27"
- yes | sdkmanager "platforms;android-28"
script: ./gradlew -Dorg.gradle.jvmargs=-Xmx1536m assembleDebug lintDebug testDebugUnitTest
licenses:

View File

@ -1,74 +1,77 @@
<p align="center"><a href="https://newpipe.schabi.org"><img src="assets/new_pipe_icon_5.png" width="150"/></a></p>
<p align="center"><a href="https://newpipe.schabi.org"><img src="assets/new_pipe_icon_5.png" width="150"></a></p>
<h2 align="center"><b>NewPipe</b></h2>
<h4 align="center">A free lightweight YouTube frontend for Android.</h4>
<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>
<h4 align="center">A libre lightweight streaming frontend for Android.</h4>
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://f-droid.org/wiki/images/0/06/F-Droid-button_get-it-on.png"></a></p>
<p align="center">
<a href="https://github.com/TeamNewPipe/NewPipe" alt="GitHub release"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" /></a>
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="License: GPL v3"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg" /></a>
<a href="https://travis-ci.org/TeamNewPipe/NewPipe" alt="Build Status"><img src="https://travis-ci.org/TeamNewPipe/NewPipe.svg" /></a>
<a href="https://hosted.weblate.org/engage/NewPipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/NewPipe/-/svg-badge.svg" /></a>
<a href="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://github.com/TeamNewPipe/NewPipe" alt="GitHub release"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a>
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="License: GPLv3"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg"></a>
<a href="https://travis-ci.org/TeamNewPipe/NewPipe" alt="Build Status"><img src="https://travis-ci.org/TeamNewPipe/NewPipe.svg"></a>
<a href="https://hosted.weblate.org/engage/NewPipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/NewPipe/-/svg-badge.svg"></a>
<a href="http://webchat.freenode.net/?channels=%23newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
<a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
</p>
<hr />
<hr>
<p align="center"><a href="#screenshots">Screenshots</a> &bull; <a href="#description">Description</a> &bull; <a href="#features">Features</a> &bull; <a href="#contribution">Contribution</a> &bull; <a href="#donate">Donate</a> &bull; <a href="#license">License</a></p>
<p align="center"><a href="https://newpipe.schabi.org">Website</a> &bull; <a href="https://newpipe.schabi.org/blog/">Blog</a> &bull; <a href="https://newpipe.schabi.org/press/">Press</a></p>
<hr />
WARNING: PUTTING NEWPIPE OR ANY FORK OF IT INTO GOOGLE PLAYSTORE VIOLATES THEIR TERMS OF CONDITIONS.
<hr>
<b>WARNING: PUTTING NEWPIPE OR ANY FORK OF IT INTO GOOGLE PLAYSTORE VIOLATES THEIR TERMS OF CONDITIONS.</b>
## Screenshots
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_1.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_1.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_2.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_2.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_3.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_3.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_4.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_4.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_5.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_5.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_6.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_6.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_7.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_7.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_8.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_8.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_9.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_9.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png)
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png)
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png)
## Description
NewPipe does not use any Google framework libraries, or the YouTube API. It only parses the website in order to gain the information it needs. Therefore this app can be used on devices without Google Services installed. Also, you don't need a YouTube account to use NewPipe, and it's FLOSS.
NewPipe does not use any Google framework libraries, nor the YouTube API. Websites are only parsed to fetch required info, so this app can be used on devices without Google services installed. Also, you don't need a YouTube account to use NewPipe, which is copylefted libre software.
### Features
* Search videos
* Display general information about a video
* Display general info about videos
* Watch YouTube videos
* Listen to YouTube videos
* Popup mode (floating player)
* Select the streaming player to watch the video with
* Download videos
* Select streaming player to watch video with
* Download videos
* Download audio only
* Open a video in Kodi
* Show Next/Related videos
* Show next/related videos
* Search YouTube in a specific language
* Watch/Block age restricted material
* Display general information about channels
* Display general info about channels
* Search channels
* Watch videos from a channel
* Orbot/Tor support (not yet directly)
* 1080p/2k/4k support
* 1080p/2K/4K support
* View history
* Subscribe to channels
* Search history
* Search/Watch Playlists
* Watch as queues Playlists
* Queuing videos
* Search/watch playlists
* Watch as enqueued playlists
* Enqueue videos
* Local playlists
* Subtitles
* Multi-service support (eg. SoundCloud in NewPipe Beta)
* Multi-service support (e.g. SoundCloud \[beta\])
* Livestream support
### Coming Features
* Livestream support
* Cast to UPnP and Cast
* Show comments
* ... and many more
* and many more
## Contribution
Whether you have ideas, translations, design changes, code cleaning, or real heavy code changes, help is always welcome.
@ -77,26 +80,31 @@ The more is done the better it gets!
If you'd like to get involved, check our [contribution notes](.github/CONTRIBUTING.md).
## Donate
If you like NewPipe we'd be happy about a donation. You can either donate via Bitcoin, Bountysource or Liberapay. For further information about donating to NewPipe, please visit our [website](https://newpipe.schabi.org/donate).
If you like NewPipe we'd be happy about a donation. You can either send bitcoin or donate via Bountysource or Liberapay. For further info on donating to NewPipe, please visit our [website](https://newpipe.schabi.org/donate).
<table>
<tr>
<td><img src="https://bitcoin.org/img/icons/logotop.svg" alt="Bitcoin" /></td>
<td><img src="assets/bitcoin_qr_code.png" alt="Bitcoin QR Code" width="100px"/></td>
<td><img src="https://bitcoin.org/img/icons/logotop.svg" alt="Bitcoin"></td>
<td><img src="assets/bitcoin_qr_code.png" alt="Bitcoin QR code" width="100px"></td>
<td><samp>16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh</samp></td>
</tr>
<tr>
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="https://upload.wikimedia.org/wikipedia/commons/2/27/Liberapay_logo_v2_white-on-yellow.svg" alt="Liberapay" width="80px" /></a></td>
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="assets/liberapay_qr_code.png" alt="Visit NewPipe at liberapay.com" width="100px"/></a></td>
<td><a href="https://liberapay.com/TeamNewPipe/donate"><img src="assets/liberapay_donate_button.svg" alt="Donate via Liberapay" height="35px" /></a></td>
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="https://upload.wikimedia.org/wikipedia/commons/2/27/Liberapay_logo_v2_white-on-yellow.svg" alt="Liberapay" width="80px" ></a></td>
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="assets/liberapay_qr_code.png" alt="Visit NewPipe at liberapay.com" width="100px"></a></td>
<td><a href="https://liberapay.com/TeamNewPipe/donate"><img src="assets/liberapay_donate_button.svg" alt="Donate via Liberapay" height="35px"></a></td>
</tr>
<tr>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alt="Bountysource" width="190px" /></a></td>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="Visit NewPipe at bountysource.com" width="100px"/></a></td>
<td><a href="https://www.bountysource.com/teams/newpipe/issues"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f" height="30px" alt="Check out how many bounties you can earn." /></a></td>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alt="Bountysource" width="190px"></a></td>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="Visit NewPipe at bountysource.com" width="100px"></a></td>
<td><a href="https://www.bountysource.com/teams/newpipe/issues"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f" height="30px" alt="Check out how many bounties you can earn."></a></td>
</tr>
</table>
## Privacy Policy
The NewPipe project aims to provide a private, anonymous experience for using media web services.
Therefore, the app does not collect any data without your consent. NewPipe's privacy policy explains in detail what data is sent and stored when you send a crash report, or comment in our blog. You can find the document [here](https://newpipe.schabi.org/legal/privacy/).
## License
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html)

View File

@ -1,15 +1,15 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 27
buildToolsVersion '27.0.3'
compileSdkVersion 28
buildToolsVersion '28.0.3'
defaultConfig {
applicationId "org.schabi.newpipe"
minSdkVersion 15
targetSdkVersion 27
versionCode 68
versionName "0.14.1"
minSdkVersion 19
targetSdkVersion 28
versionCode 69
versionName "0.14.2"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
@ -22,7 +22,6 @@ android {
}
debug {
multiDexEnabled true
debuggable true
applicationIdSuffix ".debug"
}
@ -41,62 +40,61 @@ android {
}
ext {
supportLibVersion = '27.1.1'
exoPlayerLibVersion = '2.8.2'
supportLibVersion = '28.0.0'
exoPlayerLibVersion = '2.8.4' //2.9.0
roomDbLibVersion = '1.1.1'
leakCanaryLibVersion = '1.5.4'
okHttpLibVersion = '3.10.0'
leakCanaryLibVersion = '1.5.4' //1.6.1
okHttpLibVersion = '3.11.0'
icepickLibVersion = '3.2.0'
stethoLibVersion = '1.5.0'
}
dependencies {
androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2') {
androidTestImplementation('com.android.support.test.espresso:espresso-core:3.0.2', {
exclude module: 'support-annotations'
}
})
implementation 'com.github.yausername:NewPipeExtractor:4883b6f'
implementation 'com.github.yausername:NewPipeExtractor:df0db84'
testImplementation 'junit:junit:4.12'
testImplementation 'org.mockito:mockito-core:2.8.9'
testImplementation 'org.mockito:mockito-core:2.23.0'
implementation "com.android.support:appcompat-v7:$supportLibVersion"
implementation "com.android.support:support-v4:$supportLibVersion"
implementation "com.android.support:design:$supportLibVersion"
implementation "com.android.support:recyclerview-v7:$supportLibVersion"
implementation "com.android.support:preference-v14:$supportLibVersion"
implementation "com.android.support:appcompat-v7:${supportLibVersion}"
implementation "com.android.support:support-v4:${supportLibVersion}"
implementation "com.android.support:design:${supportLibVersion}"
implementation "com.android.support:recyclerview-v7:${supportLibVersion}"
implementation "com.android.support:preference-v14:${supportLibVersion}"
implementation "com.android.support:cardview-v7:${supportLibVersion}"
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
implementation 'ch.acra:acra:4.9.2'
implementation 'ch.acra:acra:4.9.2' //4.11
implementation 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'
implementation 'de.hdodenhof:circleimageview:2.2.0'
implementation 'com.github.nirhart:ParallaxScroll:dd53d1f9d1'
implementation 'com.nononsenseapps:filepicker:4.2.1'
implementation "com.google.android.exoplayer:exoplayer:$exoPlayerLibVersion"
implementation "com.google.android.exoplayer:extension-mediasession:$exoPlayerLibVersion"
implementation "com.google.android.exoplayer:exoplayer:${exoPlayerLibVersion}"
implementation "com.google.android.exoplayer:extension-mediasession:${exoPlayerLibVersion}"
debugImplementation "com.facebook.stetho:stetho:$stethoLibVersion"
debugImplementation "com.facebook.stetho:stetho-urlconnection:$stethoLibVersion"
debugImplementation "com.facebook.stetho:stetho:${stethoLibVersion}"
debugImplementation "com.facebook.stetho:stetho-urlconnection:${stethoLibVersion}"
debugImplementation 'com.android.support:multidex:1.0.3'
implementation 'io.reactivex.rxjava2:rxjava:2.1.14'
implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
implementation 'io.reactivex.rxjava2:rxjava:2.2.2'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1'
implementation "android.arch.persistence.room:runtime:$roomDbLibVersion"
implementation "android.arch.persistence.room:rxjava2:$roomDbLibVersion"
annotationProcessor "android.arch.persistence.room:compiler:$roomDbLibVersion"
implementation "android.arch.persistence.room:runtime:${roomDbLibVersion}"
implementation "android.arch.persistence.room:rxjava2:${roomDbLibVersion}"
annotationProcessor "android.arch.persistence.room:compiler:${roomDbLibVersion}"
implementation "frankiesardo:icepick:$icepickLibVersion"
annotationProcessor "frankiesardo:icepick-processor:$icepickLibVersion"
implementation "frankiesardo:icepick:${icepickLibVersion}"
annotationProcessor "frankiesardo:icepick-processor:${icepickLibVersion}"
debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakCanaryLibVersion"
releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryLibVersion"
debugImplementation "com.squareup.leakcanary:leakcanary-android:${leakCanaryLibVersion}"
releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:${leakCanaryLibVersion}"
implementation "com.squareup.okhttp3:okhttp:$okHttpLibVersion"
debugImplementation "com.facebook.stetho:stetho-okhttp3:$stethoLibVersion"
implementation 'com.android.support.constraint:constraint-layout:1.1.2'
implementation 'com.android.support:cardview-v7:27.1.1'
implementation "com.squareup.okhttp3:okhttp:${okHttpLibVersion}"
debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoLibVersion}"
}

View File

@ -9,6 +9,7 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<application
android:name=".App"
@ -238,4 +239,4 @@
android:name=".RouterActivity$FetcherService"
android:exported="false"/>
</application>
</manifest>
</manifest>

View File

@ -0,0 +1,116 @@
package android.support.design.widget;
import android.animation.ValueAnimator;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.design.animation.AnimationUtils;
import android.util.AttributeSet;
import android.view.View;
// check this https://github.com/ToDou/appbarlayout-spring-behavior/blob/master/appbarspring/src/main/java/android/support/design/widget/AppBarFlingFixBehavior.java
public final class FlingBehavior extends AppBarLayout.Behavior {
private ValueAnimator mOffsetAnimator;
private static final int MAX_OFFSET_ANIMATION_DURATION = 600; // ms
public FlingBehavior() {
}
public FlingBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed, int type) {
if (dy != 0) {
int val = child.getBottom();
if (val != 0) {
int min, max;
if (dy < 0) {
// We're scrolling down
} else {
// We're scrolling up
if (mOffsetAnimator != null && mOffsetAnimator.isRunning()) {
mOffsetAnimator.cancel();
}
min = -child.getUpNestedPreScrollRange();
max = 0;
consumed[1] = scroll(coordinatorLayout, child, dy, min, max);
}
}
}
}
@Override
public boolean onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout, @NonNull AppBarLayout child, @NonNull View target, float velocityX, float velocityY) {
if (velocityY != 0) {
if (velocityY < 0) {
// We're flinging down
int val = child.getBottom();
if (val != 0) {
final int targetScroll =
+child.getDownNestedPreScrollRange();
animateOffsetTo(coordinatorLayout, child, targetScroll, velocityY);
}
} else {
// We're flinging up
int val = child.getBottom();
if (val != 0) {
final int targetScroll = -child.getUpNestedPreScrollRange();
if (getTopBottomOffsetForScrollingSibling() > targetScroll) {
animateOffsetTo(coordinatorLayout, child, targetScroll, velocityY);
}
}
}
}
return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
}
private void animateOffsetTo(final CoordinatorLayout coordinatorLayout,
final AppBarLayout child, final int offset, float velocity) {
final int distance = Math.abs(getTopBottomOffsetForScrollingSibling() - offset);
final int duration;
velocity = Math.abs(velocity);
if (velocity > 0) {
duration = 3 * Math.round(1000 * (distance / velocity));
} else {
final float distanceRatio = (float) distance / child.getHeight();
duration = (int) ((distanceRatio + 1) * 150);
}
animateOffsetWithDuration(coordinatorLayout, child, offset, duration);
}
private void animateOffsetWithDuration(final CoordinatorLayout coordinatorLayout,
final AppBarLayout child, final int offset, final int duration) {
final int currentOffset = getTopBottomOffsetForScrollingSibling();
if (currentOffset == offset) {
if (mOffsetAnimator != null && mOffsetAnimator.isRunning()) {
mOffsetAnimator.cancel();
}
return;
}
if (mOffsetAnimator == null) {
mOffsetAnimator = new ValueAnimator();
mOffsetAnimator.setInterpolator(AnimationUtils.DECELERATE_INTERPOLATOR);
mOffsetAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animator) {
setHeaderTopBottomOffset(coordinatorLayout, child,
(Integer) animator.getAnimatedValue());
}
});
} else {
mOffsetAnimator.cancel();
}
mOffsetAnimator.setDuration(Math.min(duration, MAX_OFFSET_ANIMATION_DURATION));
mOffsetAnimator.setIntValues(currentOffset, offset);
mOffsetAnimator.start();
}
}

View File

@ -5,6 +5,7 @@ import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.os.Build;
import android.preference.PreferenceManager;
import android.support.annotation.Nullable;
import android.util.Log;
@ -66,7 +67,8 @@ public class App extends Application {
private RefWatcher refWatcher;
@SuppressWarnings("unchecked")
private static final Class<? extends ReportSenderFactory>[] reportSenderFactoryClasses = new Class[]{AcraReportSenderFactory.class};
private static final Class<? extends ReportSenderFactory>[]
reportSenderFactoryClasses = new Class[]{AcraReportSenderFactory.class};
@Override
protected void attachBaseContext(Context base) {
@ -89,7 +91,8 @@ public class App extends Application {
// Initialize settings first because others inits can use its values
SettingsActivity.initSettings(this);
NewPipe.init(getDownloader(), new Localization("GB", "en"));
NewPipe.init(getDownloader(),
org.schabi.newpipe.util.Localization.getPreferredExtractorLocal(this));
StateSaver.init(this);
initNotificationChannel();
@ -181,7 +184,11 @@ public class App extends Application {
ACRA.init(this, acraConfig);
} catch (ACRAConfigurationException ace) {
ace.printStackTrace();
ErrorActivity.reportError(this, ace, null, null, ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
ErrorActivity.reportError(this,
ace,
null,
null,
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
"Could not initialize ACRA crash report", R.string.app_ui_crash));
}
}
@ -201,7 +208,8 @@ public class App extends Application {
NotificationChannel mChannel = new NotificationChannel(id, name, importance);
mChannel.setDescription(description);
NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
NotificationManager mNotificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
mNotificationManager.createNotificationChannel(mChannel);
}

View File

@ -10,7 +10,6 @@ import org.schabi.newpipe.extractor.utils.Localization;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@ -95,7 +94,8 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader {
.build();
response = client.newCall(request).execute();
return Long.parseLong(response.header("Content-Length"));
String contentLength = response.header("Content-Length");
return contentLength == null ? -1 : Long.parseLong(contentLength);
} catch (NumberFormatException e) {
throw new IOException("Invalid content length", e);
} finally {
@ -110,7 +110,7 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader {
* but set the HTTP header field "Accept-Language" to the supplied string.
*
* @param siteUrl the URL of the text file to return the contents of
* @param localization the language and country (usually a 2-character code for both values)
* @param localization the language and country (usually a 2-character code) to set
* @return the contents of the specified text file
*/
@Override

View File

@ -542,8 +542,7 @@ public class RouterActivity extends AppCompatActivity {
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);
boolean useOldVideoPlayer = PlayerHelper.isUsingOldPlayer(this);
boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false);;
PlayQueue playQueue;
String playerChoice = choice.playerChoice;
@ -555,9 +554,6 @@ public class RouterActivity extends AppCompatActivity {
} else if (playerChoice.equals(videoPlayerKey) && isExtVideoEnabled) {
NavigationHelper.playOnExternalVideoPlayer(this, (StreamInfo) info);
} else if (playerChoice.equals(videoPlayerKey) && useOldVideoPlayer) {
NavigationHelper.playOnOldVideoPlayer(this, (StreamInfo) info);
} else {
playQueue = new SinglePlayQueue((StreamInfo) info);

View File

@ -1,158 +0,0 @@
package org.schabi.newpipe.download;
import android.app.Activity;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.BaseTransientBottomBar;
import android.support.design.widget.Snackbar;
import android.view.View;
import org.schabi.newpipe.R;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.TimeUnit;
import io.reactivex.Completable;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import io.reactivex.subjects.PublishSubject;
import us.shandian.giga.get.DownloadManager;
import us.shandian.giga.get.DownloadMission;
public class DeleteDownloadManager {
private static final String KEY_STATE = "delete_manager_state";
private final View mView;
private final HashSet<String> mPendingMap;
private final List<Disposable> mDisposableList;
private DownloadManager mDownloadManager;
private final PublishSubject<DownloadMission> publishSubject = PublishSubject.create();
DeleteDownloadManager(Activity activity) {
mPendingMap = new HashSet<>();
mDisposableList = new ArrayList<>();
mView = activity.findViewById(android.R.id.content);
}
public Observable<DownloadMission> getUndoObservable() {
return publishSubject;
}
public boolean contains(@NonNull DownloadMission mission) {
return mPendingMap.contains(mission.url);
}
public void add(@NonNull DownloadMission mission) {
mPendingMap.add(mission.url);
if (mPendingMap.size() == 1) {
showUndoDeleteSnackbar(mission);
}
}
public void setDownloadManager(@NonNull DownloadManager downloadManager) {
mDownloadManager = downloadManager;
if (mPendingMap.size() < 1) return;
showUndoDeleteSnackbar();
}
public void restoreState(@Nullable Bundle savedInstanceState) {
if (savedInstanceState == null) return;
List<String> list = savedInstanceState.getStringArrayList(KEY_STATE);
if (list != null) {
mPendingMap.addAll(list);
}
}
public void saveState(@Nullable Bundle outState) {
if (outState == null) return;
for (Disposable disposable : mDisposableList) {
disposable.dispose();
}
outState.putStringArrayList(KEY_STATE, new ArrayList<>(mPendingMap));
}
private void showUndoDeleteSnackbar() {
if (mPendingMap.size() < 1) return;
String url = mPendingMap.iterator().next();
for (int i = 0; i < mDownloadManager.getCount(); i++) {
DownloadMission mission = mDownloadManager.getMission(i);
if (url.equals(mission.url)) {
showUndoDeleteSnackbar(mission);
break;
}
}
}
private void showUndoDeleteSnackbar(@NonNull DownloadMission mission) {
final Snackbar snackbar = Snackbar.make(mView, mission.name, Snackbar.LENGTH_INDEFINITE);
final Disposable disposable = Observable.timer(3, TimeUnit.SECONDS)
.subscribeOn(AndroidSchedulers.mainThread())
.subscribe(l -> snackbar.dismiss());
mDisposableList.add(disposable);
snackbar.setAction(R.string.undo, v -> {
mPendingMap.remove(mission.url);
publishSubject.onNext(mission);
disposable.dispose();
snackbar.dismiss();
});
snackbar.addCallback(new BaseTransientBottomBar.BaseCallback<Snackbar>() {
@Override
public void onDismissed(Snackbar transientBottomBar, int event) {
if (!disposable.isDisposed()) {
Completable.fromAction(() -> deletePending(mission))
.subscribeOn(Schedulers.io())
.subscribe();
}
mPendingMap.remove(mission.url);
snackbar.removeCallback(this);
mDisposableList.remove(disposable);
showUndoDeleteSnackbar();
}
});
snackbar.show();
}
public void deletePending() {
if (mPendingMap.size() < 1) return;
HashSet<Integer> idSet = new HashSet<>();
for (int i = 0; i < mDownloadManager.getCount(); i++) {
if (contains(mDownloadManager.getMission(i))) {
idSet.add(i);
}
}
for (Integer id : idSet) {
mDownloadManager.deleteMission(id);
}
mPendingMap.clear();
}
private void deletePending(@NonNull DownloadMission mission) {
for (int i = 0; i < mDownloadManager.getCount(); i++) {
if (mission.url.equals(mDownloadManager.getMission(i).url)) {
mDownloadManager.deleteMission(i);
break;
}
}
}
}

View File

@ -15,16 +15,12 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.settings.SettingsActivity;
import org.schabi.newpipe.util.ThemeHelper;
import io.reactivex.Completable;
import io.reactivex.schedulers.Schedulers;
import us.shandian.giga.service.DownloadManagerService;
import us.shandian.giga.ui.fragment.AllMissionsFragment;
import us.shandian.giga.ui.fragment.MissionsFragment;
public class DownloadActivity extends AppCompatActivity {
private static final String MISSIONS_FRAGMENT_TAG = "fragment_tag";
private DeleteDownloadManager mDeleteDownloadManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -47,32 +43,17 @@ public class DownloadActivity extends AppCompatActivity {
actionBar.setDisplayShowTitleEnabled(true);
}
mDeleteDownloadManager = new DeleteDownloadManager(this);
mDeleteDownloadManager.restoreState(savedInstanceState);
MissionsFragment fragment = (MissionsFragment) getFragmentManager().findFragmentByTag(MISSIONS_FRAGMENT_TAG);
if (fragment != null) {
fragment.setDeleteManager(mDeleteDownloadManager);
} else {
getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
updateFragments();
getWindow().getDecorView().getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
});
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
mDeleteDownloadManager.saveState(outState);
super.onSaveInstanceState(outState);
getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
updateFragments();
getWindow().getDecorView().getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
});
}
private void updateFragments() {
MissionsFragment fragment = new AllMissionsFragment();
fragment.setDeleteManager(mDeleteDownloadManager);
MissionsFragment fragment = new MissionsFragment();
getFragmentManager().beginTransaction()
.replace(R.id.frame, fragment, MISSIONS_FRAGMENT_TAG)
@ -99,7 +80,6 @@ public class DownloadActivity extends AppCompatActivity {
case R.id.action_settings: {
Intent intent = new Intent(this, SettingsActivity.class);
startActivity(intent);
deletePending();
return true;
}
default:
@ -108,14 +88,7 @@ public class DownloadActivity extends AppCompatActivity {
}
@Override
public void onBackPressed() {
super.onBackPressed();
deletePending();
}
private void deletePending() {
Completable.fromAction(mDeleteDownloadManager::deletePending)
.subscribeOn(Schedulers.io())
.subscribe();
public void onRestoreInstanceState(Bundle inState){
super.onRestoreInstanceState(inState);
}
}

View File

@ -1,13 +1,17 @@
package org.schabi.newpipe.download;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.IdRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.DialogFragment;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -22,38 +26,55 @@ import android.widget.Toast;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.Stream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.extractor.utils.Localization;
import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.util.FilenameUtils;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.SecondaryStreamHelper;
import org.schabi.newpipe.util.StreamItemAdapter;
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
import org.schabi.newpipe.util.ThemeHelper;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import icepick.Icepick;
import icepick.State;
import io.reactivex.disposables.CompositeDisposable;
import us.shandian.giga.postprocessing.Postprocessing;
import us.shandian.giga.service.DownloadManagerService;
public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener {
private static final String TAG = "DialogFragment";
private static final boolean DEBUG = MainActivity.DEBUG;
@State protected StreamInfo currentInfo;
@State protected StreamSizeWrapper<AudioStream> wrappedAudioStreams = StreamSizeWrapper.empty();
@State protected StreamSizeWrapper<VideoStream> wrappedVideoStreams = StreamSizeWrapper.empty();
@State protected int selectedVideoIndex = 0;
@State protected int selectedAudioIndex = 0;
@State
protected StreamInfo currentInfo;
@State
protected StreamSizeWrapper<AudioStream> wrappedAudioStreams = StreamSizeWrapper.empty();
@State
protected StreamSizeWrapper<VideoStream> wrappedVideoStreams = StreamSizeWrapper.empty();
@State
protected StreamSizeWrapper<SubtitlesStream> wrappedSubtitleStreams = StreamSizeWrapper.empty();
@State
protected int selectedVideoIndex = 0;
@State
protected int selectedAudioIndex = 0;
@State
protected int selectedSubtitleIndex = 0;
private StreamItemAdapter<AudioStream> audioStreamsAdapter;
private StreamItemAdapter<VideoStream> videoStreamsAdapter;
private StreamItemAdapter<AudioStream, Stream> audioStreamsAdapter;
private StreamItemAdapter<VideoStream, AudioStream> videoStreamsAdapter;
private StreamItemAdapter<SubtitlesStream, Stream> subtitleStreamsAdapter;
private final CompositeDisposable disposables = new CompositeDisposable();
@ -63,6 +84,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
private TextView threadsCountTextView;
private SeekBar threadsSeekBar;
private SharedPreferences prefs;
public static DownloadDialog newInstance(StreamInfo info) {
DownloadDialog dialog = new DownloadDialog();
dialog.setInfo(info);
@ -78,6 +101,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
instance.setVideoStreams(streamsList);
instance.setSelectedVideoStream(selectedStreamIndex);
instance.setAudioStreams(info.getAudioStreams());
instance.setSubtitleStreams(info.getSubtitles());
return instance;
}
@ -86,7 +111,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
}
public void setAudioStreams(List<AudioStream> audioStreams) {
setAudioStreams(new StreamSizeWrapper<>(audioStreams));
setAudioStreams(new StreamSizeWrapper<>(audioStreams, getContext()));
}
public void setAudioStreams(StreamSizeWrapper<AudioStream> wrappedAudioStreams) {
@ -94,13 +119,21 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
}
public void setVideoStreams(List<VideoStream> videoStreams) {
setVideoStreams(new StreamSizeWrapper<>(videoStreams));
setVideoStreams(new StreamSizeWrapper<>(videoStreams, getContext()));
}
public void setVideoStreams(StreamSizeWrapper<VideoStream> wrappedVideoStreams) {
this.wrappedVideoStreams = wrappedVideoStreams;
}
public void setSubtitleStreams(List<SubtitlesStream> subtitleStreams) {
setSubtitleStreams(new StreamSizeWrapper<>(subtitleStreams, getContext()));
}
public void setSubtitleStreams(StreamSizeWrapper<SubtitlesStream> wrappedSubtitleStreams) {
this.wrappedSubtitleStreams = wrappedSubtitleStreams;
}
public void setSelectedVideoStream(int selectedVideoIndex) {
this.selectedVideoIndex = selectedVideoIndex;
}
@ -109,6 +142,10 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
this.selectedAudioIndex = selectedAudioIndex;
}
public void setSelectedSubtitleStream(int selectedSubtitleIndex) {
this.selectedSubtitleIndex = selectedSubtitleIndex;
}
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@ -116,7 +153,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
if (DEBUG)
Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
if (!PermissionHelper.checkStoragePermissions(getActivity(), PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
getDialog().dismiss();
return;
@ -125,13 +163,29 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(getContext()));
Icepick.restoreInstanceState(this, savedInstanceState);
this.videoStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedVideoStreams, true);
SparseArray<SecondaryStreamHelper<AudioStream>> secondaryStreams = new SparseArray<>(4);
List<VideoStream> videoStreams = wrappedVideoStreams.getStreamsList();
for (int i = 0; i < videoStreams.size(); i++) {
if (!videoStreams.get(i).isVideoOnly()) continue;
AudioStream audioStream = SecondaryStreamHelper.getAudioStreamFor(wrappedAudioStreams.getStreamsList(), videoStreams.get(i));
if (audioStream != null) {
secondaryStreams.append(i, new SecondaryStreamHelper<>(wrappedAudioStreams, audioStream));
} else if (DEBUG) {
Log.w(TAG, "No audio stream candidates for video format " + videoStreams.get(i).getFormat().name());
}
}
this.videoStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedVideoStreams, secondaryStreams);
this.audioStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedAudioStreams);
this.subtitleStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedSubtitleStreams);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
if (DEBUG) Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]");
if (DEBUG)
Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]");
return inflater.inflate(R.layout.download_dialog, container);
}
@ -142,6 +196,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
nameEditText.setText(FilenameUtils.createFilename(getContext(), currentInfo.getName()));
selectedAudioIndex = ListHelper.getDefaultAudioFormat(getContext(), currentInfo.getAudioStreams());
selectedSubtitleIndex = getSubtitleIndexBy(subtitleStreamsAdapter.getAll());
streamsSpinner = view.findViewById(R.id.quality_spinner);
streamsSpinner.setOnItemSelectedListener(this);
@ -154,14 +210,18 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
initToolbar(view.findViewById(R.id.toolbar));
setupDownloadOptions();
int def = 3;
threadsCountTextView.setText(String.valueOf(def));
threadsSeekBar.setProgress(def - 1);
prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
int threads = prefs.getInt(getString(R.string.default_download_threads), 3);
threadsCountTextView.setText(String.valueOf(threads));
threadsSeekBar.setProgress(threads - 1);
threadsSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekbar, int progress, boolean fromUser) {
threadsCountTextView.setText(String.valueOf(progress + 1));
progress++;
prefs.edit().putInt(getString(R.string.default_download_threads), progress).apply();
threadsCountTextView.setText(String.valueOf(progress));
}
@Override
@ -189,6 +249,11 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
setupAudioSpinner();
}
}));
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedSubtitleStreams).subscribe(result -> {
if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.subtitle_button) {
setupSubtitleSpinner();
}
}));
}
@Override
@ -216,7 +281,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
toolbar.setOnMenuItemClickListener(item -> {
if (item.getItemId() == R.id.okay) {
downloadSelected();
prepareSelectedDownload();
return true;
}
return false;
@ -239,13 +304,24 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
setRadioButtonsState(true);
}
private void setupSubtitleSpinner() {
if (getContext() == null) return;
streamsSpinner.setAdapter(subtitleStreamsAdapter);
streamsSpinner.setSelection(selectedSubtitleIndex);
setRadioButtonsState(true);
}
/*//////////////////////////////////////////////////////////////////////////
// Radio group Video&Audio options - Listener
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCheckedChanged(RadioGroup group, @IdRes int checkedId) {
if (DEBUG) Log.d(TAG, "onCheckedChanged() called with: group = [" + group + "], checkedId = [" + checkedId + "]");
if (DEBUG)
Log.d(TAG, "onCheckedChanged() called with: group = [" + group + "], checkedId = [" + checkedId + "]");
boolean flag = true;
switch (checkedId) {
case R.id.audio_button:
setupAudioSpinner();
@ -253,7 +329,13 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
case R.id.video_button:
setupVideoSpinner();
break;
case R.id.subtitle_button:
setupSubtitleSpinner();
flag = false;
break;
}
threadsSeekBar.setEnabled(flag);
}
/*//////////////////////////////////////////////////////////////////////////
@ -262,7 +344,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (DEBUG) Log.d(TAG, "onItemSelected() called with: parent = [" + parent + "], view = [" + view + "], position = [" + position + "], id = [" + id + "]");
if (DEBUG)
Log.d(TAG, "onItemSelected() called with: parent = [" + parent + "], view = [" + view + "], position = [" + position + "], id = [" + id + "]");
switch (radioVideoAudioGroup.getCheckedRadioButtonId()) {
case R.id.audio_button:
selectedAudioIndex = position;
@ -270,6 +353,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
case R.id.video_button:
selectedVideoIndex = position;
break;
case R.id.subtitle_button:
selectedSubtitleIndex = position;
break;
}
}
@ -286,11 +372,14 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
final RadioButton audioButton = radioVideoAudioGroup.findViewById(R.id.audio_button);
final RadioButton videoButton = radioVideoAudioGroup.findViewById(R.id.video_button);
final RadioButton subtitleButton = radioVideoAudioGroup.findViewById(R.id.subtitle_button);
final boolean isVideoStreamsAvailable = videoStreamsAdapter.getCount() > 0;
final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0;
final boolean isSubtitleStreamsAvailable = subtitleStreamsAdapter.getCount() > 0;
audioButton.setVisibility(isAudioStreamsAvailable ? View.VISIBLE : View.GONE);
videoButton.setVisibility(isVideoStreamsAvailable ? View.VISIBLE : View.GONE);
subtitleButton.setVisibility(isSubtitleStreamsAvailable ? View.VISIBLE : View.GONE);
if (isVideoStreamsAvailable) {
videoButton.setChecked(true);
@ -298,6 +387,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
} else if (isAudioStreamsAvailable) {
audioButton.setChecked(true);
setupAudioSpinner();
} else if (isSubtitleStreamsAvailable) {
subtitleButton.setChecked(true);
setupSubtitleSpinner();
} else {
Toast.makeText(getContext(), R.string.no_streams_available_download, Toast.LENGTH_SHORT).show();
getDialog().dismiss();
@ -307,28 +399,144 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
private void setRadioButtonsState(boolean enabled) {
radioVideoAudioGroup.findViewById(R.id.audio_button).setEnabled(enabled);
radioVideoAudioGroup.findViewById(R.id.video_button).setEnabled(enabled);
radioVideoAudioGroup.findViewById(R.id.subtitle_button).setEnabled(enabled);
}
private void downloadSelected() {
Stream stream;
String location;
private int getSubtitleIndexBy(List<SubtitlesStream> streams) {
Localization loc = NewPipe.getPreferredLocalization();
String fileName = nameEditText.getText().toString().trim();
if (fileName.isEmpty()) fileName = FilenameUtils.createFilename(getContext(), currentInfo.getName());
boolean isAudio = radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button;
if (isAudio) {
stream = audioStreamsAdapter.getItem(selectedAudioIndex);
location = NewPipeSettings.getAudioDownloadPath(getContext());
} else {
stream = videoStreamsAdapter.getItem(selectedVideoIndex);
location = NewPipeSettings.getVideoDownloadPath(getContext());
for (int i = 0; i < streams.size(); i++) {
Locale streamLocale = streams.get(i).getLocale();
String tag = streamLocale.getLanguage().concat("-").concat(streamLocale.getCountry());
if (tag.equalsIgnoreCase(loc.getLanguage())) {
return i;
}
}
String url = stream.getUrl();
fileName += "." + stream.getFormat().getSuffix();
// fallback
// 1st loop match country & language
// 2nd loop match language only
int index = loc.getLanguage().indexOf("-");
String lang = index > 0 ? loc.getLanguage().substring(0, index) : loc.getLanguage();
for (int j = 0; j < 2; j++) {
for (int i = 0; i < streams.size(); i++) {
Locale streamLocale = streams.get(i).getLocale();
if (streamLocale.getLanguage().equalsIgnoreCase(lang)) {
if (j > 0 || streamLocale.getCountry().equalsIgnoreCase(loc.getCountry())) {
return i;
}
}
}
}
return 0;
}
private void prepareSelectedDownload() {
final Context context = getContext();
Stream stream;
String location;
char kind;
String fileName = nameEditText.getText().toString().trim();
if (fileName.isEmpty())
fileName = FilenameUtils.createFilename(context, currentInfo.getName());
switch (radioVideoAudioGroup.getCheckedRadioButtonId()) {
case R.id.audio_button:
stream = audioStreamsAdapter.getItem(selectedAudioIndex);
location = NewPipeSettings.getAudioDownloadPath(context);
kind = 'a';
break;
case R.id.video_button:
stream = videoStreamsAdapter.getItem(selectedVideoIndex);
location = NewPipeSettings.getVideoDownloadPath(context);
kind = 'v';
break;
case R.id.subtitle_button:
stream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex);
location = NewPipeSettings.getVideoDownloadPath(context);// assume that subtitle & video go together
kind = 's';
break;
default:
return;
}
int threads;
if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.subtitle_button) {
threads = 1;// use unique thread for subtitles due small file size
fileName += ".srt";// final subtitle format
} else {
threads = threadsSeekBar.getProgress() + 1;
fileName += "." + stream.getFormat().getSuffix();
}
final String finalFileName = fileName;
DownloadManagerService.checkForRunningMission(context, location, fileName, (listed, finished) -> {
// should be safe run the following code without "getActivity().runOnUiThread()"
if (listed) {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(R.string.download_dialog_title)
.setMessage(finished ? R.string.overwrite_warning : R.string.download_already_running)
.setPositiveButton(
finished ? R.string.overwrite : R.string.generate_unique_name,
(dialog, which) -> downloadSelected(context, stream, location, finalFileName, kind, threads)
)
.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
dialog.cancel();
})
.create()
.show();
} else {
downloadSelected(context, stream, location, finalFileName, kind, threads);
}
});
}
private void downloadSelected(Context context, Stream selectedStream, String location, String fileName, char kind, int threads) {
String[] urls;
String psName = null;
String[] psArgs = null;
String secondaryStreamUrl = null;
long nearLength = 0;
if (selectedStream instanceof VideoStream) {
SecondaryStreamHelper<AudioStream> secondaryStream = videoStreamsAdapter
.getAllSecondary()
.get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream));
if (secondaryStream != null) {
secondaryStreamUrl = secondaryStream.getStream().getUrl();
psName = selectedStream.getFormat() == MediaFormat.MPEG_4 ? Postprocessing.ALGORITHM_MP4_DASH_MUXER : Postprocessing.ALGORITHM_WEBM_MUXER;
psArgs = null;
long videoSize = wrappedVideoStreams.getSizeInBytes((VideoStream) selectedStream);
// set nearLength, only, if both sizes are fetched or known. this probably does not work on weak internet connections
if (secondaryStream.getSizeInBytes() > 0 && videoSize > 0) {
nearLength = secondaryStream.getSizeInBytes() + videoSize;
}
}
} else if ((selectedStream instanceof SubtitlesStream) && selectedStream.getFormat() == MediaFormat.TTML) {
psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
psArgs = new String[]{
selectedStream.getFormat().getSuffix(),
"false",// ignore empty frames
"false",// detect youtube duplicate lines
};
}
if (secondaryStreamUrl == null) {
urls = new String[]{selectedStream.getUrl()};
} else {
urls = new String[]{selectedStream.getUrl(), secondaryStreamUrl};
}
DownloadManagerService.startMission(context, urls, location, fileName, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength);
DownloadManagerService.startMission(getContext(), url, location, fileName, isAudio, threadsSeekBar.getProgress() + 1);
getDialog().dismiss();
}
}

View File

@ -1,10 +1,9 @@
package org.schabi.newpipe.fragments.detail;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.PagerAdapter;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;
@ -13,10 +12,11 @@ public class TabAdaptor extends FragmentPagerAdapter {
private final List<Fragment> mFragmentList = new ArrayList<>();
private final List<String> mFragmentTitleList = new ArrayList<>();
int baseId = 0;
private final FragmentManager fragmentManager;
public TabAdaptor(FragmentManager fm) {
super(fm);
this.fragmentManager = fm;
}
@Override
@ -29,18 +29,6 @@ public class TabAdaptor extends FragmentPagerAdapter {
return mFragmentList.size();
}
@Nullable
@Override
public CharSequence getPageTitle(int position) {
return mFragmentTitleList.get(position);
}
@Override
public long getItemId(int position) {
// give an ID different from position when position has been changed
return baseId + position;
}
public void addFragment(Fragment fragment, String title) {
mFragmentList.add(fragment);
mFragmentTitleList.add(title);
@ -73,19 +61,13 @@ public class TabAdaptor extends FragmentPagerAdapter {
else return POSITION_NONE;
}
/**
* Notify that the position of a fragment has been changed.
* Create a new ID for each position to force recreation of the fragment
* @param n number of items which have been changed
*/
public void notifyChangeInPosition(int n) {
// shift the ID returned by getItemId outside the range of all previous fragments
// https://stackoverflow.com/questions/10396321/remove-fragment-page-from-viewpager-in-android
baseId += getCount() + n;
}
public void notifyDataSetUpdate(){
notifyChangeInPosition(1);
notifyDataSetChanged();
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
fragmentManager.beginTransaction().remove((Fragment) object).commitNowAllowingStateLoss();
}
}

View File

@ -10,7 +10,6 @@ import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.DrawableRes;
import android.support.annotation.FloatRange;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.AppBarLayout;
@ -18,7 +17,6 @@ import android.support.design.widget.TabLayout;
import android.support.v4.app.Fragment;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.ViewPager;
import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
@ -29,7 +27,6 @@ import android.text.method.LinkMovementMethod;
import android.text.util.Linkify;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@ -39,7 +36,6 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
@ -56,8 +52,6 @@ import org.schabi.newpipe.ReCaptchaActivity;
import org.schabi.newpipe.download.DownloadDialog;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.comments.CommentsInfo;
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
@ -72,18 +66,16 @@ import org.schabi.newpipe.fragments.BackPressable;
import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.fragments.list.comments.CommentsFragment;
import org.schabi.newpipe.fragments.list.videos.RelatedVideosFragment;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.info_list.InfoItemDialog;
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.player.MainVideoPlayer;
import org.schabi.newpipe.player.PopupVideoPlayer;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.old.PlayVideoActivity;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ImageDisplayConstants;
@ -91,11 +83,9 @@ 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.OnClickGesture;
import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.StreamItemAdapter;
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
import org.schabi.newpipe.util.ThemeHelper;
import java.io.Serializable;
import java.util.Collection;
@ -192,6 +182,7 @@ public class VideoDetailFragment
private ViewPager viewPager;
private TabAdaptor pageAdapter;
private TabLayout tabLayout;
private FrameLayout relatedStreamsLayout;
/*////////////////////////////////////////////////////////////////////////*/
@ -383,14 +374,14 @@ public class VideoDetailFragment
Log.w(TAG, "Can't open channel because we got no channel URL");
} else {
try {
NavigationHelper.openChannelFragment(
getFragmentManager(),
currentInfo.getServiceId(),
currentInfo.getUploaderUrl(),
currentInfo.getUploaderName());
NavigationHelper.openChannelFragment(
getFragmentManager(),
currentInfo.getServiceId(),
currentInfo.getUploaderUrl(),
currentInfo.getUploaderName());
} catch (Exception e) {
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
}
}
}
break;
case R.id.detail_thumbnail_root_layout:
@ -489,6 +480,8 @@ public class VideoDetailFragment
tabLayout = rootView.findViewById(R.id.tablayout);
tabLayout.setupWithViewPager(viewPager);
relatedStreamsLayout = rootView.findViewById(R.id.relatedStreamsLayout);
setHeightThumbnail();
@ -588,6 +581,7 @@ public class VideoDetailFragment
}
}
/*//////////////////////////////////////////////////////////////////////////
// Menu
//////////////////////////////////////////////////////////////////////////*/
@ -676,7 +670,7 @@ public class VideoDetailFragment
sortedVideoStreams = ListHelper.getSortedStreamVideosList(activity, info.getVideoStreams(), info.getVideoOnlyStreams(), false);
selectedVideoStreamIndex = ListHelper.getDefaultResolutionIndex(activity, sortedVideoStreams);
final StreamItemAdapter<VideoStream> streamsAdapter = new StreamItemAdapter<>(activity, new StreamSizeWrapper<>(sortedVideoStreams), isExternalPlayerEnabled);
final StreamItemAdapter<VideoStream, Stream> streamsAdapter = new StreamItemAdapter<>(activity, new StreamSizeWrapper<>(sortedVideoStreams, activity), isExternalPlayerEnabled);
spinnerToolbar.setAdapter(streamsAdapter);
spinnerToolbar.setSelection(selectedVideoStreamIndex);
spinnerToolbar.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@ -771,11 +765,9 @@ public class VideoDetailFragment
initTabs();
if (scrollToTop) appBarLayout.setExpanded(true, true);
animateView(contentRootLayoutHiding,
false, 0, 0, () -> {
handleResult(info);
showContentWithAnimation(120, 0, .01f);
});
handleResult(info);
showContent();
}
protected void prepareAndLoadInfo() {
@ -798,8 +790,8 @@ public class VideoDetailFragment
.subscribe((@NonNull StreamInfo result) -> {
isLoading.set(false);
currentInfo = result;
showContentWithAnimation(120, 0, 0);
handleResult(result);
showContent();
}, (@NonNull Throwable throwable) -> {
isLoading.set(false);
onError(throwable);
@ -814,7 +806,7 @@ public class VideoDetailFragment
pageAdapter.addFragment(CommentsFragment.getInstance(serviceId, url, name), COMMENTS_TAB_TAG);
}
if(showRelatedStreams){
if(showRelatedStreams && null == relatedStreamsLayout){
//temp empty fragment. will be updated in handleResult
pageAdapter.addFragment(new Fragment(), RELATED_TAB_TAG);
}
@ -879,7 +871,7 @@ public class VideoDetailFragment
.getBoolean(this.getString(R.string.use_external_video_player_key), false)) {
startOnExternalPlayer(activity, currentInfo, selectedVideoStream);
} else {
openNormalPlayer(selectedVideoStream);
openNormalPlayer();
}
}
@ -892,24 +884,13 @@ public class VideoDetailFragment
}
}
private void openNormalPlayer(VideoStream selectedVideoStream) {
private void openNormalPlayer() {
Intent mIntent;
boolean useOldPlayer = PlayerHelper.isUsingOldPlayer(activity) || (Build.VERSION.SDK_INT < 16);
if (!useOldPlayer) {
// ExoPlayer
final PlayQueue playQueue = new SinglePlayQueue(currentInfo);
mIntent = NavigationHelper.getPlayerIntent(activity,
MainVideoPlayer.class,
playQueue,
getSelectedVideoStream().getResolution());
} else {
// Internal Player
mIntent = new Intent(activity, PlayVideoActivity.class)
.putExtra(PlayVideoActivity.VIDEO_TITLE, currentInfo.getName())
.putExtra(PlayVideoActivity.STREAM_URL, selectedVideoStream.getUrl())
.putExtra(PlayVideoActivity.VIDEO_URL, currentInfo.getUrl())
.putExtra(PlayVideoActivity.START_POSITION, currentInfo.getStartPosition());
}
final PlayQueue playQueue = new SinglePlayQueue(currentInfo);
mIntent = NavigationHelper.getPlayerIntent(activity,
MainVideoPlayer.class,
playQueue,
getSelectedVideoStream().getResolution());
startActivity(mIntent);
}
@ -964,24 +945,6 @@ public class VideoDetailFragment
}));
}
private View getSeparatorView() {
View separator = new View(activity);
LinearLayout.LayoutParams params =
new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1);
int m8 = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 8, getResources().getDisplayMetrics());
int m5 = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 5, getResources().getDisplayMetrics());
params.setMargins(m8, m5, m8, m5);
separator.setLayoutParams(params);
TypedValue typedValue = new TypedValue();
activity.getTheme().resolveAttribute(R.attr.separator_color, typedValue, true);
separator.setBackgroundColor(typedValue.data);
return separator;
}
private void setHeightThumbnail() {
final DisplayMetrics metrics = getResources().getDisplayMetrics();
boolean isPortrait = metrics.heightPixels > metrics.widthPixels;
@ -993,36 +956,8 @@ public class VideoDetailFragment
thumbnailImageView.setMinimumHeight(height);
}
private void showContentWithAnimation(long duration,
long delay,
@FloatRange(from = 0.0f, to = 1.0f) float translationPercent) {
int translationY = (int) (getResources().getDisplayMetrics().heightPixels *
(translationPercent > 0.0f ? translationPercent : .06f));
contentRootLayoutHiding.animate().setListener(null).cancel();
contentRootLayoutHiding.setAlpha(0f);
contentRootLayoutHiding.setTranslationY(translationY);
contentRootLayoutHiding.setVisibility(View.VISIBLE);
contentRootLayoutHiding.animate()
.alpha(1f)
.translationY(0)
.setStartDelay(delay)
.setDuration(duration)
.setInterpolator(new FastOutSlowInInterpolator())
.start();
uploaderRootLayout.animate().setListener(null).cancel();
uploaderRootLayout.setAlpha(0f);
uploaderRootLayout.setTranslationY(translationY);
uploaderRootLayout.setVisibility(View.VISIBLE);
uploaderRootLayout.animate()
.alpha(1f)
.translationY(0)
.setStartDelay((long) (duration * .5f) + delay)
.setDuration(duration)
.setInterpolator(new FastOutSlowInInterpolator())
.start();
private void showContent() {
AnimationUtils.slideUp(contentRootLayoutHiding,120, 96, 0.06f);
}
protected void setInitialData(int serviceId, String url, String name) {
@ -1057,7 +992,7 @@ public class VideoDetailFragment
public void showLoading() {
super.showLoading();
animateView(contentRootLayoutHiding, false, 200);
contentRootLayoutHiding.setVisibility(View.INVISIBLE);
animateView(spinnerToolbar, false, 200);
animateView(thumbnailPlayButton, false, 50);
animateView(detailDurationView, false, 100);
@ -1071,6 +1006,14 @@ public class VideoDetailFragment
videoTitleToggleArrow.setVisibility(View.GONE);
videoTitleRoot.setClickable(false);
if(relatedStreamsLayout != null){
if(showRelatedStreams){
relatedStreamsLayout.setVisibility(View.INVISIBLE);
}else{
relatedStreamsLayout.setVisibility(View.GONE);
}
}
imageLoader.cancelDisplayTask(thumbnailImageView);
imageLoader.cancelDisplayTask(uploaderThumb);
thumbnailImageView.setImageBitmap(null);
@ -1084,8 +1027,15 @@ public class VideoDetailFragment
setInitialData(info.getServiceId(), info.getOriginalUrl(), info.getName());
if(showRelatedStreams){
pageAdapter.updateItem(RELATED_TAB_TAG, RelatedVideosFragment.getInstance(currentInfo));
pageAdapter.notifyDataSetUpdate();
if(null == relatedStreamsLayout){ //phone
pageAdapter.updateItem(RELATED_TAB_TAG, RelatedVideosFragment.getInstance(currentInfo));
pageAdapter.notifyDataSetUpdate();
}else{ //tablet
getChildFragmentManager().beginTransaction()
.replace(R.id.relatedStreamsLayout, RelatedVideosFragment.getInstance(currentInfo))
.commitNow();
relatedStreamsLayout.setVisibility(View.VISIBLE);
}
}
pushToStack(serviceId, url, name);
@ -1149,10 +1099,10 @@ public class VideoDetailFragment
detailDurationView.setVisibility(View.GONE);
}
videoDescriptionView.setVisibility(View.GONE);
videoTitleRoot.setClickable(true);
videoTitleToggleArrow.setVisibility(View.VISIBLE);
videoTitleToggleArrow.setImageResource(R.drawable.arrow_down);
videoDescriptionView.setVisibility(View.GONE);
videoDescriptionRootLayout.setVisibility(View.GONE);
if (!TextUtils.isEmpty(info.getUploadDate())) {
videoUploadDateView.setText(Localization.localizeDate(activity, info.getUploadDate()));
@ -1205,6 +1155,7 @@ public class VideoDetailFragment
downloadDialog.setVideoStreams(sortedVideoStreams);
downloadDialog.setAudioStreams(currentInfo.getAudioStreams());
downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex);
downloadDialog.setSubtitleStreams(currentInfo.getSubtitles());
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
} catch (Exception e) {
@ -1253,4 +1204,4 @@ public class VideoDetailFragment
showError(getString(R.string.blocked_by_gema), false, R.drawable.gruese_die_gema);
}
}
}

View File

@ -3,10 +3,15 @@ package org.schabi.newpipe.fragments.list;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
@ -22,9 +27,9 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
import org.schabi.newpipe.info_list.InfoItemDialog;
import org.schabi.newpipe.info_list.InfoListAdapter;
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.util.NavigationHelper;
@ -37,7 +42,7 @@ import java.util.Queue;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implements ListViewContract<I, N>, StateSaver.WriteRead {
public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implements ListViewContract<I, N>, StateSaver.WriteRead, SharedPreferences.OnSharedPreferenceChangeListener {
/*//////////////////////////////////////////////////////////////////////////
// Views
@ -45,6 +50,9 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
protected InfoListAdapter infoListAdapter;
protected RecyclerView itemsList;
private int updateFlags = 0;
private static final int LIST_MODE_UPDATE_FLAG = 0x32;
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
@ -60,12 +68,31 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
PreferenceManager.getDefaultSharedPreferences(activity)
.registerOnSharedPreferenceChangeListener(this);
}
@Override
public void onDestroy() {
super.onDestroy();
StateSaver.onDestroy(savedState);
PreferenceManager.getDefaultSharedPreferences(activity)
.unregisterOnSharedPreferenceChangeListener(this);
}
@Override
public void onResume() {
super.onResume();
if (updateFlags != 0) {
if ((updateFlags & LIST_MODE_UPDATE_FLAG) != 0) {
final boolean useGrid = isGridLayout();
itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager());
infoListAdapter.setGridItemVariants(useGrid);
infoListAdapter.notifyDataSetChanged();
}
updateFlags = 0;
}
}
/*//////////////////////////////////////////////////////////////////////////
@ -120,13 +147,25 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
return new LinearLayoutManager(activity);
}
protected RecyclerView.LayoutManager getGridLayoutManager() {
final Resources resources = activity.getResources();
int width = resources.getDimensionPixelSize(R.dimen.video_item_grid_thumbnail_image_width);
width += (24 * resources.getDisplayMetrics().density);
final int spanCount = (int) Math.floor(resources.getDisplayMetrics().widthPixels / (double)width);
final GridLayoutManager lm = new GridLayoutManager(activity, spanCount);
lm.setSpanSizeLookup(infoListAdapter.getSpanSizeLookup(spanCount));
return lm;
}
@Override
protected void initViews(View rootView, Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
final boolean useGrid = isGridLayout();
itemsList = rootView.findViewById(R.id.items_list);
itemsList.setLayoutManager(getListLayoutManager());
itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager());
infoListAdapter.setGridItemVariants(useGrid);
infoListAdapter.setFooter(getListFooter());
infoListAdapter.setHeader(getListHeader());
@ -185,7 +224,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
infoListAdapter.setOnCommentsSelectedListener(new OnClickGesture<CommentsInfoItem>() {
@Override
public void selected(CommentsInfoItem selectedItem) {
//Log.d("comments" , "this comment was clicked" + selectedItem.getCommentText());
onItemSelected(selectedItem);
}
});
@ -316,4 +355,22 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
public void handleNextItems(N result) {
isLoading.set(false);
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (key.equals(getString(R.string.list_view_mode_key))) {
updateFlags |= LIST_MODE_UPDATE_FLAG;
}
}
protected boolean isGridLayout() {
final String list_mode = PreferenceManager.getDefaultSharedPreferences(activity).getString(getString(R.string.list_view_mode_key), getString(R.string.list_view_mode_value));
if ("auto".equals(list_mode)) {
final Configuration configuration = getResources().getConfiguration();
return configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
&& configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE);
} else {
return "grid".equals(list_mode);
}
}
}

View File

@ -1,72 +1,26 @@
package org.schabi.newpipe.fragments.list.comments;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.ActionBar;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.jakewharton.rxbinding2.view.RxView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.comments.CommentsInfo;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.info_list.InfoItemDialog;
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
import org.schabi.newpipe.local.subscription.SubscriptionService;
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Action;
import io.reactivex.functions.Consumer;
import io.reactivex.functions.Function;
import io.reactivex.schedulers.Schedulers;
import static org.schabi.newpipe.util.AnimationUtils.animateBackgroundColor;
import static org.schabi.newpipe.util.AnimationUtils.animateTextColor;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
@ -139,6 +93,8 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
public void handleResult(@NonNull CommentsInfo result) {
super.handleResult(result);
AnimationUtils.slideUp(getView(),120, 96, 0.06f);
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.REQUESTED_COMMENTS, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
}
@ -167,6 +123,7 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
protected boolean onError(Throwable exception) {
if (super.onError(exception)) return true;
hideLoading();
showSnackBarError(exception, UserAction.REQUESTED_COMMENTS, NewPipe.getNameOfService(serviceId), url, R.string.error_unable_to_load_comments);
return true;
}
@ -184,4 +141,9 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
return;
}
@Override
protected boolean isGridLayout() {
return false;
}
}

View File

@ -128,26 +128,16 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
@Override
public Single<KioskInfo> loadResult(boolean forceReload) {
String contentCountry = PreferenceManager
.getDefaultSharedPreferences(activity)
.getString(getString(R.string.content_country_key),
getString(R.string.default_country_value));
return ExtractorHelper.getKioskInfo(serviceId,
url,
contentCountry,
forceReload);
}
@Override
public Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
String contentCountry = PreferenceManager
.getDefaultSharedPreferences(activity)
.getString(getString(R.string.content_country_key),
getString(R.string.default_country_value));
return ExtractorHelper.getMoreKioskItems(serviceId,
url,
currentNextPageUrl,
contentCountry);
currentNextPageUrl);
}
/*//////////////////////////////////////////////////////////////////////////

View File

@ -626,7 +626,7 @@ public class SearchFragment
}
final Observable<List<SuggestionItem>> network = ExtractorHelper
.suggestionsFor(serviceId, query, contentCountry)
.suggestionsFor(serviceId, query)
.toObservable()
.map(strings -> {
List<SuggestionItem> result = new ArrayList<>();
@ -726,8 +726,7 @@ public class SearchFragment
searchDisposable = ExtractorHelper.searchFor(serviceId,
searchString,
Arrays.asList(contentFilter),
sortFilter,
contentCountry)
sortFilter)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnEvent((searchResult, throwable) -> isLoading.set(false))
@ -745,8 +744,7 @@ public class SearchFragment
searchString,
asList(contentFilter),
sortFilter,
nextPageUrl,
contentCountry)
nextPageUrl)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnEvent((nextItemsResult, throwable) -> isLoading.set(false))

View File

@ -1,7 +1,9 @@
package org.schabi.newpipe.fragments.list.videos;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
@ -9,36 +11,32 @@ import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CompoundButton;
import android.widget.Switch;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.ListInfo;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.comments.CommentsInfo;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.kiosk.KioskInfo;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.RelatedStreamInfo;
import java.util.List;
import java.io.Serializable;
import io.reactivex.Single;
import io.reactivex.disposables.CompositeDisposable;
public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInfo> {
public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInfo> implements SharedPreferences.OnSharedPreferenceChangeListener{
private CompositeDisposable disposables = new CompositeDisposable();
private RelatedStreamInfo relatedStreamInfo;
/*//////////////////////////////////////////////////////////////////////////
// Views
//////////////////////////////////////////////////////////////////////////*/
private View headerRootLayout;
private Switch aSwitch;
private boolean mIsVisibleToUser = false;
@ -74,6 +72,28 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
if (disposables != null) disposables.clear();
}
protected View getListHeader(){
if(relatedStreamInfo != null && relatedStreamInfo.getNextStream() != null){
headerRootLayout = activity.getLayoutInflater().inflate(R.layout.related_streams_header, itemsList, false);
aSwitch = headerRootLayout.findViewById(R.id.autoplay_switch);
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getContext());
Boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
aSwitch.setChecked(autoplay);
aSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
SharedPreferences.Editor prefEdit = PreferenceManager.getDefaultSharedPreferences(getContext()).edit();
prefEdit.putBoolean(getString(R.string.auto_queue_key), b);
prefEdit.apply();
}
});
return headerRootLayout;
}else{
return null;
}
}
@Override
protected Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
return Single.fromCallable(() -> ListExtractor.InfoItemsPage.emptyPage());
@ -91,12 +111,17 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
@Override
public void showLoading() {
super.showLoading();
headerRootLayout.setVisibility(View.INVISIBLE);
}
@Override
public void handleResult(@NonNull RelatedStreamInfo result) {
super.handleResult(result);
headerRootLayout.setVisibility(View.VISIBLE);
AnimationUtils.slideUp(getView(),120, 96, 0.06f);
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.REQUESTED_STREAM, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
}
@ -125,6 +150,7 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
protected boolean onError(Throwable exception) {
if (super.onError(exception)) return true;
hideLoading();
showSnackBarError(exception, UserAction.REQUESTED_STREAM, NewPipe.getNameOfService(serviceId), url, R.string.general_error);
return true;
}
@ -145,6 +171,38 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
private void setInitialData(StreamInfo info) {
super.setInitialData(info.getServiceId(), info.getUrl(), info.getName());
this.relatedStreamInfo = RelatedStreamInfo.getInfo(info);
if(this.relatedStreamInfo == null) this.relatedStreamInfo = RelatedStreamInfo.getInfo(info);
}
private static final String INFO_KEY = "related_info_key";
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable(INFO_KEY, relatedStreamInfo);
}
@Override
protected void onRestoreInstanceState(@NonNull Bundle savedState) {
super.onRestoreInstanceState(savedState);
if (savedState != null) {
Serializable serializable = savedState.getSerializable(INFO_KEY);
if(serializable instanceof RelatedStreamInfo){
this.relatedStreamInfo = (RelatedStreamInfo) serializable;
}
}
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) {
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getContext());
Boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
aSwitch.setChecked(autoplay);
}
@Override
protected boolean isGridLayout() {
return false;
}
}

View File

@ -1,6 +1,7 @@
package org.schabi.newpipe.info_list;
import android.app.Activity;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.View;
@ -15,9 +16,12 @@ import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
import org.schabi.newpipe.info_list.holder.CommentsInfoItemHolder;
import org.schabi.newpipe.info_list.holder.CommentsMiniInfoItemHolder;
import org.schabi.newpipe.info_list.holder.ChannelGridInfoItemHolder;
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
import org.schabi.newpipe.info_list.holder.PlaylistGridInfoItemHolder;
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder;
import org.schabi.newpipe.info_list.holder.StreamGridInfoItemHolder;
import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder;
import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder;
import org.schabi.newpipe.util.FallbackViewHolder;
@ -55,16 +59,20 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
private static final int MINI_STREAM_HOLDER_TYPE = 0x100;
private static final int STREAM_HOLDER_TYPE = 0x101;
private static final int GRID_STREAM_HOLDER_TYPE = 0x102;
private static final int MINI_CHANNEL_HOLDER_TYPE = 0x200;
private static final int CHANNEL_HOLDER_TYPE = 0x201;
private static final int GRID_CHANNEL_HOLDER_TYPE = 0x202;
private static final int MINI_PLAYLIST_HOLDER_TYPE = 0x300;
private static final int PLAYLIST_HOLDER_TYPE = 0x301;
private static final int GRID_PLAYLIST_HOLDER_TYPE = 0x302;
private static final int MINI_COMMENT_HOLDER_TYPE = 0x400;
private static final int COMMENT_HOLDER_TYPE = 0x401;
private final InfoItemBuilder infoItemBuilder;
private final ArrayList<InfoItem> infoItemList;
private boolean useMiniVariant = false;
private boolean useGridVariant = false;
private boolean showFooter = false;
private View header = null;
private View footer = null;
@ -103,6 +111,10 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
this.useMiniVariant = useMiniVariant;
}
public void setGridItemVariants(boolean useGridVariant) {
this.useGridVariant = useGridVariant;
}
public void addInfoItemList(List<InfoItem> data) {
if (data != null) {
if (DEBUG) {
@ -215,11 +227,11 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
final InfoItem item = infoItemList.get(position);
switch (item.getInfoType()) {
case STREAM:
return useMiniVariant ? MINI_STREAM_HOLDER_TYPE : STREAM_HOLDER_TYPE;
return useGridVariant ? GRID_STREAM_HOLDER_TYPE : useMiniVariant ? MINI_STREAM_HOLDER_TYPE : STREAM_HOLDER_TYPE;
case CHANNEL:
return useMiniVariant ? MINI_CHANNEL_HOLDER_TYPE : CHANNEL_HOLDER_TYPE;
return useGridVariant ? GRID_CHANNEL_HOLDER_TYPE : useMiniVariant ? MINI_CHANNEL_HOLDER_TYPE : CHANNEL_HOLDER_TYPE;
case PLAYLIST:
return useMiniVariant ? MINI_PLAYLIST_HOLDER_TYPE : PLAYLIST_HOLDER_TYPE;
return useGridVariant ? GRID_PLAYLIST_HOLDER_TYPE : useMiniVariant ? MINI_PLAYLIST_HOLDER_TYPE : PLAYLIST_HOLDER_TYPE;
case COMMENT:
return useMiniVariant ? MINI_COMMENT_HOLDER_TYPE : COMMENT_HOLDER_TYPE;
default:
@ -241,14 +253,20 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
return new StreamMiniInfoItemHolder(infoItemBuilder, parent);
case STREAM_HOLDER_TYPE:
return new StreamInfoItemHolder(infoItemBuilder, parent);
case GRID_STREAM_HOLDER_TYPE:
return new StreamGridInfoItemHolder(infoItemBuilder, parent);
case MINI_CHANNEL_HOLDER_TYPE:
return new ChannelMiniInfoItemHolder(infoItemBuilder, parent);
case CHANNEL_HOLDER_TYPE:
return new ChannelInfoItemHolder(infoItemBuilder, parent);
case GRID_CHANNEL_HOLDER_TYPE:
return new ChannelGridInfoItemHolder(infoItemBuilder, parent);
case MINI_PLAYLIST_HOLDER_TYPE:
return new PlaylistMiniInfoItemHolder(infoItemBuilder, parent);
case PLAYLIST_HOLDER_TYPE:
return new PlaylistInfoItemHolder(infoItemBuilder, parent);
case GRID_PLAYLIST_HOLDER_TYPE:
return new PlaylistGridInfoItemHolder(infoItemBuilder, parent);
case MINI_COMMENT_HOLDER_TYPE:
return new CommentsMiniInfoItemHolder(infoItemBuilder, parent);
case COMMENT_HOLDER_TYPE:
@ -273,4 +291,14 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
((HFHolder) holder).view = footer;
}
}
public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final int spanCount) {
return new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
final int type = getItemViewType(position);
return type == HEADER_TYPE || type == FOOTER_TYPE ? spanCount : 1;
}
};
}
}

View File

@ -0,0 +1,13 @@
package org.schabi.newpipe.info_list.holder;
import android.view.ViewGroup;
import org.schabi.newpipe.R;
import org.schabi.newpipe.info_list.InfoItemBuilder;
public class ChannelGridInfoItemHolder extends ChannelMiniInfoItemHolder {
public ChannelGridInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
super(infoItemBuilder, R.layout.list_channel_grid_item, parent);
}
}

View File

@ -47,6 +47,13 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder {
itemBuilder.getOnChannelSelectedListener().selected(item);
}
});
itemView.setOnLongClickListener(view -> {
if (itemBuilder.getOnChannelSelectedListener() != null) {
itemBuilder.getOnChannelSelectedListener().held(item);
}
return true;
});
}
protected String getDetailLine(final ChannelInfoItem item) {

View File

@ -2,7 +2,6 @@ package org.schabi.newpipe.info_list.holder;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
@ -13,7 +12,6 @@ import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import de.hdodenhof.circleimageview.CircleImageView;
@ -23,6 +21,7 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
private final TextView itemContentView;
private final TextView itemLikesCountView;
private final TextView itemDislikesCountView;
private final TextView itemPublishedTime;
private static final int commentDefaultLines = 2;
private static final int commentExpandedLines = 1000;
@ -31,9 +30,10 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
super(infoItemBuilder, layoutId, parent);
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
itemContentView = itemView.findViewById(R.id.itemCommentContentView);
itemLikesCountView = itemView.findViewById(R.id.detail_thumbs_up_count_view);
itemDislikesCountView = itemView.findViewById(R.id.detail_thumbs_down_count_view);
itemPublishedTime = itemView.findViewById(R.id.itemPublishedTime);
itemContentView = itemView.findViewById(R.id.itemCommentContentView);
}
public CommentsMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
@ -66,10 +66,17 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
}
});
// ellipsize if not already ellipsized
if (null == itemContentView.getEllipsize()) {
itemContentView.setEllipsize(TextUtils.TruncateAt.END);
itemContentView.setMaxLines(commentDefaultLines);
}
itemContentView.setText(item.getCommentText());
if (null != item.getLikeCount()) {
itemLikesCountView.setText(String.valueOf(item.getLikeCount()));
}
itemPublishedTime.setText(item.getPublishedTime());
itemView.setOnClickListener(view -> {
toggleEllipsize(item.getCommentText());

View File

@ -0,0 +1,13 @@
package org.schabi.newpipe.info_list.holder;
import android.view.ViewGroup;
import org.schabi.newpipe.R;
import org.schabi.newpipe.info_list.InfoItemBuilder;
public class PlaylistGridInfoItemHolder extends PlaylistMiniInfoItemHolder {
public PlaylistGridInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
super(infoItemBuilder, R.layout.list_playlist_grid_item, parent);
}
}

View File

@ -0,0 +1,13 @@
package org.schabi.newpipe.info_list.holder;
import android.view.ViewGroup;
import org.schabi.newpipe.R;
import org.schabi.newpipe.info_list.InfoItemBuilder;
public class StreamGridInfoItemHolder extends StreamMiniInfoItemHolder {
public StreamGridInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
super(infoItemBuilder, R.layout.list_stream_grid_item, parent);
}
}

View File

@ -1,8 +1,13 @@
package org.schabi.newpipe.local;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v4.app.Fragment;
import android.support.v7.app.ActionBar;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
@ -25,7 +30,7 @@ import static org.schabi.newpipe.util.AnimationUtils.animateView;
* called and is memory efficient when in backstack.
* */
public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
implements ListViewContract<I, N> {
implements ListViewContract<I, N>, SharedPreferences.OnSharedPreferenceChangeListener {
/*//////////////////////////////////////////////////////////////////////////
// Views
@ -36,6 +41,9 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
protected LocalItemListAdapter itemListAdapter;
protected RecyclerView itemsList;
private int updateFlags = 0;
private static final int LIST_MODE_UPDATE_FLAG = 0x32;
/*//////////////////////////////////////////////////////////////////////////
// Lifecycle - Creation
@ -45,6 +53,29 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
PreferenceManager.getDefaultSharedPreferences(activity)
.registerOnSharedPreferenceChangeListener(this);
}
@Override
public void onDestroy() {
super.onDestroy();
PreferenceManager.getDefaultSharedPreferences(activity)
.unregisterOnSharedPreferenceChangeListener(this);
}
@Override
public void onResume() {
super.onResume();
if (updateFlags != 0) {
if ((updateFlags & LIST_MODE_UPDATE_FLAG) != 0) {
final boolean useGrid = isGridLayout();
itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager());
itemListAdapter.setGridItemVariants(useGrid);
itemListAdapter.notifyDataSetChanged();
}
updateFlags = 0;
}
}
/*//////////////////////////////////////////////////////////////////////////
@ -59,6 +90,16 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
return activity.getLayoutInflater().inflate(R.layout.pignate_footer, itemsList, false);
}
protected RecyclerView.LayoutManager getGridLayoutManager() {
final Resources resources = activity.getResources();
int width = resources.getDimensionPixelSize(R.dimen.video_item_grid_thumbnail_image_width);
width += (24 * resources.getDisplayMetrics().density);
final int spanCount = (int) Math.floor(resources.getDisplayMetrics().widthPixels / (double)width);
final GridLayoutManager lm = new GridLayoutManager(activity, spanCount);
lm.setSpanSizeLookup(itemListAdapter.getSpanSizeLookup(spanCount));
return lm;
}
protected RecyclerView.LayoutManager getListLayoutManager() {
return new LinearLayoutManager(activity);
}
@ -67,10 +108,13 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
protected void initViews(View rootView, Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
itemsList = rootView.findViewById(R.id.items_list);
itemsList.setLayoutManager(getListLayoutManager());
itemListAdapter = new LocalItemListAdapter(activity);
final boolean useGrid = isGridLayout();
itemsList = rootView.findViewById(R.id.items_list);
itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager());
itemListAdapter.setGridItemVariants(useGrid);
itemListAdapter.setHeader(headerRootView = getListHeader());
itemListAdapter.setFooter(footerRootView = getListFooter());
@ -174,4 +218,22 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
resetFragment();
return super.onError(exception);
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (key.equals(getString(R.string.list_view_mode_key))) {
updateFlags |= LIST_MODE_UPDATE_FLAG;
}
}
protected boolean isGridLayout() {
final String list_mode = PreferenceManager.getDefaultSharedPreferences(activity).getString(getString(R.string.list_view_mode_key), getString(R.string.list_view_mode_value));
if ("auto".equals(list_mode)) {
final Configuration configuration = getResources().getConfiguration();
return configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
&& configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE);
} else {
return "grid".equals(list_mode);
}
}
}

View File

@ -1,18 +1,21 @@
package org.schabi.newpipe.local;
import android.app.Activity;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.local.HeaderFooterHolder;
import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.holder.LocalItemHolder;
import org.schabi.newpipe.local.holder.LocalPlaylistGridItemHolder;
import org.schabi.newpipe.local.holder.LocalPlaylistItemHolder;
import org.schabi.newpipe.local.holder.LocalPlaylistStreamGridItemHolder;
import org.schabi.newpipe.local.holder.LocalPlaylistStreamItemHolder;
import org.schabi.newpipe.local.holder.LocalStatisticStreamGridItemHolder;
import org.schabi.newpipe.local.holder.LocalStatisticStreamItemHolder;
import org.schabi.newpipe.local.holder.RemotePlaylistGridItemHolder;
import org.schabi.newpipe.local.holder.RemotePlaylistItemHolder;
import org.schabi.newpipe.util.FallbackViewHolder;
import org.schabi.newpipe.util.Localization;
@ -52,14 +55,19 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
private static final int STREAM_STATISTICS_HOLDER_TYPE = 0x1000;
private static final int STREAM_PLAYLIST_HOLDER_TYPE = 0x1001;
private static final int STREAM_STATISTICS_GRID_HOLDER_TYPE = 0x1002;
private static final int STREAM_PLAYLIST_GRID_HOLDER_TYPE = 0x1004;
private static final int LOCAL_PLAYLIST_HOLDER_TYPE = 0x2000;
private static final int REMOTE_PLAYLIST_HOLDER_TYPE = 0x2001;
private static final int LOCAL_PLAYLIST_GRID_HOLDER_TYPE = 0x2002;
private static final int REMOTE_PLAYLIST_GRID_HOLDER_TYPE = 0x2004;
private final LocalItemBuilder localItemBuilder;
private final ArrayList<LocalItem> localItems;
private final DateFormat dateFormat;
private boolean showFooter = false;
private boolean useGridVariant = false;
private View header = null;
private View footer = null;
@ -134,6 +142,10 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
notifyDataSetChanged();
}
public void setGridItemVariants(boolean useGridVariant) {
this.useGridVariant = useGridVariant;
}
public void setHeader(View header) {
boolean changed = header != this.header;
this.header = header;
@ -195,11 +207,11 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
final LocalItem item = localItems.get(position);
switch (item.getLocalItemType()) {
case PLAYLIST_LOCAL_ITEM: return LOCAL_PLAYLIST_HOLDER_TYPE;
case PLAYLIST_REMOTE_ITEM: return REMOTE_PLAYLIST_HOLDER_TYPE;
case PLAYLIST_LOCAL_ITEM: return useGridVariant ? LOCAL_PLAYLIST_GRID_HOLDER_TYPE : LOCAL_PLAYLIST_HOLDER_TYPE;
case PLAYLIST_REMOTE_ITEM: return useGridVariant ? REMOTE_PLAYLIST_GRID_HOLDER_TYPE : REMOTE_PLAYLIST_HOLDER_TYPE;
case PLAYLIST_STREAM_ITEM: return STREAM_PLAYLIST_HOLDER_TYPE;
case STATISTIC_STREAM_ITEM: return STREAM_STATISTICS_HOLDER_TYPE;
case PLAYLIST_STREAM_ITEM: return useGridVariant ? STREAM_PLAYLIST_GRID_HOLDER_TYPE : STREAM_PLAYLIST_HOLDER_TYPE;
case STATISTIC_STREAM_ITEM: return useGridVariant ? STREAM_STATISTICS_GRID_HOLDER_TYPE : STREAM_STATISTICS_HOLDER_TYPE;
default:
Log.e(TAG, "No holder type has been considered for item: [" +
item.getLocalItemType() + "]");
@ -218,12 +230,20 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
return new HeaderFooterHolder(footer);
case LOCAL_PLAYLIST_HOLDER_TYPE:
return new LocalPlaylistItemHolder(localItemBuilder, parent);
case LOCAL_PLAYLIST_GRID_HOLDER_TYPE:
return new LocalPlaylistGridItemHolder(localItemBuilder, parent);
case REMOTE_PLAYLIST_HOLDER_TYPE:
return new RemotePlaylistItemHolder(localItemBuilder, parent);
case REMOTE_PLAYLIST_GRID_HOLDER_TYPE:
return new RemotePlaylistGridItemHolder(localItemBuilder, parent);
case STREAM_PLAYLIST_HOLDER_TYPE:
return new LocalPlaylistStreamItemHolder(localItemBuilder, parent);
case STREAM_PLAYLIST_GRID_HOLDER_TYPE:
return new LocalPlaylistStreamGridItemHolder(localItemBuilder, parent);
case STREAM_STATISTICS_HOLDER_TYPE:
return new LocalStatisticStreamItemHolder(localItemBuilder, parent);
case STREAM_STATISTICS_GRID_HOLDER_TYPE:
return new LocalStatisticStreamGridItemHolder(localItemBuilder, parent);
default:
Log.e(TAG, "No view type has been considered for holder: [" + type + "]");
return new FallbackViewHolder(new View(parent.getContext()));
@ -247,4 +267,14 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
((HeaderFooterHolder) holder).view = footer;
}
}
public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final int spanCount) {
return new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
final int type = getItemViewType(position);
return type == HEADER_TYPE || type == FOOTER_TYPE ? spanCount : 1;
}
};
}
}

View File

@ -1,9 +1,11 @@
package org.schabi.newpipe.local.dialog;
import android.app.Dialog;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.DialogFragment;
import android.view.Window;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.util.StateSaver;
@ -41,6 +43,18 @@ public abstract class PlaylistDialog extends DialogFragment implements StateSave
StateSaver.onDestroy(savedState);
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final Dialog dialog = super.onCreateDialog(savedInstanceState);
//remove title
final Window window = dialog.getWindow();
if (window != null) {
window.requestFeature(Window.FEATURE_NO_TITLE);
}
return dialog;
}
/*//////////////////////////////////////////////////////////////////////////
// State Saving
//////////////////////////////////////////////////////////////////////////*/

View File

@ -0,0 +1,13 @@
package org.schabi.newpipe.local.holder;
import android.view.ViewGroup;
import org.schabi.newpipe.R;
import org.schabi.newpipe.local.LocalItemBuilder;
public class LocalPlaylistGridItemHolder extends LocalPlaylistItemHolder {
public LocalPlaylistGridItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) {
super(infoItemBuilder, R.layout.list_playlist_grid_item, parent);
}
}

View File

@ -16,6 +16,10 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder {
super(infoItemBuilder, parent);
}
LocalPlaylistItemHolder(LocalItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) {
super(infoItemBuilder, layoutId, parent);
}
@Override
public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) {
if (!(localItem instanceof PlaylistMetadataEntry)) return;

View File

@ -0,0 +1,13 @@
package org.schabi.newpipe.local.holder;
import android.view.ViewGroup;
import org.schabi.newpipe.R;
import org.schabi.newpipe.local.LocalItemBuilder;
public class LocalPlaylistStreamGridItemHolder extends LocalPlaylistStreamItemHolder {
public LocalPlaylistStreamGridItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) {
super(infoItemBuilder, R.layout.list_stream_playlist_grid_item, parent); //TODO
}
}

View File

@ -0,0 +1,13 @@
package org.schabi.newpipe.local.holder;
import android.view.ViewGroup;
import org.schabi.newpipe.R;
import org.schabi.newpipe.local.LocalItemBuilder;
public class LocalStatisticStreamGridItemHolder extends LocalStatisticStreamItemHolder {
public LocalStatisticStreamGridItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) {
super(infoItemBuilder, R.layout.list_stream_grid_item, parent);
}
}

View File

@ -1,5 +1,6 @@
package org.schabi.newpipe.local.holder;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.view.View;
import android.view.ViewGroup;
@ -42,10 +43,15 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
public final TextView itemVideoTitleView;
public final TextView itemUploaderView;
public final TextView itemDurationView;
@Nullable
public final TextView itemAdditionalDetails;
public LocalStatisticStreamItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) {
super(infoItemBuilder, R.layout.list_stream_item, parent);
public LocalStatisticStreamItemHolder(LocalItemBuilder itemBuilder, ViewGroup parent) {
this(itemBuilder, R.layout.list_stream_item, parent);
}
LocalStatisticStreamItemHolder(LocalItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) {
super(infoItemBuilder, layoutId, parent);
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
itemVideoTitleView = itemView.findViewById(R.id.itemVideoTitleView);
@ -80,7 +86,9 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
itemDurationView.setVisibility(View.GONE);
}
itemAdditionalDetails.setText(getStreamInfoDetailLine(item, dateFormat));
if (itemAdditionalDetails != null) {
itemAdditionalDetails.setText(getStreamInfoDetailLine(item, dateFormat));
}
// Default thumbnail is shown on error, while loading and if the url is empty
itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView,

View File

@ -0,0 +1,13 @@
package org.schabi.newpipe.local.holder;
import android.view.ViewGroup;
import org.schabi.newpipe.R;
import org.schabi.newpipe.local.LocalItemBuilder;
public class RemotePlaylistGridItemHolder extends RemotePlaylistItemHolder {
public RemotePlaylistGridItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) {
super(infoItemBuilder, R.layout.list_playlist_grid_item, parent);
}
}

View File

@ -16,6 +16,10 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
super(infoItemBuilder, parent);
}
RemotePlaylistItemHolder(LocalItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) {
super(infoItemBuilder, layoutId, parent);
}
@Override
public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) {
if (!(localItem instanceof PlaylistRemoteEntity)) return;

View File

@ -459,7 +459,11 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN,
int directions = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
if (isGridLayout()) {
directions |= ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
}
return new ItemTouchHelper.SimpleCallback(directions,
ItemTouchHelper.ACTION_STATE_IDLE) {
@Override
public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize,

View File

@ -1,21 +1,30 @@
package org.schabi.newpipe.local.subscription;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.os.Bundle;
import android.os.Environment;
import android.os.Parcelable;
import android.preference.PreferenceManager;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.FragmentManager;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
@ -42,6 +51,8 @@ import org.schabi.newpipe.info_list.InfoListAdapter;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService;
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.FilePickerActivityHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
@ -70,7 +81,7 @@ import static org.schabi.newpipe.local.subscription.services.SubscriptionsImport
import static org.schabi.newpipe.util.AnimationUtils.animateRotation;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEntity>> {
public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEntity>> implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final int REQUEST_EXPORT_CODE = 666;
private static final int REQUEST_IMPORT_CODE = 667;
@ -78,6 +89,9 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
@State
protected Parcelable itemsListState;
private InfoListAdapter infoListAdapter;
private int updateFlags = 0;
private static final int LIST_MODE_UPDATE_FLAG = 0x32;
private View whatsNewItemListHeader;
private View importExportListHeader;
@ -97,6 +111,8 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
PreferenceManager.getDefaultSharedPreferences(activity)
.registerOnSharedPreferenceChangeListener(this);
}
@Override
@ -124,6 +140,15 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
public void onResume() {
super.onResume();
setupBroadcastReceiver();
if (updateFlags != 0) {
if ((updateFlags & LIST_MODE_UPDATE_FLAG) != 0) {
final boolean useGrid = isGridLayout();
itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager());
infoListAdapter.setGridItemVariants(useGrid);
infoListAdapter.notifyDataSetChanged();
}
updateFlags = 0;
}
}
@Override
@ -150,9 +175,25 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
disposables = null;
subscriptionService = null;
PreferenceManager.getDefaultSharedPreferences(activity)
.unregisterOnSharedPreferenceChangeListener(this);
super.onDestroy();
}
protected RecyclerView.LayoutManager getListLayoutManager() {
return new LinearLayoutManager(activity);
}
protected RecyclerView.LayoutManager getGridLayoutManager() {
final Resources resources = activity.getResources();
int width = resources.getDimensionPixelSize(R.dimen.video_item_grid_thumbnail_image_width);
width += (24 * resources.getDisplayMetrics().density);
final int spanCount = (int) Math.floor(resources.getDisplayMetrics().widthPixels / (double)width);
final GridLayoutManager lm = new GridLayoutManager(activity, spanCount);
lm.setSpanSizeLookup(infoListAdapter.getSpanSizeLookup(spanCount));
return lm;
}
/*/////////////////////////////////////////////////////////////////////////
// Menu
/////////////////////////////////////////////////////////////////////////*/
@ -284,9 +325,10 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
protected void initViews(View rootView, Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
final boolean useGrid = isGridLayout();
infoListAdapter = new InfoListAdapter(getActivity());
itemsList = rootView.findViewById(R.id.items_list);
itemsList.setLayoutManager(new LinearLayoutManager(activity));
itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager());
View headerRootLayout;
infoListAdapter.setHeader(headerRootLayout = activity.getLayoutInflater().inflate(R.layout.subscription_header, itemsList, false));
@ -295,6 +337,7 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
importExportOptions = headerRootLayout.findViewById(R.id.import_export_options);
infoListAdapter.useMiniItemVariants(true);
infoListAdapter.setGridItemVariants(useGrid);
itemsList.setAdapter(infoListAdapter);
setupImportFromItems(headerRootLayout.findViewById(R.id.import_from_options));
@ -318,7 +361,7 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
super.initListeners();
infoListAdapter.setOnChannelSelectedListener(new OnClickGesture<ChannelInfoItem>() {
@Override
public void selected(ChannelInfoItem selectedItem) {
final FragmentManager fragmentManager = getFM();
NavigationHelper.openChannelFragment(fragmentManager,
@ -326,6 +369,11 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
selectedItem.getUrl(),
selectedItem.getName());
}
public void held(ChannelInfoItem selectedItem) {
showLongTapDialog(selectedItem);
}
});
//noinspection ConstantConditions
@ -336,6 +384,85 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
importExportListHeader.setOnClickListener(v -> importExportOptions.switchState());
}
private void showLongTapDialog(ChannelInfoItem selectedItem) {
final Context context = getContext();
final Activity activity = getActivity();
if (context == null || context.getResources() == null || getActivity() == null) return;
final String[] commands = new String[]{
context.getResources().getString(R.string.share),
context.getResources().getString(R.string.unsubscribe)
};
final DialogInterface.OnClickListener actions = (dialogInterface, i) -> {
switch (i) {
case 0:
shareChannel(selectedItem);
break;
case 1:
deleteChannel(selectedItem);
break;
default:
break;
}
};
final View bannerView = View.inflate(activity, R.layout.dialog_title, null);
bannerView.setSelected(true);
TextView titleView = bannerView.findViewById(R.id.itemTitleView);
titleView.setText(selectedItem.getName());
TextView detailsView = bannerView.findViewById(R.id.itemAdditionalDetails);
detailsView.setVisibility(View.GONE);
new AlertDialog.Builder(activity)
.setCustomTitle(bannerView)
.setItems(commands, actions)
.create()
.show();
}
private void shareChannel (ChannelInfoItem selectedItem) {
shareUrl(selectedItem.getName(), selectedItem.getUrl());
}
@SuppressLint("CheckResult")
private void deleteChannel (ChannelInfoItem selectedItem) {
subscriptionService.subscriptionTable()
.getSubscription(selectedItem.getServiceId(), selectedItem.getUrl())
.toObservable()
.observeOn(Schedulers.io())
.subscribe(getDeleteObserver());
Toast.makeText(activity, getString(R.string.channel_unsubscribed), Toast.LENGTH_SHORT).show();
}
private Observer<List<SubscriptionEntity>> getDeleteObserver(){
return new Observer<List<SubscriptionEntity>>() {
@Override
public void onSubscribe(Disposable d) {
disposables.add(d);
}
@Override
public void onNext(List<SubscriptionEntity> subscriptionEntities) {
subscriptionService.subscriptionTable().delete(subscriptionEntities);
}
@Override
public void onError(Throwable exception) {
SubscriptionFragment.this.onError(exception);
}
@Override
public void onComplete() { }
};
}
private void resetFragment() {
if (disposables != null) disposables.clear();
if (infoListAdapter != null) infoListAdapter.clearStreamItemList();
@ -445,4 +572,22 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
R.string.general_error);
return true;
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (key.equals(getString(R.string.list_view_mode_key))) {
updateFlags |= LIST_MODE_UPDATE_FLAG;
}
}
protected boolean isGridLayout() {
final String list_mode = PreferenceManager.getDefaultSharedPreferences(activity).getString(getString(R.string.list_view_mode_key), getString(R.string.list_view_mode_value));
if ("auto".equals(list_mode)) {
final Configuration configuration = getResources().getConfiguration();
return configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
&& configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE);
} else {
return "grid".equals(list_mode);
}
}
}

View File

@ -98,35 +98,54 @@ public abstract class BasePlayer implements
Player.EventListener, PlaybackListener, ImageLoadingListener {
public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release");
@NonNull public static final String TAG = "BasePlayer";
@NonNull
public static final String TAG = "BasePlayer";
@NonNull final protected Context context;
@NonNull
final protected Context context;
@NonNull final protected BroadcastReceiver broadcastReceiver;
@NonNull final protected IntentFilter intentFilter;
@NonNull
final protected BroadcastReceiver broadcastReceiver;
@NonNull
final protected IntentFilter intentFilter;
@NonNull final protected HistoryRecordManager recordManager;
@NonNull
final protected HistoryRecordManager recordManager;
@NonNull final protected CustomTrackSelector trackSelector;
@NonNull final protected PlayerDataSource dataSource;
@NonNull
final protected CustomTrackSelector trackSelector;
@NonNull
final protected PlayerDataSource dataSource;
@NonNull final private LoadControl loadControl;
@NonNull final private RenderersFactory renderFactory;
@NonNull
final private LoadControl loadControl;
@NonNull
final private RenderersFactory renderFactory;
@NonNull final private SerialDisposable progressUpdateReactor;
@NonNull final private CompositeDisposable databaseUpdateReactor;
@NonNull
final private SerialDisposable progressUpdateReactor;
@NonNull
final private CompositeDisposable databaseUpdateReactor;
/*//////////////////////////////////////////////////////////////////////////
// Intent
//////////////////////////////////////////////////////////////////////////*/
@NonNull public static final String REPEAT_MODE = "repeat_mode";
@NonNull public static final String PLAYBACK_PITCH = "playback_pitch";
@NonNull public static final String PLAYBACK_SPEED = "playback_speed";
@NonNull public static final String PLAYBACK_SKIP_SILENCE = "playback_skip_silence";
@NonNull public static final String PLAYBACK_QUALITY = "playback_quality";
@NonNull public static final String PLAY_QUEUE_KEY = "play_queue_key";
@NonNull public static final String APPEND_ONLY = "append_only";
@NonNull public static final String SELECT_ON_APPEND = "select_on_append";
@NonNull
public static final String REPEAT_MODE = "repeat_mode";
@NonNull
public static final String PLAYBACK_PITCH = "playback_pitch";
@NonNull
public static final String PLAYBACK_SPEED = "playback_speed";
@NonNull
public static final String PLAYBACK_SKIP_SILENCE = "playback_skip_silence";
@NonNull
public static final String PLAYBACK_QUALITY = "playback_quality";
@NonNull
public static final String PLAY_QUEUE_KEY = "play_queue_key";
@NonNull
public static final String APPEND_ONLY = "append_only";
@NonNull
public static final String SELECT_ON_APPEND = "select_on_append";
/*//////////////////////////////////////////////////////////////////////////
// Playback
@ -137,13 +156,18 @@ public abstract class BasePlayer implements
protected PlayQueue playQueue;
protected PlayQueueAdapter playQueueAdapter;
@Nullable protected MediaSourceManager playbackManager;
@Nullable
protected MediaSourceManager playbackManager;
@Nullable private PlayQueueItem currentItem;
@Nullable private MediaSourceTag currentMetadata;
@Nullable private Bitmap currentThumbnail;
@Nullable
private PlayQueueItem currentItem;
@Nullable
private MediaSourceTag currentMetadata;
@Nullable
private Bitmap currentThumbnail;
@Nullable protected Toast errorToast;
@Nullable
protected Toast errorToast;
/*//////////////////////////////////////////////////////////////////////////
// Player
@ -213,7 +237,8 @@ public abstract class BasePlayer implements
registerBroadcastReceiver();
}
public void initListeners() {}
public void initListeners() {
}
public void handleIntent(Intent intent) {
if (DEBUG) Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]");
@ -230,7 +255,8 @@ public abstract class BasePlayer implements
int sizeBeforeAppend = playQueue.size();
playQueue.append(queue.getStreams());
if (intent.getBooleanExtra(SELECT_ON_APPEND, false) &&
if ((intent.getBooleanExtra(SELECT_ON_APPEND, false) ||
getCurrentState() == STATE_COMPLETED) &&
queue.getStreams().size() > 0) {
playQueue.setIndex(sizeBeforeAppend);
}
@ -296,7 +322,6 @@ public abstract class BasePlayer implements
databaseUpdateReactor.clear();
progressUpdateReactor.set(null);
simpleExoPlayer = null;
}
/*//////////////////////////////////////////////////////////////////////////
@ -424,13 +449,15 @@ public abstract class BasePlayer implements
if (!isProgressLoopRunning()) startProgressLoop();
}
public void onBuffering() {}
public void onBuffering() {
}
public void onPaused() {
if (isProgressLoopRunning()) stopProgressLoop();
}
public void onPausedSeek() {}
public void onPausedSeek() {
}
public void onCompleted() {
if (DEBUG) Log.d(TAG, "onCompleted() called");
@ -601,19 +628,19 @@ public abstract class BasePlayer implements
/**
* Processes the exceptions produced by {@link com.google.android.exoplayer2.ExoPlayer ExoPlayer}.
* There are multiple types of errors: <br><br>
*
* <p>
* {@link ExoPlaybackException#TYPE_SOURCE TYPE_SOURCE}: <br><br>
*
* <p>
* {@link ExoPlaybackException#TYPE_UNEXPECTED TYPE_UNEXPECTED}: <br><br>
* If a runtime error occurred, then we can try to recover it by restarting the playback
* after setting the timestamp recovery. <br><br>
*
* <p>
* {@link ExoPlaybackException#TYPE_RENDERER TYPE_RENDERER}: <br><br>
* If the renderer failed, treat the error as unrecoverable.
*
* @see #processSourceError(IOException)
* @see Player.EventListener#onPlayerError(ExoPlaybackException)
* */
*/
@Override
public void onPlayerError(ExoPlaybackException error) {
if (DEBUG) Log.d(TAG, "ExoPlayer - onPlayerError() called with: " +
@ -899,8 +926,8 @@ public abstract class BasePlayer implements
if (DEBUG) Log.d(TAG, "onPlayPrevious() called");
/* If current playback has run for PLAY_PREV_ACTIVATION_LIMIT_MILLIS milliseconds,
* restart current track. Also restart the track if the current track
* is the first in a queue.*/
* restart current track. Also restart the track if the current track
* is the first in a queue.*/
if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT_MILLIS ||
playQueue.getIndex() == 0) {
seekToDefault();
@ -1009,8 +1036,8 @@ public abstract class BasePlayer implements
try {
metadata = (MediaSourceTag) simpleExoPlayer.getCurrentTag();
} catch (IndexOutOfBoundsException | ClassCastException error) {
if(DEBUG) Log.d(TAG, "Could not update metadata: " + error.getMessage());
if(DEBUG) error.printStackTrace();
if (DEBUG) Log.d(TAG, "Could not update metadata: " + error.getMessage());
if (DEBUG) error.printStackTrace();
return;
}
@ -1074,7 +1101,9 @@ public abstract class BasePlayer implements
currentThumbnail;
}
/** Checks if the current playback is a livestream AND is playing at or beyond the live edge */
/**
* Checks if the current playback is a livestream AND is playing at or beyond the live edge
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean isLiveEdge() {
if (simpleExoPlayer == null || !isLive()) return false;
@ -1098,13 +1127,14 @@ public abstract class BasePlayer implements
} catch (@NonNull IndexOutOfBoundsException ignored) {
// Why would this even happen =(
// But lets log it anyway. Save is save
if(DEBUG) Log.d(TAG, "Could not update metadata: " + ignored.getMessage());
if(DEBUG) ignored.printStackTrace();
if (DEBUG) Log.d(TAG, "Could not update metadata: " + ignored.getMessage());
if (DEBUG) ignored.printStackTrace();
return false;
}
}
public boolean isPlaying() {
if (simpleExoPlayer == null) return false;
final int state = simpleExoPlayer.getPlaybackState();
return (state == Player.STATE_READY || state == Player.STATE_BUFFERING)
&& simpleExoPlayer.getPlayWhenReady();
@ -1112,7 +1142,9 @@ public abstract class BasePlayer implements
@Player.RepeatMode
public int getRepeatMode() {
return simpleExoPlayer == null ? Player.REPEAT_MODE_OFF : simpleExoPlayer.getRepeatMode();
return simpleExoPlayer == null
? Player.REPEAT_MODE_OFF
: simpleExoPlayer.getRepeatMode();
}
public void setRepeatMode(@Player.RepeatMode final int repeatMode) {
@ -1178,4 +1210,8 @@ public abstract class BasePlayer implements
if (DEBUG) Log.d(TAG, "Setting recovery, queue: " + queuePos + ", pos: " + windowPos);
playQueue.setRecovery(queuePos, windowPos);
}
public boolean gotDestroyed() {
return simpleExoPlayer == null;
}
}

View File

@ -175,6 +175,10 @@ public final class MainVideoPlayer extends AppCompatActivity
setLandscape(lastOrientationWasLandscape);
}
final int lastResizeMode = defaultPreferences.getInt(
getString(R.string.last_resize_mode), AspectRatioFrameLayout.RESIZE_MODE_FIT);
playerImpl.setResizeMode(lastResizeMode);
// Upon going in or out of multiwindow mode, isInMultiWindow will always be false,
// since the first onResume needs to restore the player.
// Subsequent onResume calls while multiwindow mode remains the same and the player is
@ -213,10 +217,9 @@ public final class MainVideoPlayer extends AppCompatActivity
if (playerImpl == null) return;
playerImpl.setRecovery();
playerState = new PlayerState(playerImpl.getPlayQueue(), playerImpl.getRepeatMode(),
playerImpl.getPlaybackSpeed(), playerImpl.getPlaybackPitch(),
playerImpl.getPlaybackQuality(), playerImpl.getPlaybackSkipSilence(),
playerImpl.isPlaying());
if(!playerImpl.gotDestroyed()) {
playerState = createPlayerState();
}
StateSaver.tryToSave(isChangingConfigurations(), null, outState, this);
}
@ -231,6 +234,7 @@ public final class MainVideoPlayer extends AppCompatActivity
if (!isBackPressed) {
playerImpl.minimize();
}
playerState = createPlayerState();
playerImpl.destroy();
isInMultiWindow = false;
@ -241,6 +245,13 @@ public final class MainVideoPlayer extends AppCompatActivity
// State Saving
//////////////////////////////////////////////////////////////////////////*/
private PlayerState createPlayerState() {
return new PlayerState(playerImpl.getPlayQueue(), playerImpl.getRepeatMode(),
playerImpl.getPlaybackSpeed(), playerImpl.getPlaybackPitch(),
playerImpl.getPlaybackQuality(), playerImpl.getPlaybackSkipSilence(),
playerImpl.isPlaying());
}
@Override
public String generateSuffix() {
return "." + UUID.randomUUID().toString() + ".player";
@ -705,14 +716,27 @@ public final class MainVideoPlayer extends AppCompatActivity
@Override
protected int nextResizeMode(int currentResizeMode) {
final int newResizeMode;
switch (currentResizeMode) {
case AspectRatioFrameLayout.RESIZE_MODE_FIT:
return AspectRatioFrameLayout.RESIZE_MODE_FILL;
newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FILL;
break;
case AspectRatioFrameLayout.RESIZE_MODE_FILL:
return AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
break;
default:
return AspectRatioFrameLayout.RESIZE_MODE_FIT;
newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT;
break;
}
storeResizeMode(newResizeMode);
return newResizeMode;
}
private void storeResizeMode(@AspectRatioFrameLayout.ResizeMode int resizeMode) {
defaultPreferences.edit()
.putInt(getString(R.string.last_resize_mode), resizeMode)
.apply();
}
@Override
@ -876,6 +900,11 @@ public final class MainVideoPlayer extends AppCompatActivity
public void onMove(int sourceIndex, int targetIndex) {
if (playQueue != null) playQueue.move(sourceIndex, targetIndex);
}
@Override
public void onSwiped(int index) {
if(index != -1) playQueue.remove(index);
}
};
}
@ -989,12 +1018,14 @@ public final class MainVideoPlayer extends AppCompatActivity
private static final int MOVEMENT_THRESHOLD = 40;
private final boolean isPlayerGestureEnabled = PlayerHelper.isPlayerGestureEnabled(getApplicationContext());
private final boolean isVolumeGestureEnabled = PlayerHelper.isVolumeGestureEnabled(getApplicationContext());
private final boolean isBrightnessGestureEnabled = PlayerHelper.isBrightnessGestureEnabled(getApplicationContext());
private final int maxVolume = playerImpl.getAudioReactor().getMaxVolume();
@Override
public boolean onScroll(MotionEvent initialEvent, MotionEvent movingEvent, float distanceX, float distanceY) {
if (!isPlayerGestureEnabled) return false;
if (!isVolumeGestureEnabled && !isBrightnessGestureEnabled) return false;
//noinspection PointlessBooleanExpression
if (DEBUG && false) Log.d(TAG, "MainVideoPlayer.onScroll = " +
@ -1010,7 +1041,11 @@ public final class MainVideoPlayer extends AppCompatActivity
isMoving = true;
if (initialEvent.getX() > playerImpl.getRootView().getWidth() / 2) {
boolean acceptAnyArea = isVolumeGestureEnabled != isBrightnessGestureEnabled;
boolean acceptVolumeArea = acceptAnyArea || initialEvent.getX() > playerImpl.getRootView().getWidth() / 2;
boolean acceptBrightnessArea = acceptAnyArea || !acceptVolumeArea;
if (isVolumeGestureEnabled && acceptVolumeArea) {
playerImpl.getVolumeProgressBar().incrementProgressBy((int) distanceY);
float currentProgressPercent =
(float) playerImpl.getVolumeProgressBar().getProgress() / playerImpl.getMaxGestureLength();
@ -1035,7 +1070,7 @@ public final class MainVideoPlayer extends AppCompatActivity
if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) {
playerImpl.getBrightnessRelativeLayout().setVisibility(View.GONE);
}
} else {
} else if (isBrightnessGestureEnabled && acceptBrightnessArea) {
playerImpl.getBrightnessProgressBar().incrementProgressBy((int) distanceY);
float currentProgressPercent =
(float) playerImpl.getBrightnessProgressBar().getProgress() / playerImpl.getMaxGestureLength();

View File

@ -68,7 +68,6 @@ import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.player.event.PlayerEventListener;
import org.schabi.newpipe.player.helper.LockManager;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.old.PlayVideoActivity;
import org.schabi.newpipe.player.resolver.MediaSourceTag;
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
import org.schabi.newpipe.util.ListHelper;
@ -80,7 +79,6 @@ import java.util.List;
import static org.schabi.newpipe.player.BasePlayer.STATE_PLAYING;
import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION;
import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME;
import static org.schabi.newpipe.player.helper.PlayerHelper.isUsingOldPlayer;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
/**
@ -554,27 +552,17 @@ public final class PopupVideoPlayer extends Service {
if (DEBUG) Log.d(TAG, "onFullScreenButtonClicked() called");
setRecovery();
Intent intent;
if (!isUsingOldPlayer(getApplicationContext())) {
intent = NavigationHelper.getPlayerIntent(
context,
MainVideoPlayer.class,
this.getPlayQueue(),
this.getRepeatMode(),
this.getPlaybackSpeed(),
this.getPlaybackPitch(),
this.getPlaybackSkipSilence(),
this.getPlaybackQuality()
);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
} else {
intent = new Intent(PopupVideoPlayer.this, PlayVideoActivity.class)
.putExtra(PlayVideoActivity.VIDEO_TITLE, getVideoTitle())
.putExtra(PlayVideoActivity.STREAM_URL, getSelectedVideoStream().getUrl())
.putExtra(PlayVideoActivity.VIDEO_URL, getVideoUrl())
.putExtra(PlayVideoActivity.START_POSITION, Math.round(getPlayer().getCurrentPosition() / 1000f));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
final Intent intent = NavigationHelper.getPlayerIntent(
context,
MainVideoPlayer.class,
this.getPlayQueue(),
this.getRepeatMode(),
this.getPlaybackSpeed(),
this.getPlaybackPitch(),
this.getPlaybackSkipSilence(),
this.getPlaybackQuality()
);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
closePopup();
}

View File

@ -375,6 +375,11 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
public void onMove(int sourceIndex, int targetIndex) {
if (player != null) player.getPlayQueue().move(sourceIndex, targetIndex);
}
@Override
public void onSwiped(int index) {
if (index != -1) player.getPlayQueue().remove(index);
}
};
}

View File

@ -683,12 +683,17 @@ public abstract class VideoPlayer extends BasePlayer
if (getAspectRatioFrameLayout() != null) {
final int currentResizeMode = getAspectRatioFrameLayout().getResizeMode();
final int newResizeMode = nextResizeMode(currentResizeMode);
getAspectRatioFrameLayout().setResizeMode(newResizeMode);
getResizeView().setText(PlayerHelper.resizeTypeOf(context, newResizeMode));
setResizeMode(newResizeMode);
}
}
protected void setResizeMode(@AspectRatioFrameLayout.ResizeMode final int resizeMode) {
getAspectRatioFrameLayout().setResizeMode(resizeMode);
getResizeView().setText(PlayerHelper.resizeTypeOf(context, resizeMode));
}
protected abstract int nextResizeMode(@AspectRatioFrameLayout.ResizeMode final int resizeMode);
/*//////////////////////////////////////////////////////////////////////////
// SeekBar Listener
//////////////////////////////////////////////////////////////////////////*/

View File

@ -19,11 +19,11 @@ import com.google.android.exoplayer2.util.MimeTypes;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.Subtitles;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.SubtitlesFormat;
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
@ -45,7 +45,9 @@ import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MOD
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT;
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.*;
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND;
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE;
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP;
public class PlayerHelper {
private PlayerHelper() {}
@ -87,7 +89,7 @@ public class PlayerHelper {
return pitchFormatter.format(pitch);
}
public static String mimeTypesOf(final SubtitlesFormat format) {
public static String subtitleMimeTypesOf(final MediaFormat format) {
switch (format) {
case VTT: return MimeTypes.TEXT_VTT;
case TTML: return MimeTypes.APPLICATION_TTML;
@ -97,8 +99,8 @@ public class PlayerHelper {
@NonNull
public static String captionLanguageOf(@NonNull final Context context,
@NonNull final Subtitles subtitles) {
final String displayName = subtitles.getLocale().getDisplayName(subtitles.getLocale());
@NonNull final SubtitlesStream subtitles) {
final String displayName = subtitles.getDisplayLanguageName();
return displayName + (subtitles.isAutoGenerated() ? " (" + context.getString(R.string.caption_auto_generated)+ ")" : "");
}
@ -145,7 +147,7 @@ public class PlayerHelper {
final StreamInfoItem nextVideo = info.getNextVideo();
if (nextVideo != null && !urls.contains(nextVideo.getUrl())) {
return new SinglePlayQueue(nextVideo);
return getAutoQueuedSinglePlayQueue(nextVideo);
}
final List<InfoItem> relatedItems = info.getRelatedStreams();
@ -158,7 +160,7 @@ public class PlayerHelper {
}
}
Collections.shuffle(autoQueueItems);
return autoQueueItems.isEmpty() ? null : new SinglePlayQueue(autoQueueItems.get(0));
return autoQueueItems.isEmpty() ? null : getAutoQueuedSinglePlayQueue(autoQueueItems.get(0));
}
////////////////////////////////////////////////////////////////////////////
@ -169,12 +171,12 @@ public class PlayerHelper {
return isResumeAfterAudioFocusGain(context, false);
}
public static boolean isPlayerGestureEnabled(@NonNull final Context context) {
return isPlayerGestureEnabled(context, true);
public static boolean isVolumeGestureEnabled(@NonNull final Context context) {
return isVolumeGestureEnabled(context, true);
}
public static boolean isUsingOldPlayer(@NonNull final Context context) {
return isUsingOldPlayer(context, false);
public static boolean isBrightnessGestureEnabled(@NonNull final Context context) {
return isBrightnessGestureEnabled(context, true);
}
public static boolean isRememberingPopupDimensions(@NonNull final Context context) {
@ -306,12 +308,12 @@ public class PlayerHelper {
return getPreferences(context).getBoolean(context.getString(R.string.resume_on_audio_focus_gain_key), b);
}
private static boolean isPlayerGestureEnabled(@NonNull final Context context, final boolean b) {
return getPreferences(context).getBoolean(context.getString(R.string.player_gesture_controls_key), b);
private static boolean isVolumeGestureEnabled(@NonNull final Context context, final boolean b) {
return getPreferences(context).getBoolean(context.getString(R.string.volume_gesture_control_key), b);
}
private static boolean isUsingOldPlayer(@NonNull final Context context, final boolean b) {
return getPreferences(context).getBoolean(context.getString(R.string.use_old_player_key), b);
private static boolean isBrightnessGestureEnabled(@NonNull final Context context, final boolean b) {
return getPreferences(context).getBoolean(context.getString(R.string.brightness_gesture_control_key), b);
}
private static boolean isRememberingPopupDimensions(@NonNull final Context context, final boolean b) {
@ -350,4 +352,10 @@ public class PlayerHelper {
return getPreferences(context).getString(context.getString(R.string.minimize_on_exit_key),
key);
}
private static SinglePlayQueue getAutoQueuedSinglePlayQueue(StreamInfoItem streamInfoItem) {
SinglePlayQueue singlePlayQueue = new SinglePlayQueue(streamInfoItem);
singlePlayQueue.getItem().setAutoQueued(true);
return singlePlayQueue;
}
}

View File

@ -1,369 +0,0 @@
package org.schabi.newpipe.player.old;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Display;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.MediaController;
import android.widget.ProgressBar;
import android.widget.VideoView;
import org.schabi.newpipe.R;
/*
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
* PlayVideoActivity.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class PlayVideoActivity extends AppCompatActivity {
//// TODO: 11.09.15 add "choose stream" menu
private static final String TAG = PlayVideoActivity.class.toString();
public static final String VIDEO_URL = "video_url";
public static final String STREAM_URL = "stream_url";
public static final String VIDEO_TITLE = "video_title";
private static final String POSITION = "position";
public static final String START_POSITION = "start_position";
private static final long HIDING_DELAY = 3000;
private String videoUrl = "";
private ActionBar actionBar;
private VideoView videoView;
private int position;
private MediaController mediaController;
private ProgressBar progressBar;
private View decorView;
private boolean uiIsHidden;
private static long lastUiShowTime;
private boolean isLandscape = true;
private boolean hasSoftKeys;
private SharedPreferences prefs;
private static final String PREF_IS_LANDSCAPE = "is_landscape";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_play_video);
setVolumeControlStream(AudioManager.STREAM_MUSIC);
//set background arrow style
getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_arrow_back_white_24dp);
isLandscape = checkIfLandscape();
hasSoftKeys = checkIfHasSoftKeys();
actionBar = getSupportActionBar();
assert actionBar != null;
actionBar.setDisplayHomeAsUpEnabled(true);
Intent intent = getIntent();
if(mediaController == null) {
//prevents back button hiding media controller controls (after showing them)
//instead of exiting video
//see http://stackoverflow.com/questions/6051825
//also solves https://github.com/theScrabi/NewPipe/issues/99
mediaController = new MediaController(this) {
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
int keyCode = event.getKeyCode();
final boolean uniqueDown = event.getRepeatCount() == 0
&& event.getAction() == KeyEvent.ACTION_DOWN;
if (keyCode == KeyEvent.KEYCODE_BACK) {
if (uniqueDown)
{
if (isShowing()) {
finish();
} else {
hide();
}
}
return true;
}
return super.dispatchKeyEvent(event);
}
};
}
position = intent.getIntExtra(START_POSITION, 0)*1000;//convert from seconds to milliseconds
videoView = findViewById(R.id.video_view);
progressBar = findViewById(R.id.play_video_progress_bar);
try {
videoView.setMediaController(mediaController);
videoView.setVideoURI(Uri.parse(intent.getStringExtra(STREAM_URL)));
} catch (Exception e) {
e.printStackTrace();
}
videoView.requestFocus();
videoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
progressBar.setVisibility(View.GONE);
videoView.seekTo(position);
if (position <= 0) {
videoView.start();
showUi();
} else {
videoView.pause();
}
}
});
videoUrl = intent.getStringExtra(VIDEO_URL);
Button button = findViewById(R.id.content_button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(uiIsHidden) {
showUi();
} else {
hideUi();
}
}
});
decorView = getWindow().getDecorView();
decorView.setOnSystemUiVisibilityChangeListener(new View.OnSystemUiVisibilityChangeListener() {
@Override
public void onSystemUiVisibilityChange(int visibility) {
if (visibility == View.VISIBLE && uiIsHidden) {
showUi();
}
}
});
if (android.os.Build.VERSION.SDK_INT >= 17) {
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
}
prefs = getPreferences(Context.MODE_PRIVATE);
if(prefs.getBoolean(PREF_IS_LANDSCAPE, false) && !isLandscape) {
toggleOrientation();
}
}
@Override
public boolean onCreatePanelMenu(int featured, Menu menu) {
super.onCreatePanelMenu(featured, menu);
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.video_player, menu);
return true;
}
@Override
public void onPause() {
super.onPause();
videoView.pause();
}
@Override
public void onResume() {
super.onResume();
}
@Override
protected void onDestroy() {
super.onDestroy();
prefs = getPreferences(Context.MODE_PRIVATE);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
switch(id) {
case android.R.id.home:
finish();
break;
case R.id.menu_item_share:
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_TEXT, videoUrl);
intent.setType("text/plain");
startActivity(Intent.createChooser(intent, getString(R.string.share_dialog_title)));
break;
case R.id.menu_item_screen_rotation:
toggleOrientation();
break;
default:
Log.e(TAG, "Error: MenuItem not known");
return false;
}
return true;
}
@Override
public void onConfigurationChanged(Configuration config) {
super.onConfigurationChanged(config);
if (config.orientation == Configuration.ORIENTATION_LANDSCAPE) {
isLandscape = true;
adjustMediaControlMetrics();
} else if (config.orientation == Configuration.ORIENTATION_PORTRAIT){
isLandscape = false;
adjustMediaControlMetrics();
}
}
@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
super.onSaveInstanceState(savedInstanceState);
//savedInstanceState.putInt(POSITION, videoView.getCurrentPosition());
//videoView.pause();
}
@Override
public void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
position = savedInstanceState.getInt(POSITION);
//videoView.seekTo(position);
}
private void showUi() {
try {
uiIsHidden = false;
mediaController.show(100000);
actionBar.show();
adjustMediaControlMetrics();
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
@Override
public void run() {
if ((System.currentTimeMillis() - lastUiShowTime) >= HIDING_DELAY) {
hideUi();
}
}
}, HIDING_DELAY);
lastUiShowTime = System.currentTimeMillis();
}catch(Exception e) {
e.printStackTrace();
}
}
private void hideUi() {
uiIsHidden = true;
actionBar.hide();
mediaController.hide();
if (android.os.Build.VERSION.SDK_INT >= 17) {
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
}
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
private void adjustMediaControlMetrics() {
MediaController.LayoutParams mediaControllerLayout
= new MediaController.LayoutParams(MediaController.LayoutParams.MATCH_PARENT,
MediaController.LayoutParams.WRAP_CONTENT);
if(!hasSoftKeys) {
mediaControllerLayout.setMargins(20, 0, 20, 20);
} else {
int width = getNavigationBarWidth();
int height = getNavigationBarHeight();
mediaControllerLayout.setMargins(width + 20, 0, width + 20, height + 20);
}
mediaController.setLayoutParams(mediaControllerLayout);
}
private boolean checkIfHasSoftKeys(){
return Build.VERSION.SDK_INT >= 17 ||
getNavigationBarHeight() != 0 ||
getNavigationBarWidth() != 0;
}
private int getNavigationBarHeight() {
if(Build.VERSION.SDK_INT >= 17) {
Display d = getWindowManager().getDefaultDisplay();
DisplayMetrics realDisplayMetrics = new DisplayMetrics();
d.getRealMetrics(realDisplayMetrics);
DisplayMetrics displayMetrics = new DisplayMetrics();
d.getMetrics(displayMetrics);
int realHeight = realDisplayMetrics.heightPixels;
int displayHeight = displayMetrics.heightPixels;
return realHeight - displayHeight;
} else {
return 50;
}
}
private int getNavigationBarWidth() {
if(Build.VERSION.SDK_INT >= 17) {
Display d = getWindowManager().getDefaultDisplay();
DisplayMetrics realDisplayMetrics = new DisplayMetrics();
d.getRealMetrics(realDisplayMetrics);
DisplayMetrics displayMetrics = new DisplayMetrics();
d.getMetrics(displayMetrics);
int realWidth = realDisplayMetrics.widthPixels;
int displayWidth = displayMetrics.widthPixels;
return realWidth - displayWidth;
} else {
return 50;
}
}
private boolean checkIfLandscape() {
DisplayMetrics displayMetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
return displayMetrics.heightPixels < displayMetrics.widthPixels;
}
private void toggleOrientation() {
if(isLandscape) {
isLandscape = false;
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
} else {
isLandscape = true;
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
}
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean(PREF_IS_LANDSCAPE, isLandscape);
editor.apply();
}
}

View File

@ -233,6 +233,9 @@ public abstract class PlayQueue implements Serializable {
backup.addAll(itemList);
Collections.shuffle(itemList);
}
if (!streams.isEmpty() && streams.get(streams.size() - 1).isAutoQueued() && !itemList.get(0).isAutoQueued()) {
streams.remove(streams.size() - 1);
}
streams.addAll(itemList);
broadcast(new AppendEvent(itemList.size()));
@ -314,7 +317,9 @@ public abstract class PlayQueue implements Serializable {
queueIndex.incrementAndGet();
}
streams.add(target, streams.remove(source));
PlayQueueItem playQueueItem = streams.remove(source);
playQueueItem.setAutoQueued(false);
streams.add(target, playQueueItem);
broadcast(new MoveEvent(source, target));
}

View File

@ -25,9 +25,10 @@ public class PlayQueueItem implements Serializable {
@NonNull final private String uploader;
@NonNull final private StreamType streamType;
private boolean isAutoQueued;
private long recoveryPosition;
private Throwable error;
PlayQueueItem(@NonNull final StreamInfo info) {
this(info.getName(), info.getUrl(), info.getServiceId(), info.getDuration(),
info.getThumbnailUrl(), info.getUploaderName(), info.getStreamType());
@ -105,6 +106,14 @@ public class PlayQueueItem implements Serializable {
.doOnError(throwable -> error = throwable);
}
public boolean isAutoQueued() {
return isAutoQueued;
}
public void setAutoQueued(boolean autoQueued) {
isAutoQueued = autoQueued;
}
////////////////////////////////////////////////////////////////////////////
// Item States, keep external access out
////////////////////////////////////////////////////////////////////////////

View File

@ -8,11 +8,13 @@ public abstract class PlayQueueItemTouchCallback extends ItemTouchHelper.SimpleC
private static final int MAXIMUM_INITIAL_DRAG_VELOCITY = 25;
public PlayQueueItemTouchCallback() {
super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0);
super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.RIGHT);
}
public abstract void onMove(final int sourceIndex, final int targetIndex);
public abstract void onSwiped(int index);
@Override
public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize,
int viewSizeOutOfBounds, int totalSize,
@ -44,9 +46,11 @@ public abstract class PlayQueueItemTouchCallback extends ItemTouchHelper.SimpleC
@Override
public boolean isItemViewSwipeEnabled() {
return false;
return true;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {}
public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {
onSwiped(viewHolder.getAdapterPosition());
}
}

View File

@ -10,9 +10,10 @@ import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MergingMediaSource;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.Subtitles;
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.player.helper.PlayerDataSource;
import org.schabi.newpipe.player.helper.PlayerHelper;
@ -93,8 +94,8 @@ public class VideoPlaybackResolver implements PlaybackResolver {
// Below are auxiliary media sources
// Create subtitle sources
for (final Subtitles subtitle : info.getSubtitles()) {
final String mimeType = PlayerHelper.mimeTypesOf(subtitle.getFileType());
for (final SubtitlesStream subtitle : info.getSubtitles()) {
final String mimeType = PlayerHelper.subtitleMimeTypesOf(subtitle.getFormat());
if (mimeType == null) continue;
final Format textFormat = Format.createTextSampleFormat(null, mimeType,

View File

@ -17,6 +17,8 @@ import com.nononsenseapps.filepicker.Utils;
import com.nostra13.universalimageloader.core.ImageLoader;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.utils.Localization;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.FilePickerActivityHelper;
@ -106,6 +108,20 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
startActivityForResult(i, REQUEST_EXPORT_PATH);
return true;
});
Preference setPreferredLanguage = findPreference(getString(R.string.content_language_key));
setPreferredLanguage.setOnPreferenceChangeListener((Preference p, Object newLanguage) -> {
Localization oldLocal = org.schabi.newpipe.util.Localization.getPreferredExtractorLocal(getActivity());
NewPipe.setLocalization(new Localization(oldLocal.getCountry(), (String) newLanguage));
return true;
});
Preference setPreferredCountry = findPreference(getString(R.string.content_country_key));
setPreferredCountry.setOnPreferenceChangeListener((Preference p, Object newCountry) -> {
Localization oldLocal = org.schabi.newpipe.util.Localization.getPreferredExtractorLocal(getActivity());
NewPipe.setLocalization(new Localization((String) newCountry, oldLocal.getLanguage()));
return true;
});
}
@Override

View File

@ -0,0 +1,103 @@
package org.schabi.newpipe.streams;
import java.io.EOFException;
import java.io.IOException;
import org.schabi.newpipe.streams.io.SharpStream;
/**
* @author kapodamy
*/
public class DataReader {
public final static int SHORT_SIZE = 2;
public final static int LONG_SIZE = 8;
public final static int INTEGER_SIZE = 4;
public final static int FLOAT_SIZE = 4;
private long pos;
public final SharpStream stream;
private final boolean rewind;
public DataReader(SharpStream stream) {
this.rewind = stream.canRewind();
this.stream = stream;
this.pos = 0L;
}
public long position() {
return pos;
}
public final int readInt() throws IOException {
primitiveRead(INTEGER_SIZE);
return primitive[0] << 24 | primitive[1] << 16 | primitive[2] << 8 | primitive[3];
}
public final int read() throws IOException {
int value = stream.read();
if (value == -1) {
throw new EOFException();
}
pos++;
return value;
}
public final long skipBytes(long amount) throws IOException {
amount = stream.skip(amount);
pos += amount;
return amount;
}
public final long readLong() throws IOException {
primitiveRead(LONG_SIZE);
long high = primitive[0] << 24 | primitive[1] << 16 | primitive[2] << 8 | primitive[3];
long low = primitive[4] << 24 | primitive[5] << 16 | primitive[6] << 8 | primitive[7];
return high << 32 | low;
}
public final short readShort() throws IOException {
primitiveRead(SHORT_SIZE);
return (short) (primitive[0] << 8 | primitive[1]);
}
public final int read(byte[] buffer) throws IOException {
return read(buffer, 0, buffer.length);
}
public final int read(byte[] buffer, int offset, int count) throws IOException {
int res = stream.read(buffer, offset, count);
pos += res;
return res;
}
public final boolean available() {
return stream.available() > 0;
}
public void rewind() throws IOException {
stream.rewind();
pos = 0;
}
public boolean canRewind() {
return rewind;
}
private short[] primitive = new short[LONG_SIZE];
private void primitiveRead(int amount) throws IOException {
byte[] buffer = new byte[amount];
int read = stream.read(buffer, 0, amount);
pos += read;
if (read != amount) {
throw new EOFException("Truncated data, missing " + String.valueOf(amount - read) + " bytes");
}
for (int i = 0; i < buffer.length; i++) {
primitive[i] = (short) (buffer[i] & 0xFF);// the "byte" datatype is signed and is very annoying
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,370 @@
package org.schabi.newpipe.streams;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.text.ParseException;
import java.util.Locale;
import org.schabi.newpipe.streams.io.SharpStream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPathExpressionException;
/**
* @author kapodamy
*/
public class SubtitleConverter {
private static final String NEW_LINE = "\r\n";
public void dumpTTML(SharpStream in, final SharpStream out, final boolean ignoreEmptyFrames, final boolean detectYoutubeDuplicateLines
) throws IOException, ParseException, SAXException, ParserConfigurationException, XPathExpressionException {
final FrameWriter callback = new FrameWriter() {
int frameIndex = 0;
final Charset charset = Charset.forName("utf-8");
@Override
public void yield(SubtitleFrame frame) throws IOException {
if (ignoreEmptyFrames && frame.isEmptyText()) {
return;
}
out.write(String.valueOf(frameIndex++).getBytes(charset));
out.write(NEW_LINE.getBytes(charset));
out.write(getTime(frame.start, true).getBytes(charset));
out.write(" --> ".getBytes(charset));
out.write(getTime(frame.end, true).getBytes(charset));
out.write(NEW_LINE.getBytes(charset));
out.write(frame.text.getBytes(charset));
out.write(NEW_LINE.getBytes(charset));
out.write(NEW_LINE.getBytes(charset));
}
};
read_xml_based(in, callback, detectYoutubeDuplicateLines,
"tt", "xmlns", "http://www.w3.org/ns/ttml",
new String[]{"timedtext", "head", "wp"},
new String[]{"body", "div", "p"},
"begin", "end", true
);
}
private void read_xml_based(SharpStream source, FrameWriter callback, boolean detectYoutubeDuplicateLines,
String root, String formatAttr, String formatVersion, String[] cuePath, String[] framePath,
String timeAttr, String durationAttr, boolean hasTimestamp
) throws IOException, ParseException, SAXException, ParserConfigurationException, XPathExpressionException {
/*
* XML based subtitles parser with BASIC support
* multiple CUE is not supported
* styling is not supported
* tag timestamps (in auto-generated subtitles) are not supported, maybe in the future
* also TimestampTagOption enum is not applicable
* Language parsing is not supported
*/
byte[] buffer = new byte[source.available()];
source.read(buffer);
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
DocumentBuilder builder = factory.newDocumentBuilder();
Document xml = builder.parse(new ByteArrayInputStream(buffer));
String attr;
// get the format version or namespace
Element node = xml.getDocumentElement();
if (node == null) {
throw new ParseException("Can't get the format version. ¿wrong namespace?", -1);
} else if (!node.getNodeName().equals(root)) {
throw new ParseException("Invalid root", -1);
}
if (formatAttr.equals("xmlns")) {
if (!node.getNamespaceURI().equals(formatVersion)) {
throw new UnsupportedOperationException("Expected xml namespace: " + formatVersion);
}
} else {
attr = node.getAttributeNS(formatVersion, formatAttr);
if (attr == null) {
throw new ParseException("Can't get the format attribute", -1);
}
if (!attr.equals(formatVersion)) {
throw new ParseException("Invalid format version : " + attr, -1);
}
}
NodeList node_list;
int line_break = 0;// Maximum characters per line if present (valid for TranScript v3)
if (!hasTimestamp) {
node_list = selectNodes(xml, cuePath, formatVersion);
if (node_list != null) {
// if the subtitle has multiple CUEs, use the highest value
for (int i = 0; i < node_list.getLength(); i++) {
try {
int tmp = Integer.parseInt(((Element) node_list.item(i)).getAttributeNS(formatVersion, "ah"));
if (tmp > line_break) {
line_break = tmp;
}
} catch (Exception err) {
}
}
}
}
// parse every frame
node_list = selectNodes(xml, framePath, formatVersion);
if (node_list == null) {
return;// no frames detected
}
int fs_ff = -1;// first timestamp of first frame
boolean limit_lines = false;
for (int i = 0; i < node_list.getLength(); i++) {
Element elem = (Element) node_list.item(i);
SubtitleFrame obj = new SubtitleFrame();
obj.text = elem.getTextContent();
attr = elem.getAttribute(timeAttr);// ¡this cant be null!
obj.start = hasTimestamp ? parseTimestamp(attr) : Integer.parseInt(attr);
attr = elem.getAttribute(durationAttr);
if (obj.text == null || attr == null) {
continue;// normally is a blank line (on auto-generated subtitles) ignore
}
if (hasTimestamp) {
obj.end = parseTimestamp(attr);
if (detectYoutubeDuplicateLines) {
if (limit_lines) {
int swap = obj.end;
obj.end = fs_ff;
fs_ff = swap;
} else {
if (fs_ff < 0) {
fs_ff = obj.end;
} else {
if (fs_ff < obj.start) {
limit_lines = true;// the subtitles has duplicated lines
} else {
detectYoutubeDuplicateLines = false;
}
}
}
}
} else {
obj.end = obj.start + Integer.parseInt(attr);
}
if (/*node.getAttribute("w").equals("1") &&*/line_break > 1 && obj.text.length() > line_break) {
// implement auto line breaking (once)
StringBuilder text = new StringBuilder(obj.text);
obj.text = null;
switch (text.charAt(line_break)) {
case ' ':
case '\t':
putBreakAt(line_break, text);
break;
default:// find the word start position
for (int j = line_break - 1; j > 0; j--) {
switch (text.charAt(j)) {
case ' ':
case '\t':
putBreakAt(j, text);
j = -1;
break;
case '\r':
case '\n':
j = -1;// long word, just ignore
break;
}
}
break;
}
obj.text = text.toString();// set the processed text
}
callback.yield(obj);
}
}
private static NodeList selectNodes(Document xml, String[] path, String namespaceUri) throws XPathExpressionException {
Element ref = xml.getDocumentElement();
for (int i = 0; i < path.length - 1; i++) {
NodeList nodes = ref.getChildNodes();
if (nodes.getLength() < 1) {
return null;
}
Element elem;
for (int j = 0; j < nodes.getLength(); j++) {
if (nodes.item(j).getNodeType() == Node.ELEMENT_NODE) {
elem = (Element) nodes.item(j);
if (elem.getNodeName().equals(path[i]) && elem.getNamespaceURI().equals(namespaceUri)) {
ref = elem;
break;
}
}
}
}
return ref.getElementsByTagNameNS(namespaceUri, path[path.length - 1]);
}
private static int parseTimestamp(String multiImpl) throws NumberFormatException, ParseException {
if (multiImpl.length() < 1) {
return 0;
} else if (multiImpl.length() == 1) {
return Integer.parseInt(multiImpl) * 1000;// ¡this must be a number in seconds!
}
// detect wallclock-time
if (multiImpl.startsWith("wallclock(")) {
throw new UnsupportedOperationException("Parsing wallclock timestamp is not implemented");
}
// detect offset-time
if (multiImpl.indexOf(':') < 0) {
int multiplier = 1000;
char metric = multiImpl.charAt(multiImpl.length() - 1);
switch (metric) {
case 'h':
multiplier *= 3600000;
break;
case 'm':
multiplier *= 60000;
break;
case 's':
if (multiImpl.charAt(multiImpl.length() - 2) == 'm') {
multiplier = 1;// ms
}
break;
default:
if (!Character.isDigit(metric)) {
throw new NumberFormatException("Invalid metric suffix found on : " + multiImpl);
}
metric = '\0';
break;
}
try {
String offset_time = multiImpl;
if (multiplier == 1) {
offset_time = offset_time.substring(0, offset_time.length() - 2);
} else if (metric != '\0') {
offset_time = offset_time.substring(0, offset_time.length() - 1);
}
double time_metric_based = Double.parseDouble(offset_time);
if (Math.abs(time_metric_based) <= Double.MAX_VALUE) {
return (int) (time_metric_based * multiplier);
}
} catch (Exception err) {
throw new UnsupportedOperationException("Invalid or not implemented timestamp on: " + multiImpl);
}
}
// detect clock-time
int time = 0;
String[] units = multiImpl.split(":");
if (units.length < 3) {
throw new ParseException("Invalid clock-time timestamp", -1);
}
time += Integer.parseInt(units[0]) * 3600000;// hours
time += Integer.parseInt(units[1]) * 60000;//minutes
time += Float.parseFloat(units[2]) * 1000f;// seconds and milliseconds (if present)
// frames and sub-frames are ignored (not implemented)
// time += units[3] * fps;
return time;
}
private static void putBreakAt(int idx, StringBuilder str) {
// this should be optimized at compile time
if (NEW_LINE.length() > 1) {
str.delete(idx, idx + 1);// remove after replace
str.insert(idx, NEW_LINE);
} else {
str.setCharAt(idx, NEW_LINE.charAt(0));
}
}
private static String getTime(int time, boolean comma) {
// cast every value to integer to avoid auto-round in ToString("00").
StringBuilder str = new StringBuilder(12);
str.append(numberToString(time / 1000 / 3600, 2));// hours
str.append(':');
str.append(numberToString(time / 1000 / 60 % 60, 2));// minutes
str.append(':');
str.append(numberToString(time / 1000 % 60, 2));// seconds
str.append(comma ? ',' : '.');
str.append(numberToString(time % 1000, 3));// miliseconds
return str.toString();
}
private static String numberToString(int nro, int pad) {
return String.format(Locale.ENGLISH, "%0".concat(String.valueOf(pad)).concat("d"), nro);
}
/******************
* helper classes *
******************/
private interface FrameWriter {
void yield(SubtitleFrame frame) throws IOException;
}
private static class SubtitleFrame {
//Java no support unsigned int
public int end;
public int start;
public String text = "";
private boolean isEmptyText() {
if (text == null) {
return true;
}
for (int i = 0; i < text.length(); i++) {
switch (text.charAt(i)) {
case ' ':
case '\t':
case '\r':
case '\n':
break;
default:
return false;
}
}
return true;
}
}
}

View File

@ -0,0 +1,65 @@
package org.schabi.newpipe.streams;
import java.io.InputStream;
import java.io.IOException;
public class TrackDataChunk extends InputStream {
private final DataReader base;
private int size;
public TrackDataChunk(DataReader base, int size) {
this.base = base;
this.size = size;
}
@Override
public int read() throws IOException {
if (size < 1) {
return -1;
}
int res = base.read();
if (res >= 0) {
size--;
}
return res;
}
@Override
public int read(byte[] buffer) throws IOException {
return read(buffer, 0, buffer.length);
}
@Override
public int read(byte[] buffer, int offset, int count) throws IOException {
count = Math.min(size, count);
int read = base.read(buffer, offset, count);
size -= count;
return read;
}
@Override
public long skip(long amount) throws IOException {
long res = base.skipBytes(Math.min(amount, size));
size -= res;
return res;
}
@Override
public int available() {
return size;
}
@Override
public void close() {
size = 0;
}
@Override
public boolean markSupported() {
return false;
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,47 @@
package org.schabi.newpipe.streams.io;
import java.io.IOException;
/**
* based c#
*/
public abstract class SharpStream {
public abstract int read() throws IOException;
public abstract int read(byte buffer[]) throws IOException;
public abstract int read(byte buffer[], int offset, int count) throws IOException;
public abstract long skip(long amount) throws IOException;
public abstract int available();
public abstract void rewind() throws IOException;
public abstract void dispose();
public abstract boolean isDisposed();
public abstract boolean canRewind();
public abstract boolean canRead();
public abstract boolean canWrite();
public abstract void write(byte value) throws IOException;
public abstract void write(byte[] buffer) throws IOException;
public abstract void write(byte[] buffer, int offset, int count) throws IOException;
public abstract void flush() throws IOException;
public void setLength(long length) throws IOException {
throw new IOException("Not implemented");
}
}

View File

@ -25,6 +25,7 @@ import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
import android.content.res.ColorStateList;
import android.support.annotation.ColorInt;
import android.support.annotation.FloatRange;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.util.Log;
@ -363,4 +364,24 @@ public class AnimationUtils {
}).start();
}
}
public static void slideUp(final View view,
long duration,
long delay,
@FloatRange(from = 0.0f, to = 1.0f) float translationPercent) {
int translationY = (int) (view.getResources().getDisplayMetrics().heightPixels *
(translationPercent));
view.animate().setListener(null).cancel();
view.setAlpha(0f);
view.setTranslationY(translationY);
view.setVisibility(View.VISIBLE);
view.animate()
.alpha(1f)
.translationY(0)
.setStartDelay(delay)
.setDuration(duration)
.setInterpolator(new FastOutSlowInInterpolator())
.start();
}
}

View File

@ -69,8 +69,7 @@ public final class ExtractorHelper {
public static Single<SearchInfo> searchFor(final int serviceId,
final String searchString,
final List<String> contentFilter,
final String sortFilter,
final String contentCountry) {
final String sortFilter) {
checkServiceId(serviceId);
return Single.fromCallable(() ->
SearchInfo.getInfo(NewPipe.getService(serviceId),
@ -83,8 +82,7 @@ public final class ExtractorHelper {
final String searchString,
final List<String> contentFilter,
final String sortFilter,
final String pageUrl,
final String contentCountry) {
final String pageUrl) {
checkServiceId(serviceId);
return Single.fromCallable(() ->
SearchInfo.getMoreItems(NewPipe.getService(serviceId),
@ -96,8 +94,7 @@ public final class ExtractorHelper {
}
public static Single<List<String>> suggestionsFor(final int serviceId,
final String query,
final String contentCountry) {
final String query) {
checkServiceId(serviceId);
return Single.fromCallable(() ->
NewPipe.getService(serviceId)
@ -126,7 +123,7 @@ public final class ExtractorHelper {
final String nextStreamsUrl) {
checkServiceId(serviceId);
return Single.fromCallable(() ->
ChannelInfo.getMoreItems(NewPipe.getService(serviceId), url, nextStreamsUrl, NewPipe.getLocalization()));
ChannelInfo.getMoreItems(NewPipe.getService(serviceId), url, nextStreamsUrl));
}
public static Single<CommentsInfo> getCommentsInfo(final int serviceId,
@ -163,19 +160,17 @@ public final class ExtractorHelper {
public static Single<KioskInfo> getKioskInfo(final int serviceId,
final String url,
final String contentCountry,
boolean forceLoad) {
return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.PLAYLIST, Single.fromCallable(() ->
KioskInfo.getInfo(NewPipe.getService(serviceId), url, contentCountry)));
KioskInfo.getInfo(NewPipe.getService(serviceId), url)));
}
public static Single<InfoItemsPage> getMoreKioskItems(final int serviceId,
final String url,
final String nextStreamsUrl,
final String contentCountry) {
final String url,
final String nextStreamsUrl) {
return Single.fromCallable(() ->
KioskInfo.getMoreItems(NewPipe.getService(serviceId),
url, nextStreamsUrl, contentCountry));
url, nextStreamsUrl));
}
/*//////////////////////////////////////////////////////////////////////////

View File

@ -110,7 +110,8 @@ public final class ListHelper {
: context.getString(R.string.best_resolution_key);
String maxResolution = getResolutionLimit(context);
if (maxResolution != null && compareVideoStreamResolution(maxResolution, resolution) < 1){
if (maxResolution != null && (resolution.equals(context.getString(R.string.best_resolution_key))
|| compareVideoStreamResolution(maxResolution, resolution) < 1)) {
resolution = maxResolution;
}
return resolution;

View File

@ -69,10 +69,23 @@ public class Localization {
return stringBuilder.toString();
}
public static org.schabi.newpipe.extractor.utils.Localization getPreferredExtractorLocal(Context context) {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
String languageCode = sp.getString(context.getString(R.string.content_language_key),
context.getString(R.string.default_language_value));
String countryCode = sp.getString(context.getString(R.string.content_country_key),
context.getString(R.string.default_country_value));
return new org.schabi.newpipe.extractor.utils.Localization(countryCode, languageCode);
}
public static Locale getPreferredLocale(Context context) {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
String languageCode = sp.getString(context.getString(R.string.search_language_key), context.getString(R.string.default_language_value));
String languageCode = sp.getString(context.getString(R.string.content_language_key),
context.getString(R.string.default_language_value));
try {
if (languageCode.length() == 2) {

View File

@ -50,7 +50,6 @@ import org.schabi.newpipe.player.MainVideoPlayer;
import org.schabi.newpipe.player.PopupVideoPlayer;
import org.schabi.newpipe.player.PopupVideoPlayerActivity;
import org.schabi.newpipe.player.VideoPlayer;
import org.schabi.newpipe.player.old.PlayVideoActivity;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.settings.SettingsActivity;
@ -118,26 +117,6 @@ public class NavigationHelper {
context.startActivity(playerIntent);
}
public static void playOnOldVideoPlayer(Context context, StreamInfo info) {
ArrayList<VideoStream> videoStreamsList = new ArrayList<>(ListHelper.getSortedStreamVideosList(context, info.getVideoStreams(), null, false));
int index = ListHelper.getDefaultResolutionIndex(context, videoStreamsList);
if (index == -1) {
Toast.makeText(context, R.string.video_streams_empty, Toast.LENGTH_SHORT).show();
return;
}
VideoStream videoStream = videoStreamsList.get(index);
Intent intent = new Intent(context, PlayVideoActivity.class)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(PlayVideoActivity.VIDEO_TITLE, info.getName())
.putExtra(PlayVideoActivity.STREAM_URL, videoStream.getUrl())
.putExtra(PlayVideoActivity.VIDEO_URL, info.getUrl())
.putExtra(PlayVideoActivity.START_POSITION, info.getStartPosition());
context.startActivity(intent);
}
public static void playOnPopupPlayer(final Context context, final PlayQueue queue) {
if (!PermissionHelper.isPopupEnabled(context)) {
PermissionHelper.showPopupEnablementToast(context);

View File

@ -4,11 +4,15 @@ import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListInfo;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class RelatedStreamInfo extends ListInfo<InfoItem> {
private StreamInfoItem nextStream;
public RelatedStreamInfo(int serviceId, ListLinkHandler listUrlIdHandler, String name) {
super(serviceId, listUrlIdHandler, name);
@ -17,7 +21,21 @@ public class RelatedStreamInfo extends ListInfo<InfoItem> {
public static RelatedStreamInfo getInfo(StreamInfo info) {
ListLinkHandler handler = new ListLinkHandler(info.getOriginalUrl(), info.getUrl(), info.getId(), Collections.emptyList(), null);
RelatedStreamInfo relatedStreamInfo = new RelatedStreamInfo(info.getServiceId(), handler, info.getName());
relatedStreamInfo.setRelatedItems(info.getRelatedStreams());
return relatedStreamInfo;
List<InfoItem> streams = new ArrayList<>();
if(info.getNextVideo() != null){
streams.add(info.getNextVideo());
}
streams.addAll(info.getRelatedStreams());
relatedStreamInfo.setRelatedItems(streams);
relatedStreamInfo.setNextStream(info.getNextVideo());
return relatedStreamInfo;
}
public StreamInfoItem getNextStream() {
return nextStream;
}
public void setNextStream(StreamInfoItem nextStream) {
this.nextStream = nextStream;
}
}

View File

@ -0,0 +1,66 @@
package org.schabi.newpipe.util;
import android.support.annotation.NonNull;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.Stream;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
import java.util.List;
public class SecondaryStreamHelper<T extends Stream> {
private final int position;
private final StreamSizeWrapper<T> streams;
public SecondaryStreamHelper(StreamSizeWrapper<T> streams, T selectedStream) {
this.streams = streams;
this.position = streams.getStreamsList().indexOf(selectedStream);
if (this.position < 0) throw new RuntimeException("selected stream not found");
}
public T getStream() {
return streams.getStreamsList().get(position);
}
public long getSizeInBytes() {
return streams.getSizeInBytes(position);
}
/**
* find the correct audio stream for the desired video stream
*
* @param audioStreams list of audio streams
* @param videoStream desired video ONLY stream
* @return selected audio stream or null if a candidate was not found
*/
public static AudioStream getAudioStreamFor(@NonNull List<AudioStream> audioStreams, @NonNull VideoStream videoStream) {
// TODO: check if m4v and m4a selected streams are DASH compliant
switch (videoStream.getFormat()) {
case WEBM:
case MPEG_4:
break;
default:
return null;
}
boolean m4v = videoStream.getFormat() == MediaFormat.MPEG_4;
for (AudioStream audio : audioStreams) {
if (audio.getFormat() == (m4v ? MediaFormat.M4A : MediaFormat.WEBMA)) {
return audio;
}
}
// retry, but this time in reverse order
for (int i = audioStreams.size() - 1; i >= 0; i--) {
AudioStream audio = audioStreams.get(i);
if (audio.getFormat() == (m4v ? MediaFormat.MP3 : MediaFormat.OPUS)) {
return audio;
}
}
return null;
}
}

View File

@ -1,6 +1,7 @@
package org.schabi.newpipe.util;
import android.content.Context;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -13,6 +14,7 @@ import org.schabi.newpipe.Downloader;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.Stream;
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
import org.schabi.newpipe.extractor.stream.VideoStream;
import java.io.Serializable;
@ -28,26 +30,34 @@ import us.shandian.giga.util.Utility;
/**
* A list adapter for a list of {@link Stream streams}, currently supporting {@link VideoStream} and {@link AudioStream}.
*/
public class StreamItemAdapter<T extends Stream> extends BaseAdapter {
public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseAdapter {
private final Context context;
private final StreamSizeWrapper<T> streamsWrapper;
private final boolean showIconNoAudio;
private final SparseArray<SecondaryStreamHelper<U>> secondaryStreams;
public StreamItemAdapter(Context context, StreamSizeWrapper<T> streamsWrapper, boolean showIconNoAudio) {
public StreamItemAdapter(Context context, StreamSizeWrapper<T> streamsWrapper, SparseArray<SecondaryStreamHelper<U>> secondaryStreams) {
this.context = context;
this.streamsWrapper = streamsWrapper;
this.showIconNoAudio = showIconNoAudio;
this.secondaryStreams = secondaryStreams;
}
public StreamItemAdapter(Context context, StreamSizeWrapper<T> streamsWrapper, boolean showIconNoAudio) {
this(context, streamsWrapper, showIconNoAudio ? new SparseArray<>() : null);
}
public StreamItemAdapter(Context context, StreamSizeWrapper<T> streamsWrapper) {
this(context, streamsWrapper, false);
this(context, streamsWrapper, null);
}
public List<T> getAll() {
return streamsWrapper.getStreamsList();
}
public SparseArray<SecondaryStreamHelper<U>> getAllSecondary() {
return secondaryStreams;
}
@Override
public int getCount() {
return streamsWrapper.getStreamsList().size();
@ -89,29 +99,46 @@ public class StreamItemAdapter<T extends Stream> extends BaseAdapter {
String qualityString;
if (stream instanceof VideoStream) {
qualityString = ((VideoStream) stream).getResolution();
VideoStream videoStream = ((VideoStream) stream);
qualityString = videoStream.getResolution();
if (!showIconNoAudio) {
woSoundIconVisibility = View.GONE;
} else if (((VideoStream) stream).isVideoOnly()) {
woSoundIconVisibility = View.VISIBLE;
} else if (isDropdownItem) {
woSoundIconVisibility = View.INVISIBLE;
if (secondaryStreams != null) {
if (videoStream.isVideoOnly()) {
woSoundIconVisibility = secondaryStreams.get(position) == null ? View.VISIBLE : View.INVISIBLE;
} else if (isDropdownItem) {
woSoundIconVisibility = View.INVISIBLE;
}
}
} else if (stream instanceof AudioStream) {
qualityString = ((AudioStream) stream).getAverageBitrate() + "kbps";
} else if (stream instanceof SubtitlesStream) {
qualityString = ((SubtitlesStream) stream).getDisplayLanguageName();
if (((SubtitlesStream) stream).isAutoGenerated()) {
qualityString += " (" + context.getString(R.string.caption_auto_generated) + ")";
}
} else {
qualityString = stream.getFormat().getSuffix();
}
if (streamsWrapper.getSizeInBytes(position) > 0) {
sizeView.setText(streamsWrapper.getFormattedSize(position));
SecondaryStreamHelper secondary = secondaryStreams == null ? null : secondaryStreams.get(position);
if (secondary != null) {
long size = secondary.getSizeInBytes() + streamsWrapper.getSizeInBytes(position);
sizeView.setText(Utility.formatBytes(size));
} else {
sizeView.setText(streamsWrapper.getFormattedSize(position));
}
sizeView.setVisibility(View.VISIBLE);
} else {
sizeView.setVisibility(View.GONE);
}
formatNameView.setText(stream.getFormat().getName());
if (stream instanceof SubtitlesStream) {
formatNameView.setText(((SubtitlesStream) stream).getLanguageTag());
} else {
formatNameView.setText(stream.getFormat().getName());
}
qualityView.setText(qualityString);
woSoundIconView.setVisibility(woSoundIconVisibility);
@ -122,15 +149,17 @@ public class StreamItemAdapter<T extends Stream> extends BaseAdapter {
* A wrapper class that includes a way of storing the stream sizes.
*/
public static class StreamSizeWrapper<T extends Stream> implements Serializable {
private static final StreamSizeWrapper<Stream> EMPTY = new StreamSizeWrapper<>(Collections.emptyList());
private static final StreamSizeWrapper<Stream> EMPTY = new StreamSizeWrapper<>(Collections.emptyList(), null);
private final List<T> streamsList;
private final long[] streamSizes;
private final String unknownSize;
public StreamSizeWrapper(List<T> streamsList) {
public StreamSizeWrapper(List<T> streamsList, Context context) {
this.streamsList = streamsList;
this.streamSizes = new long[streamsList.size()];
this.unknownSize = context == null ? "--.-" : context.getString(R.string.unknown_content);
for (int i = 0; i < streamSizes.length; i++) streamSizes[i] = -1;
for (int i = 0; i < streamSizes.length; i++) streamSizes[i] = -2;
}
/**
@ -143,7 +172,7 @@ public class StreamItemAdapter<T extends Stream> extends BaseAdapter {
final Callable<Boolean> fetchAndSet = () -> {
boolean hasChanged = false;
for (X stream : streamsWrapper.getStreamsList()) {
if (streamsWrapper.getSizeInBytes(stream) > 0) {
if (streamsWrapper.getSizeInBytes(stream) > -2) {
continue;
}
@ -173,11 +202,18 @@ public class StreamItemAdapter<T extends Stream> extends BaseAdapter {
}
public String getFormattedSize(int streamIndex) {
return Utility.formatBytes(getSizeInBytes(streamIndex));
return formatSize(getSizeInBytes(streamIndex));
}
public String getFormattedSize(T stream) {
return Utility.formatBytes(getSizeInBytes(stream));
return formatSize(getSizeInBytes(stream));
}
private String formatSize(long size) {
if (size > -1) {
return Utility.formatBytes(size);
}
return unknownSize;
}
public void setSize(int streamIndex, long sizeInBytes) {
@ -193,4 +229,4 @@ public class StreamItemAdapter<T extends Stream> extends BaseAdapter {
return (StreamSizeWrapper<X>) EMPTY;
}
}
}
}

View File

@ -1,40 +0,0 @@
package us.shandian.giga.get;
import java.util.List;
/**
* Provides access to the storage of {@link DownloadMission}s
*/
public interface DownloadDataSource {
/**
* Load all missions
*
* @return a list of download missions
*/
List<DownloadMission> loadMissions();
/**
* Add a download mission to the storage
*
* @param downloadMission the download mission to add
* @return the identifier of the mission
*/
void addMission(DownloadMission downloadMission);
/**
* Update a download mission which exists in the storage
*
* @param downloadMission the download mission to update
* @throws IllegalArgumentException if the mission was not added to storage
*/
void updateMission(DownloadMission downloadMission);
/**
* Delete a download mission
*
* @param downloadMission the mission to delete
*/
void deleteMission(DownloadMission downloadMission);
}

View File

@ -0,0 +1,186 @@
package us.shandian.giga.get;
import android.support.annotation.NonNull;
import android.util.Log;
import java.io.File;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.nio.channels.ClosedByInterruptException;
import us.shandian.giga.util.Utility;
import static org.schabi.newpipe.BuildConfig.DEBUG;
public class DownloadInitializer extends Thread {
private final static String TAG = "DownloadInitializer";
final static int mId = 0;
private DownloadMission mMission;
private HttpURLConnection mConn;
DownloadInitializer(@NonNull DownloadMission mission) {
mMission = mission;
mConn = null;
}
@Override
public void run() {
if (mMission.current > 0) mMission.resetState();
int retryCount = 0;
while (true) {
try {
mMission.currentThreadCount = mMission.threadCount;
mConn = mMission.openConnection(mId, -1, -1);
mMission.establishConnection(mId, mConn);
if (!mMission.running || Thread.interrupted()) return;
mMission.length = Utility.getContentLength(mConn);
if (mMission.length == 0) {
mMission.notifyError(DownloadMission.ERROR_HTTP_NO_CONTENT, null);
return;
}
// check for dynamic generated content
if (mMission.length == -1 && mConn.getResponseCode() == 200) {
mMission.blocks = 0;
mMission.length = 0;
mMission.fallback = true;
mMission.unknownLength = true;
mMission.currentThreadCount = 1;
if (DEBUG) {
Log.d(TAG, "falling back (unknown length)");
}
} else {
// Open again
mConn = mMission.openConnection(mId, mMission.length - 10, mMission.length);
mMission.establishConnection(mId, mConn);
if (!mMission.running || Thread.interrupted()) return;
synchronized (mMission.blockState) {
if (mConn.getResponseCode() == 206) {
if (mMission.currentThreadCount > 1) {
mMission.blocks = mMission.length / DownloadMission.BLOCK_SIZE;
if (mMission.currentThreadCount > mMission.blocks) {
mMission.currentThreadCount = (int) mMission.blocks;
}
if (mMission.currentThreadCount <= 0) {
mMission.currentThreadCount = 1;
}
if (mMission.blocks * DownloadMission.BLOCK_SIZE < mMission.length) {
mMission.blocks++;
}
} else {
// if one thread is solicited don't calculate blocks, is useless
mMission.blocks = 1;
mMission.fallback = true;
mMission.unknownLength = false;
}
if (DEBUG) {
Log.d(TAG, "http response code = " + mConn.getResponseCode());
}
} else {
// Fallback to single thread
mMission.blocks = 0;
mMission.fallback = true;
mMission.unknownLength = false;
mMission.currentThreadCount = 1;
if (DEBUG) {
Log.d(TAG, "falling back due http response code = " + mConn.getResponseCode());
}
}
for (long i = 0; i < mMission.currentThreadCount; i++) {
mMission.threadBlockPositions.add(i);
mMission.threadBytePositions.add(0L);
}
}
if (!mMission.running || Thread.interrupted()) return;
}
File file;
if (mMission.current == 0) {
file = new File(mMission.location);
if (!Utility.mkdir(file, true)) {
mMission.notifyError(DownloadMission.ERROR_PATH_CREATION, null);
return;
}
file = new File(file, mMission.name);
// if the name is used by another process, delete it
if (file.exists() && !file.isFile() && !file.delete()) {
mMission.notifyError(DownloadMission.ERROR_FILE_CREATION, null);
return;
}
if (!file.exists() && !file.createNewFile()) {
mMission.notifyError(DownloadMission.ERROR_FILE_CREATION, null);
return;
}
} else {
file = new File(mMission.location, mMission.name);
}
RandomAccessFile af = new RandomAccessFile(file, "rw");
af.setLength(mMission.offsets[mMission.current] + mMission.length);
af.seek(mMission.offsets[mMission.current]);
af.close();
if (!mMission.running || Thread.interrupted()) return;
mMission.running = false;
break;
} catch (InterruptedIOException | ClosedByInterruptException e) {
return;
} catch (Exception e) {
if (!mMission.running) return;
if (e instanceof IOException && e.getMessage().contains("Permission denied")) {
mMission.notifyError(DownloadMission.ERROR_PERMISSION_DENIED, e);
return;
}
if (retryCount++ > mMission.maxRetry) {
Log.e(TAG, "initializer failed", e);
mMission.running = false;
mMission.notifyError(e);
return;
}
Log.e(TAG, "initializer failed, retrying", e);
}
}
// hide marquee in the progress bar
mMission.done++;
mMission.start();
}
@Override
public void interrupt() {
super.interrupt();
if (mConn != null) {
try {
mConn.disconnect();
} catch (Exception e) {
// nothing to do
}
}
}
}

View File

@ -1,53 +0,0 @@
package us.shandian.giga.get;
public interface DownloadManager {
int BLOCK_SIZE = 512 * 1024;
/**
* Start a new download mission
*
* @param url the url to download
* @param location the location
* @param name the name of the file to create
* @param isAudio true if the download is an audio file
* @param threads the number of threads maximal used to download chunks of the file. @return the identifier of the mission.
*/
int startMission(String url, String location, String name, boolean isAudio, int threads);
/**
* Resume the execution of a download mission.
*
* @param id the identifier of the mission to resume.
*/
void resumeMission(int id);
/**
* Pause the execution of a download mission.
*
* @param id the identifier of the mission to pause.
*/
void pauseMission(int id);
/**
* Deletes the mission from the downloaded list but keeps the downloaded file.
*
* @param id The mission identifier
*/
void deleteMission(int id);
/**
* Get the download mission by its identifier
*
* @param id the identifier of the download mission
* @return the download mission or null if the mission doesn't exist
*/
DownloadMission getMission(int id);
/**
* Get the number of download missions.
*
* @return the number of download missions.
*/
int getCount();
}

View File

@ -1,395 +0,0 @@
package us.shandian.giga.get;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import org.schabi.newpipe.download.ExtSDDownloadFailedActivity;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import us.shandian.giga.util.Utility;
import static org.schabi.newpipe.BuildConfig.DEBUG;
public class DownloadManagerImpl implements DownloadManager {
private static final String TAG = DownloadManagerImpl.class.getSimpleName();
private final DownloadDataSource mDownloadDataSource;
private final ArrayList<DownloadMission> mMissions = new ArrayList<>();
@NonNull
private final Context context;
/**
* Create a new instance
*
* @param searchLocations the directories to search for unfinished downloads
* @param downloadDataSource the data source for finished downloads
*/
public DownloadManagerImpl(Collection<String> searchLocations, DownloadDataSource downloadDataSource) {
mDownloadDataSource = downloadDataSource;
this.context = null;
loadMissions(searchLocations);
}
public DownloadManagerImpl(Collection<String> searchLocations, DownloadDataSource downloadDataSource, Context context) {
mDownloadDataSource = downloadDataSource;
this.context = context;
loadMissions(searchLocations);
}
@Override
public int startMission(String url, String location, String name, boolean isAudio, int threads) {
DownloadMission existingMission = getMissionByLocation(location, name);
if (existingMission != null) {
// Already downloaded or downloading
if (existingMission.finished) {
// Overwrite mission
deleteMission(mMissions.indexOf(existingMission));
} else {
// Rename file (?)
try {
name = generateUniqueName(location, name);
} catch (Exception e) {
Log.e(TAG, "Unable to generate unique name", e);
name = System.currentTimeMillis() + name;
Log.i(TAG, "Using " + name);
}
}
}
DownloadMission mission = new DownloadMission(name, url, location);
mission.timestamp = System.currentTimeMillis();
mission.threadCount = threads;
mission.addListener(new MissionListener(mission));
new Initializer(mission).start();
return insertMission(mission);
}
@Override
public void resumeMission(int i) {
DownloadMission d = getMission(i);
if (!d.running && d.errCode == -1) {
d.start();
}
}
@Override
public void pauseMission(int i) {
DownloadMission d = getMission(i);
if (d.running) {
d.pause();
}
}
@Override
public void deleteMission(int i) {
DownloadMission mission = getMission(i);
if (mission.finished) {
mDownloadDataSource.deleteMission(mission);
}
mission.delete();
mMissions.remove(i);
}
private void loadMissions(Iterable<String> searchLocations) {
mMissions.clear();
loadFinishedMissions();
for (String location : searchLocations) {
loadMissions(location);
}
}
/**
* Sort a list of mission by its timestamp. Oldest first
* @param missions the missions to sort
*/
static void sortByTimestamp(List<DownloadMission> missions) {
Collections.sort(missions, new Comparator<DownloadMission>() {
@Override
public int compare(DownloadMission o1, DownloadMission o2) {
return Long.compare(o1.timestamp, o2.timestamp);
}
});
}
/**
* Loads finished missions from the data source
*/
private void loadFinishedMissions() {
List<DownloadMission> finishedMissions = mDownloadDataSource.loadMissions();
if (finishedMissions == null) {
finishedMissions = new ArrayList<>();
}
// Ensure its sorted
sortByTimestamp(finishedMissions);
mMissions.ensureCapacity(mMissions.size() + finishedMissions.size());
for (DownloadMission mission : finishedMissions) {
File downloadedFile = mission.getDownloadedFile();
if (!downloadedFile.isFile()) {
if (DEBUG) {
Log.d(TAG, "downloaded file removed: " + downloadedFile.getAbsolutePath());
}
mDownloadDataSource.deleteMission(mission);
} else {
mission.length = downloadedFile.length();
mission.finished = true;
mission.running = false;
mMissions.add(mission);
}
}
}
private void loadMissions(String location) {
File f = new File(location);
if (f.exists() && f.isDirectory()) {
File[] subs = f.listFiles();
if (subs == null) {
Log.e(TAG, "listFiles() returned null");
return;
}
for (File sub : subs) {
if (sub.isFile() && sub.getName().endsWith(".giga")) {
DownloadMission mis = Utility.readFromFile(sub.getAbsolutePath());
if (mis != null) {
if (mis.finished) {
if (!sub.delete()) {
Log.w(TAG, "Unable to delete .giga file: " + sub.getPath());
}
continue;
}
mis.running = false;
mis.recovered = true;
insertMission(mis);
}
}
}
}
}
@Override
public DownloadMission getMission(int i) {
return mMissions.get(i);
}
@Override
public int getCount() {
return mMissions.size();
}
private int insertMission(DownloadMission mission) {
int i = -1;
DownloadMission m = null;
if (mMissions.size() > 0) {
do {
m = mMissions.get(++i);
} while (m.timestamp > mission.timestamp && i < mMissions.size() - 1);
//if (i > 0) i--;
} else {
i = 0;
}
mMissions.add(i, mission);
return i;
}
/**
* Get a mission by its location and name
*
* @param location the location
* @param name the name
* @return the mission or null if no such mission exists
*/
private
@Nullable
DownloadMission getMissionByLocation(String location, String name) {
for (DownloadMission mission : mMissions) {
if (location.equals(mission.location) && name.equals(mission.name)) {
return mission;
}
}
return null;
}
/**
* Splits the filename into name and extension
* <p>
* Dots are ignored if they appear: not at all, at the beginning of the file,
* at the end of the file
*
* @param name the name to split
* @return a string array with a length of 2 containing the name and the extension
*/
private static String[] splitName(String name) {
int dotIndex = name.lastIndexOf('.');
if (dotIndex <= 0 || (dotIndex == name.length() - 1)) {
return new String[]{name, ""};
} else {
return new String[]{name.substring(0, dotIndex), name.substring(dotIndex + 1)};
}
}
/**
* Generates a unique file name.
* <p>
* e.g. "myname (1).txt" if the name "myname.txt" exists.
*
* @param location the location (to check for existing files)
* @param name the name of the file
* @return the unique file name
* @throws IllegalArgumentException if the location is not a directory
* @throws SecurityException if the location is not readable
*/
private static String generateUniqueName(String location, String name) {
if (location == null) throw new NullPointerException("location is null");
if (name == null) throw new NullPointerException("name is null");
File destination = new File(location);
if (!destination.isDirectory()) {
throw new IllegalArgumentException("location is not a directory: " + location);
}
final String[] nameParts = splitName(name);
String[] existingName = destination.list(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.startsWith(nameParts[0]);
}
});
Arrays.sort(existingName);
String newName;
int downloadIndex = 0;
do {
newName = nameParts[0] + " (" + downloadIndex + ")." + nameParts[1];
++downloadIndex;
if (downloadIndex == 1000) { // Probably an error on our side
throw new RuntimeException("Too many existing files");
}
} while (Arrays.binarySearch(existingName, newName) >= 0);
return newName;
}
private class Initializer extends Thread {
private final DownloadMission mission;
private final Handler handler;
public Initializer(DownloadMission mission) {
this.mission = mission;
this.handler = new Handler();
}
@Override
public void run() {
try {
URL url = new URL(mission.url);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
mission.length = conn.getContentLength();
if (mission.length <= 0) {
mission.errCode = DownloadMission.ERROR_SERVER_UNSUPPORTED;
//mission.notifyError(DownloadMission.ERROR_SERVER_UNSUPPORTED);
return;
}
// Open again
conn = (HttpURLConnection) url.openConnection();
conn.setRequestProperty("Range", "bytes=" + (mission.length - 10) + "-" + mission.length);
if (conn.getResponseCode() != 206) {
// Fallback to single thread if no partial content support
mission.fallback = true;
if (DEBUG) {
Log.d(TAG, "falling back");
}
}
if (DEBUG) {
Log.d(TAG, "response = " + conn.getResponseCode());
}
mission.blocks = mission.length / BLOCK_SIZE;
if (mission.threadCount > mission.blocks) {
mission.threadCount = (int) mission.blocks;
}
if (mission.threadCount <= 0) {
mission.threadCount = 1;
}
if (mission.blocks * BLOCK_SIZE < mission.length) {
mission.blocks++;
}
new File(mission.location).mkdirs();
new File(mission.location + "/" + mission.name).createNewFile();
RandomAccessFile af = new RandomAccessFile(mission.location + "/" + mission.name, "rw");
af.setLength(mission.length);
af.close();
mission.start();
} catch (IOException ie) {
if(context == null) throw new RuntimeException(ie);
if(ie.getMessage().contains("Permission denied")) {
handler.post(() ->
context.startActivity(new Intent(context, ExtSDDownloadFailedActivity.class)));
} else throw new RuntimeException(ie);
} catch (Exception e) {
// TODO Notify
throw new RuntimeException(e);
}
}
}
/**
* Waits for mission to finish to add it to the {@link #mDownloadDataSource}
*/
private class MissionListener implements DownloadMission.MissionListener {
private final DownloadMission mMission;
private MissionListener(DownloadMission mission) {
if (mission == null) throw new NullPointerException("mission is null");
// Could the mission be passed in onFinish()?
mMission = mission;
}
@Override
public void onProgressUpdate(DownloadMission downloadMission, long done, long total) {
}
@Override
public void onFinish(DownloadMission downloadMission) {
mDownloadDataSource.addMission(mMission);
}
@Override
public void onError(DownloadMission downloadMission, int errCode) {
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -2,9 +2,11 @@ package us.shandian.giga.get;
import android.util.Log;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.channels.ClosedByInterruptException;
import static org.schabi.newpipe.BuildConfig.DEBUG;
@ -12,142 +14,166 @@ import static org.schabi.newpipe.BuildConfig.DEBUG;
* Runnable to download blocks of a file until the file is completely downloaded,
* an error occurs or the process is stopped.
*/
public class DownloadRunnable implements Runnable {
public class DownloadRunnable extends Thread {
private static final String TAG = DownloadRunnable.class.getSimpleName();
private final DownloadMission mMission;
private final int mId;
public DownloadRunnable(DownloadMission mission, int id) {
private HttpURLConnection mConn;
DownloadRunnable(DownloadMission mission, int id) {
if (mission == null) throw new NullPointerException("mission is null");
mMission = mission;
mId = id;
mConn = null;
}
@Override
public void run() {
boolean retry = mMission.recovered;
long position = mMission.getPosition(mId);
long blockPosition = mMission.getBlockPosition(mId);
int retryCount = 0;
if (DEBUG) {
Log.d(TAG, mId + ":default pos " + position);
Log.d(TAG, mId + ":default pos " + blockPosition);
Log.d(TAG, mId + ":recovered: " + mMission.recovered);
}
while (mMission.errCode == -1 && mMission.running && position < mMission.blocks) {
RandomAccessFile f;
InputStream is = null;
if (Thread.currentThread().isInterrupted()) {
mMission.pause();
return;
}
try {
f = new RandomAccessFile(mMission.getDownloadedFile(), "rw");
} catch (FileNotFoundException e) {
mMission.notifyError(e);// this never should happen
return;
}
while (mMission.running && mMission.errCode == DownloadMission.ERROR_NOTHING && blockPosition < mMission.blocks) {
if (DEBUG && retry) {
Log.d(TAG, mId + ":retry is true. Resuming at " + position);
Log.d(TAG, mId + ":retry is true. Resuming at " + blockPosition);
}
// Wait for an unblocked position
while (!retry && position < mMission.blocks && mMission.isBlockPreserved(position)) {
while (!retry && blockPosition < mMission.blocks && mMission.isBlockPreserved(blockPosition)) {
if (DEBUG) {
Log.d(TAG, mId + ":position " + position + " preserved, passing");
Log.d(TAG, mId + ":position " + blockPosition + " preserved, passing");
}
position++;
blockPosition++;
}
retry = false;
if (position >= mMission.blocks) {
if (blockPosition >= mMission.blocks) {
break;
}
if (DEBUG) {
Log.d(TAG, mId + ":preserving position " + position);
Log.d(TAG, mId + ":preserving position " + blockPosition);
}
mMission.preserveBlock(position);
mMission.setPosition(mId, position);
mMission.preserveBlock(blockPosition);
mMission.setBlockPosition(mId, blockPosition);
long start = position * DownloadManager.BLOCK_SIZE;
long end = start + DownloadManager.BLOCK_SIZE - 1;
long start = blockPosition * DownloadMission.BLOCK_SIZE;
long end = start + DownloadMission.BLOCK_SIZE - 1;
long offset = mMission.getThreadBytePosition(mId);
start += offset;
if (end >= mMission.length) {
end = mMission.length - 1;
}
HttpURLConnection conn = null;
int total = 0;
long total = 0;
try {
URL url = new URL(mMission.url);
conn = (HttpURLConnection) url.openConnection();
conn.setRequestProperty("Range", "bytes=" + start + "-" + end);
mConn = mMission.openConnection(mId, start, end);
mMission.establishConnection(mId, mConn);
if (DEBUG) {
Log.d(TAG, mId + ":" + conn.getRequestProperty("Range"));
Log.d(TAG, mId + ":Content-Length=" + conn.getContentLength() + " Code:" + conn.getResponseCode());
// check if the download can be resumed
if (mConn.getResponseCode() == 416 && offset > 0) {
retryCount--;
throw new DownloadMission.HttpError(416);
}
// A server may be ignoring the range request
if (conn.getResponseCode() != 206) {
mMission.errCode = DownloadMission.ERROR_SERVER_UNSUPPORTED;
notifyError();
// The server may be ignoring the range request
if (mConn.getResponseCode() != 206) {
mMission.notifyError(new DownloadMission.HttpError(mConn.getResponseCode()));
if (DEBUG) {
Log.e(TAG, mId + ":Unsupported " + conn.getResponseCode());
Log.e(TAG, mId + ":Unsupported " + mConn.getResponseCode());
}
break;
}
RandomAccessFile f = new RandomAccessFile(mMission.location + "/" + mMission.name, "rw");
f.seek(start);
java.io.InputStream ipt = conn.getInputStream();
byte[] buf = new byte[64*1024];
f.seek(mMission.offsets[mMission.current] + start);
while (start < end && mMission.running) {
int len = ipt.read(buf, 0, buf.length);
is = mConn.getInputStream();
if (len == -1) {
break;
} else {
start += len;
total += len;
f.write(buf, 0, len);
notifyProgress(len);
}
byte[] buf = new byte[DownloadMission.BUFFER_SIZE];
int len;
while (start < end && mMission.running && (len = is.read(buf, 0, buf.length)) != -1) {
f.write(buf, 0, len);
start += len;
total += len;
mMission.notifyProgress(len);
}
if (DEBUG && mMission.running) {
Log.d(TAG, mId + ":position " + position + " finished, total length " + total);
Log.d(TAG, mId + ":position " + blockPosition + " finished, " + total + " bytes downloaded");
}
f.close();
ipt.close();
if (mMission.running)
mMission.setThreadBytePosition(mId, 0L);// clear byte position for next block
else
mMission.setThreadBytePosition(mId, total);// download paused, save progress for this block
// TODO We should save progress for each thread
} catch (Exception e) {
// TODO Retry count limit & notify error
retry = true;
mMission.setThreadBytePosition(mId, total);
notifyProgress(-total);
if (!mMission.running || e instanceof ClosedByInterruptException) break;
if (retryCount++ >= mMission.maxRetry) {
mMission.notifyError(e);
break;
}
if (DEBUG) {
Log.d(TAG, mId + ":position " + position + " retrying", e);
Log.d(TAG, mId + ":position " + blockPosition + " retrying due exception", e);
}
retry = true;
}
}
try {
if (is != null) is.close();
} catch (Exception err) {
// nothing to do
}
try {
f.close();
} catch (Exception err) {
// ¿ejected media storage? ¿file deleted? ¿storage ran out of space?
}
if (DEBUG) {
Log.d(TAG, "thread " + mId + " exited main loop");
Log.d(TAG, "thread " + mId + " exited from main download loop");
}
if (mMission.errCode == -1 && mMission.running) {
if (mMission.errCode == DownloadMission.ERROR_NOTHING && mMission.running) {
if (DEBUG) {
Log.d(TAG, "no error has happened, notifying");
}
notifyFinished();
mMission.notifyFinished();
}
if (DEBUG && !mMission.running) {
@ -155,22 +181,15 @@ public class DownloadRunnable implements Runnable {
}
}
private void notifyProgress(final long len) {
synchronized (mMission) {
mMission.notifyProgress(len);
@Override
public void interrupt() {
super.interrupt();
try {
if (mConn != null) mConn.disconnect();
} catch (Exception e) {
// nothing to do
}
}
private void notifyError() {
synchronized (mMission) {
mMission.notifyError(DownloadMission.ERROR_SERVER_UNSUPPORTED);
mMission.pause();
}
}
private void notifyFinished() {
synchronized (mMission) {
mMission.notifyFinished();
}
}
}

View File

@ -1,74 +1,139 @@
package us.shandian.giga.get;
import java.io.BufferedInputStream;
import android.annotation.SuppressLint;
import android.support.annotation.NonNull;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.channels.ClosedByInterruptException;
import us.shandian.giga.util.Utility;
import static org.schabi.newpipe.BuildConfig.DEBUG;
/**
* Single-threaded fallback mode
*/
public class DownloadRunnableFallback extends Thread {
private static final String TAG = "DownloadRunnableFallback";
// Single-threaded fallback mode
public class DownloadRunnableFallback implements Runnable {
private final DownloadMission mMission;
//private int mId;
private final int mId = 1;
public DownloadRunnableFallback(DownloadMission mission) {
if (mission == null) throw new NullPointerException("mission is null");
//mId = id;
private int mRetryCount = 0;
private InputStream mIs;
private RandomAccessFile mF;
private HttpURLConnection mConn;
DownloadRunnableFallback(@NonNull DownloadMission mission) {
mMission = mission;
mIs = null;
mF = null;
mConn = null;
}
private void dispose() {
try {
if (mIs != null) mIs.close();
} catch (IOException e) {
// nothing to do
}
try {
if (mF != null) mF.close();
} catch (IOException e) {
// ¿ejected media storage? ¿file deleted? ¿storage ran out of space?
}
}
@Override
@SuppressLint("LongLogTag")
public void run() {
try {
URL url = new URL(mMission.url);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
boolean done;
if (conn.getResponseCode() != 200 && conn.getResponseCode() != 206) {
notifyError(DownloadMission.ERROR_SERVER_UNSUPPORTED);
} else {
RandomAccessFile f = new RandomAccessFile(mMission.location + "/" + mMission.name, "rw");
f.seek(0);
BufferedInputStream ipt = new BufferedInputStream(conn.getInputStream());
byte[] buf = new byte[512];
int len = 0;
long start = 0;
while ((len = ipt.read(buf, 0, 512)) != -1 && mMission.running) {
f.write(buf, 0, len);
notifyProgress(len);
if (Thread.interrupted()) {
break;
}
}
f.close();
ipt.close();
if (!mMission.unknownLength) {
start = mMission.getThreadBytePosition(0);
if (DEBUG && start > 0) {
Log.i(TAG, "Resuming a single-thread download at " + start);
}
}
try {
long rangeStart = (mMission.unknownLength || start < 1) ? -1 : start;
mConn = mMission.openConnection(mId, rangeStart, -1);
mMission.establishConnection(mId, mConn);
// check if the download can be resumed
if (mConn.getResponseCode() == 416 && start > 0) {
start = 0;
mRetryCount--;
throw new DownloadMission.HttpError(416);
}
// secondary check for the file length
if (!mMission.unknownLength)
mMission.unknownLength = Utility.getContentLength(mConn) == -1;
mF = new RandomAccessFile(mMission.getDownloadedFile(), "rw");
mF.seek(mMission.offsets[mMission.current] + start);
mIs = mConn.getInputStream();
byte[] buf = new byte[64 * 1024];
int len = 0;
while (mMission.running && (len = mIs.read(buf, 0, buf.length)) != -1) {
mF.write(buf, 0, len);
start += len;
mMission.notifyProgress(len);
}
// if thread goes interrupted check if the last part mIs written. This avoid re-download the whole file
done = len == -1;
} catch (Exception e) {
notifyError(DownloadMission.ERROR_UNKNOWN);
dispose();
// save position
mMission.setThreadBytePosition(0, start);
if (!mMission.running || e instanceof ClosedByInterruptException) return;
if (mRetryCount++ >= mMission.maxRetry) {
mMission.notifyError(e);
return;
}
run();// try again
return;
}
if (mMission.errCode == -1 && mMission.running) {
notifyFinished();
}
}
dispose();
private void notifyProgress(final long len) {
synchronized (mMission) {
mMission.notifyProgress(len);
}
}
private void notifyError(final int err) {
synchronized (mMission) {
mMission.notifyError(err);
mMission.pause();
}
}
private void notifyFinished() {
synchronized (mMission) {
if (done) {
mMission.notifyFinished();
} else {
mMission.setThreadBytePosition(0, start);
}
}
@Override
public void interrupt() {
super.interrupt();
if (mConn != null) {
try {
mConn.disconnect();
} catch (Exception e) {
// nothing to do
}
}
}
}

View File

@ -0,0 +1,16 @@
package us.shandian.giga.get;
public class FinishedMission extends Mission {
public FinishedMission() {
}
public FinishedMission(DownloadMission mission) {
source = mission.source;
length = mission.length;// ¿or mission.done?
timestamp = mission.timestamp;
name = mission.name;
location = mission.location;
kind = mission.kind;
}
}

View File

@ -0,0 +1,66 @@
package us.shandian.giga.get;
import java.io.File;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Calendar;
public abstract class Mission implements Serializable {
private static final long serialVersionUID = 0L;// last bump: 5 october 2018
/**
* Source url of the resource
*/
public String source;
/**
* Length of the current resource
*/
public long length;
/**
* creation timestamp (and maybe unique identifier)
*/
public long timestamp;
/**
* The filename
*/
public String name;
/**
* The directory to store the download
*/
public String location;
/**
* pre-defined content type
*/
public char kind;
/**
* get the target file on the storage
*
* @return File object
*/
public File getDownloadedFile() {
return new File(location, name);
}
public boolean delete() {
deleted = true;
return getDownloadedFile().delete();
}
/**
* Indicate if this mission is deleted whatever is stored
*/
public transient boolean deleted = false;
@Override
public String toString() {
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(timestamp);
return "[" + calendar.getTime().toString() + "] " + location + File.separator + name;
}
}

View File

@ -0,0 +1,73 @@
package us.shandian.giga.get.sqlite;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.get.FinishedMission;
import us.shandian.giga.get.Mission;
import static us.shandian.giga.get.sqlite.DownloadMissionHelper.KEY_LOCATION;
import static us.shandian.giga.get.sqlite.DownloadMissionHelper.KEY_NAME;
import static us.shandian.giga.get.sqlite.DownloadMissionHelper.MISSIONS_TABLE_NAME;
public class DownloadDataSource {
private static final String TAG = "DownloadDataSource";
private final DownloadMissionHelper downloadMissionHelper;
public DownloadDataSource(Context context) {
downloadMissionHelper = new DownloadMissionHelper(context);
}
public ArrayList<FinishedMission> loadFinishedMissions() {
SQLiteDatabase database = downloadMissionHelper.getReadableDatabase();
Cursor cursor = database.query(MISSIONS_TABLE_NAME, null, null,
null, null, null, DownloadMissionHelper.KEY_TIMESTAMP);
int count = cursor.getCount();
if (count == 0) return new ArrayList<>(1);
ArrayList<FinishedMission> result = new ArrayList<>(count);
while (cursor.moveToNext()) {
result.add(DownloadMissionHelper.getMissionFromCursor(cursor));
}
return result;
}
public void addMission(DownloadMission downloadMission) {
if (downloadMission == null) throw new NullPointerException("downloadMission is null");
SQLiteDatabase database = downloadMissionHelper.getWritableDatabase();
ContentValues values = DownloadMissionHelper.getValuesOfMission(downloadMission);
database.insert(MISSIONS_TABLE_NAME, null, values);
}
public void deleteMission(Mission downloadMission) {
if (downloadMission == null) throw new NullPointerException("downloadMission is null");
SQLiteDatabase database = downloadMissionHelper.getWritableDatabase();
database.delete(MISSIONS_TABLE_NAME,
KEY_LOCATION + " = ? AND " +
KEY_NAME + " = ?",
new String[]{downloadMission.location, downloadMission.name});
}
public void updateMission(DownloadMission downloadMission) {
if (downloadMission == null) throw new NullPointerException("downloadMission is null");
SQLiteDatabase database = downloadMissionHelper.getWritableDatabase();
ContentValues values = DownloadMissionHelper.getValuesOfMission(downloadMission);
String whereClause = KEY_LOCATION + " = ? AND " +
KEY_NAME + " = ?";
int rowsAffected = database.update(MISSIONS_TABLE_NAME, values,
whereClause, new String[]{downloadMission.location, downloadMission.name});
if (rowsAffected != 1) {
Log.e(TAG, "Expected 1 row to be affected by update but got " + rowsAffected);
}
}
}

View File

@ -7,19 +7,19 @@ import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.get.FinishedMission;
/**
* SqliteHelper to store {@link us.shandian.giga.get.DownloadMission}
* SQLiteHelper to store finished {@link us.shandian.giga.get.DownloadMission}'s
*/
public class DownloadMissionSQLiteHelper extends SQLiteOpenHelper {
public class DownloadMissionHelper extends SQLiteOpenHelper {
private final String TAG = "DownloadMissionHelper";
// TODO: use NewPipeSQLiteHelper ('s constants) when playlist branch is merged (?)
private static final String DATABASE_NAME = "downloads.db";
private static final int DATABASE_VERSION = 2;
private static final int DATABASE_VERSION = 3;
/**
* The table name of download missions
*/
@ -30,9 +30,9 @@ public class DownloadMissionSQLiteHelper extends SQLiteOpenHelper {
*/
static final String KEY_LOCATION = "location";
/**
* The key to the url of a mission
* The key to the urls of a mission
*/
static final String KEY_URL = "url";
static final String KEY_SOURCE_URL = "url";
/**
* The key to the name of a mission
*/
@ -45,6 +45,8 @@ public class DownloadMissionSQLiteHelper extends SQLiteOpenHelper {
static final String KEY_TIMESTAMP = "timestamp";
static final String KEY_KIND = "kind";
/**
* The statement to create the table
*/
@ -52,16 +54,28 @@ public class DownloadMissionSQLiteHelper extends SQLiteOpenHelper {
"CREATE TABLE " + MISSIONS_TABLE_NAME + " (" +
KEY_LOCATION + " TEXT NOT NULL, " +
KEY_NAME + " TEXT NOT NULL, " +
KEY_URL + " TEXT NOT NULL, " +
KEY_SOURCE_URL + " TEXT NOT NULL, " +
KEY_DONE + " INTEGER NOT NULL, " +
KEY_TIMESTAMP + " INTEGER NOT NULL, " +
KEY_KIND + " TEXT NOT NULL, " +
" UNIQUE(" + KEY_LOCATION + ", " + KEY_NAME + "));";
DownloadMissionSQLiteHelper(Context context) {
public DownloadMissionHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(MISSIONS_CREATE_TABLE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (oldVersion == 2) {
db.execSQL("ALTER TABLE " + MISSIONS_TABLE_NAME + " ADD COLUMN " + KEY_KIND + " TEXT;");
}
}
/**
* Returns all values of the download mission as ContentValues.
*
@ -70,34 +84,29 @@ public class DownloadMissionSQLiteHelper extends SQLiteOpenHelper {
*/
public static ContentValues getValuesOfMission(DownloadMission downloadMission) {
ContentValues values = new ContentValues();
values.put(KEY_URL, downloadMission.url);
values.put(KEY_SOURCE_URL, downloadMission.source);
values.put(KEY_LOCATION, downloadMission.location);
values.put(KEY_NAME, downloadMission.name);
values.put(KEY_DONE, downloadMission.done);
values.put(KEY_TIMESTAMP, downloadMission.timestamp);
values.put(KEY_KIND, String.valueOf(downloadMission.kind));
return values;
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(MISSIONS_CREATE_TABLE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// Currently nothing to do
}
public static DownloadMission getMissionFromCursor(Cursor cursor) {
public static FinishedMission getMissionFromCursor(Cursor cursor) {
if (cursor == null) throw new NullPointerException("cursor is null");
int pos;
String name = cursor.getString(cursor.getColumnIndexOrThrow(KEY_NAME));
String location = cursor.getString(cursor.getColumnIndexOrThrow(KEY_LOCATION));
String url = cursor.getString(cursor.getColumnIndexOrThrow(KEY_URL));
DownloadMission mission = new DownloadMission(name, url, location);
mission.done = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_DONE));
String kind = cursor.getString(cursor.getColumnIndex(KEY_KIND));
if (kind == null || kind.isEmpty()) kind = "?";
FinishedMission mission = new FinishedMission();
mission.name = cursor.getString(cursor.getColumnIndexOrThrow(KEY_NAME));
mission.location = cursor.getString(cursor.getColumnIndexOrThrow(KEY_LOCATION));
mission.source = cursor.getString(cursor.getColumnIndexOrThrow(KEY_SOURCE_URL));;
mission.length = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_DONE));
mission.timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_TIMESTAMP));
mission.finished = true;
mission.kind = kind.charAt(0);
return mission;
}
}

View File

@ -1,79 +0,0 @@
package us.shandian.giga.get.sqlite;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
import us.shandian.giga.get.DownloadDataSource;
import us.shandian.giga.get.DownloadMission;
import static us.shandian.giga.get.sqlite.DownloadMissionSQLiteHelper.KEY_LOCATION;
import static us.shandian.giga.get.sqlite.DownloadMissionSQLiteHelper.KEY_NAME;
import static us.shandian.giga.get.sqlite.DownloadMissionSQLiteHelper.MISSIONS_TABLE_NAME;
/**
* Non-thread-safe implementation of {@link DownloadDataSource}
*/
public class SQLiteDownloadDataSource implements DownloadDataSource {
private static final String TAG = "DownloadDataSourceImpl";
private final DownloadMissionSQLiteHelper downloadMissionSQLiteHelper;
public SQLiteDownloadDataSource(Context context) {
downloadMissionSQLiteHelper = new DownloadMissionSQLiteHelper(context);
}
@Override
public List<DownloadMission> loadMissions() {
ArrayList<DownloadMission> result;
SQLiteDatabase database = downloadMissionSQLiteHelper.getReadableDatabase();
Cursor cursor = database.query(MISSIONS_TABLE_NAME, null, null,
null, null, null, DownloadMissionSQLiteHelper.KEY_TIMESTAMP);
int count = cursor.getCount();
if (count == 0) return new ArrayList<>();
result = new ArrayList<>(count);
while (cursor.moveToNext()) {
result.add(DownloadMissionSQLiteHelper.getMissionFromCursor(cursor));
}
return result;
}
@Override
public void addMission(DownloadMission downloadMission) {
if (downloadMission == null) throw new NullPointerException("downloadMission is null");
SQLiteDatabase database = downloadMissionSQLiteHelper.getWritableDatabase();
ContentValues values = DownloadMissionSQLiteHelper.getValuesOfMission(downloadMission);
database.insert(MISSIONS_TABLE_NAME, null, values);
}
@Override
public void updateMission(DownloadMission downloadMission) {
if (downloadMission == null) throw new NullPointerException("downloadMission is null");
SQLiteDatabase database = downloadMissionSQLiteHelper.getWritableDatabase();
ContentValues values = DownloadMissionSQLiteHelper.getValuesOfMission(downloadMission);
String whereClause = KEY_LOCATION + " = ? AND " +
KEY_NAME + " = ?";
int rowsAffected = database.update(MISSIONS_TABLE_NAME, values,
whereClause, new String[]{downloadMission.location, downloadMission.name});
if (rowsAffected != 1) {
Log.e(TAG, "Expected 1 row to be affected by update but got " + rowsAffected);
}
}
@Override
public void deleteMission(DownloadMission downloadMission) {
if (downloadMission == null) throw new NullPointerException("downloadMission is null");
SQLiteDatabase database = downloadMissionSQLiteHelper.getWritableDatabase();
database.delete(MISSIONS_TABLE_NAME,
KEY_LOCATION + " = ? AND " +
KEY_NAME + " = ?",
new String[]{downloadMission.location, downloadMission.name});
}
}

View File

@ -0,0 +1,31 @@
package us.shandian.giga.postprocessing;
import org.schabi.newpipe.streams.Mp4DashWriter;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
import us.shandian.giga.get.DownloadMission;
/**
* @author kapodamy
*/
class Mp4DashMuxer extends Postprocessing {
Mp4DashMuxer(DownloadMission mission) {
super(mission);
recommendedReserve = 15360 * 1024;// 15 MiB
worksOnSameFile = true;
}
@Override
int process(SharpStream out, SharpStream... sources) throws IOException {
Mp4DashWriter muxer = new Mp4DashWriter(sources);
muxer.parseSources();
muxer.selectTracks(0, 0);
muxer.build(out);
return OK_RESULT;
}
}

View File

@ -0,0 +1,151 @@
package us.shandian.giga.postprocessing;
import android.os.Message;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.File;
import java.io.IOException;
import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.postprocessing.io.ChunkFileInputStream;
import us.shandian.giga.postprocessing.io.CircularFile;
import us.shandian.giga.service.DownloadManagerService;
public abstract class Postprocessing {
static final byte OK_RESULT = DownloadMission.ERROR_NOTHING;
public static final String ALGORITHM_TTML_CONVERTER = "ttml";
public static final String ALGORITHM_MP4_DASH_MUXER = "mp4D";
public static final String ALGORITHM_WEBM_MUXER = "webm";
private static final String ALGORITHM_TEST_ALGO = "test";
public static Postprocessing getAlgorithm(String algorithmName, DownloadMission mission) {
if (null == algorithmName) {
throw new NullPointerException("algorithmName");
} else switch (algorithmName) {
case ALGORITHM_TTML_CONVERTER:
return new TttmlConverter(mission);
case ALGORITHM_MP4_DASH_MUXER:
return new Mp4DashMuxer(mission);
case ALGORITHM_WEBM_MUXER:
return new WebMMuxer(mission);
case ALGORITHM_TEST_ALGO:
return new TestAlgo(mission);
/*case "example-algorithm":
return new ExampleAlgorithm(mission);*/
default:
throw new RuntimeException("Unimplemented post-processing algorithm: " + algorithmName);
}
}
/**
* Get a boolean value that indicate if the given algorithm work on the same
* file
*/
public boolean worksOnSameFile;
/**
* Get the recommended space to reserve for the given algorithm. The amount
* is in bytes
*/
public int recommendedReserve;
protected DownloadMission mission;
Postprocessing(DownloadMission mission) {
this.mission = mission;
}
public void run() throws IOException {
File file = mission.getDownloadedFile();
CircularFile out = null;
ChunkFileInputStream[] sources = new ChunkFileInputStream[mission.urls.length];
try {
int i = 0;
for (; i < sources.length - 1; i++) {
sources[i] = new ChunkFileInputStream(file, mission.offsets[i], mission.offsets[i + 1], "rw");
}
sources[i] = new ChunkFileInputStream(file, mission.offsets[i], mission.getDownloadedFile().length(), "rw");
int[] idx = {0};
CircularFile.OffsetChecker checker = () -> {
while (idx[0] < sources.length) {
/*
* WARNING: never use rewind() in any chunk after any writing (especially on first chunks)
* or the CircularFile can lead to unexpected results
*/
if (sources[idx[0]].isDisposed() || sources[idx[0]].available() < 1) {
idx[0]++;
continue;// the selected source is not used anymore
}
return sources[idx[0]].getFilePointer() - 1;
}
return -1;
};
out = new CircularFile(file, 0, this::progressReport, checker);
mission.done = 0;
mission.length = file.length();
int result = process(out, sources);
if (result == OK_RESULT) {
long finalLength = out.finalizeFile();
mission.done = finalLength;
mission.length = finalLength;
} else {
mission.errCode = DownloadMission.ERROR_UNKNOWN_EXCEPTION;
mission.errObject = new RuntimeException("post-processing algorithm returned " + result);
}
if (result != OK_RESULT && worksOnSameFile) {
//noinspection ResultOfMethodCallIgnored
new File(mission.location, mission.name).delete();
}
} finally {
for (SharpStream source : sources) {
if (source != null && !source.isDisposed()) {
source.dispose();
}
}
if (out != null) {
out.dispose();
}
}
}
/**
* Abstract method to execute the pos-processing algorithm
*
* @param out output stream
* @param sources files to be processed
* @return a error code, 0 means the operation was successful
* @throws IOException if an I/O error occurs.
*/
abstract int process(SharpStream out, SharpStream... sources) throws IOException;
String getArgumentAt(int index, String defaultValue) {
if (mission.postprocessingArgs == null || index >= mission.postprocessingArgs.length) {
return defaultValue;
}
return mission.postprocessingArgs[index];
}
private void progressReport(long done) {
mission.done = done;
if (mission.length < mission.done) mission.length = mission.done;
Message m = new Message();
m.what = DownloadManagerService.MESSAGE_PROGRESS;
m.obj = mission;
mission.mHandler.sendMessage(m);
}
}

View File

@ -0,0 +1,54 @@
package us.shandian.giga.postprocessing;
import android.util.Log;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
import java.util.Random;
import us.shandian.giga.get.DownloadMission;
/**
* Algorithm for testing proposes
*/
class TestAlgo extends Postprocessing {
public TestAlgo(DownloadMission mission) {
super(mission);
worksOnSameFile = true;
recommendedReserve = 4096 * 1024;// 4 KiB
}
@Override
int process(SharpStream out, SharpStream... sources) throws IOException {
int written = 0;
int size = 5 * 1024 * 1024;// 5 MiB
byte[] buffer = new byte[8 * 1024];//8 KiB
mission.length = size;
Random rnd = new Random();
// only write random data
sources[0].dispose();
while (written < size) {
rnd.nextBytes(buffer);
int read = Math.min(buffer.length, size - written);
out.write(buffer, 0, read);
try {
Thread.sleep((int) (Math.random() * 10));
} catch (InterruptedException e) {
return -1;
}
written += read;
}
return Postprocessing.OK_RESULT;
}
}

View File

@ -0,0 +1,76 @@
package us.shandian.giga.postprocessing;
import android.util.Log;
import org.schabi.newpipe.streams.io.SharpStream;
import org.schabi.newpipe.streams.SubtitleConverter;
import org.xml.sax.SAXException;
import java.io.IOException;
import java.text.ParseException;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPathExpressionException;
import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.postprocessing.io.SharpInputStream;
/**
* @author kapodamy
*/
class TttmlConverter extends Postprocessing {
private static final String TAG = "TttmlConverter";
TttmlConverter(DownloadMission mission) {
super(mission);
recommendedReserve = 0;// due how XmlPullParser works, the xml is fully loaded on the ram
worksOnSameFile = true;
}
@Override
int process(SharpStream out, SharpStream... sources) throws IOException {
// check if the subtitle is already in srt and copy, this should never happen
String format = getArgumentAt(0, null);
if (format == null || format.equals("ttml")) {
SubtitleConverter ttmlDumper = new SubtitleConverter();
try {
ttmlDumper.dumpTTML(
sources[0],
out,
getArgumentAt(1, "true").equals("true"),
getArgumentAt(2, "true").equals("true")
);
} catch (Exception err) {
Log.e(TAG, "subtitle parse failed", err);
if (err instanceof IOException) {
return 1;
} else if (err instanceof ParseException) {
return 2;
} else if (err instanceof SAXException) {
return 3;
} else if (err instanceof ParserConfigurationException) {
return 4;
} else if (err instanceof XPathExpressionException) {
return 7;
}
return 8;
}
return OK_RESULT;
} else if (format.equals("srt")) {
byte[] buffer = new byte[8 * 1024];
int read;
while ((read = sources[0].read(buffer)) > 0) {
out.write(buffer, 0, read);
}
return OK_RESULT;
}
throw new UnsupportedOperationException("Can't convert this subtitle, unimplemented format: " + format);
}
}

View File

@ -0,0 +1,44 @@
package us.shandian.giga.postprocessing;
import org.schabi.newpipe.streams.WebMReader.TrackKind;
import org.schabi.newpipe.streams.WebMReader.WebMTrack;
import org.schabi.newpipe.streams.WebMWriter;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
import us.shandian.giga.get.DownloadMission;
/**
* @author kapodamy
*/
class WebMMuxer extends Postprocessing {
WebMMuxer(DownloadMission mission) {
super(mission);
recommendedReserve = 2048 * 1024;// 2 MiB
worksOnSameFile = true;
}
@Override
int process(SharpStream out, SharpStream... sources) throws IOException {
WebMWriter muxer = new WebMWriter(sources);
muxer.parseSources();
// youtube uses a webm with a fake video track that acts as a "cover image"
WebMTrack[] tracks = muxer.getTracksFromSource(1);
int audioTrackIndex = 0;
for (int i = 0; i < tracks.length; i++) {
if (tracks[i].kind == TrackKind.Audio) {
audioTrackIndex = i;
break;
}
}
muxer.selectTracks(0, audioTrackIndex);
muxer.build(out);
return OK_RESULT;
}
}

View File

@ -0,0 +1,153 @@
package us.shandian.giga.postprocessing.io;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
public class ChunkFileInputStream extends SharpStream {
private RandomAccessFile source;
private final long offset;
private final long length;
private long position;
public ChunkFileInputStream(File file, long start, long end, String mode) throws IOException {
source = new RandomAccessFile(file, mode);
offset = start;
length = end - start;
position = 0;
if (length < 1) {
source.close();
throw new IOException("The chunk is empty or invalid");
}
if (source.length() < end) {
try {
throw new IOException(String.format("invalid file length. expected = %s found = %s", end, source.length()));
} finally {
source.close();
}
}
source.seek(offset);
}
/**
* Get absolute position on file
*
* @return the position
*/
public long getFilePointer() {
return offset + position;
}
@Override
public int read() throws IOException {
if ((position + 1) > length) {
return 0;
}
int res = source.read();
if (res >= 0) {
position++;
}
return res;
}
@Override
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
@Override
public int read(byte b[], int off, int len) throws IOException {
if ((position + len) > length) {
len = (int) (length - position);
}
if (len == 0) {
return 0;
}
int res = source.read(b, off, len);
position += res;
return res;
}
@Override
public long skip(long pos) throws IOException {
pos = Math.min(pos + position, length);
if (pos == 0) {
return 0;
}
source.seek(offset + pos);
long oldPos = position;
position = pos;
return pos - oldPos;
}
@Override
public int available() {
return (int) (length - position);
}
@SuppressWarnings("EmptyCatchBlock")
@Override
public void dispose() {
try {
source.close();
} catch (IOException err) {
} finally {
source = null;
}
}
@Override
public boolean isDisposed() {
return source == null;
}
@Override
public void rewind() throws IOException {
position = 0;
source.seek(offset);
}
@Override
public boolean canRewind() {
return true;
}
@Override
public boolean canRead() {
return true;
}
@Override
public boolean canWrite() {
return false;
}
@Override
public void write(byte value) {
}
@Override
public void write(byte[] buffer) {
}
@Override
public void write(byte[] buffer, int offset, int count) {
}
@Override
public void flush() {
}
}

View File

@ -0,0 +1,375 @@
package us.shandian.giga.postprocessing.io;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
public class CircularFile extends SharpStream {
private final static int AUX_BUFFER_SIZE = 1024 * 1024;// 1 MiB
private final static int AUX_BUFFER_SIZE2 = 512 * 1024;// 512 KiB
private final static int NOTIFY_BYTES_INTERVAL = 64 * 1024;// 64 KiB
private final static int QUEUE_BUFFER_SIZE = 8 * 1024;// 8 KiB
private final static boolean IMMEDIATE_AUX_BUFFER_FLUSH = false;
private RandomAccessFile out;
private long position;
private long maxLengthKnown = -1;
private ArrayList<ManagedBuffer> auxiliaryBuffers;
private OffsetChecker callback;
private ManagedBuffer queue;
private long startOffset;
private ProgressReport onProgress;
private long reportPosition;
public CircularFile(File file, long offset, ProgressReport progressReport, OffsetChecker checker) throws IOException {
if (checker == null) {
throw new NullPointerException("checker is null");
}
try {
queue = new ManagedBuffer(QUEUE_BUFFER_SIZE);
out = new RandomAccessFile(file, "rw");
out.seek(offset);
position = offset;
} catch (IOException err) {
try {
if (out != null) {
out.close();
}
} catch (IOException e) {
// nothing to do
}
throw err;
}
auxiliaryBuffers = new ArrayList<>(15);
callback = checker;
startOffset = offset;
reportPosition = offset;
onProgress = progressReport;
}
/**
* Close the file without flushing any buffer
*/
@Override
public void dispose() {
try {
auxiliaryBuffers = null;
if (out != null) {
out.close();
out = null;
}
} catch (IOException err) {
// nothing to do
}
}
/**
* Flush any buffer and close the output file. Use this method if the
* operation is successful
*
* @return the final length of the file
* @throws IOException if an I/O error occurs
*/
public long finalizeFile() throws IOException {
flushEverything();
if (maxLengthKnown > -1) {
position = maxLengthKnown;
}
if (position < out.length()) {
out.setLength(position);
}
dispose();
return position;
}
@Override
public void write(byte b) throws IOException {
write(new byte[]{b}, 0, 1);
}
@Override
public void write(byte b[]) throws IOException {
write(b, 0, b.length);
}
@Override
public void write(byte b[], int off, int len) throws IOException {
if (len == 0) {
return;
}
long end = callback.check();
long available;
if (end == -1) {
available = Long.MAX_VALUE;
} else {
if (end < startOffset) {
throw new IOException("The reported offset is invalid. reported offset is " + String.valueOf(end));
}
available = end - position;
}
// Check if possible flush one or more auxiliary buffer
if (auxiliaryBuffers.size() > 0) {
ManagedBuffer aux = auxiliaryBuffers.get(0);
// check if there is enough space to flush it completely
while (available >= (aux.size + queue.size)) {
available -= aux.size;
writeQueue(aux.buffer, 0, aux.size);
aux.dereference();
auxiliaryBuffers.remove(0);
if (auxiliaryBuffers.size() < 1) {
aux = null;
break;
}
aux = auxiliaryBuffers.get(0);
}
if (IMMEDIATE_AUX_BUFFER_FLUSH) {
// try partial flush to avoid allocate another auxiliary buffer
if (aux != null && aux.available() < len && available > queue.size) {
int size = Math.min(aux.size, (int) available - queue.size);
writeQueue(aux.buffer, 0, size);
aux.dereference(size);
available -= size;
}
}
}
if (auxiliaryBuffers.size() < 1 && available > (len + queue.size)) {
writeQueue(b, off, len);
} else {
int i = auxiliaryBuffers.size() - 1;
while (len > 0) {
if (i < 0) {
// allocate a new auxiliary buffer
auxiliaryBuffers.add(new ManagedBuffer(AUX_BUFFER_SIZE));
i++;
}
ManagedBuffer aux = auxiliaryBuffers.get(i);
available = aux.available();
if (available < 1) {
// secondary auxiliary buffer
available = len;
aux = new ManagedBuffer(Math.max(len, AUX_BUFFER_SIZE2));
auxiliaryBuffers.add(aux);
i++;
} else {
available = Math.min(len, available);
}
aux.write(b, off, (int) available);
len -= available;
if (len > 0) off += available;
}
}
}
private void writeOutside(byte buffer[], int offset, int length) throws IOException {
out.write(buffer, offset, length);
position += length;
if (onProgress != null && position > reportPosition) {
reportPosition = position + NOTIFY_BYTES_INTERVAL;
onProgress.report(position);
}
}
private void writeQueue(byte[] buffer, int offset, int length) throws IOException {
while (length > 0) {
if (queue.available() < length) {
flushQueue();
if (length >= queue.buffer.length) {
writeOutside(buffer, offset, length);
return;
}
}
int size = Math.min(queue.available(), length);
queue.write(buffer, offset, size);
offset += size;
length -= size;
}
if (queue.size >= queue.buffer.length) {
flushQueue();
}
}
private void flushQueue() throws IOException {
writeOutside(queue.buffer, 0, queue.size);
queue.size = 0;
}
private void flushEverything() throws IOException {
flushQueue();
if (auxiliaryBuffers.size() > 0) {
for (ManagedBuffer aux : auxiliaryBuffers) {
writeOutside(aux.buffer, 0, aux.size);
aux.dereference();
}
auxiliaryBuffers.clear();
}
}
/**
* Flush any buffer directly to the file. Warning: use this method ONLY if
* all read dependencies are disposed
*
* @throws IOException if the dependencies are not disposed
*/
@Override
public void flush() throws IOException {
if (callback.check() != -1) {
throw new IOException("All read dependencies of this file must be disposed first");
}
flushEverything();
// Save the current file length in case the method {@code rewind()} is called
if (position > maxLengthKnown) {
maxLengthKnown = position;
}
}
@Override
public void rewind() throws IOException {
flush();
out.seek(startOffset);
if (onProgress != null) {
onProgress.report(-position);
}
position = startOffset;
reportPosition = startOffset;
}
@Override
public long skip(long amount) throws IOException {
flush();
position += amount;
out.seek(position);
return amount;
}
@Override
public boolean isDisposed() {
return out == null;
}
@Override
public boolean canRewind() {
return true;
}
@Override
public boolean canWrite() {
return true;
}
//<editor-fold defaultState="collapsed" desc="stub read methods">
@Override
public boolean canRead() {
return false;
}
@Override
public int read() {
throw new UnsupportedOperationException("write-only");
}
@Override
public int read(byte[] buffer) {
throw new UnsupportedOperationException("write-only");
}
@Override
public int read(byte[] buffer, int offset, int count) {
throw new UnsupportedOperationException("write-only");
}
@Override
public int available() {
throw new UnsupportedOperationException("write-only");
}
//</editor-fold>
public interface OffsetChecker {
/**
* Checks the amount of available space ahead
*
* @return absolute offset in the file where no more data SHOULD NOT be
* written. If the value is -1 the whole file will be used
*/
long check();
}
public interface ProgressReport {
void report(long progress);
}
class ManagedBuffer {
byte[] buffer;
int size;
ManagedBuffer(int length) {
buffer = new byte[length];
}
void dereference() {
buffer = null;
size = 0;
}
void dereference(int amount) {
if (amount > size) {
throw new IndexOutOfBoundsException("Invalid dereference amount (" + amount + ">=" + size + ")");
}
size -= amount;
System.arraycopy(buffer, amount, buffer, 0, size);
}
protected int available() {
return buffer.length - size;
}
private void write(byte[] b, int off, int len) {
System.arraycopy(b, off, buffer, size, len);
size += len;
}
@Override
public String toString() {
return "holding: " + String.valueOf(size) + " length: " + String.valueOf(buffer.length) + " available: " + String.valueOf(available());
}
}
}

View File

@ -0,0 +1,126 @@
package us.shandian.giga.postprocessing.io;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
/**
* @author kapodamy
*/
public class FileStream extends SharpStream {
public enum Mode {
Read,
ReadWrite
}
public RandomAccessFile source;
private final Mode mode;
public FileStream(String path, Mode mode) throws IOException {
String flags;
if (mode == Mode.Read) {
flags = "r";
} else {
flags = "rw";
}
this.mode = mode;
source = new RandomAccessFile(path, flags);
}
@Override
public int read() throws IOException {
return source.read();
}
@Override
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
@Override
public int read(byte b[], int off, int len) throws IOException {
return source.read(b, off, len);
}
@Override
public long skip(long pos) throws IOException {
FileChannel fc = source.getChannel();
fc.position(fc.position() + pos);
return pos;
}
@Override
public int available() {
try {
return (int) (source.length() - source.getFilePointer());
} catch (IOException ex) {
return 0;
}
}
@SuppressWarnings("EmptyCatchBlock")
@Override
public void dispose() {
try {
source.close();
} catch (IOException err) {
} finally {
source = null;
}
}
@Override
public boolean isDisposed() {
return source == null;
}
@Override
public void rewind() throws IOException {
source.getChannel().position(0);
}
@Override
public boolean canRewind() {
return true;
}
@Override
public boolean canRead() {
return mode == Mode.Read || mode == Mode.ReadWrite;
}
@Override
public boolean canWrite() {
return mode == Mode.ReadWrite;
}
@Override
public void write(byte value) throws IOException {
source.write(value);
}
@Override
public void write(byte[] buffer) throws IOException {
source.write(buffer);
}
@Override
public void write(byte[] buffer, int offset, int count) throws IOException {
source.write(buffer, offset, count);
}
@Override
public void flush() {
}
@Override
public void setLength(long length) throws IOException {
source.setLength(length);
}
}

View File

@ -0,0 +1,59 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package us.shandian.giga.postprocessing.io;
import android.support.annotation.NonNull;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
import java.io.InputStream;
/**
* Wrapper for the classic {@link java.io.InputStream}
* @author kapodamy
*/
public class SharpInputStream extends InputStream {
private final SharpStream base;
public SharpInputStream(SharpStream base) throws IOException {
if (!base.canRead()) {
throw new IOException("The provided stream is not readable");
}
this.base = base;
}
@Override
public int read() throws IOException {
return base.read();
}
@Override
public int read(@NonNull byte[] bytes) throws IOException {
return base.read(bytes);
}
@Override
public int read(@NonNull byte[] bytes, int i, int i1) throws IOException {
return base.read(bytes, i, i1);
}
@Override
public long skip(long l) throws IOException {
return base.skip(l);
}
@Override
public int available() {
return base.available();
}
@Override
public void close() {
base.dispose();
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,177 @@
package us.shandian.giga.ui.common;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.support.design.widget.Snackbar;
import android.view.View;
import org.schabi.newpipe.R;
import java.util.ArrayList;
import us.shandian.giga.get.FinishedMission;
import us.shandian.giga.get.Mission;
import us.shandian.giga.service.DownloadManager;
import us.shandian.giga.service.DownloadManager.MissionIterator;
import us.shandian.giga.ui.adapter.MissionAdapter;
public class Deleter {
private static final int TIMEOUT = 5000;// ms
private static final int DELAY = 350;// ms
private static final int DELAY_RESUME = 400;// ms
private static final String BUNDLE_NAMES = "us.shandian.giga.ui.common.deleter.names";
private static final String BUNDLE_LOCATIONS = "us.shandian.giga.ui.common.deleter.locations";
private Snackbar snackbar;
private ArrayList<Mission> items;
private boolean running = true;
private Context mContext;
private MissionAdapter mAdapter;
private DownloadManager mDownloadManager;
private MissionIterator mIterator;
private Handler mHandler;
private View mView;
private final Runnable rShow;
private final Runnable rNext;
private final Runnable rCommit;
public Deleter(Bundle b, View v, Context c, MissionAdapter a, DownloadManager d, MissionIterator i, Handler h) {
mView = v;
mContext = c;
mAdapter = a;
mDownloadManager = d;
mIterator = i;
mHandler = h;
// use variables to know the reference of the lambdas
rShow = this::show;
rNext = this::next;
rCommit = this::commit;
items = new ArrayList<>(2);
if (b != null) {
String[] names = b.getStringArray(BUNDLE_NAMES);
String[] locations = b.getStringArray(BUNDLE_LOCATIONS);
if (names == null || locations == null) return;
if (names.length < 1 || locations.length < 1) return;
if (names.length != locations.length) return;
items.ensureCapacity(names.length);
for (int j = 0; j < locations.length; j++) {
Mission mission = mDownloadManager.getAnyMission(locations[j], names[j]);
if (mission == null) continue;
items.add(mission);
mIterator.hide(mission);
}
if (items.size() > 0) resume();
}
}
public void append(Mission item) {
mIterator.hide(item);
items.add(0, item);
show();
}
private void forget() {
mIterator.unHide(items.remove(0));
mAdapter.applyChanges();
show();
}
private void show() {
if (items.size() < 1) return;
pause();
running = true;
mHandler.postDelayed(rNext, DELAY);
}
private void next() {
if (items.size() < 1) return;
String msg = mContext.getString(R.string.file_deleted).concat(":\n").concat(items.get(0).name);
snackbar = Snackbar.make(mView, msg, Snackbar.LENGTH_INDEFINITE);
snackbar.setAction(R.string.undo, s -> forget());
snackbar.setActionTextColor(Color.YELLOW);
snackbar.show();
mHandler.postDelayed(rCommit, TIMEOUT);
}
private void commit() {
if (items.size() < 1) return;
while (items.size() > 0) {
Mission mission = items.remove(0);
if (mission.deleted) continue;
mIterator.unHide(mission);
mDownloadManager.deleteMission(mission);
if (mission instanceof FinishedMission) {
mContext.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(mission.getDownloadedFile())));
}
break;
}
if (items.size() < 1) {
pause();
return;
}
show();
}
private void pause() {
running = false;
mHandler.removeCallbacks(rNext);
mHandler.removeCallbacks(rShow);
mHandler.removeCallbacks(rCommit);
if (snackbar != null) snackbar.dismiss();
}
public void resume() {
if (running) return;
mHandler.postDelayed(rShow, DELAY_RESUME);
}
public void dispose(Bundle bundle) {
if (items.size() < 1) return;
pause();
if (bundle == null) {
for (Mission mission : items) mDownloadManager.deleteMission(mission);
items = null;
return;
}
String[] names = new String[items.size()];
String[] locations = new String[items.size()];
for (int i = 0; i < items.size(); i++) {
Mission mission = items.get(i);
names[i] = mission.name;
locations[i] = mission.location;
}
bundle.putStringArray(BUNDLE_NAMES, names);
bundle.putStringArray(BUNDLE_LOCATIONS, locations);
}
}

View File

@ -1,25 +1,36 @@
package us.shandian.giga.ui.common;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.annotation.ColorRes;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
public class ProgressDrawable extends Drawable {
private float mProgress;
private final int mBackgroundColor;
private final int mForegroundColor;
private static final int MARQUEE_INTERVAL = 150;
public ProgressDrawable(Context context, @ColorRes int background, @ColorRes int foreground) {
this(ContextCompat.getColor(context, background), ContextCompat.getColor(context, foreground));
private float mProgress;
private int mBackgroundColor, mForegroundColor;
private Handler mMarqueeHandler;
private float mMarqueeProgress;
private Path mMarqueeLine;
private int mMarqueeSize;
private long mMarqueeNext;
public ProgressDrawable() {
mMarqueeLine = null;// marquee disabled
mMarqueeProgress = 0f;
mMarqueeSize = 0;
mMarqueeNext = 0;
}
public ProgressDrawable(int background, int foreground) {
public void setColors(@ColorInt int background, @ColorInt int foreground) {
mBackgroundColor = background;
mForegroundColor = foreground;
}
@ -29,10 +40,20 @@ public class ProgressDrawable extends Drawable {
invalidateSelf();
}
public void setMarquee(boolean marquee) {
if (marquee == (mMarqueeLine != null)) {
return;
}
mMarqueeLine = marquee ? new Path() : null;
mMarqueeHandler = marquee ? new Handler(Looper.getMainLooper()) : null;
mMarqueeSize = 0;
mMarqueeNext = 0;
}
@Override
public void draw(@NonNull Canvas canvas) {
int width = canvas.getWidth();
int height = canvas.getHeight();
int width = getBounds().width();
int height = getBounds().height();
Paint paint = new Paint();
@ -40,6 +61,42 @@ public class ProgressDrawable extends Drawable {
canvas.drawRect(0, 0, width, height, paint);
paint.setColor(mForegroundColor);
if (mMarqueeLine != null) {
if (mMarqueeSize < 1) setupMarquee(width, height);
int size = mMarqueeSize;
Paint paint2 = new Paint();
paint2.setColor(mForegroundColor);
paint2.setStrokeWidth(size);
paint2.setStyle(Paint.Style.STROKE);
size *= 2;
if (mMarqueeProgress >= size) {
mMarqueeProgress = 1;
} else {
mMarqueeProgress++;
}
// render marquee
width += size * 2;
Path marquee = new Path();
for (float i = -size; i < width; i += size) {
marquee.addPath(mMarqueeLine, i + mMarqueeProgress, 0);
}
marquee.close();
canvas.drawPath(marquee, paint2);// draw marquee
if (System.currentTimeMillis() >= mMarqueeNext) {
// program next update
mMarqueeNext = System.currentTimeMillis() + MARQUEE_INTERVAL;
mMarqueeHandler.postDelayed(this::invalidateSelf, MARQUEE_INTERVAL);
}
return;
}
canvas.drawRect(0, 0, (int) (mProgress * width), height, paint);
}
@ -58,4 +115,17 @@ public class ProgressDrawable extends Drawable {
return PixelFormat.OPAQUE;
}
@Override
public void onBoundsChange(Rect rect) {
if (mMarqueeLine != null) setupMarquee(rect.width(), rect.height());
}
private void setupMarquee(int width, int height) {
mMarqueeSize = (int) ((width * 10f) / 100f);// the size is 10% of the width
mMarqueeLine.rewind();
mMarqueeLine.moveTo(-mMarqueeSize, -mMarqueeSize);
mMarqueeLine.lineTo(-mMarqueeSize * 4, height + mMarqueeSize);
mMarqueeLine.close();
}
}

View File

@ -1,12 +0,0 @@
package us.shandian.giga.ui.fragment;
import us.shandian.giga.get.DownloadManager;
import us.shandian.giga.service.DownloadManagerService;
public class AllMissionsFragment extends MissionsFragment {
@Override
protected DownloadManager setupDownloadManager(DownloadManagerService.DMBinder binder) {
return binder.getDownloadManager();
}
}

View File

@ -10,50 +10,58 @@ import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import org.schabi.newpipe.R;
import org.schabi.newpipe.download.DeleteDownloadManager;
import io.reactivex.disposables.Disposable;
import us.shandian.giga.get.DownloadManager;
import us.shandian.giga.service.DownloadManager;
import us.shandian.giga.service.DownloadManagerService;
import us.shandian.giga.service.DownloadManagerService.DMBinder;
import us.shandian.giga.ui.adapter.MissionAdapter;
public abstract class MissionsFragment extends Fragment {
private DownloadManager mDownloadManager;
private DownloadManagerService.DMBinder mBinder;
public class MissionsFragment extends Fragment {
private static final int SPAN_SIZE = 2;
private SharedPreferences mPrefs;
private boolean mLinear;
private MenuItem mSwitch;
private MenuItem mClear = null;
private RecyclerView mList;
private View mEmpty;
private MissionAdapter mAdapter;
private GridLayoutManager mGridManager;
private LinearLayoutManager mLinearManager;
private Context mActivity;
private DeleteDownloadManager mDeleteDownloadManager;
private Disposable mDeleteDisposable;
private final ServiceConnection mConnection = new ServiceConnection() {
private DMBinder mBinder;
private Bundle mBundle;
private boolean mForceUpdate;
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder binder) {
mBinder = (DownloadManagerService.DMBinder) binder;
mDownloadManager = setupDownloadManager(mBinder);
if (mDeleteDownloadManager != null) {
mDeleteDownloadManager.setDownloadManager(mDownloadManager);
updateList();
}
mBinder.clearDownloadNotifications();
mAdapter = new MissionAdapter(mActivity, mBinder.getDownloadManager(), mClear, mEmpty);
mAdapter.deleterLoad(mBundle, getView());
mBundle = null;
mBinder.addMissionEventListener(mAdapter.getMessenger());
mBinder.enableNotifications(false);
updateList();
}
@Override
@ -64,14 +72,6 @@ public abstract class MissionsFragment extends Fragment {
};
public void setDeleteManager(@NonNull DeleteDownloadManager deleteDownloadManager) {
mDeleteDownloadManager = deleteDownloadManager;
if (mDownloadManager != null) {
mDeleteDownloadManager.setDownloadManager(mDownloadManager);
updateList();
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.missions, container, false);
@ -79,18 +79,32 @@ public abstract class MissionsFragment extends Fragment {
mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
mLinear = mPrefs.getBoolean("linear", false);
mActivity = getActivity();
mBundle = savedInstanceState;
// Bind the service
Intent i = new Intent();
i.setClass(getActivity(), DownloadManagerService.class);
getActivity().bindService(i, mConnection, Context.BIND_AUTO_CREATE);
mActivity.bindService(new Intent(mActivity, DownloadManagerService.class), mConnection, Context.BIND_AUTO_CREATE);
// Views
mEmpty = v.findViewById(R.id.list_empty_view);
mList = v.findViewById(R.id.mission_recycler);
// Init
mGridManager = new GridLayoutManager(getActivity(), 2);
mGridManager = new GridLayoutManager(getActivity(), SPAN_SIZE);
mGridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
switch (mAdapter.getItemViewType(position)) {
case DownloadManager.SPECIAL_PENDING:
case DownloadManager.SPECIAL_FINISHED:
return SPAN_SIZE;
default:
return 1;
}
}
});
mLinearManager = new LinearLayoutManager(getActivity());
mList.setLayoutManager(mGridManager);
setHasOptionsMenu(true);
@ -121,63 +135,98 @@ public abstract class MissionsFragment extends Fragment {
mActivity = activity;
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (mDeleteDownloadManager != null) {
mDeleteDisposable = mDeleteDownloadManager.getUndoObservable().subscribe(mission -> {
if (mAdapter != null) {
mAdapter.updateItemList();
mAdapter.notifyDataSetChanged();
}
});
}
public void onDestroy() {
super.onDestroy();
if (mBinder == null || mAdapter == null) return;
mBinder.removeMissionEventListener(mAdapter.getMessenger());
mBinder.enableNotifications(true);
mActivity.unbindService(mConnection);
mAdapter.deleterDispose(null);
mBinder = null;
mAdapter = null;
}
@Override
public void onDestroyView() {
super.onDestroyView();
getActivity().unbindService(mConnection);
if (mDeleteDisposable != null) {
mDeleteDisposable.dispose();
}
public void onPrepareOptionsMenu(Menu menu) {
mSwitch = menu.findItem(R.id.switch_mode);
mClear = menu.findItem(R.id.clear_list);
if (mAdapter != null) mAdapter.setClearButton(mClear);
super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
return super.onOptionsItemSelected(item);
/*switch (item.getItemId()) {
switch (item.getItemId()) {
case R.id.switch_mode:
mLinear = !mLinear;
updateList();
return true;
default:
return super.onOptionsItemSelected(item);
}*/
}
public void notifyChange() {
mAdapter.notifyDataSetChanged();
mLinear = !mLinear;
updateList();
return true;
case R.id.clear_list:
mAdapter.clearFinishedDownloads();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private void updateList() {
mAdapter = new MissionAdapter((Activity) mActivity, mBinder, mDownloadManager, mDeleteDownloadManager, mLinear);
if (mLinear) {
mList.setLayoutManager(mLinearManager);
} else {
mList.setLayoutManager(mGridManager);
}
// destroy all created views in the recycler
mList.setAdapter(null);
mAdapter.notifyDataSetChanged();
// re-attach the adapter in grid/lineal mode
mAdapter.setLinear(mLinear);
mList.setAdapter(mAdapter);
if (mSwitch != null) {
mSwitch.setIcon(mLinear ? R.drawable.grid : R.drawable.list);
mSwitch.setTitle(mLinear ? R.string.grid : R.string.list);
mPrefs.edit().putBoolean("linear", mLinear).apply();
}
mPrefs.edit().putBoolean("linear", mLinear).apply();
}
protected abstract DownloadManager setupDownloadManager(DownloadManagerService.DMBinder binder);
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (mAdapter != null) {
mAdapter.deleterDispose(outState);
mForceUpdate = true;
mBinder.removeMissionEventListener(mAdapter.getMessenger());
}
}
@Override
public void onResume() {
super.onResume();
if (mAdapter != null) {
mAdapter.deleterResume();
if (mForceUpdate) {
mForceUpdate = false;
mAdapter.forceUpdate();
}
mBinder.addMissionEventListener(mAdapter.getMessenger());
}
if (mBinder != null) mBinder.enableNotifications(false);
}
@Override
public void onPause() {
super.onPause();
if (mAdapter != null) mAdapter.onPaused();
if (mBinder != null) mBinder.enableNotifications(true);
}
}

View File

@ -3,15 +3,18 @@ package us.shandian.giga.util;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.support.annotation.ColorRes;
import android.os.Build;
import android.support.annotation.ColorInt;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.widget.Toast;
import org.schabi.newpipe.R;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
@ -19,14 +22,17 @@ import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.net.HttpURLConnection;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Locale;
public class Utility {
public enum FileType {
VIDEO,
MUSIC,
SUBTITLE,
UNKNOWN
}
@ -34,11 +40,11 @@ public class Utility {
if (bytes < 1024) {
return String.format("%d B", bytes);
} else if (bytes < 1024 * 1024) {
return String.format("%.2f kB", (float) bytes / 1024);
return String.format("%.2f kB", bytes / 1024d);
} else if (bytes < 1024 * 1024 * 1024) {
return String.format("%.2f MB", (float) bytes / 1024 / 1024);
return String.format("%.2f MB", bytes / 1024d / 1024d);
} else {
return String.format("%.2f GB", (float) bytes / 1024 / 1024 / 1024);
return String.format("%.2f GB", bytes / 1024d / 1024d / 1024d);
}
}
@ -54,41 +60,32 @@ public class Utility {
}
}
public static void writeToFile(@NonNull String fileName, @NonNull Serializable serializable) {
ObjectOutputStream objectOutputStream = null;
public static void writeToFile(@NonNull File file, @NonNull Serializable serializable) {
try {
objectOutputStream = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(fileName)));
try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(file)))) {
objectOutputStream.writeObject(serializable);
} catch (Exception e) {
//nothing to do
} finally {
if(objectOutputStream != null) {
try {
objectOutputStream.close();
} catch (Exception e) {
//nothing to do
}
}
}
//nothing to do
}
@Nullable
@SuppressWarnings("unchecked")
public static <T> T readFromFile(String file) {
T object = null;
public static <T> T readFromFile(File file) {
T object;
ObjectInputStream objectInputStream = null;
try {
objectInputStream = new ObjectInputStream(new FileInputStream(file));
object = (T) objectInputStream.readObject();
} catch (Exception e) {
//nothing to do
object = null;
}
if(objectInputStream != null){
if (objectInputStream != null) {
try {
objectInputStream .close();
objectInputStream.close();
} catch (Exception e) {
//nothing to do
}
@ -119,39 +116,68 @@ public class Utility {
}
}
public static FileType getFileType(String file) {
if (file.endsWith(".mp3") || file.endsWith(".wav") || file.endsWith(".flac") || file.endsWith(".m4a")) {
public static FileType getFileType(char kind, String file) {
switch (kind) {
case 'v':
return FileType.VIDEO;
case 'a':
return FileType.MUSIC;
case 's':
return FileType.SUBTITLE;
//default '?':
}
if (file.endsWith(".srt") || file.endsWith(".vtt") || file.endsWith(".ssa")) {
return FileType.SUBTITLE;
} else if (file.endsWith(".mp3") || file.endsWith(".wav") || file.endsWith(".flac") || file.endsWith(".m4a") || file.endsWith(".opus")) {
return FileType.MUSIC;
} else if (file.endsWith(".mp4") || file.endsWith(".mpeg") || file.endsWith(".rm") || file.endsWith(".rmvb")
|| file.endsWith(".flv") || file.endsWith(".webp") || file.endsWith(".webm")) {
return FileType.VIDEO;
} else {
return FileType.UNKNOWN;
}
return FileType.UNKNOWN;
}
@ColorRes
public static int getBackgroundForFileType(FileType type) {
@ColorInt
public static int getBackgroundForFileType(Context ctx, FileType type) {
int colorRes;
switch (type) {
case MUSIC:
return R.color.audio_left_to_load_color;
colorRes = R.color.audio_left_to_load_color;
break;
case VIDEO:
return R.color.video_left_to_load_color;
colorRes = R.color.video_left_to_load_color;
break;
case SUBTITLE:
colorRes = R.color.subtitle_left_to_load_color;
break;
default:
return R.color.gray;
colorRes = R.color.gray;
}
return ContextCompat.getColor(ctx, colorRes);
}
@ColorRes
public static int getForegroundForFileType(FileType type) {
@ColorInt
public static int getForegroundForFileType(Context ctx, FileType type) {
int colorRes;
switch (type) {
case MUSIC:
return R.color.audio_already_load_color;
colorRes = R.color.audio_already_load_color;
break;
case VIDEO:
return R.color.video_already_load_color;
colorRes = R.color.video_already_load_color;
break;
case SUBTITLE:
colorRes = R.color.subtitle_already_load_color;
break;
default:
return R.color.gray;
colorRes = R.color.gray;
break;
}
return ContextCompat.getColor(ctx, colorRes);
}
@DrawableRes
@ -161,6 +187,8 @@ public class Utility {
return R.drawable.music;
case VIDEO:
return R.drawable.video;
case SUBTITLE:
return R.drawable.subtitle;
default:
return R.drawable.video;
}
@ -168,12 +196,18 @@ public class Utility {
public static void copyToClipboard(Context context, String str) {
ClipboardManager cm = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
if (cm == null) {
Toast.makeText(context, R.string.permission_denied, Toast.LENGTH_LONG).show();
return;
}
cm.setPrimaryClip(ClipData.newPlainText("text", str));
Toast.makeText(context, R.string.msg_copied, Toast.LENGTH_SHORT).show();
}
public static String checksum(String path, String algorithm) {
MessageDigest md = null;
MessageDigest md;
try {
md = MessageDigest.getInstance(algorithm);
@ -181,7 +215,7 @@ public class Utility {
throw new RuntimeException(e);
}
FileInputStream i = null;
FileInputStream i;
try {
i = new FileInputStream(path);
@ -190,14 +224,14 @@ public class Utility {
}
byte[] buf = new byte[1024];
int len = 0;
int len;
try {
while ((len = i.read(buf)) != -1) {
md.update(buf, 0, len);
}
} catch (IOException ignored) {
} catch (IOException e) {
// nothing to do
}
byte[] digest = md.digest();
@ -211,4 +245,31 @@ public class Utility {
return sb.toString();
}
@SuppressWarnings("ResultOfMethodCallIgnored")
public static boolean mkdir(File path, boolean allDirs) {
if (path.exists()) return true;
if (allDirs)
path.mkdirs();
else
path.mkdir();
return path.exists();
}
public static long getContentLength(HttpURLConnection connection) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return connection.getContentLengthLong();
}
try {
long length = Long.parseLong(connection.getHeaderField("Content-Length"));
if (length >= 0) return length;
} catch (Exception err) {
// nothing to do
}
return -1;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape
android:innerRadius="0dp"
android:shape="ring"
android:thickness="4dp"
android:useLevel="false">
<solid android:color="@android:color/darker_gray"/>
</shape>
</item>
</layer-list>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape
android:innerRadius="0dp"
android:shape="ring"
android:thickness="6dp"
android:useLevel="false">
<solid android:color="@android:color/darker_gray"/>
</shape>
</item>
</layer-list>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/selected_dot"
android:state_selected="true"/>
<item android:drawable="@drawable/default_dot"/>
</selector>

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