diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 873c1780f..f61e320c9 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -12,57 +12,53 @@ add a comment to it. You'll see exactly what is sent, the system is 100% transpa ## Issue reporting/feature requests * Search the [existing issues](https://github.com/TeamNewPipe/NewPipe/issues) first to make sure your issue/feature -hasn't been reported/requested before -* Check whether your issue/feature is already fixed/implemented -* Check if the issue still exists in the latest release/beta version -* If you are an Android/Java developer, you are always welcome to fix/implement an issue/a feature yourself. PRs welcome! +hasn't been reported/requested before. +* Check whether your issue/feature is already fixed/implemented. +* Check if the issue still exists in the latest release/beta version. +* If you are an Android/Java developer, you are always welcome to fix an issue or implement a feature yourself. PRs welcome! * We use English for development. Issues in other languages will be closed and ignored. * Please only add *one* issue at a time. Do not put multiple issues into one thread. -* When reporting a bug please give us a context, and a description how to reproduce it. -* Issues that only contain a generated bug report, but no description might be closed. +* Follow the template! Issues or feature requests not matching the template might be closed. ## Bug Fixing * If you want to help NewPipe to become free of bugs (this is our utopic goal for NewPipe), you can send us an email to -tnp@newpipe.schabi.org to let me know that you intend to help. We'll send you further instructions. You may, on request, -register at our [Sentry](https://sentry.schabi.org) instance (see section "Crash reporting" for more information. +tnp@newpipe.schabi.org to let us know that you intend to help. We'll send you further instructions. You may, on request, +register at our [Sentry](https://sentry.schabi.org) instance (see section "Crash reporting" for more information). ## Translation -* NewPipe can be translated via [Weblate](https://hosted.weblate.org/projects/newpipe/strings/). You can log in there +* NewPipe is translated via [Weblate](https://hosted.weblate.org/projects/newpipe/strings/). You can log in there with your GitHub account. +* If the language you want to translate is not on Weblate, you can add it: see [How to add a new language](https://github.com/TeamNewPipe/NewPipe/wiki/How-to-add-a-new-language-to-NewPipe) in the wiki. ## Code contribution -* Stick to NewPipe's style conventions (well, just look the other code and then do it the same way :)) -* Do not bring non-free software (e.g., binary blobs) into the project. Also, make sure you do not introduce Google +* Stick to NewPipe's style conventions: follow [checkStyle](https://github.com/checkstyle/checkstyle). It will run each time you build the project. +* Do not bring non-free software (e.g. binary blobs) into the project. Also, make sure you do not introduce Google libraries. -* Stick to [F-Droid contribution guidelines](https://f-droid.org/wiki/page/Inclusion_Policy) -* Make changes on a separate branch, not on the master branch. This is commonly known as *feature branch workflow*. You - may then send your changes as a pull request on GitHub. Patches to the email address mentioned in this document might - not be considered, GitHub is the primary platform. (This only affects you if you are a member of TeamNewPipe) +* Stick to [F-Droid contribution guidelines](https://f-droid.org/wiki/page/Inclusion_Policy). +* Make changes on a separate branch with a meaningful name, not on the master neither dev branch. This is commonly known as *feature branch workflow*. You + may then send your changes as a pull request (PR) on GitHub. * When submitting changes, you confirm that your code is licensed under the terms of the [GNU General Public License v3](https://www.gnu.org/licenses/gpl-3.0.html). * Please test (compile and run) your code before you submit changes! Ideally, provide test feedback in the PR description. Untested code will **not** be merged! * Try to figure out yourself why builds on our CI fail. * Make sure your PR is up-to-date with the rest of the code. Often, a simple click on "Update branch" will do the job, - but if not, you are asked to merge the master branch manually and resolve the problems on your own. That will make the + but if not, you are asked to rebase the dev branch manually and resolve the problems on your own. You can find help [on the wiki](https://github.com/TeamNewPipe/NewPipe/wiki/How-to-merge-a-PR). That will make the maintainers' jobs way easier. * Please show intention to maintain your features and code after you contributed it. Unmaintained code is a hassle for the core developers, and just adds work. If you do not intend to maintain features you contributed, please think again about submission, or clearly state that in the description of your PR. * Respond yourselves if someone requests changes or otherwise raises issues about your PRs. -* Check if your contributions align with the [fdroid inclusion guidelines](https://f-droid.org/en/docs/Inclusion_Policy/). -* Check if your submission can be build with the current fdroid build server setup. * Send PR that only cover one specific issue/solution/bug. Do not send PRs that are huge and consists of multiple independent solutions. ## Communication -* WE DO NOW HAVE A MAILING LIST: [newpipe@list.schabi.org](https://list.schabi.org/cgi-bin/mailman/listinfo/newpipe). * There is an IRC channel on Freenode which is regularly visited by the core team and other developers: [#newpipe](irc:irc.freenode.net/newpipe). [Click here for Webchat](https://webchat.freenode.net/?channels=newpipe)! * If you want to get in touch with the core team or one of our other contributors you can send an email to - tnp(at)schabi.org. Please do not send issue reports, they will be ignored and remain unanswered! Use the GitHub issue + tnp@newpipe.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! +* Feel free to post suggestions, changes, ideas etc. on GitHub or IRC! diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 8073503ad..000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,3 +0,0 @@ -- [ ] I carefully read the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md) and agree to them. -- [ ] I checked if the issue/feature exists in the latest version. -- [ ] I did use the [incredible bugreport to markdown converter](https://teamnewpipe.github.io/CrashReportToMarkdown/) to paste bug reports. diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..dbc1c05a5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,46 @@ +--- +name: Bug report +about: Create a bug report to help us improve +labels: bug +assignees: '' + +--- + + + + + +### Version + +- + +### Steps to reproduce the bug + + + + +### Expected behavior + + +### Actual behaviour + + +### Screenshots/Screen recordings + + +### Logs + + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..90134a204 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,39 @@ +--- +name: Feature request +about: Suggest an idea for this project +labels: enhancement +assignees: '' + +--- + + + + +#### Describe the feature you want + + + + +#### Is your feature request related to a problem? Please describe it + + + + +#### Additional context + + + + +#### How will you/everyone benefit from this feature? + + + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d0e58680a..f12eb2fe8 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1 +1,28 @@ + + +#### What is it? +- [ ] Bug fix (user facing) +- [ ] Feature (user facing) +- [ ] Code base improvement (dev facing) +- [ ] Meta improvement to the project (dev facing) + +#### Description of the changes in your PR + +- record videos +- create clones +- take over the world + +#### Fixes the following issue(s) + +- + +#### Relies on the following changes + +- + +#### Testing apk + +debug.zip + +#### Agreement - [ ] I carefully read the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md) and agree to them. diff --git a/.gitignore b/.gitignore index f4f47c5ee..5c6962be1 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,11 @@ *~ .weblate *.class + +# vscode / eclipse files +*.classpath +*.project +*.settings +bin/ +.vscode/ +*.code-workspace diff --git a/.travis.yml b/.travis.yml index d6f97ab55..1714c70d5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,13 +5,13 @@ android: components: # The BuildTools version used by NewPipe - tools - - build-tools-28.0.3 + - build-tools-29.0.3 # The SDK version used to compile NewPipe - - android-28 + - android-29 before_install: - - yes | sdkmanager "platforms;android-28" + - yes | sdkmanager "platforms;android-29" script: ./gradlew -Dorg.gradle.jvmargs=-Xmx1536m assembleDebug lintDebug testDebugUnitTest licenses: diff --git a/README.md b/README.md index f78725338..50eb40594 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,16 @@

- + - +


ScreenshotsDescriptionFeaturesUpdatesContributionDonateLicense

-

WebsiteBlogPress

+

WebsiteBlogFAQPress


WARNING: THIS IS A BETA VERSION, THEREFORE YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE VIA OUR GITHUB REPOSITORY. diff --git a/app/.gitignore b/app/.gitignore index d9a86a57c..53edac5e4 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1,3 +1,3 @@ .gitignore /build -app.iml +*.iml diff --git a/app/build.gradle b/app/build.gradle index 424ed6211..7fe72f1f1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,33 +2,57 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' +apply plugin: 'checkstyle' android { - compileSdkVersion 28 - buildToolsVersion '28.0.3' + compileSdkVersion 29 + buildToolsVersion '29.0.3' defaultConfig { applicationId "org.schabi.newpipe" + resValue "string", "app_name", "NewPipe" minSdkVersion 19 - targetSdkVersion 28 - versionCode 800 - versionName "0.18.0" + targetSdkVersion 29 + versionCode 950 + versionName "0.19.5" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true + + javaCompileOptions { + annotationProcessorOptions { + arguments = ["room.schemaLocation": "$projectDir/schemas".toString()] + } + } } buildTypes { + debug { + multiDexEnabled true + debuggable true + + // suffix the app id and the app name with git branch name + def workingBranch = getGitWorkingBranch() + def normalizedWorkingBranch = workingBranch.replaceAll("[^A-Za-z]+", "").toLowerCase() + if (normalizedWorkingBranch.isEmpty() || workingBranch == "master" || workingBranch == "dev") { + // default values when branch name could not be determined or is master or dev + applicationIdSuffix ".debug" + resValue "string", "app_name", "NewPipe Debug" + } else { + applicationIdSuffix ".debug." + normalizedWorkingBranch + resValue "string", "app_name", "NewPipe " + workingBranch + archivesBaseName = 'NewPipe_' + normalizedWorkingBranch + } + } + + // Keep the release build type at the end of the list to override 'archivesBaseName' of + // debug build. This seems to be a Gradle bug, therefore + // TODO: update Gradle version release { minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - - debug { - multiDexEnabled true - debuggable true - applicationIdSuffix ".debug" + archivesBaseName = 'app' } } @@ -42,70 +66,161 @@ android { compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 + encoding 'utf-8' + } + + // Required and used only by groupie + androidExtensions { + experimental = true + } + + sourceSets { + androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) } } ext { - androidxLibVersion = '1.0.0' - exoPlayerLibVersion = '2.10.8' - roomDbLibVersion = '2.1.0' - leakCanaryLibVersion = '1.5.4' //1.6.1 - okHttpLibVersion = '3.12.6' - icepickLibVersion = '3.2.0' - stethoLibVersion = '1.5.0' + icepickVersion = '3.2.0' + checkstyleVersion = '8.32' + stethoVersion = '1.5.1' + leakCanaryVersion = '2.2' + exoPlayerVersion = '2.11.6' + androidxLifecycleVersion = '2.2.0' + androidxRoomVersion = '2.2.5' + groupieVersion = '2.8.0' + markwonVersion = '4.3.1' +} + +configurations { + checkstyle + ktlint +} + +checkstyle { + configFile rootProject.file('checkstyle.xml') + ignoreFailures false + showViolations true + toolVersion = checkstyleVersion +} + +task runCheckstyle(type: Checkstyle) { + source 'src' + include '**/*.java' + exclude '**/gen/**' + exclude '**/R.java' + exclude '**/BuildConfig.java' + exclude 'main/java/us/shandian/giga/**' + + classpath = configurations.checkstyle + + showViolations true + + reports { + xml.enabled true + html.enabled true + } +} + +task runKtlint(type: JavaExec) { + main = "com.pinterest.ktlint.Main" + classpath = configurations.ktlint + args "src/**/*.kt" +} + +task formatKtlint(type: JavaExec) { + main = "com.pinterest.ktlint.Main" + classpath = configurations.ktlint + args "-F", "src/**/*.kt" +} + +afterEvaluate { + preDebugBuild.dependsOn runCheckstyle, runKtlint } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - androidTestImplementation('androidx.test.espresso:espresso-core:3.1.0', { + implementation "frankiesardo:icepick:${icepickVersion}" + kapt "frankiesardo:icepick-processor:${icepickVersion}" + + checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}" + ktlint "com.pinterest:ktlint:0.35.0" + + debugImplementation "com.facebook.stetho:stetho:${stethoVersion}" + debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoVersion}" + + debugImplementation "com.squareup.leakcanary:leakcanary-android:${leakCanaryVersion}" + implementation "com.squareup.leakcanary:leakcanary-object-watcher-android:${leakCanaryVersion}" + + debugImplementation "androidx.multidex:multidex:2.0.1" + + testImplementation 'junit:junit:4.13' + testImplementation 'org.mockito:mockito-core:3.3.3' + + androidTestImplementation "androidx.test.ext:junit:1.1.1" + androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}" + androidTestImplementation "androidx.test.espresso:espresso-core:3.2.0", { exclude module: 'support-annotations' - }) + } - implementation 'com.github.TeamNewPipe:NewPipeExtractor:ff61e284' - testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-core:2.23.0' + implementation 'com.github.TeamNewPipe:NewPipeExtractor:a70cb0283ffc3bba2709815673a5a7940aab0a3a' - implementation 'androidx.appcompat:appcompat:1.1.0' - implementation "androidx.legacy:legacy-support-v4:${androidxLibVersion}" - implementation "com.google.android.material:material:${androidxLibVersion}" - implementation "androidx.recyclerview:recyclerview:${androidxLibVersion}" - implementation "androidx.legacy:legacy-preference-v14:${androidxLibVersion}" - implementation "androidx.cardview:cardview:${androidxLibVersion}" - implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation "com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751" + implementation "org.jsoup:jsoup:1.13.1" - // Originally in NewPipeExtractor - implementation 'com.grack:nanojson:1.1' - implementation 'org.jsoup:jsoup:1.9.2' + implementation "com.squareup.okhttp3:okhttp:3.12.11" - implementation 'ch.acra:acra:4.9.2' //4.11 + implementation "com.google.android.exoplayer:exoplayer:${exoPlayerVersion}" + implementation "com.google.android.exoplayer:extension-mediasession:${exoPlayerVersion}" - implementation 'com.nostra13.universalimageloader:universal-image-loader:1.9.5' - implementation 'de.hdodenhof:circleimageview:2.2.0' - implementation 'com.nononsenseapps:filepicker:4.2.1' + implementation "com.google.android.material:material:1.1.0" - implementation "com.google.android.exoplayer:exoplayer:${exoPlayerLibVersion}" - implementation "com.google.android.exoplayer:extension-mediasession:${exoPlayerLibVersion}" + implementation "androidx.appcompat:appcompat:1.1.0" + implementation "androidx.preference:preference:1.1.1" + implementation "androidx.recyclerview:recyclerview:1.1.0" + implementation "androidx.cardview:cardview:1.0.0" + implementation "androidx.constraintlayout:constraintlayout:1.1.3" - debugImplementation "com.facebook.stetho:stetho:${stethoLibVersion}" - debugImplementation "com.facebook.stetho:stetho-urlconnection:${stethoLibVersion}" - debugImplementation 'androidx.multidex:multidex:2.0.1' + implementation "androidx.lifecycle:lifecycle-livedata:${androidxLifecycleVersion}" + implementation "androidx.lifecycle:lifecycle-viewmodel:${androidxLifecycleVersion}" + implementation "androidx.lifecycle:lifecycle-extensions:${androidxLifecycleVersion}" - 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 'org.ocpsoft.prettytime:prettytime:4.0.1.Final' + implementation "androidx.room:room-runtime:${androidxRoomVersion}" + implementation "androidx.room:room-rxjava2:${androidxRoomVersion}" + kapt "androidx.room:room-compiler:${androidxRoomVersion}" - implementation "androidx.room:room-runtime:${roomDbLibVersion}" - implementation "androidx.room:room-rxjava2:${roomDbLibVersion}" - kapt "androidx.room:room-compiler:${roomDbLibVersion}" + implementation "com.xwray:groupie:${groupieVersion}" + implementation "com.xwray:groupie-kotlin-android-extensions:${groupieVersion}" - implementation "frankiesardo:icepick:${icepickLibVersion}" - kapt "frankiesardo:icepick-processor:${icepickLibVersion}" + implementation "de.hdodenhof:circleimageview:3.1.0" + implementation "com.nostra13.universalimageloader:universal-image-loader:1.9.5" - debugImplementation "com.squareup.leakcanary:leakcanary-android:${leakCanaryLibVersion}" - releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:${leakCanaryLibVersion}" + implementation "io.noties.markwon:core:${markwonVersion}" + implementation "io.noties.markwon:linkify:${markwonVersion}" - implementation "com.squareup.okhttp3:okhttp:${okHttpLibVersion}" - debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoLibVersion}" + implementation "com.nononsenseapps:filepicker:4.2.1" + + implementation "ch.acra:acra-core:5.5.0" + + implementation "io.reactivex.rxjava2:rxjava:2.2.19" + implementation "io.reactivex.rxjava2:rxandroid:2.1.1" + implementation "com.jakewharton.rxbinding2:rxbinding:2.2.0" + + implementation "org.ocpsoft.prettytime:prettytime:4.0.5.Final" +} + +static String getGitWorkingBranch() { + try { + def gitProcess = "git rev-parse --abbrev-ref HEAD".execute() + gitProcess.waitFor() + if (gitProcess.exitValue() == 0) { + return gitProcess.text.trim() + } else { + // not a git repository + return "" + } + } catch (IOException ignored) { + // git was not found + return "" + } } diff --git a/app/schemas/org.schabi.newpipe.database.AppDatabase/2.json b/app/schemas/org.schabi.newpipe.database.AppDatabase/2.json new file mode 100644 index 000000000..2532e330e --- /dev/null +++ b/app/schemas/org.schabi.newpipe.database.AppDatabase/2.json @@ -0,0 +1,479 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "b7856223e2595ddf20a3ce6243ce9527", + "entities": [ + { + "tableName": "subscriptions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `name` TEXT, `avatar_url` TEXT, `subscriber_count` INTEGER, `description` TEXT)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "service_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarUrl", + "columnName": "avatar_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "subscriberCount", + "columnName": "subscriber_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_subscriptions_service_id_url", + "unique": true, + "columnNames": [ + "service_id", + "url" + ], + "createSql": "CREATE UNIQUE INDEX `index_subscriptions_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "search_history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "creationDate", + "columnName": "creation_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "serviceId", + "columnName": "service_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "search", + "columnName": "search", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_search_history_search", + "unique": false, + "columnNames": [ + "search" + ], + "createSql": "CREATE INDEX `index_search_history_search` ON `${TABLE_NAME}` (`search`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "streams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, `stream_type` TEXT, `duration` INTEGER, `uploader` TEXT, `thumbnail_url` TEXT)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "service_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "streamType", + "columnName": "stream_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uploader", + "columnName": "uploader", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnail_url", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_streams_service_id_url", + "unique": true, + "columnNames": [ + "service_id", + "url" + ], + "createSql": "CREATE UNIQUE INDEX `index_streams_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "stream_history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, `repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "streamUid", + "columnName": "stream_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accessDate", + "columnName": "access_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repeatCount", + "columnName": "repeat_count", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "stream_id", + "access_date" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_stream_history_stream_id", + "unique": false, + "columnNames": [ + "stream_id" + ], + "createSql": "CREATE INDEX `index_stream_history_stream_id` ON `${TABLE_NAME}` (`stream_id`)" + } + ], + "foreignKeys": [ + { + "table": "streams", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "stream_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "stream_state", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "streamUid", + "columnName": "stream_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progressTime", + "columnName": "progress_time", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "stream_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "streams", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "stream_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "playlists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `thumbnail_url` TEXT)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnail_url", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_playlists_name", + "unique": false, + "columnNames": [ + "name" + ], + "createSql": "CREATE INDEX `index_playlists_name` ON `${TABLE_NAME}` (`name`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "playlist_stream_join", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "playlistUid", + "columnName": "playlist_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "streamUid", + "columnName": "stream_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "join_index", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "playlist_id", + "join_index" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_playlist_stream_join_playlist_id_join_index", + "unique": true, + "columnNames": [ + "playlist_id", + "join_index" + ], + "createSql": "CREATE UNIQUE INDEX `index_playlist_stream_join_playlist_id_join_index` ON `${TABLE_NAME}` (`playlist_id`, `join_index`)" + }, + { + "name": "index_playlist_stream_join_stream_id", + "unique": false, + "columnNames": [ + "stream_id" + ], + "createSql": "CREATE INDEX `index_playlist_stream_join_stream_id` ON `${TABLE_NAME}` (`stream_id`)" + } + ], + "foreignKeys": [ + { + "table": "playlists", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "playlist_id" + ], + "referencedColumns": [ + "uid" + ] + }, + { + "table": "streams", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "stream_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "remote_playlists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "service_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnail_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uploader", + "columnName": "uploader", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "streamCount", + "columnName": "stream_count", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_remote_playlists_name", + "unique": false, + "columnNames": [ + "name" + ], + "createSql": "CREATE INDEX `index_remote_playlists_name` ON `${TABLE_NAME}` (`name`)" + }, + { + "name": "index_remote_playlists_service_id_url", + "unique": true, + "columnNames": [ + "service_id", + "url" + ], + "createSql": "CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)" + } + ], + "foreignKeys": [] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"b7856223e2595ddf20a3ce6243ce9527\")" + ] + } +} \ No newline at end of file diff --git a/app/schemas/org.schabi.newpipe.database.AppDatabase/3.json b/app/schemas/org.schabi.newpipe.database.AppDatabase/3.json new file mode 100644 index 000000000..313c3e27c --- /dev/null +++ b/app/schemas/org.schabi.newpipe.database.AppDatabase/3.json @@ -0,0 +1,707 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "identityHash": "9f825b1ee281480bedd38b971feac327", + "entities": [ + { + "tableName": "subscriptions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `name` TEXT, `avatar_url` TEXT, `subscriber_count` INTEGER, `description` TEXT)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "service_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarUrl", + "columnName": "avatar_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "subscriberCount", + "columnName": "subscriber_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_subscriptions_service_id_url", + "unique": true, + "columnNames": [ + "service_id", + "url" + ], + "createSql": "CREATE UNIQUE INDEX `index_subscriptions_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "search_history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "creationDate", + "columnName": "creation_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "serviceId", + "columnName": "service_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "search", + "columnName": "search", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_search_history_search", + "unique": false, + "columnNames": [ + "search" + ], + "createSql": "CREATE INDEX `index_search_history_search` ON `${TABLE_NAME}` (`search`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "streams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `stream_type` TEXT NOT NULL, `duration` INTEGER NOT NULL, `uploader` TEXT NOT NULL, `thumbnail_url` TEXT, `view_count` INTEGER, `textual_upload_date` TEXT, `upload_date` INTEGER, `is_upload_date_approximation` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "service_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "streamType", + "columnName": "stream_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uploader", + "columnName": "uploader", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnail_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "viewCount", + "columnName": "view_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "textualUploadDate", + "columnName": "textual_upload_date", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uploadDate", + "columnName": "upload_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isUploadDateApproximation", + "columnName": "is_upload_date_approximation", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_streams_service_id_url", + "unique": true, + "columnNames": [ + "service_id", + "url" + ], + "createSql": "CREATE UNIQUE INDEX `index_streams_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "stream_history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, `repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "streamUid", + "columnName": "stream_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accessDate", + "columnName": "access_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repeatCount", + "columnName": "repeat_count", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "stream_id", + "access_date" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_stream_history_stream_id", + "unique": false, + "columnNames": [ + "stream_id" + ], + "createSql": "CREATE INDEX `index_stream_history_stream_id` ON `${TABLE_NAME}` (`stream_id`)" + } + ], + "foreignKeys": [ + { + "table": "streams", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "stream_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "stream_state", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "streamUid", + "columnName": "stream_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progressTime", + "columnName": "progress_time", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "stream_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "streams", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "stream_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "playlists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `thumbnail_url` TEXT)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnail_url", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_playlists_name", + "unique": false, + "columnNames": [ + "name" + ], + "createSql": "CREATE INDEX `index_playlists_name` ON `${TABLE_NAME}` (`name`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "playlist_stream_join", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "playlistUid", + "columnName": "playlist_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "streamUid", + "columnName": "stream_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "join_index", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "playlist_id", + "join_index" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_playlist_stream_join_playlist_id_join_index", + "unique": true, + "columnNames": [ + "playlist_id", + "join_index" + ], + "createSql": "CREATE UNIQUE INDEX `index_playlist_stream_join_playlist_id_join_index` ON `${TABLE_NAME}` (`playlist_id`, `join_index`)" + }, + { + "name": "index_playlist_stream_join_stream_id", + "unique": false, + "columnNames": [ + "stream_id" + ], + "createSql": "CREATE INDEX `index_playlist_stream_join_stream_id` ON `${TABLE_NAME}` (`stream_id`)" + } + ], + "foreignKeys": [ + { + "table": "playlists", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "playlist_id" + ], + "referencedColumns": [ + "uid" + ] + }, + { + "table": "streams", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "stream_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "remote_playlists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "service_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnail_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uploader", + "columnName": "uploader", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "streamCount", + "columnName": "stream_count", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_remote_playlists_name", + "unique": false, + "columnNames": [ + "name" + ], + "createSql": "CREATE INDEX `index_remote_playlists_name` ON `${TABLE_NAME}` (`name`)" + }, + { + "name": "index_remote_playlists_service_id_url", + "unique": true, + "columnNames": [ + "service_id", + "url" + ], + "createSql": "CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "feed", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `subscription_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "streamId", + "columnName": "stream_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subscriptionId", + "columnName": "subscription_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "stream_id", + "subscription_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_feed_subscription_id", + "unique": false, + "columnNames": [ + "subscription_id" + ], + "createSql": "CREATE INDEX `index_feed_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)" + } + ], + "foreignKeys": [ + { + "table": "streams", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "stream_id" + ], + "referencedColumns": [ + "uid" + ] + }, + { + "table": "subscriptions", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "subscription_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "feed_group", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon_id` INTEGER NOT NULL, `sort_order` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortOrder", + "columnName": "sort_order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_feed_group_sort_order", + "unique": false, + "columnNames": [ + "sort_order" + ], + "createSql": "CREATE INDEX `index_feed_group_sort_order` ON `${TABLE_NAME}` (`sort_order`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "feed_group_subscription_join", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`group_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`group_id`, `subscription_id`), FOREIGN KEY(`group_id`) REFERENCES `feed_group`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "feedGroupId", + "columnName": "group_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subscriptionId", + "columnName": "subscription_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "group_id", + "subscription_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_feed_group_subscription_join_subscription_id", + "unique": false, + "columnNames": [ + "subscription_id" + ], + "createSql": "CREATE INDEX `index_feed_group_subscription_join_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)" + } + ], + "foreignKeys": [ + { + "table": "feed_group", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "group_id" + ], + "referencedColumns": [ + "uid" + ] + }, + { + "table": "subscriptions", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "subscription_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "feed_last_updated", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`subscription_id` INTEGER NOT NULL, `last_updated` INTEGER, PRIMARY KEY(`subscription_id`), FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "subscriptionId", + "columnName": "subscription_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "last_updated", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "subscription_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "subscriptions", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "subscription_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9f825b1ee281480bedd38b971feac327')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/org/schabi/newpipe/database/AppDatabaseTest.kt b/app/src/androidTest/java/org/schabi/newpipe/database/AppDatabaseTest.kt new file mode 100644 index 000000000..e37eb5db9 --- /dev/null +++ b/app/src/androidTest/java/org/schabi/newpipe/database/AppDatabaseTest.kt @@ -0,0 +1,119 @@ +package org.schabi.newpipe.database + +import android.content.ContentValues +import android.database.sqlite.SQLiteDatabase +import androidx.room.Room +import androidx.room.testing.MigrationTestHelper +import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.schabi.newpipe.extractor.stream.StreamType + +@RunWith(AndroidJUnit4::class) +class AppDatabaseTest { + companion object { + private const val DEFAULT_SERVICE_ID = 0 + private const val DEFAULT_URL = "https://www.youtube.com/watch?v=cDphUib5iG4" + private const val DEFAULT_TITLE = "Test Title" + private val DEFAULT_TYPE = StreamType.VIDEO_STREAM + private const val DEFAULT_DURATION = 480L + private const val DEFAULT_UPLOADER_NAME = "Uploader Test" + private const val DEFAULT_THUMBNAIL = "https://example.com/example.jpg" + + private const val DEFAULT_SECOND_SERVICE_ID = 0 + private const val DEFAULT_SECOND_URL = "https://www.youtube.com/watch?v=ncQU6iBn5Fc" + } + + @get:Rule + val testHelper = MigrationTestHelper(InstrumentationRegistry.getInstrumentation(), + AppDatabase::class.java.canonicalName, FrameworkSQLiteOpenHelperFactory()) + + @Test + fun migrateDatabaseFrom2to3() { + val databaseInV2 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_2) + + databaseInV2.run { + insert("streams", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply { + // put("uid", null) + put("service_id", DEFAULT_SERVICE_ID) + put("url", DEFAULT_URL) + put("title", DEFAULT_TITLE) + put("stream_type", DEFAULT_TYPE.name) + put("duration", DEFAULT_DURATION) + put("uploader", DEFAULT_UPLOADER_NAME) + put("thumbnail_url", DEFAULT_THUMBNAIL) + }) + insert("streams", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply { + // put("uid", null) + put("service_id", DEFAULT_SECOND_SERVICE_ID) + put("url", DEFAULT_SECOND_URL) + // put("title", null) + // put("stream_type", null) + // put("duration", null) + // put("uploader", null) + // put("thumbnail_url", null) + }) + insert("streams", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply { + // put("uid", null) + put("service_id", DEFAULT_SERVICE_ID) + // put("url", null) + // put("title", null) + // put("stream_type", null) + // put("duration", null) + // put("uploader", null) + // put("thumbnail_url", null) + }) + close() + } + + testHelper.runMigrationsAndValidate(AppDatabase.DATABASE_NAME, Migrations.DB_VER_3, + true, Migrations.MIGRATION_2_3) + + val migratedDatabaseV3 = getMigratedDatabase() + val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst() + + // Only expect 2, the one with the null url will be ignored + assertEquals(2, listFromDB.size) + + val streamFromMigratedDatabase = listFromDB[0] + assertEquals(DEFAULT_SERVICE_ID, streamFromMigratedDatabase.serviceId) + assertEquals(DEFAULT_URL, streamFromMigratedDatabase.url) + assertEquals(DEFAULT_TITLE, streamFromMigratedDatabase.title) + assertEquals(DEFAULT_TYPE, streamFromMigratedDatabase.streamType) + assertEquals(DEFAULT_DURATION, streamFromMigratedDatabase.duration) + assertEquals(DEFAULT_UPLOADER_NAME, streamFromMigratedDatabase.uploader) + assertEquals(DEFAULT_THUMBNAIL, streamFromMigratedDatabase.thumbnailUrl) + assertNull(streamFromMigratedDatabase.viewCount) + assertNull(streamFromMigratedDatabase.textualUploadDate) + assertNull(streamFromMigratedDatabase.uploadDate) + assertNull(streamFromMigratedDatabase.isUploadDateApproximation) + + val secondStreamFromMigratedDatabase = listFromDB[1] + assertEquals(DEFAULT_SECOND_SERVICE_ID, secondStreamFromMigratedDatabase.serviceId) + assertEquals(DEFAULT_SECOND_URL, secondStreamFromMigratedDatabase.url) + assertEquals("", secondStreamFromMigratedDatabase.title) + // Should fallback to VIDEO_STREAM + assertEquals(StreamType.VIDEO_STREAM, secondStreamFromMigratedDatabase.streamType) + assertEquals(0, secondStreamFromMigratedDatabase.duration) + assertEquals("", secondStreamFromMigratedDatabase.uploader) + assertEquals("", secondStreamFromMigratedDatabase.thumbnailUrl) + assertNull(secondStreamFromMigratedDatabase.viewCount) + assertNull(secondStreamFromMigratedDatabase.textualUploadDate) + assertNull(secondStreamFromMigratedDatabase.uploadDate) + assertNull(secondStreamFromMigratedDatabase.isUploadDateApproximation) + } + + private fun getMigratedDatabase(): AppDatabase { + val database: AppDatabase = Room.databaseBuilder(ApplicationProvider.getApplicationContext(), + AppDatabase::class.java, AppDatabase.DATABASE_NAME) + .build() + testHelper.closeWhenFinished(database) + return database + } +} diff --git a/app/src/androidTest/java/org/schabi/newpipe/report/ErrorInfoTest.java b/app/src/androidTest/java/org/schabi/newpipe/report/ErrorInfoTest.java index 6e51136c0..55e747cd5 100644 --- a/app/src/androidTest/java/org/schabi/newpipe/report/ErrorInfoTest.java +++ b/app/src/androidTest/java/org/schabi/newpipe/report/ErrorInfoTest.java @@ -1,9 +1,10 @@ package org.schabi.newpipe.report; import android.os.Parcel; -import androidx.test.filters.LargeTest; -import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; + +import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; import org.schabi.newpipe.R; @@ -12,15 +13,16 @@ import org.schabi.newpipe.report.ErrorActivity.ErrorInfo; import static org.junit.Assert.assertEquals; /** - * Instrumented tests for {@link ErrorInfo} + * Instrumented tests for {@link ErrorInfo}. */ @RunWith(AndroidJUnit4.class) @LargeTest public class ErrorInfoTest { @Test - public void errorInfo_testParcelable() { - ErrorInfo info = ErrorInfo.make(UserAction.USER_REPORT, "youtube", "request", R.string.general_error); + public void errorInfoTestParcelable() { + ErrorInfo info = ErrorInfo.make(UserAction.USER_REPORT, "youtube", "request", + R.string.general_error); // Obtain a Parcel object and write the parcelable object to it: Parcel parcel = Parcel.obtain(); info.writeToParcel(parcel, 0); @@ -34,4 +36,4 @@ public class ErrorInfoTest { parcel.recycle(); } -} \ No newline at end of file +} diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml index a16d6796a..5cc2fa66a 100644 --- a/app/src/debug/AndroidManifest.xml +++ b/app/src/debug/AndroidManifest.xml @@ -6,12 +6,5 @@ - - - + tools:replace="android:name" /> \ No newline at end of file diff --git a/app/src/debug/java/org/schabi/newpipe/DebugApp.java b/app/src/debug/java/org/schabi/newpipe/DebugApp.java deleted file mode 100644 index 66f73d1e9..000000000 --- a/app/src/debug/java/org/schabi/newpipe/DebugApp.java +++ /dev/null @@ -1,104 +0,0 @@ -package org.schabi.newpipe; - -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; -import androidx.annotation.NonNull; -import androidx.multidex.MultiDex; - -import com.facebook.stetho.Stetho; -import com.facebook.stetho.okhttp3.StethoInterceptor; -import com.squareup.leakcanary.AndroidHeapDumper; -import com.squareup.leakcanary.DefaultLeakDirectoryProvider; -import com.squareup.leakcanary.HeapDumper; -import com.squareup.leakcanary.LeakCanary; -import com.squareup.leakcanary.LeakDirectoryProvider; -import com.squareup.leakcanary.RefWatcher; - -import org.schabi.newpipe.extractor.downloader.Downloader; - -import java.io.File; -import java.util.concurrent.TimeUnit; - -import okhttp3.OkHttpClient; - -public class DebugApp extends App { - private static final String TAG = DebugApp.class.toString(); - - @Override - protected void attachBaseContext(Context base) { - super.attachBaseContext(base); - MultiDex.install(this); - } - - @Override - public void onCreate() { - super.onCreate(); - initStetho(); - } - - @Override - protected Downloader getDownloader() { - return DownloaderImpl.init(new OkHttpClient.Builder() - .addNetworkInterceptor(new StethoInterceptor())); - } - - private void initStetho() { - // Create an InitializerBuilder - Stetho.InitializerBuilder initializerBuilder = - Stetho.newInitializerBuilder(this); - - // Enable Chrome DevTools - initializerBuilder.enableWebKitInspector( - Stetho.defaultInspectorModulesProvider(this) - ); - - // Enable command line interface - initializerBuilder.enableDumpapp( - Stetho.defaultDumperPluginsProvider(getApplicationContext()) - ); - - // Use the InitializerBuilder to generate an Initializer - Stetho.Initializer initializer = initializerBuilder.build(); - - // Initialize Stetho with the Initializer - Stetho.initialize(initializer); - } - - @Override - protected boolean isDisposedRxExceptionsReported() { - return PreferenceManager.getDefaultSharedPreferences(this) - .getBoolean(getString(R.string.allow_disposed_exceptions_key), false); - } - - @Override - protected RefWatcher installLeakCanary() { - return LeakCanary.refWatcher(this) - .heapDumper(new ToggleableHeapDumper(this)) - // give each object 10 seconds to be gc'ed, before leak canary gets nosy on it - .watchDelay(10, TimeUnit.SECONDS) - .buildAndInstall(); - } - - public static class ToggleableHeapDumper implements HeapDumper { - private final HeapDumper dumper; - private final SharedPreferences preferences; - private final String dumpingAllowanceKey; - - ToggleableHeapDumper(@NonNull final Context context) { - LeakDirectoryProvider leakDirectoryProvider = new DefaultLeakDirectoryProvider(context); - this.dumper = new AndroidHeapDumper(context, leakDirectoryProvider); - this.preferences = PreferenceManager.getDefaultSharedPreferences(context); - this.dumpingAllowanceKey = context.getString(R.string.allow_heap_dumping_key); - } - - private boolean isDumpingAllowed() { - return preferences.getBoolean(dumpingAllowanceKey, false); - } - - @Override - public File dumpHeap() { - return isDumpingAllowed() ? dumper.dumpHeap() : HeapDumper.RETRY_LATER; - } - } -} diff --git a/app/src/debug/java/org/schabi/newpipe/DebugApp.kt b/app/src/debug/java/org/schabi/newpipe/DebugApp.kt new file mode 100644 index 000000000..5cfde80b8 --- /dev/null +++ b/app/src/debug/java/org/schabi/newpipe/DebugApp.kt @@ -0,0 +1,59 @@ +package org.schabi.newpipe + +import android.content.Context +import androidx.multidex.MultiDex +import androidx.preference.PreferenceManager +import com.facebook.stetho.Stetho +import com.facebook.stetho.okhttp3.StethoInterceptor +import leakcanary.AppWatcher +import leakcanary.LeakCanary +import okhttp3.OkHttpClient +import org.schabi.newpipe.extractor.downloader.Downloader + +class DebugApp : App() { + override fun attachBaseContext(base: Context) { + super.attachBaseContext(base) + MultiDex.install(this) + } + + override fun onCreate() { + super.onCreate() + initStetho() + + // Give each object 10 seconds to be GC'ed, before LeakCanary gets nosy on it + AppWatcher.config = AppWatcher.config.copy(watchDurationMillis = 10000) + LeakCanary.config = LeakCanary.config.copy(dumpHeap = PreferenceManager + .getDefaultSharedPreferences(this).getBoolean(getString( + R.string.allow_heap_dumping_key), false)) + } + + override fun getDownloader(): Downloader { + val downloader = DownloaderImpl.init(OkHttpClient.Builder() + .addNetworkInterceptor(StethoInterceptor())) + setCookiesToDownloader(downloader) + return downloader + } + + private fun initStetho() { + // Create an InitializerBuilder + val initializerBuilder = Stetho.newInitializerBuilder(this) + + // Enable Chrome DevTools + initializerBuilder.enableWebKitInspector(Stetho.defaultInspectorModulesProvider(this)) + + // Enable command line interface + initializerBuilder.enableDumpapp( + Stetho.defaultDumperPluginsProvider(applicationContext)) + + // Use the InitializerBuilder to generate an Initializer + val initializer = initializerBuilder.build() + + // Initialize Stetho with the Initializer + Stetho.initialize(initializer) + } + + override fun isDisposedRxExceptionsReported(): Boolean { + return PreferenceManager.getDefaultSharedPreferences(this) + .getBoolean(getString(R.string.allow_disposed_exceptions_key), false) + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3136774e1..60a7f3cb8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,22 +1,27 @@ - - - - - - - + + + + + + + + - - + + + + - + @@ -50,34 +57,36 @@ + android:label="@string/title_activity_background_player" + android:launchMode="singleTask" /> + android:label="@string/title_activity_popup_player" + android:launchMode="singleTask" /> + android:exported="false" /> + android:launchMode="singleTask" + android:theme="@style/VideoPlayerTheme" /> + android:label="@string/settings" /> + android:label="@string/title_activity_about" /> - - + + + - + - + - + android:theme="@android:style/Theme.NoDisplay" /> + + android:launchMode="singleTask" /> - + + android:label="@string/recaptcha" /> + android:resource="@xml/nnf_provider_paths" /> - - - + + + - - + + - - - - - + + + + + + - - - - + + + + - - + + + - + - - - + + + - - + + - - - - + + + + - - - + + + - - + + - - - - + + + + - - - + + + - - + + - - + + - - - + + + - - + + - - - - + + + + - - - + + + - - + + - - - + + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - + + + - - + + - - - - - - + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + android:exported="false" /> diff --git a/app/src/main/java/androidx/fragment/app/FragmentStatePagerAdapterMenuWorkaround.java b/app/src/main/java/androidx/fragment/app/FragmentStatePagerAdapterMenuWorkaround.java new file mode 100644 index 000000000..11f457b6c --- /dev/null +++ b/app/src/main/java/androidx/fragment/app/FragmentStatePagerAdapterMenuWorkaround.java @@ -0,0 +1,329 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.fragment.app; + +import android.os.Bundle; +import android.os.Parcelable; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.Lifecycle; +import androidx.viewpager.widget.PagerAdapter; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; + +// TODO: Replace this deprecated class with its ViewPager2 counterpart + +/** + * This is a copy from {@link androidx.fragment.app.FragmentStatePagerAdapter}. + *

+ * It includes a workaround to fix the menu visibility when the adapter is restored. + *

+ *

+ * When restoring the state of this adapter, all the fragments' menu visibility were set to false, + * effectively disabling the menu from the user until he switched pages or another event + * that triggered the menu to be visible again happened. + *

+ *

+ * Check out the changes in: + *

+ * + */ +@SuppressWarnings("deprecation") +public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapter { + private static final String TAG = "FragmentStatePagerAdapt"; + private static final boolean DEBUG = false; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({BEHAVIOR_SET_USER_VISIBLE_HINT, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT}) + private @interface Behavior { } + + /** + * Indicates that {@link Fragment#setUserVisibleHint(boolean)} will be called when the current + * fragment changes. + * + * @deprecated This behavior relies on the deprecated + * {@link Fragment#setUserVisibleHint(boolean)} API. Use + * {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT} to switch to its replacement, + * {@link FragmentTransaction#setMaxLifecycle}. + * @see #FragmentStatePagerAdapterMenuWorkaround(FragmentManager, int) + */ + @Deprecated + public static final int BEHAVIOR_SET_USER_VISIBLE_HINT = 0; + + /** + * Indicates that only the current fragment will be in the {@link Lifecycle.State#RESUMED} + * state. All other Fragments are capped at {@link Lifecycle.State#STARTED}. + * + * @see #FragmentStatePagerAdapterMenuWorkaround(FragmentManager, int) + */ + public static final int BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT = 1; + + private final FragmentManager mFragmentManager; + private final int mBehavior; + private FragmentTransaction mCurTransaction = null; + + private ArrayList mSavedState = new ArrayList(); + private ArrayList mFragments = new ArrayList(); + private Fragment mCurrentPrimaryItem = null; + + /** + * Constructor for {@link FragmentStatePagerAdapterMenuWorkaround} + * that sets the fragment manager for the adapter. This is the equivalent of calling + * {@link #FragmentStatePagerAdapterMenuWorkaround(FragmentManager, int)} and passing in + * {@link #BEHAVIOR_SET_USER_VISIBLE_HINT}. + * + *

Fragments will have {@link Fragment#setUserVisibleHint(boolean)} called whenever the + * current Fragment changes.

+ * + * @param fm fragment manager that will interact with this adapter + * @deprecated use {@link #FragmentStatePagerAdapterMenuWorkaround(FragmentManager, int)} with + * {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT} + */ + @Deprecated + public FragmentStatePagerAdapterMenuWorkaround(@NonNull final FragmentManager fm) { + this(fm, BEHAVIOR_SET_USER_VISIBLE_HINT); + } + + /** + * Constructor for {@link FragmentStatePagerAdapterMenuWorkaround}. + * + * If {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT} is passed in, then only the current + * Fragment is in the {@link Lifecycle.State#RESUMED} state, while all other fragments are + * capped at {@link Lifecycle.State#STARTED}. If {@link #BEHAVIOR_SET_USER_VISIBLE_HINT} is + * passed, all fragments are in the {@link Lifecycle.State#RESUMED} state and there will be + * callbacks to {@link Fragment#setUserVisibleHint(boolean)}. + * + * @param fm fragment manager that will interact with this adapter + * @param behavior determines if only current fragments are in a resumed state + */ + public FragmentStatePagerAdapterMenuWorkaround(@NonNull final FragmentManager fm, + @Behavior final int behavior) { + mFragmentManager = fm; + mBehavior = behavior; + } + + /** + * @param position the position of the item you want + * @return the {@link Fragment} associated with a specified position + */ + @NonNull + public abstract Fragment getItem(int position); + + @Override + public void startUpdate(@NonNull final ViewGroup container) { + if (container.getId() == View.NO_ID) { + throw new IllegalStateException("ViewPager with adapter " + this + + " requires a view id"); + } + } + + @SuppressWarnings("deprecation") + @NonNull + @Override + public Object instantiateItem(@NonNull final ViewGroup container, final int position) { + // If we already have this item instantiated, there is nothing + // to do. This can happen when we are restoring the entire pager + // from its saved state, where the fragment manager has already + // taken care of restoring the fragments we previously had instantiated. + if (mFragments.size() > position) { + Fragment f = mFragments.get(position); + if (f != null) { + return f; + } + } + + if (mCurTransaction == null) { + mCurTransaction = mFragmentManager.beginTransaction(); + } + + Fragment fragment = getItem(position); + if (DEBUG) { + Log.v(TAG, "Adding item #" + position + ": f=" + fragment); + } + if (mSavedState.size() > position) { + Fragment.SavedState fss = mSavedState.get(position); + if (fss != null) { + fragment.setInitialSavedState(fss); + } + } + while (mFragments.size() <= position) { + mFragments.add(null); + } + fragment.setMenuVisibility(false); + if (mBehavior == BEHAVIOR_SET_USER_VISIBLE_HINT) { + fragment.setUserVisibleHint(false); + } + + mFragments.set(position, fragment); + mCurTransaction.add(container.getId(), fragment); + + if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED); + } + + return fragment; + } + + @Override + public void destroyItem(@NonNull final ViewGroup container, final int position, + @NonNull final Object object) { + Fragment fragment = (Fragment) object; + + if (mCurTransaction == null) { + mCurTransaction = mFragmentManager.beginTransaction(); + } + if (DEBUG) { + Log.v(TAG, "Removing item #" + position + ": f=" + object + + " v=" + ((Fragment) object).getView()); + } + while (mSavedState.size() <= position) { + mSavedState.add(null); + } + mSavedState.set(position, fragment.isAdded() + ? mFragmentManager.saveFragmentInstanceState(fragment) : null); + mFragments.set(position, null); + + mCurTransaction.remove(fragment); + if (fragment == mCurrentPrimaryItem) { + mCurrentPrimaryItem = null; + } + } + + @Override + @SuppressWarnings({"ReferenceEquality", "deprecation"}) + public void setPrimaryItem(@NonNull final ViewGroup container, final int position, + @NonNull final Object object) { + Fragment fragment = (Fragment) object; + if (fragment != mCurrentPrimaryItem) { + if (mCurrentPrimaryItem != null) { + mCurrentPrimaryItem.setMenuVisibility(false); + if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + if (mCurTransaction == null) { + mCurTransaction = mFragmentManager.beginTransaction(); + } + mCurTransaction.setMaxLifecycle(mCurrentPrimaryItem, Lifecycle.State.STARTED); + } else { + mCurrentPrimaryItem.setUserVisibleHint(false); + } + } + fragment.setMenuVisibility(true); + if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + if (mCurTransaction == null) { + mCurTransaction = mFragmentManager.beginTransaction(); + } + mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED); + } else { + fragment.setUserVisibleHint(true); + } + + mCurrentPrimaryItem = fragment; + } + } + + @Override + public void finishUpdate(@NonNull final ViewGroup container) { + if (mCurTransaction != null) { + mCurTransaction.commitNowAllowingStateLoss(); + mCurTransaction = null; + } + } + + @Override + public boolean isViewFromObject(@NonNull final View view, @NonNull final Object object) { + return ((Fragment) object).getView() == view; + } + + //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + private final String selectedFragment = "selected_fragment"; + //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + @Override + @Nullable + public Parcelable saveState() { + Bundle state = null; + if (mSavedState.size() > 0) { + state = new Bundle(); + Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()]; + mSavedState.toArray(fss); + state.putParcelableArray("states", fss); + } + for (int i = 0; i < mFragments.size(); i++) { + Fragment f = mFragments.get(i); + if (f != null && f.isAdded()) { + if (state == null) { + state = new Bundle(); + } + String key = "f" + i; + mFragmentManager.putFragment(state, key, f); + + //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // Check if it's the same fragment instance + if (f == mCurrentPrimaryItem) { + state.putString(selectedFragment, key); + } + //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + } + } + return state; + } + + @Override + public void restoreState(@Nullable final Parcelable state, @Nullable final ClassLoader loader) { + if (state != null) { + Bundle bundle = (Bundle) state; + bundle.setClassLoader(loader); + Parcelable[] fss = bundle.getParcelableArray("states"); + mSavedState.clear(); + mFragments.clear(); + if (fss != null) { + for (int i = 0; i < fss.length; i++) { + mSavedState.add((Fragment.SavedState) fss[i]); + } + } + Iterable keys = bundle.keySet(); + for (String key: keys) { + if (key.startsWith("f")) { + int index = Integer.parseInt(key.substring(1)); + Fragment f = mFragmentManager.getFragment(bundle, key); + if (f != null) { + while (mFragments.size() <= index) { + mFragments.add(null); + } + //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + final boolean wasSelected = bundle.getString(selectedFragment, "") + .equals(key); + f.setMenuVisibility(wasSelected); + //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + mFragments.set(index, f); + } else { + Log.w(TAG, "Bad fragment at key " + key); + } + } + } + } + } +} diff --git a/app/src/main/java/com/google/android/material/appbar/FlingBehavior.java b/app/src/main/java/com/google/android/material/appbar/FlingBehavior.java index 95137414e..2e8a0103f 100644 --- a/app/src/main/java/com/google/android/material/appbar/FlingBehavior.java +++ b/app/src/main/java/com/google/android/material/appbar/FlingBehavior.java @@ -8,6 +8,7 @@ import android.view.View; import android.view.ViewGroup; import android.widget.OverScroller; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.coordinatorlayout.widget.CoordinatorLayout; import org.jetbrains.annotations.NotNull; @@ -15,10 +16,11 @@ import org.schabi.newpipe.R; import java.lang.reflect.Field; -// check this https://stackoverflow.com/questions/56849221/recyclerview-fling-causes-laggy-while-appbarlayout-is-scrolling/57997489#57997489 +// See https://stackoverflow.com/questions/56849221#57997489 public final class FlingBehavior extends AppBarLayout.Behavior { + private final Rect focusScrollRect = new Rect(); - public FlingBehavior(Context context, AttributeSet attrs) { + public FlingBehavior(final Context context, final AttributeSet attrs) { super(context, attrs); } @@ -26,7 +28,40 @@ public final class FlingBehavior extends AppBarLayout.Behavior { private final Rect globalRect = new Rect(); @Override - public boolean onInterceptTouchEvent(CoordinatorLayout parent, AppBarLayout child, MotionEvent ev) { + public boolean onRequestChildRectangleOnScreen( + @NonNull final CoordinatorLayout coordinatorLayout, @NonNull final AppBarLayout child, + @NonNull final Rect rectangle, final boolean immediate) { + + focusScrollRect.set(rectangle); + + coordinatorLayout.offsetDescendantRectToMyCoords(child, focusScrollRect); + + int height = coordinatorLayout.getHeight(); + + if (focusScrollRect.top <= 0 && focusScrollRect.bottom >= height) { + // the child is too big to fit inside ourselves completely, ignore request + return false; + } + + int dy; + + if (focusScrollRect.bottom > height) { + dy = focusScrollRect.top; + } else if (focusScrollRect.top < 0) { + // scrolling up + dy = -(height - focusScrollRect.bottom); + } else { + // nothing to do + return false; + } + + int consumed = scroll(coordinatorLayout, child, dy, getMaxDragOffset(child), 0); + + return consumed == dy; + } + + public boolean onInterceptTouchEvent(final CoordinatorLayout parent, final AppBarLayout child, + final MotionEvent ev) { final ViewGroup playQueue = child.findViewById(R.id.playQueuePanel); if (playQueue != null) { final boolean visible = playQueue.getGlobalVisibleRect(globalRect); @@ -36,30 +71,36 @@ public final class FlingBehavior extends AppBarLayout.Behavior { } } allowScroll = true; - - if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) { - // remove reference to old nested scrolling child - resetNestedScrollingChild(); - // Stop fling when your finger touches the screen - stopAppBarLayoutFling(); + switch (ev.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + // remove reference to old nested scrolling child + resetNestedScrollingChild(); + // Stop fling when your finger touches the screen + stopAppBarLayoutFling(); + break; + default: + break; } return super.onInterceptTouchEvent(parent, child, ev); } @Override - public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes, int type) { + public boolean onStartNestedScroll(@NotNull final CoordinatorLayout parent, @NotNull final AppBarLayout child, + @NotNull final View directTargetChild, final View target, final int nestedScrollAxes, final int type) { return allowScroll && super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes, type); } @Override - public boolean onNestedFling(@NotNull CoordinatorLayout coordinatorLayout, @NotNull AppBarLayout child, @NotNull View target, float velocityX, float velocityY, boolean consumed) { + public boolean onNestedFling(@NotNull final CoordinatorLayout coordinatorLayout, @NotNull final AppBarLayout child, + @NotNull final View target, final float velocityX, final float velocityY, final boolean consumed) { return allowScroll && super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed); } @Nullable private OverScroller getScrollerField() { try { - Class headerBehaviorType = this.getClass().getSuperclass().getSuperclass().getSuperclass(); + Class headerBehaviorType = this.getClass() + .getSuperclass().getSuperclass().getSuperclass(); if (headerBehaviorType != null) { Field field = headerBehaviorType.getDeclaredField("scroller"); field.setAccessible(true); @@ -86,12 +127,14 @@ public final class FlingBehavior extends AppBarLayout.Behavior { return null; } - private void resetNestedScrollingChild(){ + private void resetNestedScrollingChild() { Field field = getLastNestedScrollingChildRefField(); - if(field != null){ + if (field != null) { try { Object value = field.get(this); - if(value != null) field.set(this, null); + if (value != null) { + field.set(this, null); + } } catch (IllegalAccessException e) { // ? } @@ -100,7 +143,8 @@ public final class FlingBehavior extends AppBarLayout.Behavior { private void stopAppBarLayoutFling() { OverScroller scroller = getScrollerField(); - if (scroller != null) scroller.forceFinished(true); + if (scroller != null) { + scroller.forceFinished(true); + } } - -} \ No newline at end of file +} diff --git a/app/src/main/java/org/schabi/newpipe/ActivityCommunicator.java b/app/src/main/java/org/schabi/newpipe/ActivityCommunicator.java index da601a42f..9321b3071 100644 --- a/app/src/main/java/org/schabi/newpipe/ActivityCommunicator.java +++ b/app/src/main/java/org/schabi/newpipe/ActivityCommunicator.java @@ -23,17 +23,25 @@ package org.schabi.newpipe; /** * Singleton: * Used to send data between certain Activity/Services within the same process. - * This can be considered as an ugly hack inside the Android universe. **/ + * This can be considered as an ugly hack inside the Android universe. + **/ public class ActivityCommunicator { private static ActivityCommunicator activityCommunicator; + private volatile Class returnActivity; public static ActivityCommunicator getCommunicator() { - if(activityCommunicator == null) { + if (activityCommunicator == null) { activityCommunicator = new ActivityCommunicator(); } return activityCommunicator; } - public volatile Class returnActivity; + public Class getReturnActivity() { + return returnActivity; + } + + public void setReturnActivity(final Class returnActivity) { + this.returnActivity = returnActivity; + } } diff --git a/app/src/main/java/org/schabi/newpipe/App.java b/app/src/main/java/org/schabi/newpipe/App.java index 7f050e6c7..531cb5a38 100644 --- a/app/src/main/java/org/schabi/newpipe/App.java +++ b/app/src/main/java/org/schabi/newpipe/App.java @@ -5,21 +5,20 @@ import android.app.Application; import android.app.NotificationChannel; import android.app.NotificationManager; import android.content.Context; +import android.content.SharedPreferences; import android.os.Build; import android.util.Log; -import androidx.annotation.Nullable; +import androidx.preference.PreferenceManager; import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.ImageLoaderConfiguration; -import com.squareup.leakcanary.LeakCanary; -import com.squareup.leakcanary.RefWatcher; import org.acra.ACRA; -import org.acra.config.ACRAConfiguration; import org.acra.config.ACRAConfigurationException; -import org.acra.config.ConfigurationBuilder; +import org.acra.config.CoreConfiguration; +import org.acra.config.CoreConfigurationBuilder; import org.acra.sender.ReportSenderFactory; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.downloader.Downloader; @@ -27,7 +26,7 @@ import org.schabi.newpipe.report.AcraReportSenderFactory; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.settings.SettingsActivity; -import org.schabi.newpipe.util.ExtractorHelper; +import org.schabi.newpipe.util.ExceptionUtils; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.StateSaver; @@ -66,15 +65,17 @@ import io.reactivex.plugins.RxJavaPlugins; public class App extends Application { protected static final String TAG = App.class.toString(); - private RefWatcher refWatcher; - private static App app; - @SuppressWarnings("unchecked") private static final Class[] - reportSenderFactoryClasses = new Class[]{AcraReportSenderFactory.class}; + REPORT_SENDER_FACTORY_CLASSES = new Class[]{AcraReportSenderFactory.class}; + private static App app; + + public static App getApp() { + return app; + } @Override - protected void attachBaseContext(Context base) { + protected void attachBaseContext(final Context base) { super.attachBaseContext(base); initACRA(); @@ -84,13 +85,6 @@ public class App extends Application { public void onCreate() { super.onCreate(); - if (LeakCanary.isInAnalyzerProcess(this)) { - // This process is dedicated to LeakCanary for heap analysis. - // You should not init your app in this process. - return; - } - refWatcher = installLeakCanary(); - app = this; // Initialize settings first because others inits can use its values @@ -99,7 +93,7 @@ public class App extends Application { NewPipe.init(getDownloader(), Localization.getPreferredLocalization(this), Localization.getPreferredContentCountry(this)); - Localization.init(); + Localization.init(getApplicationContext()); StateSaver.init(this); initNotificationChannel(); @@ -116,31 +110,47 @@ public class App extends Application { } protected Downloader getDownloader() { - return DownloaderImpl.init(null); + DownloaderImpl downloader = DownloaderImpl.init(null); + setCookiesToDownloader(downloader); + return downloader; + } + + protected void setCookiesToDownloader(final DownloaderImpl downloader) { + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences( + getApplicationContext()); + final String key = getApplicationContext().getString(R.string.recaptcha_cookies_key); + downloader.setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, prefs.getString(key, "")); + downloader.updateYoutubeRestrictedModeCookies(getApplicationContext()); } private void configureRxJavaErrorHandler() { // https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling RxJavaPlugins.setErrorHandler(new Consumer() { @Override - public void accept(@NonNull Throwable throwable) { - Log.e(TAG, "RxJavaPlugins.ErrorHandler called with -> : " + - "throwable = [" + throwable.getClass().getName() + "]"); + public void accept(@NonNull final Throwable throwable) { + Log.e(TAG, "RxJavaPlugins.ErrorHandler called with -> : " + + "throwable = [" + throwable.getClass().getName() + "]"); + final Throwable actualThrowable; if (throwable instanceof UndeliverableException) { - // As UndeliverableException is a wrapper, get the cause of it to get the "real" exception - throwable = throwable.getCause(); + // As UndeliverableException is a wrapper, + // get the cause of it to get the "real" exception + actualThrowable = throwable.getCause(); + } else { + actualThrowable = throwable; } final List errors; - if (throwable instanceof CompositeException) { - errors = ((CompositeException) throwable).getExceptions(); + if (actualThrowable instanceof CompositeException) { + errors = ((CompositeException) actualThrowable).getExceptions(); } else { - errors = Collections.singletonList(throwable); + errors = Collections.singletonList(actualThrowable); } for (final Throwable error : errors) { - if (isThrowableIgnored(error)) return; + if (isThrowableIgnored(error)) { + return; + } if (isThrowableCritical(error)) { reportException(error); return; @@ -150,22 +160,24 @@ public class App extends Application { // Out-of-lifecycle exceptions should only be reported if a debug user wishes so, // When exception is not reported, log it if (isDisposedRxExceptionsReported()) { - reportException(throwable); + reportException(actualThrowable); } else { - Log.e(TAG, "RxJavaPlugin: Undeliverable Exception received: ", throwable); + Log.e(TAG, "RxJavaPlugin: Undeliverable Exception received: ", actualThrowable); } } private boolean isThrowableIgnored(@NonNull final Throwable throwable) { // Don't crash the application over a simple network problem - return ExtractorHelper.hasAssignableCauseThrowable(throwable, - IOException.class, SocketException.class, // network api cancellation - InterruptedException.class, InterruptedIOException.class); // blocking code disposed + return ExceptionUtils.hasAssignableCause(throwable, + // network api cancellation + IOException.class, SocketException.class, + // blocking code disposed + InterruptedException.class, InterruptedIOException.class); } private boolean isThrowableCritical(@NonNull final Throwable throwable) { // Though these exceptions cannot be ignored - return ExtractorHelper.hasAssignableCauseThrowable(throwable, + return ExceptionUtils.hasAssignableCause(throwable, NullPointerException.class, IllegalArgumentException.class, // bug in app OnErrorNotImplementedException.class, MissingBackpressureException.class, IllegalStateException.class); // bug in operator @@ -190,8 +202,8 @@ public class App extends Application { private void initACRA() { try { - final ACRAConfiguration acraConfig = new ConfigurationBuilder(this) - .setReportSenderFactoryClasses(reportSenderFactoryClasses) + final CoreConfiguration acraConfig = new CoreConfigurationBuilder(this) + .setReportSenderFactoryClasses(REPORT_SENDER_FACTORY_CLASSES) .setBuildConfigClass(BuildConfig.class) .build(); ACRA.init(this, acraConfig); @@ -202,7 +214,7 @@ public class App extends Application { null, null, ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none", - "Could not initialize ACRA crash report", R.string.app_ui_crash)); + "Could not initialize ACRA crash report", R.string.app_ui_crash)); } } @@ -230,11 +242,11 @@ public class App extends Application { /** * Set up notification channel for app update. + * * @param importance */ @TargetApi(Build.VERSION_CODES.O) - private void setUpUpdateNotificationChannel(int importance) { - + private void setUpUpdateNotificationChannel(final int importance) { final String appUpdateId = getString(R.string.app_update_notification_channel_id); final CharSequence appUpdateName @@ -251,21 +263,7 @@ public class App extends Application { appUpdateNotificationManager.createNotificationChannel(appUpdateChannel); } - @Nullable - public static RefWatcher getRefWatcher(Context context) { - final App application = (App) context.getApplicationContext(); - return application.refWatcher; - } - - protected RefWatcher installLeakCanary() { - return RefWatcher.DISABLED; - } - protected boolean isDisposedRxExceptionsReported() { return false; } - - public static App getApp() { - return app; - } } diff --git a/app/src/main/java/org/schabi/newpipe/BaseFragment.java b/app/src/main/java/org/schabi/newpipe/BaseFragment.java index d4795cde2..54513a0af 100644 --- a/app/src/main/java/org/schabi/newpipe/BaseFragment.java +++ b/app/src/main/java/org/schabi/newpipe/BaseFragment.java @@ -2,32 +2,31 @@ package org.schabi.newpipe; import android.content.Context; import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import androidx.appcompat.app.AppCompatActivity; import android.util.Log; import android.view.View; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; + import com.nostra13.universalimageloader.core.ImageLoader; -import com.squareup.leakcanary.RefWatcher; import icepick.Icepick; import icepick.State; +import leakcanary.AppWatcher; public abstract class BaseFragment extends Fragment { + public static final ImageLoader IMAGE_LOADER = ImageLoader.getInstance(); protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode()); protected final boolean DEBUG = MainActivity.DEBUG; - protected AppCompatActivity activity; - public static final ImageLoader imageLoader = ImageLoader.getInstance(); - - //These values are used for controlling framgents when they are part of the frontpage + //These values are used for controlling fragments when they are part of the frontpage @State protected boolean useAsFrontPage = false; - protected boolean mIsVisibleToUser = false; + private boolean mIsVisibleToUser = false; - public void useAsFrontPage(boolean value) { + public void useAsFrontPage(final boolean value) { useAsFrontPage = value; } @@ -36,7 +35,7 @@ public abstract class BaseFragment extends Fragment { //////////////////////////////////////////////////////////////////////////*/ @Override - public void onAttach(Context context) { + public void onAttach(final Context context) { super.onAttach(context); activity = (AppCompatActivity) context; } @@ -48,43 +47,49 @@ public abstract class BaseFragment extends Fragment { } @Override - public void onCreate(Bundle savedInstanceState) { - if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]"); + public void onCreate(final Bundle savedInstanceState) { + if (DEBUG) { + Log.d(TAG, "onCreate() called with: " + + "savedInstanceState = [" + savedInstanceState + "]"); + } super.onCreate(savedInstanceState); Icepick.restoreInstanceState(this, savedInstanceState); - if (savedInstanceState != null) onRestoreInstanceState(savedInstanceState); + if (savedInstanceState != null) { + onRestoreInstanceState(savedInstanceState); + } } @Override - public void onViewCreated(View rootView, Bundle savedInstanceState) { + public void onViewCreated(final View rootView, final Bundle savedInstanceState) { super.onViewCreated(rootView, savedInstanceState); if (DEBUG) { - Log.d(TAG, "onViewCreated() called with: rootView = [" + rootView + "], savedInstanceState = [" + savedInstanceState + "]"); + Log.d(TAG, "onViewCreated() called with: " + + "rootView = [" + rootView + "], " + + "savedInstanceState = [" + savedInstanceState + "]"); } initViews(rootView, savedInstanceState); initListeners(); } @Override - public void onSaveInstanceState(Bundle outState) { + public void onSaveInstanceState(final Bundle outState) { super.onSaveInstanceState(outState); Icepick.saveInstanceState(this, outState); } - protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { + protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) { } @Override public void onDestroy() { super.onDestroy(); - RefWatcher refWatcher = App.getRefWatcher(getActivity()); - if (refWatcher != null) refWatcher.watch(this); + AppWatcher.INSTANCE.getObjectWatcher().watch(this); } @Override - public void setUserVisibleHint(boolean isVisibleToUser) { + public void setUserVisibleHint(final boolean isVisibleToUser) { super.setUserVisibleHint(isVisibleToUser); mIsVisibleToUser = isVisibleToUser; } @@ -93,7 +98,7 @@ public abstract class BaseFragment extends Fragment { // Init //////////////////////////////////////////////////////////////////////////*/ - protected void initViews(View rootView, Bundle savedInstanceState) { + protected void initViews(final View rootView, final Bundle savedInstanceState) { } protected void initListeners() { @@ -103,10 +108,12 @@ public abstract class BaseFragment extends Fragment { // Utils //////////////////////////////////////////////////////////////////////////*/ - public void setTitle(String title) { - if (DEBUG) Log.d(TAG, "setTitle() called with: title = [" + title + "]"); - if((!useAsFrontPage || mIsVisibleToUser) - && (activity != null && activity.getSupportActionBar() != null)) { + public void setTitle(final String title) { + if (DEBUG) { + Log.d(TAG, "setTitle() called with: title = [" + title + "]"); + } + if ((!useAsFrontPage || mIsVisibleToUser) + && (activity != null && activity.getSupportActionBar() != null)) { activity.getSupportActionBar().setDisplayShowTitleEnabled(true); activity.getSupportActionBar().setTitle(title); } diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java index 22f7bc558..625f514e9 100644 --- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java +++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java @@ -12,12 +12,16 @@ import android.net.ConnectivityManager; import android.net.Uri; import android.os.AsyncTask; import android.preference.PreferenceManager; -import androidx.core.app.NotificationCompat; -import androidx.core.app.NotificationManagerCompat; import android.util.Log; -import org.json.JSONException; -import org.json.JSONObject; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; + +import com.grack.nanojson.JsonObject; +import com.grack.nanojson.JsonParser; +import com.grack.nanojson.JsonParserException; + +import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; @@ -30,11 +34,6 @@ import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; -import java.util.concurrent.TimeUnit; - -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; /** * AsyncTask to check if there is a newer version of the NewPipe github apk available or not. @@ -42,149 +41,44 @@ import okhttp3.Response; * the notification, the user will be directed to the download link. */ public class CheckForNewAppVersionTask extends AsyncTask { - private static final boolean DEBUG = MainActivity.DEBUG; private static final String TAG = CheckForNewAppVersionTask.class.getSimpleName(); - private static final Application app = App.getApp(); - private static final String GITHUB_APK_SHA1 = "B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15"; - private static final String newPipeApiUrl = "https://newpipe.schabi.org/api/data.json"; - private static final int timeoutPeriod = 30; - private SharedPreferences mPrefs; - private OkHttpClient client; - - @Override - protected void onPreExecute() { - - mPrefs = PreferenceManager.getDefaultSharedPreferences(app); - - // Check if user has enabled/ disabled update checking - // and if the current apk is a github one or not. - if (!mPrefs.getBoolean(app.getString(R.string.update_app_key), true) - || !isGithubApk()) { - this.cancel(true); - } - } - - @Override - protected String doInBackground(Void... voids) { - - if(isCancelled() || !isConnected()) return null; - - // Make a network request to get latest NewPipe data. - if (client == null) { - - client = new OkHttpClient - .Builder() - .readTimeout(timeoutPeriod, TimeUnit.SECONDS) - .build(); - } - - Request request = new Request.Builder() - .url(newPipeApiUrl) - .build(); - - try { - Response response = client.newCall(request).execute(); - return response.body().string(); - } catch (IOException ex) { - // connectivity problems, do not alarm user and fail silently - if (DEBUG) Log.w(TAG, Log.getStackTraceString(ex)); - } - - return null; - } - - @Override - protected void onPostExecute(String response) { - - // Parse the json from the response. - if (response != null) { - - try { - JSONObject mainObject = new JSONObject(response); - JSONObject flavoursObject = mainObject.getJSONObject("flavors"); - JSONObject githubObject = flavoursObject.getJSONObject("github"); - JSONObject githubStableObject = githubObject.getJSONObject("stable"); - - String versionName = githubStableObject.getString("version"); - String versionCode = githubStableObject.getString("version_code"); - String apkLocationUrl = githubStableObject.getString("apk"); - - compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode); - - } catch (JSONException ex) { - // connectivity problems, do not alarm user and fail silently - if (DEBUG) Log.w(TAG, Log.getStackTraceString(ex)); - } - } - } + private static final Application APP = App.getApp(); + private static final String GITHUB_APK_SHA1 + = "B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15"; + private static final String NEWPIPE_API_URL = "https://newpipe.schabi.org/api/data.json"; /** - * Method to compare the current and latest available app version. - * If a newer version is available, we show the update notification. - * @param versionName - * @param apkLocationUrl - */ - private void compareAppVersionAndShowNotification(String versionName, - String apkLocationUrl, - String versionCode) { - - int NOTIFICATION_ID = 2000; - - if (BuildConfig.VERSION_CODE < Integer.valueOf(versionCode)) { - - // A pending intent to open the apk location url in the browser. - Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl)); - PendingIntent pendingIntent - = PendingIntent.getActivity(app, 0, intent, 0); - - NotificationCompat.Builder notificationBuilder = new NotificationCompat - .Builder(app, app.getString(R.string.app_update_notification_channel_id)) - .setSmallIcon(R.drawable.ic_newpipe_update) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setContentIntent(pendingIntent) - .setAutoCancel(true) - .setContentTitle(app.getString(R.string.app_update_notification_content_title)) - .setContentText(app.getString(R.string.app_update_notification_content_text) - + " " + versionName); - - NotificationManagerCompat notificationManager = NotificationManagerCompat.from(app); - notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()); - } - } - - /** - * Method to get the apk's SHA1 key. - * https://stackoverflow.com/questions/9293019/get-certificate-fingerprint-from-android-app#22506133 + * Method to get the apk's SHA1 key. See https://stackoverflow.com/questions/9293019/#22506133. + * + * @return String with the apk's SHA1 fingeprint in hexadecimal */ private static String getCertificateSHA1Fingerprint() { - - PackageManager pm = app.getPackageManager(); - String packageName = app.getPackageName(); - int flags = PackageManager.GET_SIGNATURES; + final PackageManager pm = APP.getPackageManager(); + final String packageName = APP.getPackageName(); + final int flags = PackageManager.GET_SIGNATURES; PackageInfo packageInfo = null; try { packageInfo = pm.getPackageInfo(packageName, flags); - } catch (PackageManager.NameNotFoundException ex) { - ErrorActivity.reportError(app, ex, null, null, + } catch (PackageManager.NameNotFoundException e) { + ErrorActivity.reportError(APP, e, null, null, ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none", "Could not find package info", R.string.app_ui_crash)); } - Signature[] signatures = packageInfo.signatures; - byte[] cert = signatures[0].toByteArray(); - InputStream input = new ByteArrayInputStream(cert); + final Signature[] signatures = packageInfo.signatures; + final byte[] cert = signatures[0].toByteArray(); + final InputStream input = new ByteArrayInputStream(cert); - CertificateFactory cf = null; X509Certificate c = null; try { - cf = CertificateFactory.getInstance("X509"); + final CertificateFactory cf = CertificateFactory.getInstance("X509"); c = (X509Certificate) cf.generateCertificate(input); - } catch (CertificateException ex) { - ErrorActivity.reportError(app, ex, null, null, + } catch (CertificateException e) { + ErrorActivity.reportError(APP, e, null, null, ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none", "Certificate error", R.string.app_ui_crash)); } @@ -193,14 +87,10 @@ public class CheckForNewAppVersionTask extends AsyncTask { try { MessageDigest md = MessageDigest.getInstance("SHA1"); - byte[] publicKey = md.digest(c.getEncoded()); + final byte[] publicKey = md.digest(c.getEncoded()); hexString = byte2HexFormatted(publicKey); - } catch (NoSuchAlgorithmException ex1) { - ErrorActivity.reportError(app, ex1, null, null, - ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none", - "Could not retrieve SHA1 key", R.string.app_ui_crash)); - } catch (CertificateEncodingException ex2) { - ErrorActivity.reportError(app, ex2, null, null, + } catch (NoSuchAlgorithmException | CertificateEncodingException e) { + ErrorActivity.reportError(APP, e, null, null, ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none", "Could not retrieve SHA1 key", R.string.app_ui_crash)); } @@ -208,31 +98,124 @@ public class CheckForNewAppVersionTask extends AsyncTask { return hexString; } - private static String byte2HexFormatted(byte[] arr) { - - StringBuilder str = new StringBuilder(arr.length * 2); + private static String byte2HexFormatted(final byte[] arr) { + final StringBuilder str = new StringBuilder(arr.length * 2); for (int i = 0; i < arr.length; i++) { String h = Integer.toHexString(arr[i]); - int l = h.length(); - if (l == 1) h = "0" + h; - if (l > 2) h = h.substring(l - 2, l); + final int l = h.length(); + if (l == 1) { + h = "0" + h; + } + if (l > 2) { + h = h.substring(l - 2, l); + } str.append(h.toUpperCase()); - if (i < (arr.length - 1)) str.append(':'); + if (i < (arr.length - 1)) { + str.append(':'); + } } return str.toString(); } public static boolean isGithubApk() { - return getCertificateSHA1Fingerprint().equals(GITHUB_APK_SHA1); } - + + @Override + protected void onPreExecute() { + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(APP); + + // Check if user has enabled/disabled update checking + // and if the current apk is a github one or not. + if (!prefs.getBoolean(APP.getString(R.string.update_app_key), true) || !isGithubApk()) { + this.cancel(true); + } + } + + @Override + protected String doInBackground(final Void... voids) { + if (isCancelled() || !isConnected()) { + return null; + } + + // Make a network request to get latest NewPipe data. + try { + return DownloaderImpl.getInstance().get(NEWPIPE_API_URL).responseBody(); + } catch (IOException | ReCaptchaException e) { + // connectivity problems, do not alarm user and fail silently + if (DEBUG) { + Log.w(TAG, Log.getStackTraceString(e)); + } + } + + return null; + } + + @Override + protected void onPostExecute(final String response) { + // Parse the json from the response. + if (response != null) { + + try { + final JsonObject githubStableObject = JsonParser.object().from(response) + .getObject("flavors").getObject("github").getObject("stable"); + + final String versionName = githubStableObject.getString("version"); + final int versionCode = githubStableObject.getInt("version_code"); + final String apkLocationUrl = githubStableObject.getString("apk"); + + compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode); + + } catch (JsonParserException e) { + // connectivity problems, do not alarm user and fail silently + if (DEBUG) { + Log.w(TAG, Log.getStackTraceString(e)); + } + } + } + } + + /** + * Method to compare the current and latest available app version. + * If a newer version is available, we show the update notification. + * + * @param versionName Name of new version + * @param apkLocationUrl Url with the new apk + * @param versionCode Code of new version + */ + private void compareAppVersionAndShowNotification(final String versionName, + final String apkLocationUrl, + final int versionCode) { + int notificationId = 2000; + + if (BuildConfig.VERSION_CODE < versionCode) { + + // A pending intent to open the apk location url in the browser. + final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl)); + final PendingIntent pendingIntent + = PendingIntent.getActivity(APP, 0, intent, 0); + + final NotificationCompat.Builder notificationBuilder = new NotificationCompat + .Builder(APP, APP.getString(R.string.app_update_notification_channel_id)) + .setSmallIcon(R.drawable.ic_newpipe_update) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setContentIntent(pendingIntent) + .setAutoCancel(true) + .setContentTitle(APP.getString(R.string.app_update_notification_content_title)) + .setContentText(APP.getString(R.string.app_update_notification_content_text) + + " " + versionName); + + final NotificationManagerCompat notificationManager + = NotificationManagerCompat.from(APP); + notificationManager.notify(notificationId, notificationBuilder.build()); + } + } + private boolean isConnected() { - - ConnectivityManager cm = - (ConnectivityManager) app.getSystemService(Context.CONNECTIVITY_SERVICE); + final ConnectivityManager cm = + (ConnectivityManager) APP.getSystemService(Context.CONNECTIVITY_SERVICE); return cm.getActiveNetworkInfo() != null - && cm.getActiveNetworkInfo().isConnected(); + && cm.getActiveNetworkInfo().isConnected(); } } diff --git a/app/src/main/java/org/schabi/newpipe/DownloaderImpl.java b/app/src/main/java/org/schabi/newpipe/DownloaderImpl.java index 7e4ac304e..95d3c2b7c 100644 --- a/app/src/main/java/org/schabi/newpipe/DownloaderImpl.java +++ b/app/src/main/java/org/schabi/newpipe/DownloaderImpl.java @@ -1,12 +1,18 @@ package org.schabi.newpipe; +import android.content.Context; import android.os.Build; -import android.text.TextUtils; +import android.preference.PreferenceManager; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import org.schabi.newpipe.extractor.downloader.Downloader; import org.schabi.newpipe.extractor.downloader.Request; import org.schabi.newpipe.extractor.downloader.Response; import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; +import org.schabi.newpipe.util.CookieUtils; +import org.schabi.newpipe.util.InfoCache; import org.schabi.newpipe.util.TLSSocketFactoryCompat; import java.io.IOException; @@ -17,6 +23,7 @@ import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -26,9 +33,6 @@ import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - import okhttp3.CipherSuite; import okhttp3.ConnectionSpec; import okhttp3.OkHttpClient; @@ -37,42 +41,139 @@ import okhttp3.ResponseBody; import static org.schabi.newpipe.MainActivity.DEBUG; -public class DownloaderImpl extends Downloader { - public static final String USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:43.0) Gecko/20100101 Firefox/43.0"; +public final class DownloaderImpl extends Downloader { + public static final String USER_AGENT + = "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:68.0) Gecko/20100101 Firefox/68.0"; + public static final String YOUTUBE_RESTRICTED_MODE_COOKIE_KEY + = "youtube_restricted_mode_key"; + public static final String YOUTUBE_RESTRICTED_MODE_COOKIE = "PREF=f2=8000000"; + public static final String YOUTUBE_DOMAIN = "youtube.com"; private static DownloaderImpl instance; - private String mCookies; + private Map mCookies; private OkHttpClient client; - private DownloaderImpl(OkHttpClient.Builder builder) { + private DownloaderImpl(final OkHttpClient.Builder builder) { if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) { enableModernTLS(builder); } this.client = builder .readTimeout(30, TimeUnit.SECONDS) - //.cache(new Cache(new File(context.getExternalCacheDir(), "okhttp"), 16 * 1024 * 1024)) +// .cache(new Cache(new File(context.getExternalCacheDir(), "okhttp"), +// 16 * 1024 * 1024)) .build(); + this.mCookies = new HashMap<>(); } /** * It's recommended to call exactly once in the entire lifetime of the application. * * @param builder if null, default builder will be used + * @return a new instance of {@link DownloaderImpl} */ - public static DownloaderImpl init(@Nullable OkHttpClient.Builder builder) { - return instance = new DownloaderImpl(builder != null ? builder : new OkHttpClient.Builder()); + public static DownloaderImpl init(@Nullable final OkHttpClient.Builder builder) { + instance = new DownloaderImpl( + builder != null ? builder : new OkHttpClient.Builder()); + return instance; } public static DownloaderImpl getInstance() { return instance; } - public String getCookies() { - return mCookies; + /** + * Enable TLS 1.2 and 1.1 on Android Kitkat. This function is mostly taken + * from the documentation of OkHttpClient.Builder.sslSocketFactory(_,_). + *

+ * If there is an error, the function will safely fall back to doing nothing + * and printing the error to the console. + *

+ * + * @param builder The HTTPClient Builder on which TLS is enabled on (will be modified in-place) + */ + private static void enableModernTLS(final OkHttpClient.Builder builder) { + try { + // get the default TrustManager + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( + TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init((KeyStore) null); + TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); + if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) { + throw new IllegalStateException("Unexpected default trust managers:" + + Arrays.toString(trustManagers)); + } + X509TrustManager trustManager = (X509TrustManager) trustManagers[0]; + + // insert our own TLSSocketFactory + SSLSocketFactory sslSocketFactory = TLSSocketFactoryCompat.getInstance(); + + builder.sslSocketFactory(sslSocketFactory, trustManager); + + // This will try to enable all modern CipherSuites(+2 more) + // that are supported on the device. + // Necessary because some servers (e.g. Framatube.org) + // don't support the old cipher suites. + // https://github.com/square/okhttp/issues/4053#issuecomment-402579554 + List cipherSuites = new ArrayList<>(); + cipherSuites.addAll(ConnectionSpec.MODERN_TLS.cipherSuites()); + cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA); + cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA); + ConnectionSpec legacyTLS = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) + .cipherSuites(cipherSuites.toArray(new CipherSuite[0])) + .build(); + + builder.connectionSpecs(Arrays.asList(legacyTLS, ConnectionSpec.CLEARTEXT)); + } catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) { + if (DEBUG) { + e.printStackTrace(); + } + } } - public void setCookies(String cookies) { - mCookies = cookies; + public String getCookies(final String url) { + List resultCookies = new ArrayList<>(); + if (url.contains(YOUTUBE_DOMAIN)) { + String youtubeCookie = getCookie(YOUTUBE_RESTRICTED_MODE_COOKIE_KEY); + if (youtubeCookie != null) { + resultCookies.add(youtubeCookie); + } + } + // Recaptcha cookie is always added TODO: not sure if this is necessary + String recaptchaCookie = getCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY); + if (recaptchaCookie != null) { + resultCookies.add(recaptchaCookie); + } + return CookieUtils.concatCookies(resultCookies); + } + + public String getCookie(final String key) { + return mCookies.get(key); + } + + public void setCookie(final String key, final String cookie) { + mCookies.put(key, cookie); + } + + public void removeCookie(final String key) { + mCookies.remove(key); + } + + public void updateYoutubeRestrictedModeCookies(final Context context) { + String restrictedModeEnabledKey = + context.getString(R.string.youtube_restricted_mode_enabled); + boolean restrictedModeEnabled = PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean(restrictedModeEnabledKey, false); + updateYoutubeRestrictedModeCookies(restrictedModeEnabled); + } + + public void updateYoutubeRestrictedModeCookies(final boolean youtubeRestrictedModeEnabled) { + if (youtubeRestrictedModeEnabled) { + setCookie(YOUTUBE_RESTRICTED_MODE_COOKIE_KEY, + YOUTUBE_RESTRICTED_MODE_COOKIE); + } else { + removeCookie(YOUTUBE_RESTRICTED_MODE_COOKIE_KEY); + } + InfoCache.getInstance().clearCache(); } /** @@ -81,7 +182,7 @@ public class DownloaderImpl extends Downloader { * @param url an url pointing to the content * @return the size of the content, in bytes */ - public long getContentLength(String url) throws IOException { + public long getContentLength(final String url) throws IOException { try { final Response response = head(url); return Long.parseLong(response.getHeader("Content-Length")); @@ -92,14 +193,15 @@ public class DownloaderImpl extends Downloader { } } - public InputStream stream(String siteUrl) throws IOException { + public InputStream stream(final String siteUrl) throws IOException { try { final okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder() .method("GET", null).url(siteUrl) .addHeader("User-Agent", USER_AGENT); - if (!TextUtils.isEmpty(mCookies)) { - requestBuilder.addHeader("Cookie", mCookies); + String cookies = getCookies(siteUrl); + if (!cookies.isEmpty()) { + requestBuilder.addHeader("Cookie", cookies); } final okhttp3.Request request = requestBuilder.build(); @@ -122,7 +224,8 @@ public class DownloaderImpl extends Downloader { } @Override - public Response execute(@NonNull Request request) throws IOException, ReCaptchaException { + public Response execute(@NonNull final Request request) + throws IOException, ReCaptchaException { final String httpMethod = request.httpMethod(); final String url = request.url(); final Map> headers = request.headers(); @@ -137,8 +240,9 @@ public class DownloaderImpl extends Downloader { .method(httpMethod, requestBody).url(url) .addHeader("User-Agent", USER_AGENT); - if (!TextUtils.isEmpty(mCookies)) { - requestBuilder.addHeader("Cookie", mCookies); + String cookies = getCookies(url); + if (!cookies.isEmpty()) { + requestBuilder.addHeader("Cookie", cookies); } for (Map.Entry> pair : headers.entrySet()) { @@ -171,49 +275,8 @@ public class DownloaderImpl extends Downloader { responseBodyToReturn = body.string(); } - return new Response(response.code(), response.message(), response.headers().toMultimap(), responseBodyToReturn); - } - - /** - * Enable TLS 1.2 and 1.1 on Android Kitkat. This function is mostly taken from the documentation of - * OkHttpClient.Builder.sslSocketFactory(_,_) - *

- * If there is an error, the function will safely fall back to doing nothing and printing the error to the console. - * - * @param builder The HTTPClient Builder on which TLS is enabled on (will be modified in-place) - */ - private static void enableModernTLS(OkHttpClient.Builder builder) { - try { - // get the default TrustManager - TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( - TrustManagerFactory.getDefaultAlgorithm()); - trustManagerFactory.init((KeyStore) null); - TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); - if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) { - throw new IllegalStateException("Unexpected default trust managers:" - + Arrays.toString(trustManagers)); - } - X509TrustManager trustManager = (X509TrustManager) trustManagers[0]; - - // insert our own TLSSocketFactory - SSLSocketFactory sslSocketFactory = TLSSocketFactoryCompat.getInstance(); - - builder.sslSocketFactory(sslSocketFactory, trustManager); - - // This will try to enable all modern CipherSuites(+2 more) that are supported on the device. - // Necessary because some servers (e.g. Framatube.org) don't support the old cipher suites. - // https://github.com/square/okhttp/issues/4053#issuecomment-402579554 - List cipherSuites = new ArrayList<>(); - cipherSuites.addAll(ConnectionSpec.MODERN_TLS.cipherSuites()); - cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA); - cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA); - ConnectionSpec legacyTLS = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) - .cipherSuites(cipherSuites.toArray(new CipherSuite[0])) - .build(); - - builder.connectionSpecs(Arrays.asList(legacyTLS, ConnectionSpec.CLEARTEXT)); - } catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) { - if (DEBUG) e.printStackTrace(); - } + final String latestUrl = response.request().url().toString(); + return new Response(response.code(), response.message(), response.headers().toMultimap(), + responseBodyToReturn, latestUrl); } } diff --git a/app/src/main/java/org/schabi/newpipe/ExitActivity.java b/app/src/main/java/org/schabi/newpipe/ExitActivity.java index 1ea3abe34..94eff9560 100644 --- a/app/src/main/java/org/schabi/newpipe/ExitActivity.java +++ b/app/src/main/java/org/schabi/newpipe/ExitActivity.java @@ -1,4 +1,3 @@ - package org.schabi.newpipe; import android.annotation.SuppressLint; @@ -27,9 +26,20 @@ import android.os.Bundle; public class ExitActivity extends Activity { + public static void exitAndRemoveFromRecentApps(final Activity activity) { + Intent intent = new Intent(activity, ExitActivity.class); + + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS + | Intent.FLAG_ACTIVITY_CLEAR_TASK + | Intent.FLAG_ACTIVITY_NO_ANIMATION); + + activity.startActivity(intent); + } + @SuppressLint("NewApi") @Override - protected void onCreate(Bundle savedInstanceState) { + protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (Build.VERSION.SDK_INT >= 21) { @@ -40,15 +50,4 @@ public class ExitActivity extends Activity { System.exit(0); } - - public static void exitAndRemoveFromRecentApps(Activity activity) { - Intent intent = new Intent(activity, ExitActivity.class); - - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK - | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS - | Intent.FLAG_ACTIVITY_CLEAR_TASK - | Intent.FLAG_ACTIVITY_NO_ANIMATION); - - activity.startActivity(intent); - } } diff --git a/app/src/main/java/org/schabi/newpipe/ImageDownloader.java b/app/src/main/java/org/schabi/newpipe/ImageDownloader.java index dfb7d3276..ca61c9655 100644 --- a/app/src/main/java/org/schabi/newpipe/ImageDownloader.java +++ b/app/src/main/java/org/schabi/newpipe/ImageDownloader.java @@ -18,7 +18,7 @@ public class ImageDownloader extends BaseImageDownloader { private final SharedPreferences preferences; private final String downloadThumbnailKey; - public ImageDownloader(Context context) { + public ImageDownloader(final Context context) { super(context); this.resources = context.getResources(); this.preferences = PreferenceManager.getDefaultSharedPreferences(context); @@ -31,7 +31,7 @@ public class ImageDownloader extends BaseImageDownloader { @SuppressLint("ResourceType") @Override - public InputStream getStream(String imageUri, Object extra) throws IOException { + public InputStream getStream(final String imageUri, final Object extra) throws IOException { if (isDownloadingThumbnail()) { return super.getStream(imageUri, extra); } else { @@ -39,7 +39,8 @@ public class ImageDownloader extends BaseImageDownloader { } } - protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException { + protected InputStream getStreamFromNetwork(final String imageUri, final Object extra) + throws IOException { final DownloaderImpl downloader = (DownloaderImpl) NewPipe.getDownloader(); return downloader.stream(imageUri); } diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 2fb5d616a..7aa9cd9ff 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -53,31 +53,36 @@ import org.schabi.newpipe.fragments.MainFragment; import org.schabi.newpipe.fragments.detail.VideoDetailFragment; import org.schabi.newpipe.fragments.list.search.SearchFragment; import org.schabi.newpipe.player.VideoPlayer; +import org.schabi.newpipe.player.event.OnKeyDownListener; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.util.*; +import org.schabi.newpipe.views.FocusOverlayView; import java.util.ArrayList; import java.util.List; +import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; + public class MainActivity extends AppCompatActivity { private static final String TAG = "MainActivity"; public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release"); - private ActionBarDrawerToggle toggle = null; - private DrawerLayout drawer = null; - private NavigationView drawerItems = null; - private TextView headerServiceView = null; - private Button toggleServiceButton = null; + private ActionBarDrawerToggle toggle; + private DrawerLayout drawer; + private NavigationView drawerItems; + private ImageView headerServiceIcon; + private TextView headerServiceView; + private Button toggleServiceButton; private boolean servicesShown = false; private ImageView serviceArrow; - private static final int ITEM_ID_SUBSCRIPTIONS = - 1; - private static final int ITEM_ID_FEED = - 2; - private static final int ITEM_ID_BOOKMARKS = - 3; - private static final int ITEM_ID_DOWNLOADS = - 4; - private static final int ITEM_ID_HISTORY = - 5; + private static final int ITEM_ID_SUBSCRIPTIONS = -1; + private static final int ITEM_ID_FEED = -2; + private static final int ITEM_ID_BOOKMARKS = -3; + private static final int ITEM_ID_DOWNLOADS = -4; + private static final int ITEM_ID_HISTORY = -5; private static final int ITEM_ID_SETTINGS = 0; private static final int ITEM_ID_ABOUT = 1; @@ -88,25 +93,24 @@ public class MainActivity extends AppCompatActivity { //////////////////////////////////////////////////////////////////////////*/ @Override - protected void onCreate(Bundle savedInstanceState) { - if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]"); + protected void onCreate(final Bundle savedInstanceState) { + if (DEBUG) { + Log.d(TAG, "onCreate() called with: " + + "savedInstanceState = [" + savedInstanceState + "]"); + } // enable TLS1.1/1.2 for kitkat devices, to fix download and play for mediaCCC sources if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) { TLSSocketFactoryCompat.setAsDefault(); } - ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this)); + assureCorrectAppLanguage(this); super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - Window w = getWindow(); - w.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); - } - - if (getSupportFragmentManager() != null && getSupportFragmentManager().getBackStackEntryCount() == 0) { + if (getSupportFragmentManager() != null + && getSupportFragmentManager().getBackStackEntryCount() == 0) { initFragments(); } @@ -116,6 +120,10 @@ public class MainActivity extends AppCompatActivity { } catch (Exception e) { ErrorActivity.reportUiError(this, e); } + + if (AndroidTvUtils.isTv(this)) { + FocusOverlayView.setupFocusObserver(this); + } } private void setupDrawer() throws Exception { @@ -131,49 +139,52 @@ public class MainActivity extends AppCompatActivity { for (final String ks : service.getKioskList().getAvailableKiosks()) { drawerItems.getMenu() - .add(R.id.menu_tabs_group, kioskId, 0, KioskTranslator.getTranslatedKioskName(ks, this)) - .setIcon(KioskTranslator.getKioskIcons(ks, this)); - kioskId ++; + .add(R.id.menu_tabs_group, kioskId, 0, KioskTranslator + .getTranslatedKioskName(ks, this)) + .setIcon(KioskTranslator.getKioskIcon(ks, this)); + kioskId++; } drawerItems.getMenu() - .add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER, R.string.tab_subscriptions) + .add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER, + R.string.tab_subscriptions) .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_channel)); drawerItems.getMenu() - .add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_whats_new) - .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.rss)); + .add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_feed_title) + .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_rss)); drawerItems.getMenu() .add(R.id.menu_tabs_group, ITEM_ID_BOOKMARKS, ORDER, R.string.tab_bookmarks) .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_bookmark)); drawerItems.getMenu() .add(R.id.menu_tabs_group, ITEM_ID_DOWNLOADS, ORDER, R.string.downloads) - .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.download)); + .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_file_download)); drawerItems.getMenu() .add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history) - .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.history)); + .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_history)); //Settings and About drawerItems.getMenu() .add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings) - .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.settings)); + .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_settings)); drawerItems.getMenu() .add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about) - .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.info)); + .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_info_outline)); - toggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.drawer_open, R.string.drawer_close); + toggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.drawer_open, + R.string.drawer_close); toggle.syncState(); drawer.addDrawerListener(toggle); drawer.addDrawerListener(new DrawerLayout.SimpleDrawerListener() { private int lastService; @Override - public void onDrawerOpened(View drawerView) { + public void onDrawerOpened(final View drawerView) { lastService = ServiceHelper.getSelectedServiceId(MainActivity.this); } @Override - public void onDrawerClosed(View drawerView) { - if(servicesShown) { + public void onDrawerClosed(final View drawerView) { + if (servicesShown) { toggleServices(); } if (lastService != ServiceHelper.getSelectedServiceId(MainActivity.this)) { @@ -186,7 +197,7 @@ public class MainActivity extends AppCompatActivity { setupDrawerHeader(); } - private boolean drawerItemSelected(MenuItem item) { + private boolean drawerItemSelected(final MenuItem item) { switch (item.getGroupId()) { case R.id.menu_services_group: changeService(item); @@ -209,19 +220,21 @@ public class MainActivity extends AppCompatActivity { return true; } - private void changeService(MenuItem item) { - drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(false); + private void changeService(final MenuItem item) { + drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)) + .setChecked(false); ServiceHelper.setSelectedServiceId(this, item.getItemId()); - drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true); + drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)) + .setChecked(true); } - private void tabSelected(MenuItem item) throws ExtractionException { - switch(item.getItemId()) { + private void tabSelected(final MenuItem item) throws ExtractionException { + switch (item.getItemId()) { case ITEM_ID_SUBSCRIPTIONS: NavigationHelper.openSubscriptionFragment(getSupportFragmentManager()); break; case ITEM_ID_FEED: - NavigationHelper.openWhatsNewFragment(getSupportFragmentManager()); + NavigationHelper.openFeedFragment(getSupportFragmentManager()); break; case ITEM_ID_BOOKMARKS: NavigationHelper.openBookmarksFragment(getSupportFragmentManager()); @@ -239,19 +252,20 @@ public class MainActivity extends AppCompatActivity { int kioskId = 0; for (final String ks : service.getKioskList().getAvailableKiosks()) { - if(kioskId == item.getItemId()) { + if (kioskId == item.getItemId()) { serviceName = ks; } - kioskId ++; + kioskId++; } - NavigationHelper.openKioskFragment(getSupportFragmentManager(), currentServiceId, serviceName); + NavigationHelper.openKioskFragment(getSupportFragmentManager(), currentServiceId, + serviceName); break; } } - private void optionsAboutSelected(MenuItem item) { - switch(item.getItemId()) { + private void optionsAboutSelected(final MenuItem item) { + switch (item.getItemId()) { case ITEM_ID_SETTINGS: NavigationHelper.openSettings(this); break; @@ -263,14 +277,27 @@ public class MainActivity extends AppCompatActivity { private void setupDrawerHeader() { NavigationView navigationView = findViewById(R.id.navigation); - View hView = navigationView.getHeaderView(0); + View hView = navigationView.getHeaderView(0); serviceArrow = hView.findViewById(R.id.drawer_arrow); + headerServiceIcon = hView.findViewById(R.id.drawer_header_service_icon); headerServiceView = hView.findViewById(R.id.drawer_header_service_view); toggleServiceButton = hView.findViewById(R.id.drawer_header_action_button); - toggleServiceButton.setOnClickListener(view -> { - toggleServices(); - }); + toggleServiceButton.setOnClickListener(view -> toggleServices()); + + // If the current app name is bigger than the default "NewPipe" (7 chars), + // let the text view grow a little more as well. + if (getString(R.string.app_name).length() > "NewPipe".length()) { + final TextView headerTitle = hView.findViewById(R.id.drawer_header_newpipe_title); + final ViewGroup.LayoutParams layoutParams = headerTitle.getLayoutParams(); + layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT; + headerTitle.setLayoutParams(layoutParams); + headerTitle.setMaxLines(2); + headerTitle.setMinWidth(getResources() + .getDimensionPixelSize(R.dimen.drawer_header_newpipe_title_default_width)); + headerTitle.setMaxWidth(getResources() + .getDimensionPixelSize(R.dimen.drawer_header_newpipe_title_max_width)); + } } private void toggleServices() { @@ -280,8 +307,7 @@ public class MainActivity extends AppCompatActivity { drawerItems.getMenu().removeGroup(R.id.menu_tabs_group); drawerItems.getMenu().removeGroup(R.id.menu_options_about_group); - - if(servicesShown) { + if (servicesShown) { showServices(); } else { try { @@ -293,57 +319,64 @@ public class MainActivity extends AppCompatActivity { } private void showServices() { - serviceArrow.setImageResource(R.drawable.ic_arrow_up_white); + serviceArrow.setImageResource(R.drawable.ic_arrow_drop_up_white_24dp); - for(StreamingService s : NewPipe.getServices()) { - final String title = s.getServiceInfo().getName() + - (ServiceHelper.isBeta(s) ? " (beta)" : ""); + for (StreamingService s : NewPipe.getServices()) { + final String title = s.getServiceInfo().getName() + + (ServiceHelper.isBeta(s) ? " (beta)" : ""); MenuItem menuItem = drawerItems.getMenu() .add(R.id.menu_services_group, s.getServiceId(), ORDER, title) .setIcon(ServiceHelper.getIcon(s.getServiceId())); // peertube specifics - if(s.getServiceId() == 3){ + if (s.getServiceId() == 3) { enhancePeertubeMenu(s, menuItem); } } - drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true); + drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)) + .setChecked(true); } - private void enhancePeertubeMenu(StreamingService s, MenuItem menuItem) { + private void enhancePeertubeMenu(final StreamingService s, final MenuItem menuItem) { PeertubeInstance currentInstace = PeertubeHelper.getCurrentInstance(); menuItem.setTitle(currentInstace.getName() + (ServiceHelper.isBeta(s) ? " (beta)" : "")); - Spinner spinner = (Spinner) LayoutInflater.from(this).inflate(R.layout.instance_spinner_layout, null); + Spinner spinner = (Spinner) LayoutInflater.from(this) + .inflate(R.layout.instance_spinner_layout, null); List instances = PeertubeHelper.getInstanceList(this); List items = new ArrayList<>(); int defaultSelect = 0; - for(PeertubeInstance instance: instances){ + for (PeertubeInstance instance : instances) { items.add(instance.getName()); - if(instance.getUrl().equals(currentInstace.getUrl())){ - defaultSelect = items.size()-1; + if (instance.getUrl().equals(currentInstace.getUrl())) { + defaultSelect = items.size() - 1; } } - ArrayAdapter adapter = new ArrayAdapter<>(this, R.layout.instance_spinner_item, items); + ArrayAdapter adapter = new ArrayAdapter<>(this, + R.layout.instance_spinner_item, items); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spinner.setAdapter(adapter); spinner.setSelection(defaultSelect, false); spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { + public void onItemSelected(final AdapterView parent, final View view, + final int position, final long id) { PeertubeInstance newInstance = instances.get(position); - if(newInstance.getUrl().equals(PeertubeHelper.getCurrentInstance().getUrl())) return; + if (newInstance.getUrl().equals(PeertubeHelper.getCurrentInstance().getUrl())) { + return; + } PeertubeHelper.selectInstance(newInstance, getApplicationContext()); changeService(menuItem); drawer.closeDrawers(); new Handler(Looper.getMainLooper()).postDelayed(() -> { - getSupportFragmentManager().popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE); + getSupportFragmentManager().popBackStack(null, + FragmentManager.POP_BACK_STACK_INCLUSIVE); recreate(); }, 300); } @Override - public void onNothingSelected(AdapterView parent) { + public void onNothingSelected(final AdapterView parent) { } }); @@ -351,7 +384,7 @@ public class MainActivity extends AppCompatActivity { } private void showTabs() throws ExtractionException { - serviceArrow.setImageResource(R.drawable.ic_arrow_down_white); + serviceArrow.setImageResource(R.drawable.ic_arrow_drop_down_white_24dp); //Tabs int currentServiceId = ServiceHelper.getSelectedServiceId(this); @@ -361,34 +394,35 @@ public class MainActivity extends AppCompatActivity { for (final String ks : service.getKioskList().getAvailableKiosks()) { drawerItems.getMenu() - .add(R.id.menu_tabs_group, kioskId, ORDER, KioskTranslator.getTranslatedKioskName(ks, this)) - .setIcon(KioskTranslator.getKioskIcons(ks, this)); - kioskId ++; + .add(R.id.menu_tabs_group, kioskId, ORDER, + KioskTranslator.getTranslatedKioskName(ks, this)) + .setIcon(KioskTranslator.getKioskIcon(ks, this)); + kioskId++; } drawerItems.getMenu() .add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER, R.string.tab_subscriptions) .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_channel)); drawerItems.getMenu() - .add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_whats_new) - .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.rss)); + .add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_feed_title) + .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_rss)); drawerItems.getMenu() .add(R.id.menu_tabs_group, ITEM_ID_BOOKMARKS, ORDER, R.string.tab_bookmarks) .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_bookmark)); drawerItems.getMenu() .add(R.id.menu_tabs_group, ITEM_ID_DOWNLOADS, ORDER, R.string.downloads) - .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.download)); + .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_file_download)); drawerItems.getMenu() .add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history) - .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.history)); + .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_history)); //Settings and About drawerItems.getMenu() .add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings) - .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.settings)); + .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_settings)); drawerItems.getMenu() .add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about) - .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.info)); + .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_info_outline)); } @Override @@ -401,15 +435,21 @@ public class MainActivity extends AppCompatActivity { @Override protected void onResume() { + assureCorrectAppLanguage(this); + // Change the date format to match the selected language on resume + Localization.init(getApplicationContext()); super.onResume(); - // close drawer on return, and don't show animation, so its looks like the drawer isn't open - // when the user returns to MainActivity + // Close drawer on return, and don't show animation, + // so it looks like the drawer isn't open when the user returns to MainActivity drawer.closeDrawer(GravityCompat.START, false); try { - String selectedServiceName = NewPipe.getService( - ServiceHelper.getSelectedServiceId(this)).getServiceInfo().getName(); + final int selectedServiceId = ServiceHelper.getSelectedServiceId(this); + final String selectedServiceName = NewPipe.getService(selectedServiceId) + .getServiceInfo().getName(); headerServiceView.setText(selectedServiceName); + headerServiceIcon.setImageResource(ServiceHelper.getIcon(selectedServiceId)); + headerServiceView.post(() -> headerServiceView.setSelected(true)); toggleServiceButton.setContentDescription( getString(R.string.drawer_header_description) + selectedServiceName); @@ -419,28 +459,42 @@ public class MainActivity extends AppCompatActivity { SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); if (sharedPreferences.getBoolean(Constants.KEY_THEME_CHANGE, false)) { - if (DEBUG) Log.d(TAG, "Theme has changed, recreating activity..."); + if (DEBUG) { + Log.d(TAG, "Theme has changed, recreating activity..."); + } sharedPreferences.edit().putBoolean(Constants.KEY_THEME_CHANGE, false).apply(); - // https://stackoverflow.com/questions/10844112/runtimeexception-performing-pause-of-activity-that-is-not-resumed - // Briefly, let the activity resume properly posting the recreate call to end of the message queue + // https://stackoverflow.com/questions/10844112/ + // Briefly, let the activity resume + // properly posting the recreate call to end of the message queue new Handler(Looper.getMainLooper()).post(MainActivity.this::recreate); } if (sharedPreferences.getBoolean(Constants.KEY_MAIN_PAGE_CHANGE, false)) { - if (DEBUG) Log.d(TAG, "main page has changed, recreating main fragment..."); + if (DEBUG) { + Log.d(TAG, "main page has changed, recreating main fragment..."); + } sharedPreferences.edit().putBoolean(Constants.KEY_MAIN_PAGE_CHANGE, false).apply(); NavigationHelper.openMainActivity(this); } + + final boolean isHistoryEnabled = sharedPreferences.getBoolean( + getString(R.string.enable_watch_history_key), true); + drawerItems.getMenu().findItem(ITEM_ID_HISTORY).setVisible(isHistoryEnabled); } @Override - protected void onNewIntent(Intent intent) { - if (DEBUG) Log.d(TAG, "onNewIntent() called with: intent = [" + intent + "]"); + protected void onNewIntent(final Intent intent) { + if (DEBUG) { + Log.d(TAG, "onNewIntent() called with: intent = [" + intent + "]"); + } if (intent != null) { // Return if launched from a launcher (e.g. Nova Launcher, Pixel Launcher ...) // to not destroy the already created backstack String action = intent.getAction(); - if ((action != null && action.equals(Intent.ACTION_MAIN)) && intent.hasCategory(Intent.CATEGORY_LAUNCHER)) return; + if ((action != null && action.equals(Intent.ACTION_MAIN)) + && intent.hasCategory(Intent.CATEGORY_LAUNCHER)) { + return; + } } super.onNewIntent(intent); @@ -448,17 +502,33 @@ public class MainActivity extends AppCompatActivity { handleIntent(intent); } + @Override + public boolean onKeyDown(final int keyCode, final KeyEvent event) { + final Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_player_holder); + if (fragment instanceof OnKeyDownListener && !bottomSheetHiddenOrCollapsed()) { + // Provide keyDown event to fragment which then sends this event to the main player service + return ((OnKeyDownListener) fragment).onKeyDown(keyCode) || super.onKeyDown(keyCode, event); + } + return super.onKeyDown(keyCode, event); + } + @Override public void onBackPressed() { - if (DEBUG) Log.d(TAG, "onBackPressed() called"); + if (DEBUG) { + Log.d(TAG, "onBackPressed() called"); + } - final FrameLayout bottomSheetLayout = findViewById(R.id.fragment_player_holder); - final BottomSheetBehavior bottomSheetBehavior = BottomSheetBehavior.from(bottomSheetLayout); + if (AndroidTvUtils.isTv(this)) { + View drawerPanel = findViewById(R.id.navigation); + if (drawer.isDrawerOpen(drawerPanel)) { + drawer.closeDrawers(); + return; + } + } - final int sheetState = bottomSheetBehavior.getState(); - // In case bottomSheet is not visible on the screen or collapsed we can assume that the user interacts with a fragment - // inside fragment_holder so all back presses should be handled by it - if (sheetState == BottomSheetBehavior.STATE_HIDDEN || sheetState == BottomSheetBehavior.STATE_COLLAPSED) { + // In case bottomSheet is not visible on the screen or collapsed we can assume that the user + // interacts with a fragment inside fragment_holder so all back presses should be handled by it + if (bottomSheetHiddenOrCollapsed()) { final Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder); // If current fragment implements BackPressable (i.e. can/wanna handle back press) delegate the back press to it if (fragment instanceof BackPressable) { @@ -469,21 +539,27 @@ public class MainActivity extends AppCompatActivity { final Fragment fragmentPlayer = getSupportFragmentManager().findFragmentById(R.id.fragment_player_holder); // If current fragment implements BackPressable (i.e. can/wanna handle back press) delegate the back press to it if (fragmentPlayer instanceof BackPressable) { - if (!((BackPressable) fragmentPlayer).onBackPressed()) - bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); + if (!((BackPressable) fragmentPlayer).onBackPressed()) { + final FrameLayout bottomSheetLayout = findViewById(R.id.fragment_player_holder); + BottomSheetBehavior.from(bottomSheetLayout).setState(BottomSheetBehavior.STATE_COLLAPSED); + } return; } } if (getSupportFragmentManager().getBackStackEntryCount() == 1) { finish(); - } else super.onBackPressed(); + } else { + super.onBackPressed(); + } } @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - for (int i: grantResults){ - if (i == PackageManager.PERMISSION_DENIED){ + public void onRequestPermissionsResult(final int requestCode, + @NonNull final String[] permissions, + @NonNull final int[] grantResults) { + for (int i : grantResults) { + if (i == PackageManager.PERMISSION_DENIED) { return; } } @@ -537,16 +613,16 @@ public class MainActivity extends AppCompatActivity { //////////////////////////////////////////////////////////////////////////*/ @Override - public boolean onCreateOptionsMenu(Menu menu) { - if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "]"); + public boolean onCreateOptionsMenu(final Menu menu) { + if (DEBUG) { + Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "]"); + } super.onCreateOptionsMenu(menu); Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder); if (!(fragment instanceof SearchFragment)) { - findViewById(R.id.toolbar).findViewById(R.id.toolbar_search_container).setVisibility(View.GONE); - - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.main_menu, menu); + findViewById(R.id.toolbar).findViewById(R.id.toolbar_search_container) + .setVisibility(View.GONE); } ActionBar actionBar = getSupportActionBar(); @@ -560,22 +636,16 @@ public class MainActivity extends AppCompatActivity { } @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (DEBUG) Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]"); + public boolean onOptionsItemSelected(final MenuItem item) { + if (DEBUG) { + Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]"); + } int id = item.getItemId(); switch (id) { case android.R.id.home: onHomeButtonPressed(); return true; - case R.id.action_show_downloads: - return NavigationHelper.openDownloads(this); - case R.id.action_history: - NavigationHelper.openStatisticFragment(getSupportFragmentManager()); - return true; - case R.id.action_settings: - NavigationHelper.openSettings(this); - return true; default: return super.onOptionsItemSelected(item); } @@ -586,7 +656,9 @@ public class MainActivity extends AppCompatActivity { //////////////////////////////////////////////////////////////////////////*/ private void initFragments() { - if (DEBUG) Log.d(TAG, "initFragments() called"); + if (DEBUG) { + Log.d(TAG, "initFragments() called"); + } StateSaver.clearStateFiles(); if (getIntent() != null && getIntent().hasExtra(Constants.KEY_LINK_TYPE)) { // When user watch a video inside popup and then tries to open the video in main player while the app is closed @@ -595,7 +667,9 @@ public class MainActivity extends AppCompatActivity { NavigationHelper.openMainFragment(getSupportFragmentManager()); handleIntent(getIntent()); - } else NavigationHelper.gotoMainFragment(getSupportFragmentManager()); + } else { + NavigationHelper.gotoMainFragment(getSupportFragmentManager()); + } } /*////////////////////////////////////////////////////////////////////////// @@ -603,12 +677,14 @@ public class MainActivity extends AppCompatActivity { //////////////////////////////////////////////////////////////////////////*/ private void updateDrawerNavigation() { - if (getSupportActionBar() == null) return; + if (getSupportActionBar() == null) { + return; + } final Toolbar toolbar = findViewById(R.id.toolbar); - final DrawerLayout drawer = findViewById(R.id.drawer_layout); - final Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder); + final Fragment fragment = getSupportFragmentManager() + .findFragmentById(R.id.fragment_holder); if (fragment instanceof MainFragment) { getSupportActionBar().setDisplayHomeAsUpEnabled(false); if (toggle != null) { @@ -623,23 +699,18 @@ public class MainActivity extends AppCompatActivity { } } - private void updateDrawerHeaderString(String content) { - NavigationView navigationView = findViewById(R.id.navigation); - View hView = navigationView.getHeaderView(0); - Button action = hView.findViewById(R.id.drawer_header_action_button); - - action.setContentDescription(content); - } - - private void handleIntent(Intent intent) { + private void handleIntent(final Intent intent) { try { - if (DEBUG) Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]"); + if (DEBUG) { + Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]"); + } if (intent.hasExtra(Constants.KEY_LINK_TYPE)) { String url = intent.getStringExtra(Constants.KEY_URL); int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0); String title = intent.getStringExtra(Constants.KEY_TITLE); - switch (((StreamingService.LinkType) intent.getSerializableExtra(Constants.KEY_LINK_TYPE))) { + switch (((StreamingService.LinkType) intent + .getSerializableExtra(Constants.KEY_LINK_TYPE))) { case STREAM: boolean autoPlay = intent.getBooleanExtra(VideoDetailFragment.AUTO_PLAY, false); final String intentCacheKey = intent.getStringExtra(VideoPlayer.PLAY_QUEUE_KEY); @@ -661,7 +732,9 @@ public class MainActivity extends AppCompatActivity { } } else if (intent.hasExtra(Constants.KEY_OPEN_SEARCH)) { String searchString = intent.getStringExtra(Constants.KEY_SEARCH_STRING); - if (searchString == null) searchString = ""; + if (searchString == null) { + searchString = ""; + } int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0); NavigationHelper.openSearchFragment( getSupportFragmentManager(), @@ -675,4 +748,15 @@ public class MainActivity extends AppCompatActivity { ErrorActivity.reportUiError(this, e); } } + /* + * Utils + * */ + + private boolean bottomSheetHiddenOrCollapsed() { + final FrameLayout bottomSheetLayout = findViewById(R.id.fragment_player_holder); + final BottomSheetBehavior bottomSheetBehavior = BottomSheetBehavior.from(bottomSheetLayout); + + final int sheetState = bottomSheetBehavior.getState(); + return sheetState == BottomSheetBehavior.STATE_HIDDEN || sheetState == BottomSheetBehavior.STATE_COLLAPSED; + } } diff --git a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java index f3356d6e8..c59c48367 100644 --- a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java @@ -1,42 +1,54 @@ package org.schabi.newpipe; -import androidx.room.Room; import android.content.Context; +import android.database.Cursor; + import androidx.annotation.NonNull; +import androidx.room.Room; import org.schabi.newpipe.database.AppDatabase; import static org.schabi.newpipe.database.AppDatabase.DATABASE_NAME; -import static org.schabi.newpipe.database.Migrations.MIGRATION_11_12; +import static org.schabi.newpipe.database.Migrations.MIGRATION_1_2; +import static org.schabi.newpipe.database.Migrations.MIGRATION_2_3; public final class NewPipeDatabase { - private static volatile AppDatabase databaseInstance; private NewPipeDatabase() { //no instance } - private static AppDatabase getDatabase(Context context) { + private static AppDatabase getDatabase(final Context context) { return Room .databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME) - .addMigrations(MIGRATION_11_12) - .fallbackToDestructiveMigration() + .addMigrations(MIGRATION_1_2, MIGRATION_2_3) .build(); } @NonNull - public static AppDatabase getInstance(@NonNull Context context) { + public static AppDatabase getInstance(@NonNull final Context context) { AppDatabase result = databaseInstance; if (result == null) { synchronized (NewPipeDatabase.class) { result = databaseInstance; if (result == null) { - databaseInstance = (result = getDatabase(context)); + databaseInstance = getDatabase(context); + result = databaseInstance; } } } return result; } + + public static void checkpoint() { + if (databaseInstance == null) { + throw new IllegalStateException("database is not initialized"); + } + Cursor c = databaseInstance.query("pragma wal_checkpoint(full)", null); + if (c.moveToFirst() && c.getInt(0) == 1) { + throw new RuntimeException("Checkpoint was blocked from completing"); + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/PanicResponderActivity.java b/app/src/main/java/org/schabi/newpipe/PanicResponderActivity.java index 4118070d5..2e1abd598 100644 --- a/app/src/main/java/org/schabi/newpipe/PanicResponderActivity.java +++ b/app/src/main/java/org/schabi/newpipe/PanicResponderActivity.java @@ -1,4 +1,3 @@ - package org.schabi.newpipe; import android.annotation.SuppressLint; @@ -26,17 +25,18 @@ import android.os.Bundle; */ public class PanicResponderActivity extends Activity { - public static final String PANIC_TRIGGER_ACTION = "info.guardianproject.panic.action.TRIGGER"; @SuppressLint("NewApi") @Override - protected void onCreate(Bundle savedInstanceState) { + protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); Intent intent = getIntent(); if (intent != null && PANIC_TRIGGER_ACTION.equals(intent.getAction())) { - // TODO explicitly clear the search results once they are restored when the app restarts - // or if the app reloads the current video after being killed, that should be cleared also + // TODO: Explicitly clear the search results + // once they are restored when the app restarts + // or if the app reloads the current video after being killed, + // that should be cleared also ExitActivity.exitAndRemoveFromRecentApps(this); } diff --git a/app/src/main/java/org/schabi/newpipe/ReCaptchaActivity.java b/app/src/main/java/org/schabi/newpipe/ReCaptchaActivity.java index 0a2d51b53..40ea4fd58 100644 --- a/app/src/main/java/org/schabi/newpipe/ReCaptchaActivity.java +++ b/app/src/main/java/org/schabi/newpipe/ReCaptchaActivity.java @@ -1,20 +1,32 @@ package org.schabi.newpipe; -import android.app.Activity; import android.content.Intent; -import android.graphics.Bitmap; +import android.content.SharedPreferences; import android.os.Build; import android.os.Bundle; -import androidx.core.app.NavUtils; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; +import android.util.Log; +import android.view.Menu; import android.view.MenuItem; import android.webkit.CookieManager; +import android.webkit.WebResourceRequest; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.core.app.NavUtils; +import androidx.preference.PreferenceManager; + +import org.schabi.newpipe.util.ThemeHelper; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; + /* * Created by beneth on 06.12.16. * @@ -37,126 +49,200 @@ import android.webkit.WebViewClient; public class ReCaptchaActivity extends AppCompatActivity { public static final int RECAPTCHA_REQUEST = 10; public static final String RECAPTCHA_URL_EXTRA = "recaptcha_url_extra"; - public static final String TAG = ReCaptchaActivity.class.toString(); public static final String YT_URL = "https://www.youtube.com"; + public static final String RECAPTCHA_COOKIES_KEY = "recaptcha_cookies"; - private String url; + private WebView webView; + private String foundCookies = ""; @Override - protected void onCreate(Bundle savedInstanceState) { + protected void onCreate(final Bundle savedInstanceState) { + ThemeHelper.setTheme(this); super.onCreate(savedInstanceState); setContentView(R.layout.activity_recaptcha); + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); - url = getIntent().getStringExtra(RECAPTCHA_URL_EXTRA); + String url = getIntent().getStringExtra(RECAPTCHA_URL_EXTRA); if (url == null || url.isEmpty()) { url = YT_URL; } - - // Set return to Cancel by default + // set return to Cancel by default setResult(RESULT_CANCELED); - Toolbar toolbar = findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setDisplayHomeAsUpEnabled(true); - actionBar.setTitle(R.string.reCaptcha_title); - actionBar.setDisplayShowTitleEnabled(true); - } + webView = findViewById(R.id.reCaptchaWebView); - WebView myWebView = findViewById(R.id.reCaptchaWebView); - - // Enable Javascript - WebSettings webSettings = myWebView.getSettings(); + // enable Javascript + WebSettings webSettings = webView.getSettings(); webSettings.setJavaScriptEnabled(true); - ReCaptchaWebViewClient webClient = new ReCaptchaWebViewClient(this); - myWebView.setWebViewClient(webClient); + webView.setWebViewClient(new WebViewClient() { + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @Override + public boolean shouldOverrideUrlLoading(final WebView view, + final WebResourceRequest request) { + String url = request.getUrl().toString(); + if (MainActivity.DEBUG) { + Log.d(TAG, "shouldOverrideUrlLoading: request.url=" + url); + } - // Cleaning cache, history and cookies from webView - myWebView.clearCache(true); - myWebView.clearHistory(); + handleCookiesFromUrl(url); + return false; + } + + @Override + public boolean shouldOverrideUrlLoading(final WebView view, final String url) { + if (MainActivity.DEBUG) { + Log.d(TAG, "shouldOverrideUrlLoading: url=" + url); + } + + handleCookiesFromUrl(url); + return false; + } + + @Override + public void onPageFinished(final WebView view, final String url) { + super.onPageFinished(view, url); + handleCookiesFromUrl(url); + } + }); + + // cleaning cache, history and cookies from webView + webView.clearCache(true); + webView.clearHistory(); android.webkit.CookieManager cookieManager = CookieManager.getInstance(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - cookieManager.removeAllCookies(aBoolean -> {}); + cookieManager.removeAllCookies(aBoolean -> { + }); } else { cookieManager.removeAllCookie(); } - myWebView.loadUrl(url); - } - - private class ReCaptchaWebViewClient extends WebViewClient { - private final Activity context; - private String mCookies; - - ReCaptchaWebViewClient(Activity ctx) { - context = ctx; - } - - @Override - public void onPageStarted(WebView view, String url, Bitmap favicon) { - // TODO: Start Loader - super.onPageStarted(view, url, favicon); - } - - @Override - public void onPageFinished(WebView view, String url) { - String cookies = CookieManager.getInstance().getCookie(url); - - // TODO: Stop Loader - - // find cookies : s_gl & goojf and Add cookies to Downloader - if (find_access_cookies(cookies)) { - // Give cookies to Downloader class - DownloaderImpl.getInstance().setCookies(mCookies); - - // Closing activity and return to parent - setResult(RESULT_OK); - finish(); - } - } - - private boolean find_access_cookies(String cookies) { - boolean ret = false; - String c_s_gl = ""; - String c_goojf = ""; - - String[] parts = cookies.split("; "); - for (String part : parts) { - if (part.trim().startsWith("s_gl")) { - c_s_gl = part.trim(); - } - if (part.trim().startsWith("goojf")) { - c_goojf = part.trim(); - } - } - if (c_s_gl.length() > 0 && c_goojf.length() > 0) { - ret = true; - //mCookies = c_s_gl + "; " + c_goojf; - // Youtube seems to also need the other cookies: - mCookies = cookies; - } - - return ret; - } + webView.loadUrl(url); } @Override - public boolean onOptionsItemSelected(MenuItem item) { + public boolean onCreateOptionsMenu(final Menu menu) { + getMenuInflater().inflate(R.menu.menu_recaptcha, menu); + + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(false); + actionBar.setTitle(R.string.title_activity_recaptcha); + actionBar.setSubtitle(R.string.subtitle_activity_recaptcha); + } + + return true; + } + + @Override + public void onBackPressed() { + saveCookiesAndFinish(); + } + + @Override + public boolean onOptionsItemSelected(final MenuItem item) { int id = item.getItemId(); switch (id) { - case android.R.id.home: { - Intent intent = new Intent(this, org.schabi.newpipe.MainActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - NavUtils.navigateUpTo(this, intent); + case R.id.menu_item_done: + saveCookiesAndFinish(); return true; - } default: return false; } } + + private void saveCookiesAndFinish() { + handleCookiesFromUrl(webView.getUrl()); // try to get cookies of unclosed page + if (MainActivity.DEBUG) { + Log.d(TAG, "saveCookiesAndFinish: foundCookies=" + foundCookies); + } + + if (!foundCookies.isEmpty()) { + // save cookies to preferences + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences( + getApplicationContext()); + final String key = getApplicationContext().getString(R.string.recaptcha_cookies_key); + prefs.edit().putString(key, foundCookies).apply(); + + // give cookies to Downloader class + DownloaderImpl.getInstance().setCookie(RECAPTCHA_COOKIES_KEY, foundCookies); + setResult(RESULT_OK); + } + + Intent intent = new Intent(this, org.schabi.newpipe.MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + NavUtils.navigateUpTo(this, intent); + } + + + private void handleCookiesFromUrl(@Nullable final String url) { + if (MainActivity.DEBUG) { + Log.d(TAG, "handleCookiesFromUrl: url=" + (url == null ? "null" : url)); + } + + if (url == null) { + return; + } + + String cookies = CookieManager.getInstance().getCookie(url); + handleCookies(cookies); + + // sometimes cookies are inside the url + int abuseStart = url.indexOf("google_abuse="); + if (abuseStart != -1) { + int abuseEnd = url.indexOf("+path"); + + try { + String abuseCookie = url.substring(abuseStart + 13, abuseEnd); + abuseCookie = URLDecoder.decode(abuseCookie, "UTF-8"); + handleCookies(abuseCookie); + } catch (UnsupportedEncodingException | StringIndexOutOfBoundsException e) { + if (MainActivity.DEBUG) { + e.printStackTrace(); + Log.d(TAG, "handleCookiesFromUrl: invalid google abuse starting at " + + abuseStart + " and ending at " + abuseEnd + " for url " + url); + } + } + } + } + + private void handleCookies(@Nullable final String cookies) { + if (MainActivity.DEBUG) { + Log.d(TAG, "handleCookies: cookies=" + (cookies == null ? "null" : cookies)); + } + + if (cookies == null) { + return; + } + + addYoutubeCookies(cookies); + // add here methods to extract cookies for other services + } + + private void addYoutubeCookies(@NonNull final String cookies) { + if (cookies.contains("s_gl=") || cookies.contains("goojf=") + || cookies.contains("VISITOR_INFO1_LIVE=") + || cookies.contains("GOOGLE_ABUSE_EXEMPTION=")) { + // youtube seems to also need the other cookies: + addCookie(cookies); + } + } + + private void addCookie(final String cookie) { + if (foundCookies.contains(cookie)) { + return; + } + + if (foundCookies.isEmpty() || foundCookies.endsWith("; ")) { + foundCookies += cookie; + } else if (foundCookies.endsWith(";")) { + foundCookies += " " + cookie; + } else { + foundCookies += "; " + cookie; + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java index 84e4a13b7..1ce35d6fe 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java @@ -9,12 +9,6 @@ import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.os.Bundle; import android.preference.PreferenceManager; -import androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.app.NotificationCompat; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; import android.text.TextUtils; import android.view.ContextThemeWrapper; import android.view.LayoutInflater; @@ -26,6 +20,13 @@ import android.widget.RadioButton; import android.widget.RadioGroup; import android.widget.Toast; +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.core.app.NotificationCompat; import androidx.fragment.app.FragmentManager; import org.schabi.newpipe.download.DownloadDialog; @@ -44,12 +45,15 @@ import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.report.UserAction; +import org.schabi.newpipe.util.AndroidTvUtils; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.ThemeHelper; +import org.schabi.newpipe.util.urlfinder.UrlFinder; +import org.schabi.newpipe.views.FocusOverlayView; import java.io.Serializable; import java.util.ArrayList; @@ -73,29 +77,31 @@ import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCap import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr; /** - * Get the url from the intent and open it in the chosen preferred player + * Get the url from the intent and open it in the chosen preferred player. */ public class RouterActivity extends AppCompatActivity { - + public static final String INTERNAL_ROUTE_KEY = "internalRoute"; + /** + * Removes invisible separators (\p{Z}) and punctuation characters including + * brackets (\p{P}). See http://www.regular-expressions.info/unicode.html for + * more details. + */ + private static final String REGEX_REMOVE_FROM_URL = "[\\p{Z}\\p{P}]"; + protected final CompositeDisposable disposables = new CompositeDisposable(); @State protected int currentServiceId = -1; - private StreamingService currentService; @State protected LinkType currentLinkType; @State protected int selectedRadioPosition = -1; protected int selectedPreviously = -1; - protected String currentUrl; protected boolean internalRoute = false; - protected final CompositeDisposable disposables = new CompositeDisposable(); - + private StreamingService currentService; private boolean selectionIsDownload = false; - public static final String internalRouteKey = "internalRoute"; - @Override - protected void onCreate(Bundle savedInstanceState) { + protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); Icepick.restoreInstanceState(this, savedInstanceState); @@ -108,14 +114,14 @@ public class RouterActivity extends AppCompatActivity { } } - internalRoute = getIntent().getBooleanExtra(internalRouteKey, false); + internalRoute = getIntent().getBooleanExtra(INTERNAL_ROUTE_KEY, false); setTheme(ThemeHelper.isLightThemeSelected(this) ? R.style.RouterActivityThemeLight : R.style.RouterActivityThemeDark); } @Override - protected void onSaveInstanceState(Bundle outState) { + protected void onSaveInstanceState(final Bundle outState) { super.onSaveInstanceState(outState); Icepick.saveInstanceState(this, outState); } @@ -134,7 +140,7 @@ public class RouterActivity extends AppCompatActivity { disposables.clear(); } - private void handleUrl(String url) { + private void handleUrl(final String url) { disposables.add(Observable .fromCallable(() -> { if (currentServiceId == -1) { @@ -159,13 +165,14 @@ public class RouterActivity extends AppCompatActivity { }, this::handleError)); } - private void handleError(Throwable error) { + private void handleError(final Throwable error) { error.printStackTrace(); if (error instanceof ExtractionException) { Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show(); } else { - ExtractorHelper.handleGeneralException(this, -1, null, error, UserAction.SOMETHING_ELSE, null); + ExtractorHelper.handleGeneralException(this, -1, null, error, + UserAction.SOMETHING_ELSE, null); } finish(); @@ -177,8 +184,11 @@ public class RouterActivity extends AppCompatActivity { } protected void onSuccess() { - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - final String selectedChoiceKey = preferences.getString(getString(R.string.preferred_open_action_key), getString(R.string.preferred_open_action_default)); + final SharedPreferences preferences = PreferenceManager + .getDefaultSharedPreferences(this); + final String selectedChoiceKey = preferences + .getString(getString(R.string.preferred_open_action_key), + getString(R.string.preferred_open_action_default)); final String showInfoKey = getString(R.string.show_info_key); final String videoPlayerKey = getString(R.string.video_player_key); @@ -188,7 +198,8 @@ public class RouterActivity extends AppCompatActivity { final String alwaysAskKey = getString(R.string.always_ask_open_action_key); if (selectedChoiceKey.equals(alwaysAskKey)) { - final List choices = getChoicesForService(currentService, currentLinkType); + final List choices + = getChoicesForService(currentService, currentLinkType); switch (choices.size()) { case 1: @@ -206,20 +217,26 @@ public class RouterActivity extends AppCompatActivity { } else if (selectedChoiceKey.equals(downloadKey)) { handleChoice(downloadKey); } else { - final boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false); - final boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false); - final boolean isVideoPlayerSelected = selectedChoiceKey.equals(videoPlayerKey) || selectedChoiceKey.equals(popupPlayerKey); + final boolean isExtVideoEnabled = preferences.getBoolean( + getString(R.string.use_external_video_player_key), false); + final boolean isExtAudioEnabled = preferences.getBoolean( + getString(R.string.use_external_audio_player_key), false); + final boolean isVideoPlayerSelected = selectedChoiceKey.equals(videoPlayerKey) + || selectedChoiceKey.equals(popupPlayerKey); final boolean isAudioPlayerSelected = selectedChoiceKey.equals(backgroundPlayerKey); if (currentLinkType != LinkType.STREAM) { - if (isExtAudioEnabled && isAudioPlayerSelected || isExtVideoEnabled && isVideoPlayerSelected) { - Toast.makeText(this, R.string.external_player_unsupported_link_type, Toast.LENGTH_LONG).show(); + if (isExtAudioEnabled && isAudioPlayerSelected + || isExtVideoEnabled && isVideoPlayerSelected) { + Toast.makeText(this, R.string.external_player_unsupported_link_type, + Toast.LENGTH_LONG).show(); handleChoice(showInfoKey); return; } } - final List capabilities = currentService.getServiceInfo().getMediaCapabilities(); + final List capabilities + = currentService.getServiceInfo().getMediaCapabilities(); boolean serviceSupportsChoice = false; if (isVideoPlayerSelected) { @@ -241,7 +258,8 @@ public class RouterActivity extends AppCompatActivity { final Context themeWrapperContext = getThemeWrapperContext(); final LayoutInflater inflater = LayoutInflater.from(themeWrapperContext); - final LinearLayout rootLayout = (LinearLayout) inflater.inflate(R.layout.preferred_player_dialog_view, null, false); + final LinearLayout rootLayout = (LinearLayout) inflater.inflate( + R.layout.preferred_player_dialog_view, null, false); final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list); final DialogInterface.OnClickListener dialogButtonsClickListener = (dialog, which) -> { @@ -252,7 +270,9 @@ public class RouterActivity extends AppCompatActivity { handleChoice(choice.key); if (which == DialogInterface.BUTTON_POSITIVE) { - preferences.edit().putString(getString(R.string.preferred_open_action_key), choice.key).apply(); + preferences.edit() + .putString(getString(R.string.preferred_open_action_key), choice.key) + .apply(); } }; @@ -263,7 +283,9 @@ public class RouterActivity extends AppCompatActivity { .setNegativeButton(R.string.just_once, dialogButtonsClickListener) .setPositiveButton(R.string.always, dialogButtonsClickListener) .setOnDismissListener((dialog) -> { - if (!selectionIsDownload) finish(); + if (!selectionIsDownload) { + finish(); + } }) .create(); @@ -272,10 +294,13 @@ public class RouterActivity extends AppCompatActivity { setDialogButtonsState(alertDialog, radioGroup.getCheckedRadioButtonId() != -1); }); - radioGroup.setOnCheckedChangeListener((group, checkedId) -> setDialogButtonsState(alertDialog, true)); + radioGroup.setOnCheckedChangeListener((group, checkedId) -> + setDialogButtonsState(alertDialog, true)); final View.OnClickListener radioButtonsClickListener = v -> { final int indexOfChild = radioGroup.indexOfChild(v); - if (indexOfChild == -1) return; + if (indexOfChild == -1) { + return; + } selectedPreviously = selectedRadioPosition; selectedRadioPosition = indexOfChild; @@ -287,18 +312,23 @@ public class RouterActivity extends AppCompatActivity { int id = 12345; for (AdapterChoiceItem item : choices) { - final RadioButton radioButton = (RadioButton) inflater.inflate(R.layout.list_radio_icon_item, null); + final RadioButton radioButton + = (RadioButton) inflater.inflate(R.layout.list_radio_icon_item, null); radioButton.setText(item.description); - radioButton.setCompoundDrawablesWithIntrinsicBounds(item.icon, 0, 0, 0); + radioButton.setCompoundDrawablesWithIntrinsicBounds( + AppCompatResources.getDrawable(getApplicationContext(), item.icon), + null, null, null); radioButton.setChecked(false); radioButton.setId(id++); - radioButton.setLayoutParams(new RadioGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + radioButton.setLayoutParams(new RadioGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); radioButton.setOnClickListener(radioButtonsClickListener); radioGroup.addView(radioButton); } if (selectedRadioPosition == -1) { - final String lastSelectedPlayer = preferences.getString(getString(R.string.preferred_open_action_last_selected_key), null); + final String lastSelectedPlayer = preferences.getString( + getString(R.string.preferred_open_action_last_selected_key), null); if (!TextUtils.isEmpty(lastSelectedPlayer)) { for (int i = 0; i < choices.size(); i++) { AdapterChoiceItem c = choices.get(i); @@ -317,48 +347,64 @@ public class RouterActivity extends AppCompatActivity { selectedPreviously = selectedRadioPosition; alertDialog.show(); + + if (AndroidTvUtils.isTv(this)) { + FocusOverlayView.setupFocusObserver(alertDialog); + } } - private List getChoicesForService(StreamingService service, LinkType linkType) { + private List getChoicesForService(final StreamingService service, + final LinkType linkType) { final Context context = getThemeWrapperContext(); final List returnList = new ArrayList<>(); - final List capabilities = service.getServiceInfo().getMediaCapabilities(); + final List capabilities + = service.getServiceInfo().getMediaCapabilities(); - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false); - boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false); + final SharedPreferences preferences = PreferenceManager + .getDefaultSharedPreferences(this); + boolean isExtVideoEnabled = preferences.getBoolean( + getString(R.string.use_external_video_player_key), false); + boolean isExtAudioEnabled = preferences.getBoolean( + getString(R.string.use_external_audio_player_key), false); - returnList.add(new AdapterChoiceItem(getString(R.string.show_info_key), getString(R.string.show_info), - resolveResourceIdFromAttr(context, R.attr.info))); + returnList.add(new AdapterChoiceItem(getString(R.string.show_info_key), + getString(R.string.show_info), + resolveResourceIdFromAttr(context, R.attr.ic_info_outline))); if (capabilities.contains(VIDEO) && !(isExtVideoEnabled && linkType != LinkType.STREAM)) { - returnList.add(new AdapterChoiceItem(getString(R.string.video_player_key), getString(R.string.video_player), - resolveResourceIdFromAttr(context, R.attr.play))); - returnList.add(new AdapterChoiceItem(getString(R.string.popup_player_key), getString(R.string.popup_player), - resolveResourceIdFromAttr(context, R.attr.popup))); + returnList.add(new AdapterChoiceItem(getString(R.string.video_player_key), + getString(R.string.video_player), + resolveResourceIdFromAttr(context, R.attr.ic_play_arrow))); + returnList.add(new AdapterChoiceItem(getString(R.string.popup_player_key), + getString(R.string.popup_player), + resolveResourceIdFromAttr(context, R.attr.ic_popup))); } if (capabilities.contains(AUDIO) && !(isExtAudioEnabled && linkType != LinkType.STREAM)) { - returnList.add(new AdapterChoiceItem(getString(R.string.background_player_key), getString(R.string.background_player), - resolveResourceIdFromAttr(context, R.attr.audio))); + returnList.add(new AdapterChoiceItem(getString(R.string.background_player_key), + getString(R.string.background_player), + resolveResourceIdFromAttr(context, R.attr.ic_headset))); } - returnList.add(new AdapterChoiceItem(getString(R.string.download_key), getString(R.string.download), - resolveResourceIdFromAttr(context, R.attr.download))); + returnList.add(new AdapterChoiceItem(getString(R.string.download_key), + getString(R.string.download), + resolveResourceIdFromAttr(context, R.attr.ic_file_download))); return returnList; } private Context getThemeWrapperContext() { - return new ContextThemeWrapper(this, - ThemeHelper.isLightThemeSelected(this) ? R.style.LightTheme : R.style.DarkTheme); + return new ContextThemeWrapper(this, ThemeHelper.isLightThemeSelected(this) + ? R.style.LightTheme : R.style.DarkTheme); } - private void setDialogButtonsState(AlertDialog dialog, boolean state) { + private void setDialogButtonsState(final AlertDialog dialog, final boolean state) { final Button negativeButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE); final Button positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE); - if (negativeButton == null || positiveButton == null) return; + if (negativeButton == null || positiveButton == null) { + return; + } negativeButton.setEnabled(state); positiveButton.setEnabled(state); @@ -374,21 +420,25 @@ public class RouterActivity extends AppCompatActivity { } private void handleChoice(final String selectedChoiceKey) { - final List validChoicesList = Arrays.asList(getResources().getStringArray(R.array.preferred_open_action_values_list)); + final List validChoicesList = Arrays.asList(getResources() + .getStringArray(R.array.preferred_open_action_values_list)); if (validChoicesList.contains(selectedChoiceKey)) { PreferenceManager.getDefaultSharedPreferences(this).edit() - .putString(getString(R.string.preferred_open_action_last_selected_key), selectedChoiceKey) + .putString(getString( + R.string.preferred_open_action_last_selected_key), selectedChoiceKey) .apply(); } - if (selectedChoiceKey.equals(getString(R.string.popup_player_key)) && !PermissionHelper.isPopupEnabled(this)) { + if (selectedChoiceKey.equals(getString(R.string.popup_player_key)) + && !PermissionHelper.isPopupEnabled(this)) { PermissionHelper.showPopupEnablementToast(this); finish(); return; } if (selectedChoiceKey.equals(getString(R.string.download_key))) { - if (PermissionHelper.checkStoragePermissions(this, PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) { + if (PermissionHelper.checkStoragePermissions(this, + PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) { selectionIsDownload = true; openDownloadDialog(); } @@ -416,7 +466,8 @@ public class RouterActivity extends AppCompatActivity { } final Intent intent = new Intent(this, FetcherService.class); - final Choice choice = new Choice(currentService.getServiceId(), currentLinkType, currentUrl, selectedChoiceKey); + final Choice choice = new Choice(currentService.getServiceId(), currentLinkType, + currentUrl, selectedChoiceKey); intent.putExtra(FetcherService.KEY_CHOICE, choice); startService(intent); @@ -429,12 +480,11 @@ public class RouterActivity extends AppCompatActivity { .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe((@NonNull StreamInfo result) -> { - List sortedVideoStreams = ListHelper.getSortedStreamVideosList(this, - result.getVideoStreams(), - result.getVideoOnlyStreams(), - false); - int selectedVideoStreamIndex = ListHelper.getDefaultResolutionIndex(this, - sortedVideoStreams); + List sortedVideoStreams = ListHelper + .getSortedStreamVideosList(this, result.getVideoStreams(), + result.getVideoOnlyStreams(), false); + int selectedVideoStreamIndex = ListHelper + .getDefaultResolutionIndex(this, sortedVideoStreams); FragmentManager fm = getSupportFragmentManager(); DownloadDialog downloadDialog = DownloadDialog.newInstance(result); @@ -452,7 +502,9 @@ public class RouterActivity extends AppCompatActivity { } @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + public void onRequestPermissionsResult(final int requestCode, + @NonNull final String[] permissions, + @NonNull final int[] grantResults) { for (int i : grantResults) { if (i == PackageManager.PERMISSION_DENIED) { finish(); @@ -464,196 +516,10 @@ public class RouterActivity extends AppCompatActivity { } } - private static class AdapterChoiceItem { - final String description, key; - @DrawableRes - final int icon; - - AdapterChoiceItem(String key, String description, int icon) { - this.description = description; - this.key = key; - this.icon = icon; - } - } - - private static class Choice implements Serializable { - final int serviceId; - final String url, playerChoice; - final LinkType linkType; - - Choice(int serviceId, LinkType linkType, String url, String playerChoice) { - this.serviceId = serviceId; - this.linkType = linkType; - this.url = url; - this.playerChoice = playerChoice; - } - - @Override - public String toString() { - return serviceId + ":" + url + " > " + linkType + " ::: " + playerChoice; - } - } - /*////////////////////////////////////////////////////////////////////////// // Service Fetcher //////////////////////////////////////////////////////////////////////////*/ - public static class FetcherService extends IntentService { - - private static final int ID = 456; - public static final String KEY_CHOICE = "key_choice"; - private Disposable fetcher; - - public FetcherService() { - super(FetcherService.class.getSimpleName()); - } - - @Override - public void onCreate() { - super.onCreate(); - startForeground(ID, createNotification().build()); - } - - @Override - protected void onHandleIntent(@Nullable Intent intent) { - if (intent == null) return; - - final Serializable serializable = intent.getSerializableExtra(KEY_CHOICE); - if (!(serializable instanceof Choice)) return; - Choice playerChoice = (Choice) serializable; - handleChoice(playerChoice); - } - - public void handleChoice(Choice choice) { - Single single = null; - UserAction userAction = UserAction.SOMETHING_ELSE; - - switch (choice.linkType) { - case STREAM: - single = ExtractorHelper.getStreamInfo(choice.serviceId, choice.url, false); - userAction = UserAction.REQUESTED_STREAM; - break; - case CHANNEL: - single = ExtractorHelper.getChannelInfo(choice.serviceId, choice.url, false); - userAction = UserAction.REQUESTED_CHANNEL; - break; - case PLAYLIST: - single = ExtractorHelper.getPlaylistInfo(choice.serviceId, choice.url, false); - userAction = UserAction.REQUESTED_PLAYLIST; - break; - } - - - if (single != null) { - final UserAction finalUserAction = userAction; - final Consumer resultHandler = getResultHandler(choice); - fetcher = single - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(info -> { - resultHandler.accept(info); - if (fetcher != null) fetcher.dispose(); - }, throwable -> ExtractorHelper.handleGeneralException(this, - choice.serviceId, choice.url, throwable, finalUserAction, ", opened with " + choice.playerChoice)); - } - } - - public Consumer getResultHandler(Choice choice) { - return info -> { - final String videoPlayerKey = getString(R.string.video_player_key); - final String backgroundPlayerKey = getString(R.string.background_player_key); - final String popupPlayerKey = getString(R.string.popup_player_key); - - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false); - boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false); - ; - - PlayQueue playQueue; - String playerChoice = choice.playerChoice; - - if (info instanceof StreamInfo) { - if (playerChoice.equals(backgroundPlayerKey) && isExtAudioEnabled) { - NavigationHelper.playOnExternalAudioPlayer(this, (StreamInfo) info); - - } else if (playerChoice.equals(videoPlayerKey) && isExtVideoEnabled) { - NavigationHelper.playOnExternalVideoPlayer(this, (StreamInfo) info); - - } else { - playQueue = new SinglePlayQueue((StreamInfo) info); - - if (playerChoice.equals(videoPlayerKey)) { - openMainPlayer(playQueue, choice); - } else if (playerChoice.equals(backgroundPlayerKey)) { - NavigationHelper.enqueueOnBackgroundPlayer(this, playQueue, true); - } else if (playerChoice.equals(popupPlayerKey)) { - NavigationHelper.enqueueOnPopupPlayer(this, playQueue, true); - } - } - } - - if (info instanceof ChannelInfo || info instanceof PlaylistInfo) { - playQueue = info instanceof ChannelInfo ? new ChannelPlayQueue((ChannelInfo) info) : new PlaylistPlayQueue((PlaylistInfo) info); - - if (playerChoice.equals(videoPlayerKey)) { - openMainPlayer(playQueue, choice); - } else if (playerChoice.equals(backgroundPlayerKey)) { - NavigationHelper.playOnBackgroundPlayer(this, playQueue, true); - } else if (playerChoice.equals(popupPlayerKey)) { - NavigationHelper.playOnPopupPlayer(this, playQueue, true); - } - } - }; - } - - private void openMainPlayer(PlayQueue playQueue, Choice choice) { - NavigationHelper.playOnMainPlayer(this, playQueue, choice.linkType, - choice.url, "", true, true); - } - - @Override - public void onDestroy() { - super.onDestroy(); - stopForeground(true); - if (fetcher != null) fetcher.dispose(); - } - - private NotificationCompat.Builder createNotification() { - return new NotificationCompat.Builder(this, getString(R.string.notification_channel_id)) - .setOngoing(true) - .setSmallIcon(R.drawable.ic_newpipe_triangle_white) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setContentTitle(getString(R.string.preferred_player_fetcher_notification_title)) - .setContentText(getString(R.string.preferred_player_fetcher_notification_message)); - } - } - - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ - - /** - * Removes invisible separators (\p{Z}) and punctuation characters including - * brackets (\p{P}). See http://www.regular-expressions.info/unicode.html for - * more details. - */ - private final static String REGEX_REMOVE_FROM_URL = "[\\p{Z}\\p{P}]"; - - private String getUrl(Intent intent) { - // first gather data and find service - String videoUrl = null; - if (intent.getData() != null) { - // this means the video was called though another app - videoUrl = intent.getData().toString(); - } else if (intent.getStringExtra(Intent.EXTRA_TEXT) != null) { - //this means that vidoe was called through share menu - String extraText = intent.getStringExtra(Intent.EXTRA_TEXT); - final String[] uris = getUris(extraText); - videoUrl = uris.length > 0 ? uris[0] : null; - } - - return videoUrl; - } - private String removeHeadingGibberish(final String input) { int start = 0; for (int i = input.indexOf("://") - 1; i >= 0; i--) { @@ -662,9 +528,13 @@ public class RouterActivity extends AppCompatActivity { break; } } - return input.substring(start, input.length()); + return input.substring(start); } + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + private String trim(final String input) { if (input == null || input.length() < 1) { return input; @@ -674,7 +544,7 @@ public class RouterActivity extends AppCompatActivity { output = output.substring(1); } while (output.length() > 0 - && output.substring(output.length() - 1, output.length()).matches(REGEX_REMOVE_FROM_URL)) { + && output.substring(output.length() - 1).matches(REGEX_REMOVE_FROM_URL)) { output = output.substring(0, output.length() - 1); } return output; @@ -705,4 +575,200 @@ public class RouterActivity extends AppCompatActivity { } return result.toArray(new String[result.size()]); } + + private static class AdapterChoiceItem { + final String description; + final String key; + @DrawableRes + final int icon; + + AdapterChoiceItem(final String key, final String description, final int icon) { + this.description = description; + this.key = key; + this.icon = icon; + } + } + + private static class Choice implements Serializable { + final int serviceId; + final String url; + final String playerChoice; + final LinkType linkType; + + Choice(final int serviceId, final LinkType linkType, + final String url, final String playerChoice) { + this.serviceId = serviceId; + this.linkType = linkType; + this.url = url; + this.playerChoice = playerChoice; + } + + @Override + public String toString() { + return serviceId + ":" + url + " > " + linkType + " ::: " + playerChoice; + } + } + + public static class FetcherService extends IntentService { + + public static final String KEY_CHOICE = "key_choice"; + private static final int ID = 456; + private Disposable fetcher; + + public FetcherService() { + super(FetcherService.class.getSimpleName()); + } + + @Override + public void onCreate() { + super.onCreate(); + startForeground(ID, createNotification().build()); + } + + @Override + protected void onHandleIntent(@Nullable final Intent intent) { + if (intent == null) { + return; + } + + final Serializable serializable = intent.getSerializableExtra(KEY_CHOICE); + if (!(serializable instanceof Choice)) { + return; + } + Choice playerChoice = (Choice) serializable; + handleChoice(playerChoice); + } + + public void handleChoice(final Choice choice) { + Single single = null; + UserAction userAction = UserAction.SOMETHING_ELSE; + + switch (choice.linkType) { + case STREAM: + single = ExtractorHelper.getStreamInfo(choice.serviceId, choice.url, false); + userAction = UserAction.REQUESTED_STREAM; + break; + case CHANNEL: + single = ExtractorHelper.getChannelInfo(choice.serviceId, choice.url, false); + userAction = UserAction.REQUESTED_CHANNEL; + break; + case PLAYLIST: + single = ExtractorHelper.getPlaylistInfo(choice.serviceId, choice.url, false); + userAction = UserAction.REQUESTED_PLAYLIST; + break; + } + + + if (single != null) { + final UserAction finalUserAction = userAction; + final Consumer resultHandler = getResultHandler(choice); + fetcher = single + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(info -> { + resultHandler.accept(info); + if (fetcher != null) { + fetcher.dispose(); + } + }, throwable -> ExtractorHelper.handleGeneralException(this, + choice.serviceId, choice.url, throwable, finalUserAction, + ", opened with " + choice.playerChoice)); + } + } + + public Consumer getResultHandler(final Choice choice) { + return info -> { + final String videoPlayerKey = getString(R.string.video_player_key); + final String backgroundPlayerKey = getString(R.string.background_player_key); + final String popupPlayerKey = getString(R.string.popup_player_key); + + final SharedPreferences preferences = PreferenceManager + .getDefaultSharedPreferences(this); + boolean isExtVideoEnabled = preferences.getBoolean( + getString(R.string.use_external_video_player_key), false); + boolean isExtAudioEnabled = preferences.getBoolean( + getString(R.string.use_external_audio_player_key), false); + + PlayQueue playQueue; + String playerChoice = choice.playerChoice; + + if (info instanceof StreamInfo) { + if (playerChoice.equals(backgroundPlayerKey) && isExtAudioEnabled) { + NavigationHelper.playOnExternalAudioPlayer(this, (StreamInfo) info); + + } else if (playerChoice.equals(videoPlayerKey) && isExtVideoEnabled) { + NavigationHelper.playOnExternalVideoPlayer(this, (StreamInfo) info); + + } else { + playQueue = new SinglePlayQueue((StreamInfo) info); + + if (playerChoice.equals(videoPlayerKey)) { + openMainPlayer(playQueue, choice); + } else if (playerChoice.equals(backgroundPlayerKey)) { + NavigationHelper.enqueueOnBackgroundPlayer(this, playQueue, true); + } else if (playerChoice.equals(popupPlayerKey)) { + NavigationHelper.enqueueOnPopupPlayer(this, playQueue, true); + } + } + } + + if (info instanceof ChannelInfo || info instanceof PlaylistInfo) { + playQueue = info instanceof ChannelInfo + ? new ChannelPlayQueue((ChannelInfo) info) + : new PlaylistPlayQueue((PlaylistInfo) info); + + if (playerChoice.equals(videoPlayerKey)) { + openMainPlayer(playQueue, choice); + } else if (playerChoice.equals(backgroundPlayerKey)) { + NavigationHelper.playOnBackgroundPlayer(this, playQueue, true); + } else if (playerChoice.equals(popupPlayerKey)) { + NavigationHelper.playOnPopupPlayer(this, playQueue, true); + } + } + }; + } + + private void openMainPlayer(PlayQueue playQueue, Choice choice) { + NavigationHelper.playOnMainPlayer(this, playQueue, choice.linkType, + choice.url, "", true, true); + } + + @Override + public void onDestroy() { + super.onDestroy(); + stopForeground(true); + if (fetcher != null) { + fetcher.dispose(); + } + } + + private NotificationCompat.Builder createNotification() { + return new NotificationCompat.Builder(this, getString(R.string.notification_channel_id)) + .setOngoing(true) + .setSmallIcon(R.drawable.ic_newpipe_triangle_white) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setContentTitle( + getString(R.string.preferred_player_fetcher_notification_title)) + .setContentText( + getString(R.string.preferred_player_fetcher_notification_message)); + } + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + @Nullable + private String getUrl(final Intent intent) { + String foundUrl = null; + if (intent.getData() != null) { + // Called from another app + foundUrl = intent.getData().toString(); + } else if (intent.getStringExtra(Intent.EXTRA_TEXT) != null) { + // Called from the share menu + final String extraText = intent.getStringExtra(Intent.EXTRA_TEXT); + foundUrl = UrlFinder.firstUrlFromInput(extraText); + } + + return foundUrl; + } } diff --git a/app/src/main/java/org/schabi/newpipe/about/AboutActivity.java b/app/src/main/java/org/schabi/newpipe/about/AboutActivity.java index 2326e795e..b5be2dde6 100644 --- a/app/src/main/java/org/schabi/newpipe/about/AboutActivity.java +++ b/app/src/main/java/org/schabi/newpipe/about/AboutActivity.java @@ -1,48 +1,67 @@ package org.schabi.newpipe.about; import android.content.Context; -import android.content.Intent; -import android.net.Uri; import android.os.Bundle; -import com.google.android.material.tabs.TabLayout; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentPagerAdapter; import androidx.fragment.app.FragmentStatePagerAdapter; import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.ViewPager; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; + +import com.google.android.material.tabs.TabLayout; import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.R; -import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.ThemeHelper; -public class AboutActivity extends AppCompatActivity { +import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; +import static org.schabi.newpipe.util.ShareUtils.openUrlInBrowser; +public class AboutActivity extends AppCompatActivity { /** - * List of all software components + * List of all software components. */ private static final SoftwareComponent[] SOFTWARE_COMPONENTS = new SoftwareComponent[]{ - new SoftwareComponent("Giga Get", "2014", "Peter Cai", "https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL2), - new SoftwareComponent("NewPipe Extractor", "2017", "Christian Schabesberger", "https://github.com/TeamNewPipe/NewPipeExtractor", StandardLicenses.GPL3), - new SoftwareComponent("Jsoup", "2017", "Jonathan Hedley", "https://github.com/jhy/jsoup", StandardLicenses.MIT), - new SoftwareComponent("Rhino", "2015", "Mozilla", "https://www.mozilla.org/rhino/", StandardLicenses.MPL2), - new SoftwareComponent("ACRA", "2013", "Kevin Gaudin", "http://www.acra.ch", StandardLicenses.APACHE2), - new SoftwareComponent("Universal Image Loader", "2011 - 2015", "Sergey Tarasevich", "https://github.com/nostra13/Android-Universal-Image-Loader", StandardLicenses.APACHE2), - new SoftwareComponent("CircleImageView", "2014 - 2017", "Henning Dodenhof", "https://github.com/hdodenhof/CircleImageView", StandardLicenses.APACHE2), - new SoftwareComponent("NoNonsense-FilePicker", "2016", "Jonas Kalderstam", "https://github.com/spacecowboy/NoNonsense-FilePicker", StandardLicenses.MPL2), - new SoftwareComponent("ExoPlayer", "2014-2017", "Google Inc", "https://github.com/google/ExoPlayer", StandardLicenses.APACHE2), - new SoftwareComponent("RxAndroid", "2015", "The RxAndroid authors", "https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2), - new SoftwareComponent("RxJava", "2016-present", "RxJava Contributors", "https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2), - new SoftwareComponent("RxBinding", "2015", "Jake Wharton", "https://github.com/JakeWharton/RxBinding", StandardLicenses.APACHE2) + new SoftwareComponent("Giga Get", "2014 - 2015", "Peter Cai", + "https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL2), + new SoftwareComponent("NewPipe Extractor", "2017 - 2020", "Christian Schabesberger", + "https://github.com/TeamNewPipe/NewPipeExtractor", StandardLicenses.GPL3), + new SoftwareComponent("Jsoup", "2017", "Jonathan Hedley", + "https://github.com/jhy/jsoup", StandardLicenses.MIT), + new SoftwareComponent("Rhino", "2015", "Mozilla", + "https://www.mozilla.org/rhino/", StandardLicenses.MPL2), + new SoftwareComponent("ACRA", "2013", "Kevin Gaudin", + "http://www.acra.ch", StandardLicenses.APACHE2), + new SoftwareComponent("Universal Image Loader", "2011 - 2015", "Sergey Tarasevich", + "https://github.com/nostra13/Android-Universal-Image-Loader", + StandardLicenses.APACHE2), + new SoftwareComponent("CircleImageView", "2014 - 2020", "Henning Dodenhof", + "https://github.com/hdodenhof/CircleImageView", StandardLicenses.APACHE2), + new SoftwareComponent("NoNonsense-FilePicker", "2016", "Jonas Kalderstam", + "https://github.com/spacecowboy/NoNonsense-FilePicker", StandardLicenses.MPL2), + new SoftwareComponent("ExoPlayer", "2014 - 2020", "Google Inc", + "https://github.com/google/ExoPlayer", StandardLicenses.APACHE2), + new SoftwareComponent("RxAndroid", "2015 - 2018", "The RxAndroid authors", + "https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2), + new SoftwareComponent("RxJava", "2016 - 2020", "RxJava Contributors", + "https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2), + new SoftwareComponent("RxBinding", "2015 - 2018", "Jake Wharton", + "https://github.com/JakeWharton/RxBinding", StandardLicenses.APACHE2), + new SoftwareComponent("PrettyTime", "2012 - 2020", "Lincoln Baxter, III", + "https://github.com/ocpsoft/prettytime", StandardLicenses.APACHE2), + new SoftwareComponent("Markwon", "2017 - 2020", "Noties", + "https://github.com/noties/Markwon", StandardLicenses.APACHE2), + new SoftwareComponent("Groupie", "2016", "Lisa Wray", + "https://github.com/lisawray/groupie", StandardLicenses.MIT) }; /** @@ -61,9 +80,11 @@ public class AboutActivity extends AppCompatActivity { private ViewPager mViewPager; @Override - protected void onCreate(Bundle savedInstanceState) { + protected void onCreate(final Bundle savedInstanceState) { + assureCorrectAppLanguage(this); super.onCreate(savedInstanceState); ThemeHelper.setTheme(this); + this.setTitle(getString(R.string.title_activity_about)); setContentView(R.layout.activity_about); @@ -82,28 +103,14 @@ public class AboutActivity extends AppCompatActivity { tabLayout.setupWithViewPager(mViewPager); } - @Override - public boolean onCreateOptionsMenu(Menu menu) { - // Inflate the menu; this adds items to the action bar if it is present. - getMenuInflater().inflate(R.menu.menu_about, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - + public boolean onOptionsItemSelected(final MenuItem item) { int id = item.getItemId(); switch (id) { case android.R.id.home: finish(); return true; - case R.id.action_settings: - NavigationHelper.openSettings(this); - return true; - case R.id.action_show_downloads: - return NavigationHelper.openDownloads(this); } return super.onOptionsItemSelected(item); @@ -113,21 +120,20 @@ public class AboutActivity extends AppCompatActivity { * A placeholder fragment containing a simple view. */ public static class AboutFragment extends Fragment { - - public AboutFragment() { - } + public AboutFragment() { } /** - * Returns a new instance of this fragment for the given section - * number. + * Created a new instance of this fragment for the given section number. + * + * @return New instance of {@link AboutFragment} */ public static AboutFragment newInstance() { return new AboutFragment(); } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + public View onCreateView(final LayoutInflater inflater, final ViewGroup container, + final Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_about, container, false); Context context = this.getContext(); @@ -135,40 +141,37 @@ public class AboutActivity extends AppCompatActivity { version.setText(BuildConfig.VERSION_NAME); View githubLink = rootView.findViewById(R.id.github_link); - githubLink.setOnClickListener(nv -> openWebsite(context.getString(R.string.github_url), context)); + githubLink.setOnClickListener(nv -> + openUrlInBrowser(context, context.getString(R.string.github_url))); View donationLink = rootView.findViewById(R.id.donation_link); - donationLink.setOnClickListener(v -> openWebsite(context.getString(R.string.donation_url), context)); + donationLink.setOnClickListener(v -> + openUrlInBrowser(context, context.getString(R.string.donation_url))); View websiteLink = rootView.findViewById(R.id.website_link); - websiteLink.setOnClickListener(nv -> openWebsite(context.getString(R.string.website_url), context)); + websiteLink.setOnClickListener(nv -> + openUrlInBrowser(context, context.getString(R.string.website_url))); View privacyPolicyLink = rootView.findViewById(R.id.privacy_policy_link); - privacyPolicyLink.setOnClickListener(v -> openWebsite(context.getString(R.string.privacy_policy_url), context)); + privacyPolicyLink.setOnClickListener(v -> + openUrlInBrowser(context, context.getString(R.string.privacy_policy_url))); return rootView; } - private void openWebsite(String url, Context context) { - Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - context.startActivity(intent); - } - } - /** * A {@link FragmentPagerAdapter} that returns a fragment corresponding to * one of the sections/tabs/pages. */ public class SectionsPagerAdapter extends FragmentPagerAdapter { - - public SectionsPagerAdapter(FragmentManager fm) { + public SectionsPagerAdapter(final FragmentManager fm) { super(fm); } @Override - public Fragment getItem(int position) { + public Fragment getItem(final int position) { switch (position) { case 0: return AboutFragment.newInstance(); @@ -185,7 +188,7 @@ public class AboutActivity extends AppCompatActivity { } @Override - public CharSequence getPageTitle(int position) { + public CharSequence getPageTitle(final int position) { switch (position) { case 0: return getString(R.string.tab_about); diff --git a/app/src/main/java/org/schabi/newpipe/about/License.java b/app/src/main/java/org/schabi/newpipe/about/License.java index e51e1d0f1..370009860 100644 --- a/app/src/main/java/org/schabi/newpipe/about/License.java +++ b/app/src/main/java/org/schabi/newpipe/about/License.java @@ -5,18 +5,17 @@ import android.os.Parcel; import android.os.Parcelable; /** - * A software license + * Class for storing information about a software license. */ public class License implements Parcelable { - public static final Creator CREATOR = new Creator() { @Override - public License createFromParcel(Parcel source) { + public License createFromParcel(final Parcel source) { return new License(source); } @Override - public License[] newArray(int size) { + public License[] newArray(final int size) { return new License[size]; } }; @@ -24,16 +23,22 @@ public class License implements Parcelable { private final String name; private String filename; - public License(String name, String abbreviation, String filename) { - if(name == null) throw new NullPointerException("name is null"); - if(abbreviation == null) throw new NullPointerException("abbreviation is null"); - if(filename == null) throw new NullPointerException("filename is null"); + public License(final String name, final String abbreviation, final String filename) { + if (name == null) { + throw new NullPointerException("name is null"); + } + if (abbreviation == null) { + throw new NullPointerException("abbreviation is null"); + } + if (filename == null) { + throw new NullPointerException("filename is null"); + } this.name = name; this.filename = filename; this.abbreviation = abbreviation; } - protected License(Parcel in) { + protected License(final Parcel in) { this.filename = in.readString(); this.abbreviation = in.readString(); this.name = in.readString(); @@ -50,7 +55,7 @@ public class License implements Parcelable { public String getAbbreviation() { return abbreviation; } - + public String getFilename() { return filename; } @@ -61,7 +66,7 @@ public class License implements Parcelable { } @Override - public void writeToParcel(Parcel dest, int flags) { + public void writeToParcel(final Parcel dest, final int flags) { dest.writeString(this.filename); dest.writeString(this.abbreviation); dest.writeString(this.name); diff --git a/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.java b/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.java index fe78ff9f1..bc6310601 100644 --- a/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.java +++ b/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.java @@ -2,29 +2,33 @@ package org.schabi.newpipe.about; import android.app.Activity; import android.content.Context; -import android.content.Intent; -import android.net.Uri; import android.os.Bundle; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; -import android.view.*; -import android.widget.TextView; + import org.schabi.newpipe.R; +import org.schabi.newpipe.util.ShareUtils; import java.util.Arrays; -import java.util.Comparator; /** - * Fragment containing the software licenses + * Fragment containing the software licenses. */ public class LicenseFragment extends Fragment { - private static final String ARG_COMPONENTS = "components"; private SoftwareComponent[] softwareComponents; - private SoftwareComponent mComponentForContextMenu; + private SoftwareComponent componentForContextMenu; - public static LicenseFragment newInstance(SoftwareComponent[] softwareComponents) { - if(softwareComponents == null) { + public static LicenseFragment newInstance(final SoftwareComponent[] softwareComponents) { + if (softwareComponents == null) { throw new NullPointerException("softwareComponents is null"); } LicenseFragment fragment = new LicenseFragment(); @@ -35,57 +39,50 @@ public class LicenseFragment extends Fragment { } /** - * Shows a popup containing the license + * Shows a popup containing the license. + * * @param context the context to use * @param license the license to show */ - public static void showLicense(Context context, License license) { + private static void showLicense(final Context context, final License license) { new LicenseFragmentHelper((Activity) context).execute(license); } @Override - public void onCreate(@Nullable Bundle savedInstanceState) { + public void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - softwareComponents = (SoftwareComponent[]) getArguments().getParcelableArray(ARG_COMPONENTS); + softwareComponents = (SoftwareComponent[]) getArguments() + .getParcelableArray(ARG_COMPONENTS); // Sort components by name - Arrays.sort(softwareComponents, new Comparator() { - @Override - public int compare(SoftwareComponent o1, SoftwareComponent o2) { - return o1.getName().compareTo(o2.getName()); - } - }); + Arrays.sort(softwareComponents, (o1, o2) -> o1.getName().compareTo(o2.getName())); } @Nullable @Override - public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - View rootView = inflater.inflate(R.layout.fragment_licenses, container, false); - ViewGroup softwareComponentsView = rootView.findViewById(R.id.software_components); + public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container, + @Nullable final Bundle savedInstanceState) { + final View rootView = inflater.inflate(R.layout.fragment_licenses, container, false); + final ViewGroup softwareComponentsView = rootView.findViewById(R.id.software_components); - View licenseLink = rootView.findViewById(R.id.app_read_license); - licenseLink.setOnClickListener(new OnReadFullLicenseClickListener()); + final View licenseLink = rootView.findViewById(R.id.app_read_license); + licenseLink.setOnClickListener(v -> + showLicense(getActivity(), StandardLicenses.GPL3)); for (final SoftwareComponent component : softwareComponents) { - View componentView = inflater.inflate(R.layout.item_software_component, container, false); - TextView softwareName = componentView.findViewById(R.id.name); - TextView copyright = componentView.findViewById(R.id.copyright); + final View componentView = inflater + .inflate(R.layout.item_software_component, container, false); + final TextView softwareName = componentView.findViewById(R.id.name); + final TextView copyright = componentView.findViewById(R.id.copyright); softwareName.setText(component.getName()); - copyright.setText(getContext().getString(R.string.copyright, + copyright.setText(getString(R.string.copyright, component.getYears(), component.getCopyrightOwner(), component.getLicense().getAbbreviation())); componentView.setTag(component); - componentView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Context context = v.getContext(); - if (context != null) { - showLicense(context, component.getLicense()); - } - } - }); + componentView.setOnClickListener(v -> + showLicense(getActivity(), component.getLicense())); softwareComponentsView.addView(componentView); registerForContextMenu(componentView); } @@ -93,41 +90,30 @@ public class LicenseFragment extends Fragment { } @Override - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { - MenuInflater inflater = getActivity().getMenuInflater(); - SoftwareComponent component = (SoftwareComponent) v.getTag(); + public void onCreateContextMenu(final ContextMenu menu, final View v, + final ContextMenu.ContextMenuInfo menuInfo) { + final MenuInflater inflater = getActivity().getMenuInflater(); + final SoftwareComponent component = (SoftwareComponent) v.getTag(); menu.setHeaderTitle(component.getName()); inflater.inflate(R.menu.software_component, menu); super.onCreateContextMenu(menu, v, menuInfo); - mComponentForContextMenu = (SoftwareComponent) v.getTag(); + componentForContextMenu = (SoftwareComponent) v.getTag(); } @Override - public boolean onContextItemSelected(MenuItem item) { + public boolean onContextItemSelected(final MenuItem item) { // item.getMenuInfo() is null so we use the tag of the view - final SoftwareComponent component = mComponentForContextMenu; + final SoftwareComponent component = componentForContextMenu; if (component == null) { return false; } switch (item.getItemId()) { case R.id.action_website: - openWebsite(component.getLink()); + ShareUtils.openUrlInBrowser(getActivity(), component.getLink()); return true; case R.id.action_show_license: - showLicense(getContext(), component.getLicense()); + showLicense(getActivity(), component.getLicense()); } return false; } - - private void openWebsite(String componentLink) { - Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(componentLink)); - startActivity(browserIntent); - } - - private static class OnReadFullLicenseClickListener implements View.OnClickListener { - @Override - public void onClick(View v) { - LicenseFragment.showLicense(v.getContext(), StandardLicenses.GPL3); - } - } } diff --git a/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.java b/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.java index eeafc1f57..1c425567f 100644 --- a/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.java +++ b/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.java @@ -2,30 +2,95 @@ package org.schabi.newpipe.about; import android.app.Activity; import android.content.Context; -import android.content.DialogInterface; import android.os.AsyncTask; +import android.util.Base64; +import android.webkit.WebView; + +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; -import android.webkit.WebView; + import org.schabi.newpipe.R; import org.schabi.newpipe.util.ThemeHelper; import java.io.BufferedReader; +import java.io.IOException; import java.io.InputStreamReader; import java.lang.ref.WeakReference; +import java.nio.charset.StandardCharsets; + +import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; public class LicenseFragmentHelper extends AsyncTask { - - final WeakReference weakReference; + private final WeakReference weakReference; private License license; - public LicenseFragmentHelper(@Nullable Activity activity) { + public LicenseFragmentHelper(@Nullable final Activity activity) { weakReference = new WeakReference<>(activity); } + /** + * @param context the context to use + * @param license the license + * @return String which contains a HTML formatted license page + * styled according to the context's theme + */ + private static String getFormattedLicense(@NonNull final Context context, + @NonNull final License license) { + final StringBuilder licenseContent = new StringBuilder(); + final String webViewData; + try { + final BufferedReader in = new BufferedReader(new InputStreamReader( + context.getAssets().open(license.getFilename()), StandardCharsets.UTF_8)); + String str; + while ((str = in.readLine()) != null) { + licenseContent.append(str); + } + in.close(); + + // split the HTML file and insert the stylesheet into the HEAD of the file + webViewData = licenseContent.toString().replace("", + ""); + } catch (IOException e) { + throw new IllegalArgumentException( + "Could not get license file: " + license.getFilename(), e); + } + return webViewData; + } + + /** + * @param context + * @return String which is a CSS stylesheet according to the context's theme + */ + private static String getLicenseStylesheet(final Context context) { + final boolean isLightTheme = ThemeHelper.isLightThemeSelected(context); + return "body{padding:12px 15px;margin:0;" + + "background:#" + getHexRGBColor(context, isLightTheme + ? R.color.light_license_background_color + : R.color.dark_license_background_color) + ";" + + "color:#" + getHexRGBColor(context, isLightTheme + ? R.color.light_license_text_color + : R.color.dark_license_text_color) + "}" + + "a[href]{color:#" + getHexRGBColor(context, isLightTheme + ? R.color.light_youtube_primary_color + : R.color.dark_youtube_primary_color) + "}" + + "pre{white-space:pre-wrap}"; + } + + /** + * Cast R.color to a hexadecimal color value. + * + * @param context the context to use + * @param color the color number from R.color + * @return a six characters long String with hexadecimal RGB values + */ + private static String getHexRGBColor(final Context context, final int color) { + return context.getResources().getString(color).substring(3); + } + @Nullable private Activity getActivity() { - Activity activity = weakReference.get(); + final Activity activity = weakReference.get(); if (activity != null && activity.isFinishing()) { return null; @@ -35,99 +100,29 @@ public class LicenseFragmentHelper extends AsyncTask { } @Override - protected Integer doInBackground(Object... objects) { + protected Integer doInBackground(final Object... objects) { license = (License) objects[0]; return 1; } @Override - protected void onPostExecute(Integer result) { - Activity activity = getActivity(); + protected void onPostExecute(final Integer result) { + final Activity activity = getActivity(); if (activity == null) { return; } - String webViewData = getFormattedLicense(activity, license); - AlertDialog.Builder alert = new AlertDialog.Builder(activity); + final String webViewData = Base64.encodeToString(getFormattedLicense(activity, license) + .getBytes(StandardCharsets.UTF_8), Base64.NO_PADDING); + final WebView webView = new WebView(activity); + webView.loadData(webViewData, "text/html; charset=UTF-8", "base64"); + + final AlertDialog.Builder alert = new AlertDialog.Builder(activity); alert.setTitle(license.getName()); - - WebView wv = new WebView(activity); - wv.loadData(webViewData, "text/html; charset=UTF-8", null); - - alert.setView(wv); - alert.setNegativeButton(android.R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - dialog.dismiss(); - } - }); + alert.setView(webView); + assureCorrectAppLanguage(activity); + alert.setNegativeButton(activity.getString(R.string.finish), + (dialog, which) -> dialog.dismiss()); alert.show(); } - - /** - * @param context the context to use - * @param license the license - * @return String which contains a HTML formatted license page styled according to the context's theme - */ - public static String getFormattedLicense(Context context, License license) { - if(context == null) { - throw new NullPointerException("context is null"); - } - if(license == null) { - throw new NullPointerException("license is null"); - } - - StringBuilder licenseContent = new StringBuilder(); - String webViewData; - try { - BufferedReader in = new BufferedReader(new InputStreamReader(context.getAssets().open(license.getFilename()), "UTF-8")); - String str; - while ((str = in.readLine()) != null) { - licenseContent.append(str); - } - in.close(); - - // split the HTML file and insert the stylesheet into the HEAD of the file - String[] insert = licenseContent.toString().split(""); - webViewData = insert[0] + "" - + insert[1]; - } catch (Exception e) { - throw new NullPointerException("could not get license file:" + getLicenseStylesheet(context)); - } - return webViewData; - } - - /** - * - * @param context - * @return String which is a CSS stylesheet according to the context's theme - */ - public static String getLicenseStylesheet(Context context) { - boolean isLightTheme = ThemeHelper.isLightThemeSelected(context); - return "body{padding:12px 15px;margin:0;background:#" - + getHexRGBColor(context, isLightTheme - ? R.color.light_license_background_color - : R.color.dark_license_background_color) - + ";color:#" - + getHexRGBColor(context, isLightTheme - ? R.color.light_license_text_color - : R.color.dark_license_text_color) + ";}" - + "a[href]{color:#" - + getHexRGBColor(context, isLightTheme - ? R.color.light_youtube_primary_color - : R.color.dark_youtube_primary_color) + ";}" - + "pre{white-space: pre-wrap;}"; - } - - /** - * Cast R.color to a hexadecimal color value - * @param context the context to use - * @param color the color number from R.color - * @return a six characters long String with hexadecimal RGB values - */ - public static String getHexRGBColor(Context context, int color) { - return context.getResources().getString(color).substring(3); - } - } diff --git a/app/src/main/java/org/schabi/newpipe/about/SoftwareComponent.java b/app/src/main/java/org/schabi/newpipe/about/SoftwareComponent.java index edab3e174..946945142 100644 --- a/app/src/main/java/org/schabi/newpipe/about/SoftwareComponent.java +++ b/app/src/main/java/org/schabi/newpipe/about/SoftwareComponent.java @@ -4,19 +4,44 @@ import android.os.Parcel; import android.os.Parcelable; public class SoftwareComponent implements Parcelable { - public static final Creator CREATOR = new Creator() { @Override - public SoftwareComponent createFromParcel(Parcel source) { + public SoftwareComponent createFromParcel(final Parcel source) { return new SoftwareComponent(source); } @Override - public SoftwareComponent[] newArray(int size) { + public SoftwareComponent[] newArray(final int size) { return new SoftwareComponent[size]; } }; + private final License license; + private final String name; + private final String years; + private final String copyrightOwner; + private final String link; + private final String version; + + public SoftwareComponent(final String name, final String years, final String copyrightOwner, + final String link, final License license) { + this.name = name; + this.years = years; + this.copyrightOwner = copyrightOwner; + this.link = link; + this.license = license; + this.version = null; + } + + protected SoftwareComponent(final Parcel in) { + this.name = in.readString(); + this.license = in.readParcelable(License.class.getClassLoader()); + this.copyrightOwner = in.readString(); + this.link = in.readString(); + this.years = in.readString(); + this.version = in.readString(); + } + public String getName() { return name; } @@ -37,31 +62,6 @@ public class SoftwareComponent implements Parcelable { return version; } - private final License license; - private final String name; - private final String years; - private final String copyrightOwner; - private final String link; - private final String version; - - public SoftwareComponent(String name, String years, String copyrightOwner, String link, License license) { - this.name = name; - this.years = years; - this.copyrightOwner = copyrightOwner; - this.link = link; - this.license = license; - this.version = null; - } - - protected SoftwareComponent(Parcel in) { - this.name = in.readString(); - this.license = in.readParcelable(License.class.getClassLoader()); - this.copyrightOwner = in.readString(); - this.link = in.readString(); - this.years = in.readString(); - this.version = in.readString(); - } - public License getLicense() { return license; } @@ -72,7 +72,7 @@ public class SoftwareComponent implements Parcelable { } @Override - public void writeToParcel(Parcel dest, int flags) { + public void writeToParcel(final Parcel dest, final int flags) { dest.writeString(name); dest.writeParcelable(license, flags); dest.writeString(copyrightOwner); diff --git a/app/src/main/java/org/schabi/newpipe/about/StandardLicenses.java b/app/src/main/java/org/schabi/newpipe/about/StandardLicenses.java index 00a479336..75a7a8613 100644 --- a/app/src/main/java/org/schabi/newpipe/about/StandardLicenses.java +++ b/app/src/main/java/org/schabi/newpipe/about/StandardLicenses.java @@ -1,12 +1,19 @@ package org.schabi.newpipe.about; /** - * Standard software licenses + * Class containing information about standard software licenses. */ public final class StandardLicenses { - public static final License GPL2 = new License("GNU General Public License, Version 2.0", "GPLv2", "gpl_2.html"); - public static final License GPL3 = new License("GNU General Public License, Version 3.0", "GPLv3", "gpl_3.html"); - public static final License APACHE2 = new License("Apache License, Version 2.0", "ALv2", "apache2.html"); - public static final License MPL2 = new License("Mozilla Public License, Version 2.0", "MPL 2.0", "mpl2.html"); - public static final License MIT = new License("MIT License", "MIT", "mit.html"); + public static final License GPL2 + = new License("GNU General Public License, Version 2.0", "GPLv2", "gpl_2.html"); + public static final License GPL3 + = new License("GNU General Public License, Version 3.0", "GPLv3", "gpl_3.html"); + public static final License APACHE2 + = new License("Apache License, Version 2.0", "ALv2", "apache2.html"); + public static final License MPL2 + = new License("Mozilla Public License, Version 2.0", "MPL 2.0", "mpl2.html"); + public static final License MIT + = new License("MIT License", "MIT", "mit.html"); + + private StandardLicenses() { } } diff --git a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java index d374f254b..3b5bda155 100644 --- a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java @@ -4,6 +4,12 @@ import androidx.room.Database; import androidx.room.RoomDatabase; import androidx.room.TypeConverters; +import org.schabi.newpipe.database.feed.dao.FeedDAO; +import org.schabi.newpipe.database.feed.dao.FeedGroupDAO; +import org.schabi.newpipe.database.feed.model.FeedEntity; +import org.schabi.newpipe.database.feed.model.FeedGroupEntity; +import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity; +import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity; import org.schabi.newpipe.database.history.dao.SearchHistoryDAO; import org.schabi.newpipe.database.history.dao.StreamHistoryDAO; import org.schabi.newpipe.database.history.model.SearchHistoryEntry; @@ -21,24 +27,22 @@ import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.database.subscription.SubscriptionDAO; import org.schabi.newpipe.database.subscription.SubscriptionEntity; -import static org.schabi.newpipe.database.Migrations.DB_VER_12_0; +import static org.schabi.newpipe.database.Migrations.DB_VER_3; @TypeConverters({Converters.class}) @Database( entities = { SubscriptionEntity.class, SearchHistoryEntry.class, StreamEntity.class, StreamHistoryEntity.class, StreamStateEntity.class, - PlaylistEntity.class, PlaylistStreamEntity.class, PlaylistRemoteEntity.class + PlaylistEntity.class, PlaylistStreamEntity.class, PlaylistRemoteEntity.class, + FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class, + FeedLastUpdatedEntity.class }, - version = DB_VER_12_0, - exportSchema = false + version = DB_VER_3 ) public abstract class AppDatabase extends RoomDatabase { - public static final String DATABASE_NAME = "newpipe.db"; - public abstract SubscriptionDAO subscriptionDAO(); - public abstract SearchHistoryDAO searchHistoryDAO(); public abstract StreamDAO streamDAO(); @@ -52,4 +56,10 @@ public abstract class AppDatabase extends RoomDatabase { public abstract PlaylistStreamDAO playlistStreamDAO(); public abstract PlaylistRemoteDAO playlistRemoteDAO(); + + public abstract FeedDAO feedDAO(); + + public abstract FeedGroupDAO feedGroupDAO(); + + public abstract SubscriptionDAO subscriptionDAO(); } diff --git a/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java b/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java index b7381b9f1..bcb9ece10 100644 --- a/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java @@ -15,13 +15,13 @@ import io.reactivex.Flowable; public interface BasicDAO { /* Inserts */ @Insert(onConflict = OnConflictStrategy.FAIL) - long insert(final Entity entity); + long insert(Entity entity); @Insert(onConflict = OnConflictStrategy.FAIL) - List insertAll(final Entity... entities); + List insertAll(Entity... entities); @Insert(onConflict = OnConflictStrategy.FAIL) - List insertAll(final Collection entities); + List insertAll(Collection entities); /* Searches */ Flowable> getAll(); @@ -30,17 +30,17 @@ public interface BasicDAO { /* Deletes */ @Delete - void delete(final Entity entity); + void delete(Entity entity); @Delete - int delete(final Collection entities); + int delete(Collection entities); int deleteAll(); /* Updates */ @Update - int update(final Entity entity); + int update(Entity entity); @Update - void update(final Collection entities); + void update(Collection entities); } diff --git a/app/src/main/java/org/schabi/newpipe/database/Converters.java b/app/src/main/java/org/schabi/newpipe/database/Converters.java index bb781d194..e1a2fe2f3 100644 --- a/app/src/main/java/org/schabi/newpipe/database/Converters.java +++ b/app/src/main/java/org/schabi/newpipe/database/Converters.java @@ -3,38 +3,58 @@ package org.schabi.newpipe.database; import androidx.room.TypeConverter; import org.schabi.newpipe.extractor.stream.StreamType; +import org.schabi.newpipe.local.subscription.FeedGroupIcon; import java.util.Date; -public class Converters { +public final class Converters { + private Converters() { } /** - * Convert a long value to a date + * Convert a long value to a date. + * * @param value the long value * @return the date */ @TypeConverter - public static Date fromTimestamp(Long value) { + public static Date fromTimestamp(final Long value) { return value == null ? null : new Date(value); } /** - * Convert a date to a long value + * Convert a date to a long value. + * * @param date the date * @return the long value */ @TypeConverter - public static Long dateToTimestamp(Date date) { + public static Long dateToTimestamp(final Date date) { return date == null ? null : date.getTime(); } @TypeConverter - public static StreamType streamTypeOf(String value) { + public static StreamType streamTypeOf(final String value) { return StreamType.valueOf(value); } @TypeConverter - public static String stringOf(StreamType streamType) { + public static String stringOf(final StreamType streamType) { return streamType.name(); } + + @TypeConverter + public static Integer integerOf(final FeedGroupIcon feedGroupIcon) { + return feedGroupIcon.getId(); + } + + @TypeConverter + public static FeedGroupIcon feedGroupIconOf(final Integer id) { + for (FeedGroupIcon icon : FeedGroupIcon.values()) { + if (icon.getId() == id) { + return icon; + } + } + + throw new IllegalArgumentException("There's no feed group icon with the id \"" + id + "\""); + } } diff --git a/app/src/main/java/org/schabi/newpipe/database/LocalItem.java b/app/src/main/java/org/schabi/newpipe/database/LocalItem.java index e121739ab..54b856b06 100644 --- a/app/src/main/java/org/schabi/newpipe/database/LocalItem.java +++ b/app/src/main/java/org/schabi/newpipe/database/LocalItem.java @@ -1,6 +1,8 @@ package org.schabi.newpipe.database; public interface LocalItem { + LocalItemType getLocalItemType(); + enum LocalItemType { PLAYLIST_LOCAL_ITEM, PLAYLIST_REMOTE_ITEM, @@ -8,6 +10,4 @@ public interface LocalItem { PLAYLIST_STREAM_ITEM, STATISTIC_STREAM_ITEM, } - - LocalItemType getLocalItemType(); } diff --git a/app/src/main/java/org/schabi/newpipe/database/Migrations.java b/app/src/main/java/org/schabi/newpipe/database/Migrations.java index 07d9749b2..088b9ed19 100644 --- a/app/src/main/java/org/schabi/newpipe/database/Migrations.java +++ b/app/src/main/java/org/schabi/newpipe/database/Migrations.java @@ -1,74 +1,164 @@ package org.schabi.newpipe.database; -import androidx.sqlite.db.SupportSQLiteDatabase; -import androidx.room.migration.Migration; -import androidx.annotation.NonNull; import android.util.Log; +import androidx.annotation.NonNull; +import androidx.room.migration.Migration; +import androidx.sqlite.db.SupportSQLiteDatabase; + import org.schabi.newpipe.BuildConfig; -public class Migrations { +public final class Migrations { + public static final int DB_VER_1 = 1; + public static final int DB_VER_2 = 2; + public static final int DB_VER_3 = 3; - public static final int DB_VER_11_0 = 1; - public static final int DB_VER_12_0 = 2; - - public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release"); private static final String TAG = Migrations.class.getName(); + public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release"); - public static final Migration MIGRATION_11_12 = new Migration(DB_VER_11_0, DB_VER_12_0) { + public static final Migration MIGRATION_1_2 = new Migration(DB_VER_1, DB_VER_2) { @Override - public void migrate(@NonNull SupportSQLiteDatabase database) { - if(DEBUG) { + public void migrate(@NonNull final SupportSQLiteDatabase database) { + if (DEBUG) { Log.d(TAG, "Start migrating database"); } /* - * Unfortunately these queries must be hardcoded due to the possibility of - * schema and names changing at a later date, thus invalidating the older migration - * scripts if they are not hardcoded. - * */ + * Unfortunately these queries must be hardcoded due to the possibility of + * schema and names changing at a later date, thus invalidating the older migration + * scripts if they are not hardcoded. + * */ // Not much we can do about this, since room doesn't create tables before migration. // It's either this or blasting the entire database anew. - database.execSQL("CREATE INDEX `index_search_history_search` ON `search_history` (`search`)"); - database.execSQL("CREATE TABLE IF NOT EXISTS `streams` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, `stream_type` TEXT, `duration` INTEGER, `uploader` TEXT, `thumbnail_url` TEXT)"); - database.execSQL("CREATE UNIQUE INDEX `index_streams_service_id_url` ON `streams` (`service_id`, `url`)"); - database.execSQL("CREATE TABLE IF NOT EXISTS `stream_history` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, `repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )"); - database.execSQL("CREATE INDEX `index_stream_history_stream_id` ON `stream_history` (`stream_id`)"); - database.execSQL("CREATE TABLE IF NOT EXISTS `stream_state` (`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )"); - database.execSQL("CREATE TABLE IF NOT EXISTS `playlists` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `thumbnail_url` TEXT)"); + database.execSQL("CREATE INDEX `index_search_history_search` " + + "ON `search_history` (`search`)"); + database.execSQL("CREATE TABLE IF NOT EXISTS `streams` " + + "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "`service_id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, " + + "`stream_type` TEXT, `duration` INTEGER, `uploader` TEXT, " + + "`thumbnail_url` TEXT)"); + database.execSQL("CREATE UNIQUE INDEX `index_streams_service_id_url` " + + "ON `streams` (`service_id`, `url`)"); + database.execSQL("CREATE TABLE IF NOT EXISTS `stream_history` " + + "(`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, " + + "`repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), " + + "FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) " + + "ON UPDATE CASCADE ON DELETE CASCADE )"); + database.execSQL("CREATE INDEX `index_stream_history_stream_id` " + + "ON `stream_history` (`stream_id`)"); + database.execSQL("CREATE TABLE IF NOT EXISTS `stream_state` " + + "(`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, " + + "PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) " + + "REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )"); + database.execSQL("CREATE TABLE IF NOT EXISTS `playlists` " + + "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "`name` TEXT, `thumbnail_url` TEXT)"); database.execSQL("CREATE INDEX `index_playlists_name` ON `playlists` (`name`)"); - database.execSQL("CREATE TABLE IF NOT EXISTS `playlist_stream_join` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)"); - database.execSQL("CREATE UNIQUE INDEX `index_playlist_stream_join_playlist_id_join_index` ON `playlist_stream_join` (`playlist_id`, `join_index`)"); - database.execSQL("CREATE INDEX `index_playlist_stream_join_stream_id` ON `playlist_stream_join` (`stream_id`)"); - database.execSQL("CREATE TABLE IF NOT EXISTS `remote_playlists` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)"); - database.execSQL("CREATE INDEX `index_remote_playlists_name` ON `remote_playlists` (`name`)"); - database.execSQL("CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` ON `remote_playlists` (`service_id`, `url`)"); + database.execSQL("CREATE TABLE IF NOT EXISTS `playlist_stream_join` " + + "(`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, " + + "`join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), " + + "FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) " + + "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, " + + "FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) " + + "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)"); + database.execSQL("CREATE UNIQUE INDEX " + + "`index_playlist_stream_join_playlist_id_join_index` " + + "ON `playlist_stream_join` (`playlist_id`, `join_index`)"); + database.execSQL("CREATE INDEX `index_playlist_stream_join_stream_id` " + + "ON `playlist_stream_join` (`stream_id`)"); + database.execSQL("CREATE TABLE IF NOT EXISTS `remote_playlists` " + + "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "`service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, " + + "`thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)"); + database.execSQL("CREATE INDEX `index_remote_playlists_name` " + + "ON `remote_playlists` (`name`)"); + database.execSQL("CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` " + + "ON `remote_playlists` (`service_id`, `url`)"); // Populate streams table with existing entries in watch history // Latest data first, thus ignoring older entries with the same indices - database.execSQL("INSERT OR IGNORE INTO streams (service_id, url, title, " + - "stream_type, duration, uploader, thumbnail_url) " + + database.execSQL("INSERT OR IGNORE INTO streams (service_id, url, title, " + + "stream_type, duration, uploader, thumbnail_url) " - "SELECT service_id, url, title, 'VIDEO_STREAM', duration, " + - "uploader, thumbnail_url " + + + "SELECT service_id, url, title, 'VIDEO_STREAM', duration, " + + "uploader, thumbnail_url " - "FROM watch_history " + - "ORDER BY creation_date DESC"); + + "FROM watch_history " + + "ORDER BY creation_date DESC"); // Once the streams have PKs, join them with the normalized history table // and populate it with the remaining data from watch history - database.execSQL("INSERT INTO stream_history (stream_id, access_date, repeat_count)" + - "SELECT uid, creation_date, 1 " + - "FROM watch_history INNER JOIN streams " + - "ON watch_history.service_id == streams.service_id " + - "AND watch_history.url == streams.url " + - "ORDER BY creation_date DESC"); + database.execSQL("INSERT INTO stream_history (stream_id, access_date, repeat_count)" + + "SELECT uid, creation_date, 1 " + + "FROM watch_history INNER JOIN streams " + + "ON watch_history.service_id == streams.service_id " + + "AND watch_history.url == streams.url " + + "ORDER BY creation_date DESC"); database.execSQL("DROP TABLE IF EXISTS watch_history"); - if(DEBUG) { + if (DEBUG) { Log.d(TAG, "Stop migrating database"); } } }; + + public static final Migration MIGRATION_2_3 = new Migration(DB_VER_2, DB_VER_3) { + @Override + public void migrate(@NonNull final SupportSQLiteDatabase database) { + // Add NOT NULLs and new fields + database.execSQL("CREATE TABLE IF NOT EXISTS streams_new " + + "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "service_id INTEGER NOT NULL, url TEXT NOT NULL, title TEXT NOT NULL, " + + "stream_type TEXT NOT NULL, duration INTEGER NOT NULL, " + + "uploader TEXT NOT NULL, thumbnail_url TEXT, view_count INTEGER, " + + "textual_upload_date TEXT, upload_date INTEGER, " + + "is_upload_date_approximation INTEGER)"); + + database.execSQL("INSERT INTO streams_new (uid, service_id, url, title, stream_type, " + + "duration, uploader, thumbnail_url, view_count, textual_upload_date, " + + "upload_date, is_upload_date_approximation) " + + + "SELECT uid, service_id, url, ifnull(title, ''), " + + "ifnull(stream_type, 'VIDEO_STREAM'), ifnull(duration, 0), " + + "ifnull(uploader, ''), ifnull(thumbnail_url, ''), NULL, NULL, NULL, NULL " + + + "FROM streams WHERE url IS NOT NULL"); + + database.execSQL("DROP TABLE streams"); + database.execSQL("ALTER TABLE streams_new RENAME TO streams"); + database.execSQL("CREATE UNIQUE INDEX index_streams_service_id_url " + + "ON streams (service_id, url)"); + + // Tables for feed feature + database.execSQL("CREATE TABLE IF NOT EXISTS feed " + + "(stream_id INTEGER NOT NULL, subscription_id INTEGER NOT NULL, " + + "PRIMARY KEY(stream_id, subscription_id), " + + "FOREIGN KEY(stream_id) REFERENCES streams(uid) " + + "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, " + + "FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) " + + "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)"); + database.execSQL("CREATE INDEX index_feed_subscription_id ON feed (subscription_id)"); + database.execSQL("CREATE TABLE IF NOT EXISTS feed_group " + + "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name TEXT NOT NULL, " + + "icon_id INTEGER NOT NULL, sort_order INTEGER NOT NULL)"); + database.execSQL("CREATE INDEX index_feed_group_sort_order ON feed_group (sort_order)"); + database.execSQL("CREATE TABLE IF NOT EXISTS feed_group_subscription_join " + + "(group_id INTEGER NOT NULL, subscription_id INTEGER NOT NULL, " + + "PRIMARY KEY(group_id, subscription_id), " + + "FOREIGN KEY(group_id) REFERENCES feed_group(uid) " + + "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, " + + "FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) " + + "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)"); + database.execSQL("CREATE INDEX index_feed_group_subscription_join_subscription_id " + + "ON feed_group_subscription_join (subscription_id)"); + database.execSQL("CREATE TABLE IF NOT EXISTS feed_last_updated " + + "(subscription_id INTEGER NOT NULL, last_updated INTEGER, " + + "PRIMARY KEY(subscription_id), " + + "FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) " + + "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)"); + } + }; + + private Migrations() { } } diff --git a/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt b/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt new file mode 100644 index 000000000..74f5b369e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt @@ -0,0 +1,152 @@ +package org.schabi.newpipe.database.feed.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import androidx.room.Update +import io.reactivex.Flowable +import java.util.Date +import org.schabi.newpipe.database.feed.model.FeedEntity +import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity +import org.schabi.newpipe.database.stream.model.StreamEntity +import org.schabi.newpipe.database.subscription.SubscriptionEntity + +@Dao +abstract class FeedDAO { + @Query("DELETE FROM feed") + abstract fun deleteAll(): Int + + @Query(""" + SELECT s.* FROM streams s + + INNER JOIN feed f + ON s.uid = f.stream_id + + ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC + + LIMIT 500 + """) + abstract fun getAllStreams(): Flowable> + + @Query(""" + SELECT s.* FROM streams s + + INNER JOIN feed f + ON s.uid = f.stream_id + + INNER JOIN feed_group_subscription_join fgs + ON fgs.subscription_id = f.subscription_id + + INNER JOIN feed_group fg + ON fg.uid = fgs.group_id + + WHERE fgs.group_id = :groupId + + ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC + LIMIT 500 + """) + abstract fun getAllStreamsFromGroup(groupId: Long): Flowable> + + @Query(""" + DELETE FROM feed WHERE + + feed.stream_id IN ( + SELECT s.uid FROM streams s + + INNER JOIN feed f + ON s.uid = f.stream_id + + WHERE s.upload_date < :date + ) + """) + abstract fun unlinkStreamsOlderThan(date: Date) + + @Query(""" + DELETE FROM feed + + WHERE feed.subscription_id = :subscriptionId + + AND feed.stream_id IN ( + SELECT s.uid FROM streams s + + INNER JOIN feed f + ON s.uid = f.stream_id + + WHERE s.stream_type = "LIVE_STREAM" OR s.stream_type = "AUDIO_LIVE_STREAM" + ) + """) + abstract fun unlinkOldLivestreams(subscriptionId: Long) + + @Insert(onConflict = OnConflictStrategy.IGNORE) + abstract fun insert(feedEntity: FeedEntity) + + @Insert(onConflict = OnConflictStrategy.IGNORE) + abstract fun insertAll(entities: List): List + + @Insert(onConflict = OnConflictStrategy.IGNORE) + internal abstract fun insertLastUpdated(lastUpdatedEntity: FeedLastUpdatedEntity): Long + + @Update(onConflict = OnConflictStrategy.IGNORE) + internal abstract fun updateLastUpdated(lastUpdatedEntity: FeedLastUpdatedEntity) + + @Transaction + open fun setLastUpdatedForSubscription(lastUpdatedEntity: FeedLastUpdatedEntity) { + val id = insertLastUpdated(lastUpdatedEntity) + + if (id == -1L) { + updateLastUpdated(lastUpdatedEntity) + } + } + + @Query(""" + SELECT MIN(lu.last_updated) FROM feed_last_updated lu + + INNER JOIN feed_group_subscription_join fgs + ON fgs.subscription_id = lu.subscription_id AND fgs.group_id = :groupId + """) + abstract fun oldestSubscriptionUpdate(groupId: Long): Flowable> + + @Query("SELECT MIN(last_updated) FROM feed_last_updated") + abstract fun oldestSubscriptionUpdateFromAll(): Flowable> + + @Query("SELECT COUNT(*) FROM feed_last_updated WHERE last_updated IS NULL") + abstract fun notLoadedCount(): Flowable + + @Query(""" + SELECT COUNT(*) FROM subscriptions s + + INNER JOIN feed_group_subscription_join fgs + ON s.uid = fgs.subscription_id AND fgs.group_id = :groupId + + LEFT JOIN feed_last_updated lu + ON s.uid = lu.subscription_id + + WHERE lu.last_updated IS NULL + """) + abstract fun notLoadedCountForGroup(groupId: Long): Flowable + + @Query(""" + SELECT s.* FROM subscriptions s + + LEFT JOIN feed_last_updated lu + ON s.uid = lu.subscription_id + + WHERE lu.last_updated IS NULL OR lu.last_updated < :outdatedThreshold + """) + abstract fun getAllOutdated(outdatedThreshold: Date): Flowable> + + @Query(""" + SELECT s.* FROM subscriptions s + + INNER JOIN feed_group_subscription_join fgs + ON s.uid = fgs.subscription_id AND fgs.group_id = :groupId + + LEFT JOIN feed_last_updated lu + ON s.uid = lu.subscription_id + + WHERE lu.last_updated IS NULL OR lu.last_updated < :outdatedThreshold + """) + abstract fun getAllOutdatedForGroup(groupId: Long, outdatedThreshold: Date): Flowable> +} diff --git a/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedGroupDAO.kt b/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedGroupDAO.kt new file mode 100644 index 000000000..c8700dea2 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedGroupDAO.kt @@ -0,0 +1,67 @@ +package org.schabi.newpipe.database.feed.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import androidx.room.Update +import io.reactivex.Flowable +import io.reactivex.Maybe +import org.schabi.newpipe.database.feed.model.FeedGroupEntity +import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity + +@Dao +abstract class FeedGroupDAO { + + @Query("SELECT * FROM feed_group ORDER BY sort_order ASC") + abstract fun getAll(): Flowable> + + @Query("SELECT * FROM feed_group WHERE uid = :groupId") + abstract fun getGroup(groupId: Long): Maybe + + @Transaction + open fun insert(feedGroupEntity: FeedGroupEntity): Long { + val nextSortOrder = nextSortOrder() + feedGroupEntity.sortOrder = nextSortOrder + return insertInternal(feedGroupEntity) + } + + @Update(onConflict = OnConflictStrategy.IGNORE) + abstract fun update(feedGroupEntity: FeedGroupEntity): Int + + @Query("DELETE FROM feed_group") + abstract fun deleteAll(): Int + + @Query("DELETE FROM feed_group WHERE uid = :groupId") + abstract fun delete(groupId: Long): Int + + @Query("SELECT subscription_id FROM feed_group_subscription_join WHERE group_id = :groupId") + abstract fun getSubscriptionIdsFor(groupId: Long): Flowable> + + @Query("DELETE FROM feed_group_subscription_join WHERE group_id = :groupId") + abstract fun deleteSubscriptionsFromGroup(groupId: Long): Int + + @Insert(onConflict = OnConflictStrategy.IGNORE) + abstract fun insertSubscriptionsToGroup(entities: List): List + + @Transaction + open fun updateSubscriptionsForGroup(groupId: Long, subscriptionIds: List) { + deleteSubscriptionsFromGroup(groupId) + insertSubscriptionsToGroup(subscriptionIds.map { FeedGroupSubscriptionEntity(groupId, it) }) + } + + @Transaction + open fun updateOrder(orderMap: Map) { + orderMap.forEach { (groupId, sortOrder) -> updateOrder(groupId, sortOrder) } + } + + @Query("UPDATE feed_group SET sort_order = :sortOrder WHERE uid = :groupId") + abstract fun updateOrder(groupId: Long, sortOrder: Long): Int + + @Query("SELECT IFNULL(MAX(sort_order) + 1, 0) FROM feed_group") + protected abstract fun nextSortOrder(): Long + + @Insert(onConflict = OnConflictStrategy.ABORT) + protected abstract fun insertInternal(feedGroupEntity: FeedGroupEntity): Long +} diff --git a/app/src/main/java/org/schabi/newpipe/database/feed/model/FeedEntity.kt b/app/src/main/java/org/schabi/newpipe/database/feed/model/FeedEntity.kt new file mode 100644 index 000000000..8a1eb65d4 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/feed/model/FeedEntity.kt @@ -0,0 +1,43 @@ +package org.schabi.newpipe.database.feed.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import org.schabi.newpipe.database.feed.model.FeedEntity.Companion.FEED_TABLE +import org.schabi.newpipe.database.feed.model.FeedEntity.Companion.STREAM_ID +import org.schabi.newpipe.database.feed.model.FeedEntity.Companion.SUBSCRIPTION_ID +import org.schabi.newpipe.database.stream.model.StreamEntity +import org.schabi.newpipe.database.subscription.SubscriptionEntity + +@Entity(tableName = FEED_TABLE, + primaryKeys = [STREAM_ID, SUBSCRIPTION_ID], + indices = [Index(SUBSCRIPTION_ID)], + foreignKeys = [ + ForeignKey( + entity = StreamEntity::class, + parentColumns = [StreamEntity.STREAM_ID], + childColumns = [STREAM_ID], + onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true), + ForeignKey( + entity = SubscriptionEntity::class, + parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID], + childColumns = [SUBSCRIPTION_ID], + onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true) + ] +) +data class FeedEntity( + @ColumnInfo(name = STREAM_ID) + var streamId: Long, + + @ColumnInfo(name = SUBSCRIPTION_ID) + var subscriptionId: Long +) { + + companion object { + const val FEED_TABLE = "feed" + + const val STREAM_ID = "stream_id" + const val SUBSCRIPTION_ID = "subscription_id" + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/feed/model/FeedGroupEntity.kt b/app/src/main/java/org/schabi/newpipe/database/feed/model/FeedGroupEntity.kt new file mode 100644 index 000000000..e772168fd --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/feed/model/FeedGroupEntity.kt @@ -0,0 +1,39 @@ +package org.schabi.newpipe.database.feed.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey +import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.FEED_GROUP_TABLE +import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.SORT_ORDER +import org.schabi.newpipe.local.subscription.FeedGroupIcon + +@Entity( + tableName = FEED_GROUP_TABLE, + indices = [Index(SORT_ORDER)] +) +data class FeedGroupEntity( + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = ID) + val uid: Long, + + @ColumnInfo(name = NAME) + var name: String, + + @ColumnInfo(name = ICON) + var icon: FeedGroupIcon, + + @ColumnInfo(name = SORT_ORDER) + var sortOrder: Long = -1 +) { + companion object { + const val FEED_GROUP_TABLE = "feed_group" + + const val ID = "uid" + const val NAME = "name" + const val ICON = "icon_id" + const val SORT_ORDER = "sort_order" + + const val GROUP_ALL_ID = -1L + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/feed/model/FeedGroupSubscriptionEntity.kt b/app/src/main/java/org/schabi/newpipe/database/feed/model/FeedGroupSubscriptionEntity.kt new file mode 100644 index 000000000..eac6bddee --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/feed/model/FeedGroupSubscriptionEntity.kt @@ -0,0 +1,45 @@ +package org.schabi.newpipe.database.feed.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.ForeignKey.CASCADE +import androidx.room.Index +import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity.Companion.FEED_GROUP_SUBSCRIPTION_TABLE +import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity.Companion.GROUP_ID +import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity.Companion.SUBSCRIPTION_ID +import org.schabi.newpipe.database.subscription.SubscriptionEntity + +@Entity( + tableName = FEED_GROUP_SUBSCRIPTION_TABLE, + primaryKeys = [GROUP_ID, SUBSCRIPTION_ID], + indices = [Index(SUBSCRIPTION_ID)], + foreignKeys = [ + ForeignKey( + entity = FeedGroupEntity::class, + parentColumns = [FeedGroupEntity.ID], + childColumns = [GROUP_ID], + onDelete = CASCADE, onUpdate = CASCADE, deferred = true), + + ForeignKey( + entity = SubscriptionEntity::class, + parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID], + childColumns = [SUBSCRIPTION_ID], + onDelete = CASCADE, onUpdate = CASCADE, deferred = true) + ] +) +data class FeedGroupSubscriptionEntity( + @ColumnInfo(name = GROUP_ID) + var feedGroupId: Long, + + @ColumnInfo(name = SUBSCRIPTION_ID) + var subscriptionId: Long +) { + + companion object { + const val FEED_GROUP_SUBSCRIPTION_TABLE = "feed_group_subscription_join" + + const val GROUP_ID = "group_id" + const val SUBSCRIPTION_ID = "subscription_id" + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/feed/model/FeedLastUpdatedEntity.kt b/app/src/main/java/org/schabi/newpipe/database/feed/model/FeedLastUpdatedEntity.kt new file mode 100644 index 000000000..78b2550a5 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/feed/model/FeedLastUpdatedEntity.kt @@ -0,0 +1,37 @@ +package org.schabi.newpipe.database.feed.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import java.util.Date +import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity.Companion.FEED_LAST_UPDATED_TABLE +import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity.Companion.SUBSCRIPTION_ID +import org.schabi.newpipe.database.subscription.SubscriptionEntity + +@Entity( + tableName = FEED_LAST_UPDATED_TABLE, + foreignKeys = [ + ForeignKey( + entity = SubscriptionEntity::class, + parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID], + childColumns = [SUBSCRIPTION_ID], + onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true) + ] +) +data class FeedLastUpdatedEntity( + @PrimaryKey + @ColumnInfo(name = SUBSCRIPTION_ID) + var subscriptionId: Long, + + @ColumnInfo(name = LAST_UPDATED) + var lastUpdated: Date? = null +) { + + companion object { + const val FEED_LAST_UPDATED_TABLE = "feed_last_updated" + + const val SUBSCRIPTION_ID = "subscription_id" + const val LAST_UPDATED = "last_updated" + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java index df8094830..972435859 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java @@ -1,8 +1,8 @@ package org.schabi.newpipe.database.history.dao; +import androidx.annotation.Nullable; import androidx.room.Dao; import androidx.room.Query; -import androidx.annotation.Nullable; import org.schabi.newpipe.database.history.model.SearchHistoryEntry; @@ -18,11 +18,10 @@ import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.TABLE @Dao public interface SearchHistoryDAO extends HistoryDAO { - String ORDER_BY_CREATION_DATE = " ORDER BY " + CREATION_DATE + " DESC"; - @Query("SELECT * FROM " + TABLE_NAME + - " WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")") + @Query("SELECT * FROM " + TABLE_NAME + + " WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")") @Nullable SearchHistoryEntry getLatestEntry(); @@ -37,13 +36,16 @@ public interface SearchHistoryDAO extends HistoryDAO { @Override Flowable> getAll(); - @Query("SELECT * FROM " + TABLE_NAME + " GROUP BY " + SEARCH + ORDER_BY_CREATION_DATE + " LIMIT :limit") + @Query("SELECT * FROM " + TABLE_NAME + " GROUP BY " + SEARCH + ORDER_BY_CREATION_DATE + + " LIMIT :limit") Flowable> getUniqueEntries(int limit); - @Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE) + @Query("SELECT * FROM " + TABLE_NAME + + " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE) @Override Flowable> listByService(int serviceId); - @Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SEARCH + " LIKE :query || '%' GROUP BY " + SEARCH + " LIMIT :limit") + @Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SEARCH + " LIKE :query || '%'" + + " GROUP BY " + SEARCH + " LIMIT :limit") Flowable> getSimilarEntries(String query, int limit); } diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java index 2703b9783..c716a2d91 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java @@ -1,32 +1,31 @@ package org.schabi.newpipe.database.history.dao; - +import androidx.annotation.Nullable; import androidx.room.Dao; import androidx.room.Query; -import androidx.annotation.Nullable; +import org.schabi.newpipe.database.history.model.StreamHistoryEntity; import org.schabi.newpipe.database.history.model.StreamHistoryEntry; import org.schabi.newpipe.database.stream.StreamStatisticsEntry; -import org.schabi.newpipe.database.history.model.StreamHistoryEntity; import java.util.List; import io.reactivex.Flowable; +import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID; +import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE; +import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_REPEAT_COUNT; import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_LATEST_DATE; import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_WATCH_COUNT; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; -import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID; -import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE; -import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; @Dao public abstract class StreamHistoryDAO implements HistoryDAO { - @Query("SELECT * FROM " + STREAM_HISTORY_TABLE + - " WHERE " + STREAM_ACCESS_DATE + " = " + - "(SELECT MAX(" + STREAM_ACCESS_DATE + ") FROM " + STREAM_HISTORY_TABLE + ")") + @Query("SELECT * FROM " + STREAM_HISTORY_TABLE + + " WHERE " + STREAM_ACCESS_DATE + " = " + + "(SELECT MAX(" + STREAM_ACCESS_DATE + ") FROM " + STREAM_HISTORY_TABLE + ")") @Override @Nullable public abstract StreamHistoryEntity getLatestEntry(); @@ -40,33 +39,40 @@ public abstract class StreamHistoryDAO implements HistoryDAO> listByService(int serviceId) { + public Flowable> listByService(final int serviceId) { throw new UnsupportedOperationException(); } - @Query("SELECT * FROM " + STREAM_TABLE + - " INNER JOIN " + STREAM_HISTORY_TABLE + - " ON " + STREAM_ID + " = " + JOIN_STREAM_ID + - " ORDER BY " + STREAM_ACCESS_DATE + " DESC") + @Query("SELECT * FROM " + STREAM_TABLE + + " INNER JOIN " + STREAM_HISTORY_TABLE + + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID + + " ORDER BY " + STREAM_ACCESS_DATE + " DESC") public abstract Flowable> getHistory(); - @Query("SELECT * FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + - " = :streamId ORDER BY " + STREAM_ACCESS_DATE + " DESC LIMIT 1") + + @Query("SELECT * FROM " + STREAM_TABLE + + " INNER JOIN " + STREAM_HISTORY_TABLE + + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID + + " ORDER BY " + STREAM_ID + " ASC") + public abstract Flowable> getHistorySortedById(); + + @Query("SELECT * FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + + " = :streamId ORDER BY " + STREAM_ACCESS_DATE + " DESC LIMIT 1") @Nullable - public abstract StreamHistoryEntity getLatestEntry(final long streamId); + public abstract StreamHistoryEntity getLatestEntry(long streamId); @Query("DELETE FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId") - public abstract int deleteStreamHistory(final long streamId); + public abstract int deleteStreamHistory(long streamId); - @Query("SELECT * FROM " + STREAM_TABLE + + @Query("SELECT * FROM " + STREAM_TABLE // Select the latest entry and watch count for each stream id on history table - " INNER JOIN " + - "(SELECT " + JOIN_STREAM_ID + ", " + - " MAX(" + STREAM_ACCESS_DATE + ") AS " + STREAM_LATEST_DATE + ", " + - " SUM(" + STREAM_REPEAT_COUNT + ") AS " + STREAM_WATCH_COUNT + - " FROM " + STREAM_HISTORY_TABLE + " GROUP BY " + JOIN_STREAM_ID + ")" + + + " INNER JOIN " + + "(SELECT " + JOIN_STREAM_ID + ", " + + " MAX(" + STREAM_ACCESS_DATE + ") AS " + STREAM_LATEST_DATE + ", " + + " SUM(" + STREAM_REPEAT_COUNT + ") AS " + STREAM_WATCH_COUNT + + " FROM " + STREAM_HISTORY_TABLE + " GROUP BY " + JOIN_STREAM_ID + ")" - " ON " + STREAM_ID + " = " + JOIN_STREAM_ID) + + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID) public abstract Flowable> getStatistics(); } diff --git a/app/src/main/java/org/schabi/newpipe/database/history/model/SearchHistoryEntry.java b/app/src/main/java/org/schabi/newpipe/database/history/model/SearchHistoryEntry.java index 222ef0a59..752835182 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/model/SearchHistoryEntry.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/model/SearchHistoryEntry.java @@ -13,7 +13,6 @@ import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SEARC @Entity(tableName = SearchHistoryEntry.TABLE_NAME, indices = {@Index(value = SEARCH)}) public class SearchHistoryEntry { - public static final String ID = "id"; public static final String TABLE_NAME = "search_history"; public static final String SERVICE_ID = "service_id"; @@ -33,7 +32,7 @@ public class SearchHistoryEntry { @ColumnInfo(name = SEARCH) private String search; - public SearchHistoryEntry(Date creationDate, int serviceId, String search) { + public SearchHistoryEntry(final Date creationDate, final int serviceId, final String search) { this.serviceId = serviceId; this.creationDate = creationDate; this.search = search; @@ -43,7 +42,7 @@ public class SearchHistoryEntry { return id; } - public void setId(long id) { + public void setId(final long id) { this.id = id; } @@ -51,7 +50,7 @@ public class SearchHistoryEntry { return creationDate; } - public void setCreationDate(Date creationDate) { + public void setCreationDate(final Date creationDate) { this.creationDate = creationDate; } @@ -59,7 +58,7 @@ public class SearchHistoryEntry { return serviceId; } - public void setServiceId(int serviceId) { + public void setServiceId(final int serviceId) { this.serviceId = serviceId; } @@ -67,13 +66,13 @@ public class SearchHistoryEntry { return search; } - public void setSearch(String search) { + public void setSearch(final String search) { this.search = search; } @Ignore - public boolean hasEqualValues(SearchHistoryEntry otherEntry) { - return getServiceId() == otherEntry.getServiceId() && - getSearch().equals(otherEntry.getSearch()); + public boolean hasEqualValues(final SearchHistoryEntry otherEntry) { + return getServiceId() == otherEntry.getServiceId() + && getSearch().equals(otherEntry.getSearch()); } } diff --git a/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.java b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.java index 64bdf34de..bf1f7a9dd 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.java @@ -1,20 +1,20 @@ package org.schabi.newpipe.database.history.model; +import androidx.annotation.NonNull; import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.ForeignKey; import androidx.room.Ignore; import androidx.room.Index; -import androidx.annotation.NonNull; import org.schabi.newpipe.database.stream.model.StreamEntity; import java.util.Date; import static androidx.room.ForeignKey.CASCADE; -import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID; import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE; +import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; @Entity(tableName = STREAM_HISTORY_TABLE, primaryKeys = {JOIN_STREAM_ID, STREAM_ACCESS_DATE}, @@ -27,10 +27,10 @@ import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STRE onDelete = CASCADE, onUpdate = CASCADE) }) public class StreamHistoryEntity { - final public static String STREAM_HISTORY_TABLE = "stream_history"; - final public static String JOIN_STREAM_ID = "stream_id"; - final public static String STREAM_ACCESS_DATE = "access_date"; - final public static String STREAM_REPEAT_COUNT = "repeat_count"; + public static final String STREAM_HISTORY_TABLE = "stream_history"; + public static final String JOIN_STREAM_ID = "stream_id"; + public static final String STREAM_ACCESS_DATE = "access_date"; + public static final String STREAM_REPEAT_COUNT = "repeat_count"; @ColumnInfo(name = JOIN_STREAM_ID) private long streamUid; @@ -42,14 +42,15 @@ public class StreamHistoryEntity { @ColumnInfo(name = STREAM_REPEAT_COUNT) private long repeatCount; - public StreamHistoryEntity(long streamUid, @NonNull Date accessDate, long repeatCount) { + public StreamHistoryEntity(final long streamUid, @NonNull final Date accessDate, + final long repeatCount) { this.streamUid = streamUid; this.accessDate = accessDate; this.repeatCount = repeatCount; } @Ignore - public StreamHistoryEntity(long streamUid, @NonNull Date accessDate) { + public StreamHistoryEntity(final long streamUid, @NonNull final Date accessDate) { this(streamUid, accessDate, 1); } @@ -57,7 +58,7 @@ public class StreamHistoryEntity { return streamUid; } - public void setStreamUid(long streamUid) { + public void setStreamUid(final long streamUid) { this.streamUid = streamUid; } @@ -65,7 +66,7 @@ public class StreamHistoryEntity { return accessDate; } - public void setAccessDate(@NonNull Date accessDate) { + public void setAccessDate(@NonNull final Date accessDate) { this.accessDate = accessDate; } @@ -73,7 +74,7 @@ public class StreamHistoryEntity { return repeatCount; } - public void setRepeatCount(long repeatCount) { + public void setRepeatCount(final long repeatCount) { this.repeatCount = repeatCount; } } diff --git a/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntry.java b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntry.java deleted file mode 100644 index ad66451e4..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntry.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.schabi.newpipe.database.history.model; - -import androidx.room.ColumnInfo; - -import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.extractor.stream.StreamType; - -import java.util.Date; - -public class StreamHistoryEntry { - @ColumnInfo(name = StreamEntity.STREAM_ID) - final public long uid; - @ColumnInfo(name = StreamEntity.STREAM_SERVICE_ID) - final public int serviceId; - @ColumnInfo(name = StreamEntity.STREAM_URL) - final public String url; - @ColumnInfo(name = StreamEntity.STREAM_TITLE) - final public String title; - @ColumnInfo(name = StreamEntity.STREAM_TYPE) - final public StreamType streamType; - @ColumnInfo(name = StreamEntity.STREAM_DURATION) - final public long duration; - @ColumnInfo(name = StreamEntity.STREAM_UPLOADER) - final public String uploader; - @ColumnInfo(name = StreamEntity.STREAM_THUMBNAIL_URL) - final public String thumbnailUrl; - @ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID) - final public long streamId; - @ColumnInfo(name = StreamHistoryEntity.STREAM_ACCESS_DATE) - final public Date accessDate; - @ColumnInfo(name = StreamHistoryEntity.STREAM_REPEAT_COUNT) - final public long repeatCount; - - public StreamHistoryEntry(long uid, int serviceId, String url, String title, - StreamType streamType, long duration, String uploader, - String thumbnailUrl, long streamId, Date accessDate, - long repeatCount) { - this.uid = uid; - this.serviceId = serviceId; - this.url = url; - this.title = title; - this.streamType = streamType; - this.duration = duration; - this.uploader = uploader; - this.thumbnailUrl = thumbnailUrl; - this.streamId = streamId; - this.accessDate = accessDate; - this.repeatCount = repeatCount; - } - - public StreamHistoryEntity toStreamHistoryEntity() { - return new StreamHistoryEntity(streamId, accessDate, repeatCount); - } - - public boolean hasEqualValues(StreamHistoryEntry other) { - return this.uid == other.uid && streamId == other.streamId && - accessDate.compareTo(other.accessDate) == 0; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntry.kt b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntry.kt new file mode 100644 index 000000000..c653e6c6f --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntry.kt @@ -0,0 +1,30 @@ +package org.schabi.newpipe.database.history.model + +import androidx.room.ColumnInfo +import androidx.room.Embedded +import java.util.Date +import org.schabi.newpipe.database.stream.model.StreamEntity + +data class StreamHistoryEntry( + @Embedded + val streamEntity: StreamEntity, + + @ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID) + val streamId: Long, + + @ColumnInfo(name = StreamHistoryEntity.STREAM_ACCESS_DATE) + val accessDate: Date, + + @ColumnInfo(name = StreamHistoryEntity.STREAM_REPEAT_COUNT) + val repeatCount: Long +) { + + fun toStreamHistoryEntity(): StreamHistoryEntity { + return StreamHistoryEntity(streamId, accessDate, repeatCount) + } + + fun hasEqualValues(other: StreamHistoryEntry): Boolean { + return this.streamEntity.uid == other.streamEntity.uid && streamId == other.streamId && + accessDate.compareTo(other.accessDate) == 0 + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java index fd99f84a1..3ce95631c 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java @@ -1,7 +1,33 @@ package org.schabi.newpipe.database.playlist; import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; public interface PlaylistLocalItem extends LocalItem { String getOrderingName(); + + static List merge( + final List localPlaylists, + final List remotePlaylists) { + final List items = new ArrayList<>( + localPlaylists.size() + remotePlaylists.size()); + items.addAll(localPlaylists); + items.addAll(remotePlaylists); + + Collections.sort(items, (left, right) -> { + final String on1 = left.getOrderingName(); + final String on2 = right.getOrderingName(); + if (on1 == null) { + return on2 == null ? 0 : 1; + } else { + return on2 == null ? -1 : on1.compareToIgnoreCase(on2); + } + }); + + return items; + } } diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java index 252ca07f0..a13894030 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java @@ -7,18 +7,19 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL; public class PlaylistMetadataEntry implements PlaylistLocalItem { - final public static String PLAYLIST_STREAM_COUNT = "streamCount"; + public static final String PLAYLIST_STREAM_COUNT = "streamCount"; @ColumnInfo(name = PLAYLIST_ID) - final public long uid; + public final long uid; @ColumnInfo(name = PLAYLIST_NAME) - final public String name; + public final String name; @ColumnInfo(name = PLAYLIST_THUMBNAIL_URL) - final public String thumbnailUrl; + public final String thumbnailUrl; @ColumnInfo(name = PLAYLIST_STREAM_COUNT) - final public long streamCount; + public final long streamCount; - public PlaylistMetadataEntry(long uid, String name, String thumbnailUrl, long streamCount) { + public PlaylistMetadataEntry(final long uid, final String name, final String thumbnailUrl, + final long streamCount) { this.uid = uid; this.name = name; this.thumbnailUrl = thumbnailUrl; diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.java deleted file mode 100644 index fb45c3564..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.schabi.newpipe.database.playlist; - -import androidx.room.ColumnInfo; - -import org.schabi.newpipe.database.LocalItem; -import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; -import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.extractor.stream.StreamType; - -public class PlaylistStreamEntry implements LocalItem { - @ColumnInfo(name = StreamEntity.STREAM_ID) - final public long uid; - @ColumnInfo(name = StreamEntity.STREAM_SERVICE_ID) - final public int serviceId; - @ColumnInfo(name = StreamEntity.STREAM_URL) - final public String url; - @ColumnInfo(name = StreamEntity.STREAM_TITLE) - final public String title; - @ColumnInfo(name = StreamEntity.STREAM_TYPE) - final public StreamType streamType; - @ColumnInfo(name = StreamEntity.STREAM_DURATION) - final public long duration; - @ColumnInfo(name = StreamEntity.STREAM_UPLOADER) - final public String uploader; - @ColumnInfo(name = StreamEntity.STREAM_THUMBNAIL_URL) - final public String thumbnailUrl; - @ColumnInfo(name = PlaylistStreamEntity.JOIN_STREAM_ID) - final public long streamId; - @ColumnInfo(name = PlaylistStreamEntity.JOIN_INDEX) - final public int joinIndex; - - public PlaylistStreamEntry(long uid, int serviceId, String url, String title, - StreamType streamType, long duration, String uploader, - String thumbnailUrl, long streamId, int joinIndex) { - this.uid = uid; - this.serviceId = serviceId; - this.url = url; - this.title = title; - this.streamType = streamType; - this.duration = duration; - this.uploader = uploader; - this.thumbnailUrl = thumbnailUrl; - this.streamId = streamId; - this.joinIndex = joinIndex; - } - - public StreamInfoItem toStreamInfoItem() throws IllegalArgumentException { - StreamInfoItem item = new StreamInfoItem(serviceId, url, title, streamType); - item.setThumbnailUrl(thumbnailUrl); - item.setUploaderName(uploader); - item.setDuration(duration); - return item; - } - - @Override - public LocalItemType getLocalItemType() { - return LocalItemType.PLAYLIST_STREAM_ITEM; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.kt new file mode 100644 index 000000000..c349a3761 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.kt @@ -0,0 +1,34 @@ +package org.schabi.newpipe.database.playlist + +import androidx.room.ColumnInfo +import androidx.room.Embedded +import org.schabi.newpipe.database.LocalItem +import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity +import org.schabi.newpipe.database.stream.model.StreamEntity +import org.schabi.newpipe.extractor.stream.StreamInfoItem + +class PlaylistStreamEntry( + @Embedded + val streamEntity: StreamEntity, + + @ColumnInfo(name = PlaylistStreamEntity.JOIN_STREAM_ID) + val streamId: Long, + + @ColumnInfo(name = PlaylistStreamEntity.JOIN_INDEX) + val joinIndex: Int +) : LocalItem { + + @Throws(IllegalArgumentException::class) + fun toStreamInfoItem(): StreamInfoItem { + val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType) + item.duration = streamEntity.duration + item.uploaderName = streamEntity.uploader + item.thumbnailUrl = streamEntity.thumbnailUrl + + return item + } + + override fun getLocalItemType(): LocalItem.LocalItemType { + return LocalItem.LocalItemType.PLAYLIST_STREAM_ITEM + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.java b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.java index f5a685a7c..2cfe5440c 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.java @@ -24,13 +24,13 @@ public abstract class PlaylistDAO implements BasicDAO { public abstract int deleteAll(); @Override - public Flowable> listByService(int serviceId) { + public Flowable> listByService(final int serviceId) { throw new UnsupportedOperationException(); } @Query("SELECT * FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId") - public abstract Flowable> getPlaylist(final long playlistId); + public abstract Flowable> getPlaylist(long playlistId); @Query("DELETE FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId") - public abstract int deletePlaylist(final long playlistId); + public abstract int deletePlaylist(long playlistId); } diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.java b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.java index b7ccf42f7..23442ceff 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.java @@ -27,22 +27,21 @@ public abstract class PlaylistRemoteDAO implements BasicDAO> listByService(int serviceId); - @Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE " + - REMOTE_PLAYLIST_URL + " = :url AND " + - REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId") + @Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE " + + REMOTE_PLAYLIST_URL + " = :url AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId") public abstract Flowable> getPlaylist(long serviceId, String url); - @Query("SELECT " + REMOTE_PLAYLIST_ID + " FROM " + REMOTE_PLAYLIST_TABLE + - " WHERE " + - REMOTE_PLAYLIST_URL + " = :url AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId") + @Query("SELECT " + REMOTE_PLAYLIST_ID + " FROM " + REMOTE_PLAYLIST_TABLE + + " WHERE " + REMOTE_PLAYLIST_URL + " = :url " + + "AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId") abstract Long getPlaylistIdInternal(long serviceId, String url); @Transaction - public long upsert(PlaylistRemoteEntity playlist) { + public long upsert(final PlaylistRemoteEntity playlist) { final Long playlistId = getPlaylistIdInternal(playlist.getServiceId(), playlist.getUrl()); if (playlistId == null) { @@ -54,7 +53,7 @@ public abstract class PlaylistRemoteDAO implements BasicDAO { @@ -29,40 +36,39 @@ public abstract class PlaylistStreamDAO implements BasicDAO> listByService(int serviceId) { + public Flowable> listByService(final int serviceId) { throw new UnsupportedOperationException(); } - @Query("DELETE FROM " + PLAYLIST_STREAM_JOIN_TABLE + - " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId") - public abstract void deleteBatch(final long playlistId); + @Query("DELETE FROM " + PLAYLIST_STREAM_JOIN_TABLE + + " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId") + public abstract void deleteBatch(long playlistId); - @Query("SELECT COALESCE(MAX(" + JOIN_INDEX + "), -1)" + - " FROM " + PLAYLIST_STREAM_JOIN_TABLE + - " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId") - public abstract Flowable getMaximumIndexOf(final long playlistId); + @Query("SELECT COALESCE(MAX(" + JOIN_INDEX + "), -1)" + + " FROM " + PLAYLIST_STREAM_JOIN_TABLE + + " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId") + public abstract Flowable getMaximumIndexOf(long playlistId); @Transaction - @Query("SELECT * FROM " + STREAM_TABLE + " INNER JOIN " + + @Query("SELECT * FROM " + STREAM_TABLE + " INNER JOIN " // get ids of streams of the given playlist - "(SELECT " + JOIN_STREAM_ID + "," + JOIN_INDEX + - " FROM " + PLAYLIST_STREAM_JOIN_TABLE + - " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId)" + + + "(SELECT " + JOIN_STREAM_ID + "," + JOIN_INDEX + + " FROM " + PLAYLIST_STREAM_JOIN_TABLE + + " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId)" // then merge with the stream metadata - " ON " + STREAM_ID + " = " + JOIN_STREAM_ID + - " ORDER BY " + JOIN_INDEX + " ASC") + + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID + + " ORDER BY " + JOIN_INDEX + " ASC") public abstract Flowable> getOrderedStreamsOf(long playlistId); @Transaction - @Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", " + - PLAYLIST_THUMBNAIL_URL + ", " + - "COALESCE(COUNT(" + JOIN_PLAYLIST_ID + "), 0) AS " + PLAYLIST_STREAM_COUNT + + @Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", " + PLAYLIST_THUMBNAIL_URL + ", " + + "COALESCE(COUNT(" + JOIN_PLAYLIST_ID + "), 0) AS " + PLAYLIST_STREAM_COUNT - " FROM " + PLAYLIST_TABLE + - " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE + - " ON " + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID + - " GROUP BY " + JOIN_PLAYLIST_ID + - " ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC") + + " FROM " + PLAYLIST_TABLE + + " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE + + " ON " + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID + + " GROUP BY " + JOIN_PLAYLIST_ID + + " ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC") public abstract Flowable> getPlaylistMetadata(); } diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java index 9d7989b21..71abf2732 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java @@ -11,10 +11,10 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST @Entity(tableName = PLAYLIST_TABLE, indices = {@Index(value = {PLAYLIST_NAME})}) public class PlaylistEntity { - final public static String PLAYLIST_TABLE = "playlists"; - final public static String PLAYLIST_ID = "uid"; - final public static String PLAYLIST_NAME = "name"; - final public static String PLAYLIST_THUMBNAIL_URL = "thumbnail_url"; + public static final String PLAYLIST_TABLE = "playlists"; + public static final String PLAYLIST_ID = "uid"; + public static final String PLAYLIST_NAME = "name"; + public static final String PLAYLIST_THUMBNAIL_URL = "thumbnail_url"; @PrimaryKey(autoGenerate = true) @ColumnInfo(name = PLAYLIST_ID) @@ -26,7 +26,7 @@ public class PlaylistEntity { @ColumnInfo(name = PLAYLIST_THUMBNAIL_URL) private String thumbnailUrl; - public PlaylistEntity(String name, String thumbnailUrl) { + public PlaylistEntity(final String name, final String thumbnailUrl) { this.name = name; this.thumbnailUrl = thumbnailUrl; } @@ -35,7 +35,7 @@ public class PlaylistEntity { return uid; } - public void setUid(long uid) { + public void setUid(final long uid) { this.uid = uid; } @@ -43,7 +43,7 @@ public class PlaylistEntity { return name; } - public void setName(String name) { + public void setName(final String name) { this.name = name; } @@ -51,7 +51,7 @@ public class PlaylistEntity { return thumbnailUrl; } - public void setThumbnailUrl(String thumbnailUrl) { + public void setThumbnailUrl(final String thumbnailUrl) { this.thumbnailUrl = thumbnailUrl; } } diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java index fa257cfed..2e9a15d7d 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java @@ -24,14 +24,14 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.RE @Index(value = {REMOTE_PLAYLIST_SERVICE_ID, REMOTE_PLAYLIST_URL}, unique = true) }) public class PlaylistRemoteEntity implements PlaylistLocalItem { - final public static String REMOTE_PLAYLIST_TABLE = "remote_playlists"; - final public static String REMOTE_PLAYLIST_ID = "uid"; - final public static String REMOTE_PLAYLIST_SERVICE_ID = "service_id"; - final public static String REMOTE_PLAYLIST_NAME = "name"; - final public static String REMOTE_PLAYLIST_URL = "url"; - final public static String REMOTE_PLAYLIST_THUMBNAIL_URL = "thumbnail_url"; - final public static String REMOTE_PLAYLIST_UPLOADER_NAME = "uploader"; - final public static String REMOTE_PLAYLIST_STREAM_COUNT = "stream_count"; + public static final String REMOTE_PLAYLIST_TABLE = "remote_playlists"; + public static final String REMOTE_PLAYLIST_ID = "uid"; + public static final String REMOTE_PLAYLIST_SERVICE_ID = "service_id"; + public static final String REMOTE_PLAYLIST_NAME = "name"; + public static final String REMOTE_PLAYLIST_URL = "url"; + public static final String REMOTE_PLAYLIST_THUMBNAIL_URL = "thumbnail_url"; + public static final String REMOTE_PLAYLIST_UPLOADER_NAME = "uploader"; + public static final String REMOTE_PLAYLIST_STREAM_COUNT = "stream_count"; @PrimaryKey(autoGenerate = true) @ColumnInfo(name = REMOTE_PLAYLIST_ID) @@ -55,8 +55,9 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem { @ColumnInfo(name = REMOTE_PLAYLIST_STREAM_COUNT) private Long streamCount; - public PlaylistRemoteEntity(int serviceId, String name, String url, String thumbnailUrl, - String uploader, Long streamCount) { + public PlaylistRemoteEntity(final int serviceId, final String name, final String url, + final String thumbnailUrl, final String uploader, + final Long streamCount) { this.serviceId = serviceId; this.name = name; this.url = url; @@ -68,7 +69,8 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem { @Ignore public PlaylistRemoteEntity(final PlaylistInfo info) { this(info.getServiceId(), info.getName(), info.getUrl(), - info.getThumbnailUrl() == null ? info.getUploaderAvatarUrl() : info.getThumbnailUrl(), + info.getThumbnailUrl() == null + ? info.getUploaderAvatarUrl() : info.getThumbnailUrl(), info.getUploaderName(), info.getStreamCount()); } @@ -90,7 +92,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem { return uid; } - public void setUid(long uid) { + public void setUid(final long uid) { this.uid = uid; } @@ -98,7 +100,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem { return serviceId; } - public void setServiceId(int serviceId) { + public void setServiceId(final int serviceId) { this.serviceId = serviceId; } @@ -106,7 +108,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem { return name; } - public void setName(String name) { + public void setName(final String name) { this.name = name; } @@ -114,7 +116,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem { return thumbnailUrl; } - public void setThumbnailUrl(String thumbnailUrl) { + public void setThumbnailUrl(final String thumbnailUrl) { this.thumbnailUrl = thumbnailUrl; } @@ -122,7 +124,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem { return url; } - public void setUrl(String url) { + public void setUrl(final String url) { this.url = url; } @@ -130,7 +132,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem { return uploader; } - public void setUploader(String uploader) { + public void setUploader(final String uploader) { this.uploader = uploader; } @@ -138,7 +140,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem { return streamCount; } - public void setStreamCount(Long streamCount) { + public void setStreamCount(final Long streamCount) { this.streamCount = streamCount; } diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.java b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.java index 87afdb4f9..f3208b6d5 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.java @@ -30,11 +30,10 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PL onDelete = CASCADE, onUpdate = CASCADE, deferred = true) }) public class PlaylistStreamEntity { - - final public static String PLAYLIST_STREAM_JOIN_TABLE = "playlist_stream_join"; - final public static String JOIN_PLAYLIST_ID = "playlist_id"; - final public static String JOIN_STREAM_ID = "stream_id"; - final public static String JOIN_INDEX = "join_index"; + public static final String PLAYLIST_STREAM_JOIN_TABLE = "playlist_stream_join"; + public static final String JOIN_PLAYLIST_ID = "playlist_id"; + public static final String JOIN_STREAM_ID = "stream_id"; + public static final String JOIN_INDEX = "join_index"; @ColumnInfo(name = JOIN_PLAYLIST_ID) private long playlistUid; @@ -55,23 +54,23 @@ public class PlaylistStreamEntity { return playlistUid; } + public void setPlaylistUid(final long playlistUid) { + this.playlistUid = playlistUid; + } + public long getStreamUid() { return streamUid; } + public void setStreamUid(final long streamUid) { + this.streamUid = streamUid; + } + public int getIndex() { return index; } - public void setPlaylistUid(long playlistUid) { - this.playlistUid = playlistUid; - } - - public void setStreamUid(long streamUid) { - this.streamUid = streamUid; - } - - public void setIndex(int index) { + public void setIndex(final int index) { this.index = index; } } diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java deleted file mode 100644 index 9b61eb469..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java +++ /dev/null @@ -1,69 +0,0 @@ -package org.schabi.newpipe.database.stream; - -import androidx.room.ColumnInfo; - -import org.schabi.newpipe.database.LocalItem; -import org.schabi.newpipe.database.history.model.StreamHistoryEntity; -import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.extractor.stream.StreamType; - -import java.util.Date; - -public class StreamStatisticsEntry implements LocalItem { - final public static String STREAM_LATEST_DATE = "latestAccess"; - final public static String STREAM_WATCH_COUNT = "watchCount"; - - @ColumnInfo(name = StreamEntity.STREAM_ID) - final public long uid; - @ColumnInfo(name = StreamEntity.STREAM_SERVICE_ID) - final public int serviceId; - @ColumnInfo(name = StreamEntity.STREAM_URL) - final public String url; - @ColumnInfo(name = StreamEntity.STREAM_TITLE) - final public String title; - @ColumnInfo(name = StreamEntity.STREAM_TYPE) - final public StreamType streamType; - @ColumnInfo(name = StreamEntity.STREAM_DURATION) - final public long duration; - @ColumnInfo(name = StreamEntity.STREAM_UPLOADER) - final public String uploader; - @ColumnInfo(name = StreamEntity.STREAM_THUMBNAIL_URL) - final public String thumbnailUrl; - @ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID) - final public long streamId; - @ColumnInfo(name = StreamStatisticsEntry.STREAM_LATEST_DATE) - final public Date latestAccessDate; - @ColumnInfo(name = StreamStatisticsEntry.STREAM_WATCH_COUNT) - final public long watchCount; - - public StreamStatisticsEntry(long uid, int serviceId, String url, String title, - StreamType streamType, long duration, String uploader, - String thumbnailUrl, long streamId, Date latestAccessDate, - long watchCount) { - this.uid = uid; - this.serviceId = serviceId; - this.url = url; - this.title = title; - this.streamType = streamType; - this.duration = duration; - this.uploader = uploader; - this.thumbnailUrl = thumbnailUrl; - this.streamId = streamId; - this.latestAccessDate = latestAccessDate; - this.watchCount = watchCount; - } - - public StreamInfoItem toStreamInfoItem() { - StreamInfoItem item = new StreamInfoItem(serviceId, url, title, streamType); - item.setDuration(duration); - item.setUploaderName(uploader); - item.setThumbnailUrl(thumbnailUrl); - return item; - } - - @Override - public LocalItemType getLocalItemType() { - return LocalItemType.STATISTIC_STREAM_ITEM; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.kt b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.kt new file mode 100644 index 000000000..dde1f0392 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.kt @@ -0,0 +1,42 @@ +package org.schabi.newpipe.database.stream + +import androidx.room.ColumnInfo +import androidx.room.Embedded +import java.util.Date +import org.schabi.newpipe.database.LocalItem +import org.schabi.newpipe.database.history.model.StreamHistoryEntity +import org.schabi.newpipe.database.stream.model.StreamEntity +import org.schabi.newpipe.extractor.stream.StreamInfoItem + +class StreamStatisticsEntry( + @Embedded + val streamEntity: StreamEntity, + + @ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID) + val streamId: Long, + + @ColumnInfo(name = STREAM_LATEST_DATE) + val latestAccessDate: Date, + + @ColumnInfo(name = STREAM_WATCH_COUNT) + val watchCount: Long +) : LocalItem { + + fun toStreamInfoItem(): StreamInfoItem { + val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType) + item.duration = streamEntity.duration + item.uploaderName = streamEntity.uploader + item.thumbnailUrl = streamEntity.thumbnailUrl + + return item + } + + override fun getLocalItemType(): LocalItem.LocalItemType { + return LocalItem.LocalItemType.STATISTIC_STREAM_ITEM + } + + companion object { + const val STREAM_LATEST_DATE = "latestAccess" + const val STREAM_WATCH_COUNT = "watchCount" + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java deleted file mode 100644 index c89f6163f..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java +++ /dev/null @@ -1,98 +0,0 @@ -package org.schabi.newpipe.database.stream.dao; - -import androidx.room.Dao; -import androidx.room.Insert; -import androidx.room.OnConflictStrategy; -import androidx.room.Query; -import androidx.room.Transaction; - -import org.schabi.newpipe.database.BasicDAO; -import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; -import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.database.history.model.StreamHistoryEntity; - -import java.util.ArrayList; -import java.util.List; - -import io.reactivex.Flowable; - -import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE; -import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID; -import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_SERVICE_ID; -import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; -import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_URL; -import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; - -@Dao -public abstract class StreamDAO implements BasicDAO { - @Override - @Query("SELECT * FROM " + STREAM_TABLE) - public abstract Flowable> getAll(); - - @Override - @Query("DELETE FROM " + STREAM_TABLE) - public abstract int deleteAll(); - - @Override - @Query("SELECT * FROM " + STREAM_TABLE + " WHERE " + STREAM_SERVICE_ID + " = :serviceId") - public abstract Flowable> listByService(int serviceId); - - @Query("SELECT * FROM " + STREAM_TABLE + " WHERE " + - STREAM_URL + " = :url AND " + - STREAM_SERVICE_ID + " = :serviceId") - public abstract Flowable> getStream(long serviceId, String url); - - @Insert(onConflict = OnConflictStrategy.IGNORE) - abstract void silentInsertAllInternal(final List streams); - - @Query("SELECT " + STREAM_ID + " FROM " + STREAM_TABLE + " WHERE " + - STREAM_URL + " = :url AND " + - STREAM_SERVICE_ID + " = :serviceId") - abstract Long getStreamIdInternal(long serviceId, String url); - - @Transaction - public long upsert(StreamEntity stream) { - final Long streamIdCandidate = getStreamIdInternal(stream.getServiceId(), stream.getUrl()); - - if (streamIdCandidate == null) { - return insert(stream); - } else { - stream.setUid(streamIdCandidate); - update(stream); - return streamIdCandidate; - } - } - - @Transaction - public List upsertAll(List streams) { - silentInsertAllInternal(streams); - - final List streamIds = new ArrayList<>(streams.size()); - for (StreamEntity stream : streams) { - final Long streamId = getStreamIdInternal(stream.getServiceId(), stream.getUrl()); - if (streamId == null) { - throw new IllegalStateException("StreamID cannot be null just after insertion."); - } - - streamIds.add(streamId); - stream.setUid(streamId); - } - - update(streams); - return streamIds; - } - - @Query("DELETE FROM " + STREAM_TABLE + " WHERE " + STREAM_ID + - " NOT IN " + - "(SELECT DISTINCT " + STREAM_ID + " FROM " + STREAM_TABLE + - - " LEFT JOIN " + STREAM_HISTORY_TABLE + - " ON " + STREAM_ID + " = " + - StreamHistoryEntity.STREAM_HISTORY_TABLE + "." + StreamHistoryEntity.JOIN_STREAM_ID + - - " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE + - " ON " + STREAM_ID + " = " + - PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE + "." + PlaylistStreamEntity.JOIN_STREAM_ID + - ")") - public abstract int deleteOrphans(); -} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.kt b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.kt new file mode 100644 index 000000000..921c08b46 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.kt @@ -0,0 +1,140 @@ +package org.schabi.newpipe.database.stream.dao + +import androidx.room.ColumnInfo +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import io.reactivex.Flowable +import java.util.Date +import org.schabi.newpipe.database.BasicDAO +import org.schabi.newpipe.database.stream.model.StreamEntity +import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID +import org.schabi.newpipe.extractor.stream.StreamType +import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM +import org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM + +@Dao +abstract class StreamDAO : BasicDAO { + @Query("SELECT * FROM streams") + abstract override fun getAll(): Flowable> + + @Query("DELETE FROM streams") + abstract override fun deleteAll(): Int + + @Query("SELECT * FROM streams WHERE service_id = :serviceId") + abstract override fun listByService(serviceId: Int): Flowable> + + @Query("SELECT * FROM streams WHERE url = :url AND service_id = :serviceId") + abstract fun getStream(serviceId: Long, url: String): Flowable> + + @Insert(onConflict = OnConflictStrategy.IGNORE) + internal abstract fun silentInsertInternal(stream: StreamEntity): Long + + @Insert(onConflict = OnConflictStrategy.IGNORE) + internal abstract fun silentInsertAllInternal(streams: List): List + + @Query(""" + SELECT uid, stream_type, textual_upload_date, upload_date, is_upload_date_approximation, duration + FROM streams WHERE url = :url AND service_id = :serviceId + """) + internal abstract fun getMinimalStreamForCompare(serviceId: Int, url: String): StreamCompareFeed? + + @Transaction + open fun upsert(newerStream: StreamEntity): Long { + val uid = silentInsertInternal(newerStream) + + if (uid != -1L) { + newerStream.uid = uid + return uid + } + + compareAndUpdateStream(newerStream) + + update(newerStream) + return newerStream.uid + } + + @Transaction + open fun upsertAll(streams: List): List { + val insertUidList = silentInsertAllInternal(streams) + + val streamIds = ArrayList(streams.size) + for ((index, uid) in insertUidList.withIndex()) { + val newerStream = streams[index] + if (uid != -1L) { + streamIds.add(uid) + newerStream.uid = uid + continue + } + + compareAndUpdateStream(newerStream) + streamIds.add(newerStream.uid) + } + + update(streams) + return streamIds + } + + private fun compareAndUpdateStream(newerStream: StreamEntity) { + val existentMinimalStream = getMinimalStreamForCompare(newerStream.serviceId, newerStream.url) + ?: throw IllegalStateException("Stream cannot be null just after insertion.") + newerStream.uid = existentMinimalStream.uid + + val isNewerStreamLive = newerStream.streamType == AUDIO_LIVE_STREAM || newerStream.streamType == LIVE_STREAM + if (!isNewerStreamLive) { + + // Use the existent upload date if the newer stream does not have a better precision + // (i.e. is an approximation). This is done to prevent unnecessary changes. + val hasBetterPrecision = + newerStream.uploadDate != null && newerStream.isUploadDateApproximation != true + if (existentMinimalStream.uploadDate != null && !hasBetterPrecision) { + newerStream.uploadDate = existentMinimalStream.uploadDate + newerStream.textualUploadDate = existentMinimalStream.textualUploadDate + newerStream.isUploadDateApproximation = existentMinimalStream.isUploadDateApproximation + } + + if (existentMinimalStream.duration > 0 && newerStream.duration < 0) { + newerStream.duration = existentMinimalStream.duration + } + } + } + + @Query(""" + DELETE FROM streams WHERE + + NOT EXISTS (SELECT 1 FROM stream_history sh + WHERE sh.stream_id = streams.uid) + + AND NOT EXISTS (SELECT 1 FROM playlist_stream_join ps + WHERE ps.stream_id = streams.uid) + + AND NOT EXISTS (SELECT 1 FROM feed f + WHERE f.stream_id = streams.uid) + """) + abstract fun deleteOrphans(): Int + + /** + * Minimal entry class used when comparing/updating an existent stream. + */ + internal data class StreamCompareFeed( + @ColumnInfo(name = STREAM_ID) + var uid: Long = 0, + + @ColumnInfo(name = StreamEntity.STREAM_TYPE) + var streamType: StreamType, + + @ColumnInfo(name = StreamEntity.STREAM_TEXTUAL_UPLOAD_DATE) + var textualUploadDate: String? = null, + + @ColumnInfo(name = StreamEntity.STREAM_UPLOAD_DATE) + var uploadDate: Date? = null, + + @ColumnInfo(name = StreamEntity.STREAM_IS_UPLOAD_DATE_APPROXIMATION) + var isUploadDateApproximation: Boolean? = null, + + @ColumnInfo(name = StreamEntity.STREAM_DURATION) + var duration: Long + ) +} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java index c85810984..eb0f77f66 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java @@ -27,21 +27,21 @@ public abstract class StreamStateDAO implements BasicDAO { public abstract int deleteAll(); @Override - public Flowable> listByService(int serviceId) { + public Flowable> listByService(final int serviceId) { throw new UnsupportedOperationException(); } @Query("SELECT * FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId") - public abstract Flowable> getState(final long streamId); + public abstract Flowable> getState(long streamId); @Query("DELETE FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId") - public abstract int deleteState(final long streamId); + public abstract int deleteState(long streamId); @Insert(onConflict = OnConflictStrategy.IGNORE) - abstract void silentInsertInternal(final StreamStateEntity streamState); + abstract void silentInsertInternal(StreamStateEntity streamState); @Transaction - public long upsert(StreamStateEntity stream) { + public long upsert(final StreamStateEntity stream) { silentInsertInternal(stream); return update(stream); } diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java deleted file mode 100644 index 1f26e214d..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java +++ /dev/null @@ -1,153 +0,0 @@ -package org.schabi.newpipe.database.stream.model; - -import androidx.room.ColumnInfo; -import androidx.room.Entity; -import androidx.room.Ignore; -import androidx.room.Index; -import androidx.room.PrimaryKey; - -import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.extractor.stream.StreamType; -import org.schabi.newpipe.player.playqueue.PlayQueueItem; -import org.schabi.newpipe.util.Constants; - -import java.io.Serializable; - -import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_SERVICE_ID; -import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; -import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_URL; - -@Entity(tableName = STREAM_TABLE, - indices = {@Index(value = {STREAM_SERVICE_ID, STREAM_URL}, unique = true)}) -public class StreamEntity implements Serializable { - - final public static String STREAM_TABLE = "streams"; - final public static String STREAM_ID = "uid"; - final public static String STREAM_SERVICE_ID = "service_id"; - final public static String STREAM_URL = "url"; - final public static String STREAM_TITLE = "title"; - final public static String STREAM_TYPE = "stream_type"; - final public static String STREAM_DURATION = "duration"; - final public static String STREAM_UPLOADER = "uploader"; - final public static String STREAM_THUMBNAIL_URL = "thumbnail_url"; - - @PrimaryKey(autoGenerate = true) - @ColumnInfo(name = STREAM_ID) - private long uid = 0; - - @ColumnInfo(name = STREAM_SERVICE_ID) - private int serviceId = Constants.NO_SERVICE_ID; - - @ColumnInfo(name = STREAM_URL) - private String url; - - @ColumnInfo(name = STREAM_TITLE) - private String title; - - @ColumnInfo(name = STREAM_TYPE) - private StreamType streamType; - - @ColumnInfo(name = STREAM_DURATION) - private Long duration; - - @ColumnInfo(name = STREAM_UPLOADER) - private String uploader; - - @ColumnInfo(name = STREAM_THUMBNAIL_URL) - private String thumbnailUrl; - - public StreamEntity(final int serviceId, final String title, final String url, - final StreamType streamType, final String thumbnailUrl, final String uploader, - final long duration) { - this.serviceId = serviceId; - this.title = title; - this.url = url; - this.streamType = streamType; - this.thumbnailUrl = thumbnailUrl; - this.uploader = uploader; - this.duration = duration; - } - - @Ignore - public StreamEntity(final StreamInfoItem item) { - this(item.getServiceId(), item.getName(), item.getUrl(), item.getStreamType(), item.getThumbnailUrl(), - item.getUploaderName(), item.getDuration()); - } - - @Ignore - public StreamEntity(final StreamInfo info) { - this(info.getServiceId(), info.getName(), info.getUrl(), info.getStreamType(), info.getThumbnailUrl(), - info.getUploaderName(), info.getDuration()); - } - - @Ignore - public StreamEntity(final PlayQueueItem item) { - this(item.getServiceId(), item.getTitle(), item.getUrl(), item.getStreamType(), - item.getThumbnailUrl(), item.getUploader(), item.getDuration()); - } - - public long getUid() { - return uid; - } - - public void setUid(long uid) { - this.uid = uid; - } - - public int getServiceId() { - return serviceId; - } - - public void setServiceId(int serviceId) { - this.serviceId = serviceId; - } - - public String getUrl() { - return url; - } - - public void setUrl(String url) { - this.url = url; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public StreamType getStreamType() { - return streamType; - } - - public void setStreamType(StreamType type) { - this.streamType = type; - } - - public Long getDuration() { - return duration; - } - - public void setDuration(Long duration) { - this.duration = duration; - } - - public String getUploader() { - return uploader; - } - - public void setUploader(String uploader) { - this.uploader = uploader; - } - - public String getThumbnailUrl() { - return thumbnailUrl; - } - - public void setThumbnailUrl(String thumbnailUrl) { - this.thumbnailUrl = thumbnailUrl; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.kt b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.kt new file mode 100644 index 000000000..d13f5cc2d --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.kt @@ -0,0 +1,121 @@ +package org.schabi.newpipe.database.stream.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Ignore +import androidx.room.Index +import androidx.room.PrimaryKey +import java.io.Serializable +import java.util.Calendar +import java.util.Date +import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_SERVICE_ID +import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_TABLE +import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_URL +import org.schabi.newpipe.extractor.localization.DateWrapper +import org.schabi.newpipe.extractor.stream.StreamInfo +import org.schabi.newpipe.extractor.stream.StreamInfoItem +import org.schabi.newpipe.extractor.stream.StreamType +import org.schabi.newpipe.player.playqueue.PlayQueueItem + +@Entity(tableName = STREAM_TABLE, + indices = [ + Index(value = [STREAM_SERVICE_ID, STREAM_URL], unique = true) + ] +) +data class StreamEntity( + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = STREAM_ID) + var uid: Long = 0, + + @ColumnInfo(name = STREAM_SERVICE_ID) + var serviceId: Int, + + @ColumnInfo(name = STREAM_URL) + var url: String, + + @ColumnInfo(name = STREAM_TITLE) + var title: String, + + @ColumnInfo(name = STREAM_TYPE) + var streamType: StreamType, + + @ColumnInfo(name = STREAM_DURATION) + var duration: Long, + + @ColumnInfo(name = STREAM_UPLOADER) + var uploader: String, + + @ColumnInfo(name = STREAM_THUMBNAIL_URL) + var thumbnailUrl: String? = null, + + @ColumnInfo(name = STREAM_VIEWS) + var viewCount: Long? = null, + + @ColumnInfo(name = STREAM_TEXTUAL_UPLOAD_DATE) + var textualUploadDate: String? = null, + + @ColumnInfo(name = STREAM_UPLOAD_DATE) + var uploadDate: Date? = null, + + @ColumnInfo(name = STREAM_IS_UPLOAD_DATE_APPROXIMATION) + var isUploadDateApproximation: Boolean? = null +) : Serializable { + + @Ignore + constructor(item: StreamInfoItem) : this( + serviceId = item.serviceId, url = item.url, title = item.name, + streamType = item.streamType, duration = item.duration, uploader = item.uploaderName, + thumbnailUrl = item.thumbnailUrl, viewCount = item.viewCount, + textualUploadDate = item.textualUploadDate, uploadDate = item.uploadDate?.date()?.time, + isUploadDateApproximation = item.uploadDate?.isApproximation + ) + + @Ignore + constructor(info: StreamInfo) : this( + serviceId = info.serviceId, url = info.url, title = info.name, + streamType = info.streamType, duration = info.duration, uploader = info.uploaderName, + thumbnailUrl = info.thumbnailUrl, viewCount = info.viewCount, + textualUploadDate = info.textualUploadDate, uploadDate = info.uploadDate?.date()?.time, + isUploadDateApproximation = info.uploadDate?.isApproximation + ) + + @Ignore + constructor(item: PlayQueueItem) : this( + serviceId = item.serviceId, url = item.url, title = item.title, + streamType = item.streamType, duration = item.duration, uploader = item.uploader, + thumbnailUrl = item.thumbnailUrl + ) + + fun toStreamInfoItem(): StreamInfoItem { + val item = StreamInfoItem(serviceId, url, title, streamType) + item.duration = duration + item.uploaderName = uploader + item.thumbnailUrl = thumbnailUrl + + if (viewCount != null) item.viewCount = viewCount as Long + item.textualUploadDate = textualUploadDate + item.uploadDate = uploadDate?.let { + DateWrapper(Calendar.getInstance().apply { time = it }, isUploadDateApproximation + ?: false) + } + + return item + } + + companion object { + const val STREAM_TABLE = "streams" + const val STREAM_ID = "uid" + const val STREAM_SERVICE_ID = "service_id" + const val STREAM_URL = "url" + const val STREAM_TITLE = "title" + const val STREAM_TYPE = "stream_type" + const val STREAM_DURATION = "duration" + const val STREAM_UPLOADER = "uploader" + const val STREAM_THUMBNAIL_URL = "thumbnail_url" + + const val STREAM_VIEWS = "view_count" + const val STREAM_TEXTUAL_UPLOAD_DATE = "textual_upload_date" + const val STREAM_UPLOAD_DATE = "upload_date" + const val STREAM_IS_UPLOAD_DATE_APPROXIMATION = "is_upload_date_approximation" + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java index 8630bfa53..d275d9a71 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java @@ -1,10 +1,9 @@ package org.schabi.newpipe.database.stream.model; - +import androidx.annotation.Nullable; import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.ForeignKey; -import androidx.annotation.Nullable; import java.util.concurrent.TimeUnit; @@ -21,14 +20,17 @@ import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_ onDelete = CASCADE, onUpdate = CASCADE) }) public class StreamStateEntity { - final public static String STREAM_STATE_TABLE = "stream_state"; - final public static String JOIN_STREAM_ID = "stream_id"; - final public static String STREAM_PROGRESS_TIME = "progress_time"; + public static final String STREAM_STATE_TABLE = "stream_state"; + public static final String JOIN_STREAM_ID = "stream_id"; + public static final String STREAM_PROGRESS_TIME = "progress_time"; - - /** Playback state will not be saved, if playback time less than this threshold */ + /** + * Playback state will not be saved, if playback time is less than this threshold. + */ private static final int PLAYBACK_SAVE_THRESHOLD_START_SECONDS = 5; - /** Playback state will not be saved, if time left less than this threshold */ + /** + * Playback state will not be saved, if time left is less than this threshold. + */ private static final int PLAYBACK_SAVE_THRESHOLD_END_SECONDS = 10; @ColumnInfo(name = JOIN_STREAM_ID) @@ -37,7 +39,7 @@ public class StreamStateEntity { @ColumnInfo(name = STREAM_PROGRESS_TIME) private long progressTime; - public StreamStateEntity(long streamUid, long progressTime) { + public StreamStateEntity(final long streamUid, final long progressTime) { this.streamUid = streamUid; this.progressTime = progressTime; } @@ -46,7 +48,7 @@ public class StreamStateEntity { return streamUid; } - public void setStreamUid(long streamUid) { + public void setStreamUid(final long streamUid) { this.streamUid = streamUid; } @@ -54,21 +56,23 @@ public class StreamStateEntity { return progressTime; } - public void setProgressTime(long progressTime) { + public void setProgressTime(final long progressTime) { this.progressTime = progressTime; } - public boolean isValid(int durationInSeconds) { + public boolean isValid(final int durationInSeconds) { final int seconds = (int) TimeUnit.MILLISECONDS.toSeconds(progressTime); return seconds > PLAYBACK_SAVE_THRESHOLD_START_SECONDS && seconds < durationInSeconds - PLAYBACK_SAVE_THRESHOLD_END_SECONDS; } @Override - public boolean equals(@Nullable Object obj) { + public boolean equals(@Nullable final Object obj) { if (obj instanceof StreamStateEntity) { return ((StreamStateEntity) obj).streamUid == streamUid && ((StreamStateEntity) obj).progressTime == progressTime; - } else return false; + } else { + return false; + } } } diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.java b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.java deleted file mode 100644 index 0869d60ff..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.java +++ /dev/null @@ -1,69 +0,0 @@ -package org.schabi.newpipe.database.subscription; - -import androidx.room.Dao; -import androidx.room.Insert; -import androidx.room.OnConflictStrategy; -import androidx.room.Query; -import androidx.room.Transaction; - -import org.schabi.newpipe.database.BasicDAO; - -import java.util.List; - -import io.reactivex.Flowable; - -import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_SERVICE_ID; -import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_TABLE; -import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_UID; -import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_URL; - -@Dao -public abstract class SubscriptionDAO implements BasicDAO { - @Override - @Query("SELECT * FROM " + SUBSCRIPTION_TABLE) - public abstract Flowable> getAll(); - - @Override - @Query("DELETE FROM " + SUBSCRIPTION_TABLE) - public abstract int deleteAll(); - - @Override - @Query("SELECT * FROM " + SUBSCRIPTION_TABLE + " WHERE " + SUBSCRIPTION_SERVICE_ID + " = :serviceId") - public abstract Flowable> listByService(int serviceId); - - @Query("SELECT * FROM " + SUBSCRIPTION_TABLE + " WHERE " + - SUBSCRIPTION_URL + " LIKE :url AND " + - SUBSCRIPTION_SERVICE_ID + " = :serviceId") - public abstract Flowable> getSubscription(int serviceId, String url); - - @Query("SELECT " + SUBSCRIPTION_UID + " FROM " + SUBSCRIPTION_TABLE + " WHERE " + - SUBSCRIPTION_URL + " LIKE :url AND " + - SUBSCRIPTION_SERVICE_ID + " = :serviceId") - abstract Long getSubscriptionIdInternal(int serviceId, String url); - - @Insert(onConflict = OnConflictStrategy.IGNORE) - abstract Long insertInternal(final SubscriptionEntity entities); - - @Transaction - public List upsertAll(List entities) { - for (SubscriptionEntity entity : entities) { - Long uid = insertInternal(entity); - - if (uid != -1) { - entity.setUid(uid); - continue; - } - - uid = getSubscriptionIdInternal(entity.getServiceId(), entity.getUrl()); - entity.setUid(uid); - - if (uid == -1) { - throw new IllegalStateException("Invalid subscription id (-1)"); - } - - update(entity); - } - - return entities; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt new file mode 100644 index 000000000..60dd343b9 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt @@ -0,0 +1,103 @@ +package org.schabi.newpipe.database.subscription + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import io.reactivex.Flowable +import io.reactivex.Maybe +import org.schabi.newpipe.database.BasicDAO + +@Dao +abstract class SubscriptionDAO : BasicDAO { + @Query("SELECT COUNT(*) FROM subscriptions") + abstract fun rowCount(): Flowable + + @Query("SELECT * FROM subscriptions WHERE service_id = :serviceId") + abstract override fun listByService(serviceId: Int): Flowable> + + @Query("SELECT * FROM subscriptions ORDER BY name COLLATE NOCASE ASC") + abstract override fun getAll(): Flowable> + + @Query(""" + SELECT * FROM subscriptions + + WHERE name LIKE '%' || :filter || '%' + + ORDER BY name COLLATE NOCASE ASC + """) + abstract fun getSubscriptionsFiltered(filter: String): Flowable> + + @Query(""" + SELECT * FROM subscriptions s + + LEFT JOIN feed_group_subscription_join fgs + ON s.uid = fgs.subscription_id + + WHERE (fgs.subscription_id IS NULL OR fgs.group_id = :currentGroupId) + + ORDER BY name COLLATE NOCASE ASC + """) + abstract fun getSubscriptionsOnlyUngrouped( + currentGroupId: Long + ): Flowable> + + @Query(""" + SELECT * FROM subscriptions s + + LEFT JOIN feed_group_subscription_join fgs + ON s.uid = fgs.subscription_id + + WHERE (fgs.subscription_id IS NULL OR fgs.group_id = :currentGroupId) + AND s.name LIKE '%' || :filter || '%' + + ORDER BY name COLLATE NOCASE ASC + """) + abstract fun getSubscriptionsOnlyUngroupedFiltered( + currentGroupId: Long, + filter: String + ): Flowable> + + @Query("SELECT * FROM subscriptions WHERE url LIKE :url AND service_id = :serviceId") + abstract fun getSubscriptionFlowable(serviceId: Int, url: String): Flowable> + + @Query("SELECT * FROM subscriptions WHERE url LIKE :url AND service_id = :serviceId") + abstract fun getSubscription(serviceId: Int, url: String): Maybe + + @Query("SELECT * FROM subscriptions WHERE uid = :subscriptionId") + abstract fun getSubscription(subscriptionId: Long): SubscriptionEntity + + @Query("DELETE FROM subscriptions") + abstract override fun deleteAll(): Int + + @Query("DELETE FROM subscriptions WHERE url LIKE :url AND service_id = :serviceId") + abstract fun deleteSubscription(serviceId: Int, url: String): Int + + @Query("SELECT uid FROM subscriptions WHERE url LIKE :url AND service_id = :serviceId") + internal abstract fun getSubscriptionIdInternal(serviceId: Int, url: String): Long? + + @Insert(onConflict = OnConflictStrategy.IGNORE) + internal abstract fun silentInsertAllInternal(entities: List): List + + @Transaction + open fun upsertAll(entities: List): List { + val insertUidList = silentInsertAllInternal(entities) + + insertUidList.forEachIndexed { index: Int, uidFromInsert: Long -> + val entity = entities[index] + + if (uidFromInsert != -1L) { + entity.uid = uidFromInsert + } else { + val subscriptionIdFromDb = getSubscriptionIdInternal(entity.serviceId, entity.url) + ?: throw IllegalStateException("Subscription cannot be null just after insertion.") + entity.uid = subscriptionIdFromDb + + update(entity) + } + } + + return entities + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java index 1e69567e1..a47f17d13 100644 --- a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java @@ -1,11 +1,11 @@ package org.schabi.newpipe.database.subscription; +import androidx.annotation.NonNull; import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.Ignore; import androidx.room.Index; import androidx.room.PrimaryKey; -import androidx.annotation.NonNull; import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.channel.ChannelInfoItem; @@ -18,15 +18,14 @@ import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCR @Entity(tableName = SUBSCRIPTION_TABLE, indices = {@Index(value = {SUBSCRIPTION_SERVICE_ID, SUBSCRIPTION_URL}, unique = true)}) public class SubscriptionEntity { - - final static String SUBSCRIPTION_UID = "uid"; - final static String SUBSCRIPTION_TABLE = "subscriptions"; - final static String SUBSCRIPTION_SERVICE_ID = "service_id"; - final static String SUBSCRIPTION_URL = "url"; - final static String SUBSCRIPTION_NAME = "name"; - final static String SUBSCRIPTION_AVATAR_URL = "avatar_url"; - final static String SUBSCRIPTION_SUBSCRIBER_COUNT = "subscriber_count"; - final static String SUBSCRIPTION_DESCRIPTION = "description"; + public static final String SUBSCRIPTION_UID = "uid"; + public static final String SUBSCRIPTION_TABLE = "subscriptions"; + public static final String SUBSCRIPTION_SERVICE_ID = "service_id"; + public static final String SUBSCRIPTION_URL = "url"; + public static final String SUBSCRIPTION_NAME = "name"; + public static final String SUBSCRIPTION_AVATAR_URL = "avatar_url"; + public static final String SUBSCRIPTION_SUBSCRIBER_COUNT = "subscriber_count"; + public static final String SUBSCRIPTION_DESCRIPTION = "description"; @PrimaryKey(autoGenerate = true) private long uid = 0; @@ -49,11 +48,21 @@ public class SubscriptionEntity { @ColumnInfo(name = SUBSCRIPTION_DESCRIPTION) private String description; + @Ignore + public static SubscriptionEntity from(@NonNull final ChannelInfo info) { + SubscriptionEntity result = new SubscriptionEntity(); + result.setServiceId(info.getServiceId()); + result.setUrl(info.getUrl()); + result.setData(info.getName(), info.getAvatarUrl(), info.getDescription(), + info.getSubscriberCount()); + return result; + } + public long getUid() { return uid; } - public void setUid(long uid) { + public void setUid(final long uid) { this.uid = uid; } @@ -61,7 +70,7 @@ public class SubscriptionEntity { return serviceId; } - public void setServiceId(int serviceId) { + public void setServiceId(final int serviceId) { this.serviceId = serviceId; } @@ -69,7 +78,7 @@ public class SubscriptionEntity { return url; } - public void setUrl(String url) { + public void setUrl(final String url) { this.url = url; } @@ -77,7 +86,7 @@ public class SubscriptionEntity { return name; } - public void setName(String name) { + public void setName(final String name) { this.name = name; } @@ -85,7 +94,7 @@ public class SubscriptionEntity { return avatarUrl; } - public void setAvatarUrl(String avatarUrl) { + public void setAvatarUrl(final String avatarUrl) { this.avatarUrl = avatarUrl; } @@ -93,7 +102,7 @@ public class SubscriptionEntity { return subscriberCount; } - public void setSubscriberCount(Long subscriberCount) { + public void setSubscriberCount(final Long subscriberCount) { this.subscriberCount = subscriberCount; } @@ -101,19 +110,16 @@ public class SubscriptionEntity { return description; } - public void setDescription(String description) { + public void setDescription(final String description) { this.description = description; } @Ignore - public void setData(final String name, - final String avatarUrl, - final String description, - final Long subscriberCount) { - this.setName(name); - this.setAvatarUrl(avatarUrl); - this.setDescription(description); - this.setSubscriberCount(subscriberCount); + public void setData(final String n, final String au, final String d, final Long sc) { + this.setName(n); + this.setAvatarUrl(au); + this.setDescription(d); + this.setSubscriberCount(sc); } @Ignore @@ -125,12 +131,54 @@ public class SubscriptionEntity { return item; } - @Ignore - public static SubscriptionEntity from(@NonNull ChannelInfo info) { - SubscriptionEntity result = new SubscriptionEntity(); - result.setServiceId(info.getServiceId()); - result.setUrl(info.getUrl()); - result.setData(info.getName(), info.getAvatarUrl(), info.getDescription(), info.getSubscriberCount()); + + // TODO: Remove these generated methods by migrating this class to a data class from Kotlin. + @Override + @SuppressWarnings("EqualsReplaceableByObjectsCall") + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + final SubscriptionEntity that = (SubscriptionEntity) o; + + if (uid != that.uid) { + return false; + } + if (serviceId != that.serviceId) { + return false; + } + if (!url.equals(that.url)) { + return false; + } + if (name != null ? !name.equals(that.name) : that.name != null) { + return false; + } + if (avatarUrl != null ? !avatarUrl.equals(that.avatarUrl) : that.avatarUrl != null) { + return false; + } + if (subscriberCount != null + ? !subscriberCount.equals(that.subscriberCount) + : that.subscriberCount != null) { + return false; + } + return description != null + ? description.equals(that.description) + : that.description == null; + } + + @Override + public int hashCode() { + int result = (int) (uid ^ (uid >>> 32)); + result = 31 * result + serviceId; + result = 31 * result + url.hashCode(); + result = 31 * result + (name != null ? name.hashCode() : 0); + result = 31 * result + (avatarUrl != null ? avatarUrl.hashCode() : 0); + result = 31 * result + (subscriberCount != null ? subscriberCount.hashCode() : 0); + result = 31 * result + (description != null ? description.hashCode() : 0); return result; } } diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java b/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java index 449a790e8..e46ded40d 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java @@ -3,32 +3,37 @@ package org.schabi.newpipe.download; import android.app.FragmentTransaction; import android.content.Intent; import android.os.Bundle; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.ViewTreeObserver; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; + import org.schabi.newpipe.R; -import org.schabi.newpipe.settings.SettingsActivity; +import org.schabi.newpipe.util.AndroidTvUtils; import org.schabi.newpipe.util.ThemeHelper; +import org.schabi.newpipe.views.FocusOverlayView; import us.shandian.giga.service.DownloadManagerService; import us.shandian.giga.ui.fragment.MissionsFragment; +import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; + public class DownloadActivity extends AppCompatActivity { private static final String MISSIONS_FRAGMENT_TAG = "fragment_tag"; @Override - protected void onCreate(Bundle savedInstanceState) { + protected void onCreate(final Bundle savedInstanceState) { // Service Intent i = new Intent(); i.setClass(this, DownloadManagerService.class); startService(i); + assureCorrectAppLanguage(this); ThemeHelper.setTheme(this); super.onCreate(savedInstanceState); setContentView(R.layout.activity_downloader); @@ -43,13 +48,18 @@ public class DownloadActivity extends AppCompatActivity { actionBar.setDisplayShowTitleEnabled(true); } - getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + getWindow().getDecorView().getViewTreeObserver() + .addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { updateFragments(); getWindow().getDecorView().getViewTreeObserver().removeOnGlobalLayoutListener(this); } }); + + if (AndroidTvUtils.isTv(this)) { + FocusOverlayView.setupFocusObserver(this); + } } private void updateFragments() { @@ -62,7 +72,7 @@ public class DownloadActivity extends AppCompatActivity { } @Override - public boolean onCreateOptionsMenu(Menu menu) { + public boolean onCreateOptionsMenu(final Menu menu) { super.onCreateOptionsMenu(menu); MenuInflater inflater = getMenuInflater(); @@ -72,17 +82,11 @@ public class DownloadActivity extends AppCompatActivity { } @Override - public boolean onOptionsItemSelected(MenuItem item) { + public boolean onOptionsItemSelected(final MenuItem item) { switch (item.getItemId()) { - case android.R.id.home: { + case android.R.id.home: onBackPressed(); return true; - } - case R.id.action_settings: { - Intent intent = new Intent(this, SettingsActivity.class); - startActivity(intent); - return true; - } default: return super.onOptionsItemSelected(item); } diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index 29208b0e0..cad0258da 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -11,15 +11,6 @@ import android.os.Bundle; import android.os.Environment; import android.os.IBinder; import android.preference.PreferenceManager; -import androidx.annotation.IdRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; -import androidx.fragment.app.DialogFragment; -import androidx.documentfile.provider.DocumentFile; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.view.menu.ActionMenuItemView; -import androidx.appcompat.widget.Toolbar; import android.util.Log; import android.util.SparseArray; import android.view.LayoutInflater; @@ -34,10 +25,21 @@ import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.IdRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.view.menu.ActionMenuItemView; +import androidx.appcompat.widget.Toolbar; +import androidx.documentfile.provider.DocumentFile; +import androidx.fragment.app.DialogFragment; + import com.nononsenseapps.filepicker.Utils; import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; +import org.schabi.newpipe.RouterActivity; import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.localization.Localization; @@ -77,25 +79,35 @@ import us.shandian.giga.service.DownloadManagerService; import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder; import us.shandian.giga.service.MissionState; -public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener { +import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; + +public class DownloadDialog extends DialogFragment + implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener { private static final String TAG = "DialogFragment"; private static final boolean DEBUG = MainActivity.DEBUG; private static final int REQUEST_DOWNLOAD_SAVE_AS = 0x1230; @State - protected StreamInfo currentInfo; + StreamInfo currentInfo; @State - protected StreamSizeWrapper wrappedAudioStreams = StreamSizeWrapper.empty(); + StreamSizeWrapper wrappedAudioStreams = StreamSizeWrapper.empty(); @State - protected StreamSizeWrapper wrappedVideoStreams = StreamSizeWrapper.empty(); + StreamSizeWrapper wrappedVideoStreams = StreamSizeWrapper.empty(); @State - protected StreamSizeWrapper wrappedSubtitleStreams = StreamSizeWrapper.empty(); + StreamSizeWrapper wrappedSubtitleStreams = StreamSizeWrapper.empty(); @State - protected int selectedVideoIndex = 0; + int selectedVideoIndex = 0; @State - protected int selectedAudioIndex = 0; + int selectedAudioIndex = 0; @State - protected int selectedSubtitleIndex = 0; + int selectedSubtitleIndex = 0; + + private StoredDirectoryHelper mainStorageAudio = null; + private StoredDirectoryHelper mainStorageVideo = null; + private DownloadManager downloadManager = null; + private ActionMenuItemView okButton = null; + private Context context; + private boolean askForSavePath; private StreamItemAdapter audioStreamsAdapter; private StreamItemAdapter videoStreamsAdapter; @@ -111,15 +123,16 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck private SharedPreferences prefs; - public static DownloadDialog newInstance(StreamInfo info) { + public static DownloadDialog newInstance(final StreamInfo info) { DownloadDialog dialog = new DownloadDialog(); dialog.setInfo(info); return dialog; } - public static DownloadDialog newInstance(Context context, StreamInfo info) { - final ArrayList streamsList = new ArrayList<>(ListHelper.getSortedStreamVideosList(context, - info.getVideoStreams(), info.getVideoOnlyStreams(), false)); + public static DownloadDialog newInstance(final Context context, final StreamInfo info) { + final ArrayList streamsList = new ArrayList<>(ListHelper + .getSortedStreamVideosList(context, info.getVideoStreams(), + info.getVideoOnlyStreams(), false)); final int selectedStreamIndex = ListHelper.getDefaultResolutionIndex(context, streamsList); final DownloadDialog instance = newInstance(info); @@ -131,57 +144,61 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck return instance; } - private void setInfo(StreamInfo info) { + private void setInfo(final StreamInfo info) { this.currentInfo = info; } - public void setAudioStreams(List audioStreams) { + public void setAudioStreams(final List audioStreams) { setAudioStreams(new StreamSizeWrapper<>(audioStreams, getContext())); } - public void setAudioStreams(StreamSizeWrapper wrappedAudioStreams) { - this.wrappedAudioStreams = wrappedAudioStreams; + public void setAudioStreams(final StreamSizeWrapper was) { + this.wrappedAudioStreams = was; } - public void setVideoStreams(List videoStreams) { + public void setVideoStreams(final List videoStreams) { setVideoStreams(new StreamSizeWrapper<>(videoStreams, getContext())); } - public void setVideoStreams(StreamSizeWrapper wrappedVideoStreams) { - this.wrappedVideoStreams = wrappedVideoStreams; - } - - public void setSubtitleStreams(List subtitleStreams) { - setSubtitleStreams(new StreamSizeWrapper<>(subtitleStreams, getContext())); - } - - public void setSubtitleStreams(StreamSizeWrapper wrappedSubtitleStreams) { - this.wrappedSubtitleStreams = wrappedSubtitleStreams; - } - - public void setSelectedVideoStream(int selectedVideoIndex) { - this.selectedVideoIndex = selectedVideoIndex; - } - - public void setSelectedAudioStream(int selectedAudioIndex) { - this.selectedAudioIndex = selectedAudioIndex; - } - - public void setSelectedSubtitleStream(int selectedSubtitleIndex) { - this.selectedSubtitleIndex = selectedSubtitleIndex; - } - /*////////////////////////////////////////////////////////////////////////// // LifeCycle //////////////////////////////////////////////////////////////////////////*/ - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (DEBUG) - Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]"); + public void setVideoStreams(final StreamSizeWrapper wvs) { + this.wrappedVideoStreams = wvs; + } - if (!PermissionHelper.checkStoragePermissions(getActivity(), PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) { + public void setSubtitleStreams(final List subtitleStreams) { + setSubtitleStreams(new StreamSizeWrapper<>(subtitleStreams, getContext())); + } + + public void setSubtitleStreams( + final StreamSizeWrapper wss) { + this.wrappedSubtitleStreams = wss; + } + + public void setSelectedVideoStream(final int svi) { + this.selectedVideoIndex = svi; + } + + public void setSelectedAudioStream(final int sai) { + this.selectedAudioIndex = sai; + } + + public void setSelectedSubtitleStream(final int ssi) { + this.selectedSubtitleIndex = ssi; + } + + @Override + public void onCreate(@Nullable final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (DEBUG) { + Log.d(TAG, "onCreate() called with: " + + "savedInstanceState = [" + savedInstanceState + "]"); + } + + if (!PermissionHelper.checkStoragePermissions(getActivity(), + PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) { getDialog().dismiss(); return; } @@ -195,17 +212,23 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck List videoStreams = wrappedVideoStreams.getStreamsList(); for (int i = 0; i < videoStreams.size(); i++) { - if (!videoStreams.get(i).isVideoOnly()) continue; - AudioStream audioStream = SecondaryStreamHelper.getAudioStreamFor(wrappedAudioStreams.getStreamsList(), videoStreams.get(i)); + if (!videoStreams.get(i).isVideoOnly()) { + continue; + } + AudioStream audioStream = SecondaryStreamHelper + .getAudioStreamFor(wrappedAudioStreams.getStreamsList(), videoStreams.get(i)); if (audioStream != null) { - secondaryStreams.append(i, new SecondaryStreamHelper<>(wrappedAudioStreams, audioStream)); + secondaryStreams + .append(i, new SecondaryStreamHelper<>(wrappedAudioStreams, audioStream)); } else if (DEBUG) { - Log.w(TAG, "No audio stream candidates for video format " + videoStreams.get(i).getFormat().name()); + Log.w(TAG, "No audio stream candidates for video format " + + videoStreams.get(i).getFormat().name()); } } - this.videoStreamsAdapter = new StreamItemAdapter<>(context, wrappedVideoStreams, secondaryStreams); + this.videoStreamsAdapter = new StreamItemAdapter<>(context, wrappedVideoStreams, + secondaryStreams); this.audioStreamsAdapter = new StreamItemAdapter<>(context, wrappedAudioStreams); this.subtitleStreamsAdapter = new StreamItemAdapter<>(context, wrappedSubtitleStreams); @@ -214,7 +237,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck context.bindService(intent, new ServiceConnection() { @Override - public void onServiceConnected(ComponentName cname, IBinder service) { + public void onServiceConnected(final ComponentName cname, final IBinder service) { DownloadManagerBinder mgr = (DownloadManagerBinder) service; mainStorageAudio = mgr.getMainStorageAudio(); @@ -228,25 +251,34 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck } @Override - public void onServiceDisconnected(ComponentName name) { + public void onServiceDisconnected(final ComponentName name) { // nothing to do } }, Context.BIND_AUTO_CREATE); } + /*////////////////////////////////////////////////////////////////////////// + // Inits + //////////////////////////////////////////////////////////////////////////*/ + @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - if (DEBUG) - Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]"); + public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container, + final Bundle savedInstanceState) { + if (DEBUG) { + Log.d(TAG, "onCreateView() called with: " + + "inflater = [" + inflater + "], container = [" + container + "], " + + "savedInstanceState = [" + savedInstanceState + "]"); + } return inflater.inflate(R.layout.download_dialog, container); } @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); nameEditText = view.findViewById(R.id.file_name); nameEditText.setText(FilenameUtils.createFilename(getContext(), currentInfo.getName())); - selectedAudioIndex = ListHelper.getDefaultAudioFormat(getContext(), currentInfo.getAudioStreams()); + selectedAudioIndex = ListHelper + .getDefaultAudioFormat(getContext(), currentInfo.getAudioStreams()); selectedSubtitleIndex = getSubtitleIndexBy(subtitleStreamsAdapter.getAll()); @@ -268,21 +300,20 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck threadsCountTextView.setText(String.valueOf(threads)); threadsSeekBar.setProgress(threads - 1); threadsSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { - @Override - public void onProgressChanged(SeekBar seekbar, int progress, boolean fromUser) { - progress++; - prefs.edit().putInt(getString(R.string.default_download_threads), progress).apply(); - threadsCountTextView.setText(String.valueOf(progress)); + public void onProgressChanged(final SeekBar seekbar, final int progress, + final boolean fromUser) { + final int newProgress = progress + 1; + prefs.edit().putInt(getString(R.string.default_download_threads), newProgress) + .apply(); + threadsCountTextView.setText(String.valueOf(newProgress)); } @Override - public void onStartTrackingTouch(SeekBar p1) { - } + public void onStartTrackingTouch(final SeekBar p1) { } @Override - public void onStopTrackingTouch(SeekBar p1) { - } + public void onStopTrackingTouch(final SeekBar p1) { } }); fetchStreamsSize(); @@ -291,17 +322,20 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck private void fetchStreamsSize() { disposables.clear(); - disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedVideoStreams).subscribe(result -> { + disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedVideoStreams) + .subscribe(result -> { if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.video_button) { setupVideoSpinner(); } })); - disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedAudioStreams).subscribe(result -> { + disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedAudioStreams) + .subscribe(result -> { if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.audio_button) { setupAudioSpinner(); } })); - disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedSubtitleStreams).subscribe(result -> { + disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedSubtitleStreams) + .subscribe(result -> { if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.subtitle_button) { setupSubtitleSpinner(); } @@ -314,14 +348,22 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck disposables.clear(); } + /*////////////////////////////////////////////////////////////////////////// + // Radio group Video&Audio options - Listener + //////////////////////////////////////////////////////////////////////////*/ + @Override - public void onSaveInstanceState(@NonNull Bundle outState) { + public void onSaveInstanceState(@NonNull final Bundle outState) { super.onSaveInstanceState(outState); Icepick.saveInstanceState(this, outState); } + /*////////////////////////////////////////////////////////////////////////// + // Streams Spinner Listener + //////////////////////////////////////////////////////////////////////////*/ + @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { + public void onActivityResult(final int requestCode, final int resultCode, final Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_DOWNLOAD_SAVE_AS && resultCode == Activity.RESULT_OK) { @@ -332,7 +374,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck if (FilePickerActivityHelper.isOwnFileUri(context, data.getData())) { File file = Utils.getFileForUri(data.getData()); - checkSelectedDownload(null, Uri.fromFile(file), file.getName(), StoredFileHelper.DEFAULT_MIME); + checkSelectedDownload(null, Uri.fromFile(file), file.getName(), + StoredFileHelper.DEFAULT_MIME); return; } @@ -343,39 +386,46 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck } // check if the selected file was previously used - checkSelectedDownload(null, data.getData(), docFile.getName(), docFile.getType()); + checkSelectedDownload(null, data.getData(), docFile.getName(), + docFile.getType()); } } - /*////////////////////////////////////////////////////////////////////////// - // Inits - //////////////////////////////////////////////////////////////////////////*/ - - private void initToolbar(Toolbar toolbar) { - if (DEBUG) Log.d(TAG, "initToolbar() called with: toolbar = [" + toolbar + "]"); - - boolean isLight = ThemeHelper.isLightThemeSelected(getActivity()); + private void initToolbar(final Toolbar toolbar) { + if (DEBUG) { + Log.d(TAG, "initToolbar() called with: toolbar = [" + toolbar + "]"); + } toolbar.setTitle(R.string.download_dialog_title); - toolbar.setNavigationIcon(isLight ? R.drawable.ic_arrow_back_black_24dp : R.drawable.ic_arrow_back_white_24dp); + toolbar.setNavigationIcon( + ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_arrow_back)); toolbar.inflateMenu(R.menu.dialog_url); - toolbar.setNavigationOnClickListener(v -> getDialog().dismiss()); + toolbar.setNavigationOnClickListener(v -> requireDialog().dismiss()); toolbar.setNavigationContentDescription(R.string.cancel); okButton = toolbar.findViewById(R.id.okay); - okButton.setEnabled(false);// disable until the download service connection is done + okButton.setEnabled(false); // disable until the download service connection is done toolbar.setOnMenuItemClickListener(item -> { if (item.getItemId() == R.id.okay) { prepareSelectedDownload(); + if (getActivity() instanceof RouterActivity) { + getActivity().finish(); + } return true; } return false; }); } + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + private void setupAudioSpinner() { - if (getContext() == null) return; + if (getContext() == null) { + return; + } streamsSpinner.setAdapter(audioStreamsAdapter); streamsSpinner.setSelection(selectedAudioIndex); @@ -383,7 +433,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck } private void setupVideoSpinner() { - if (getContext() == null) return; + if (getContext() == null) { + return; + } streamsSpinner.setAdapter(videoStreamsAdapter); streamsSpinner.setSelection(selectedVideoIndex); @@ -391,21 +443,21 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck } private void setupSubtitleSpinner() { - if (getContext() == null) return; + if (getContext() == null) { + return; + } streamsSpinner.setAdapter(subtitleStreamsAdapter); streamsSpinner.setSelection(selectedSubtitleIndex); setRadioButtonsState(true); } - /*////////////////////////////////////////////////////////////////////////// - // Radio group Video&Audio options - Listener - //////////////////////////////////////////////////////////////////////////*/ - @Override - public void onCheckedChanged(RadioGroup group, @IdRes int checkedId) { - if (DEBUG) - Log.d(TAG, "onCheckedChanged() called with: group = [" + group + "], checkedId = [" + checkedId + "]"); + public void onCheckedChanged(final RadioGroup group, @IdRes final int checkedId) { + if (DEBUG) { + Log.d(TAG, "onCheckedChanged() called with: " + + "group = [" + group + "], checkedId = [" + checkedId + "]"); + } boolean flag = true; switch (checkedId) { @@ -424,14 +476,14 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck threadsSeekBar.setEnabled(flag); } - /*////////////////////////////////////////////////////////////////////////// - // Streams Spinner Listener - //////////////////////////////////////////////////////////////////////////*/ - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { - if (DEBUG) - Log.d(TAG, "onItemSelected() called with: parent = [" + parent + "], view = [" + view + "], position = [" + position + "], id = [" + id + "]"); + public void onItemSelected(final AdapterView parent, final View view, + final int position, final long id) { + if (DEBUG) { + Log.d(TAG, "onItemSelected() called with: " + + "parent = [" + parent + "], view = [" + view + "], " + + "position = [" + position + "], id = [" + id + "]"); + } switch (radioStreamsGroup.getCheckedRadioButtonId()) { case R.id.audio_button: selectedAudioIndex = position; @@ -446,13 +498,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck } @Override - public void onNothingSelected(AdapterView parent) { + public void onNothingSelected(final AdapterView parent) { } - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ - protected void setupDownloadOptions() { setRadioButtonsState(false); @@ -477,30 +525,36 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck subtitleButton.setChecked(true); setupSubtitleSpinner(); } else { - Toast.makeText(getContext(), R.string.no_streams_available_download, Toast.LENGTH_SHORT).show(); + Toast.makeText(getContext(), R.string.no_streams_available_download, + Toast.LENGTH_SHORT).show(); getDialog().dismiss(); } } - private void setRadioButtonsState(boolean enabled) { + private void setRadioButtonsState(final boolean enabled) { radioStreamsGroup.findViewById(R.id.audio_button).setEnabled(enabled); radioStreamsGroup.findViewById(R.id.video_button).setEnabled(enabled); radioStreamsGroup.findViewById(R.id.subtitle_button).setEnabled(enabled); } - private int getSubtitleIndexBy(List streams) { + private int getSubtitleIndexBy(final List streams) { final Localization preferredLocalization = NewPipe.getPreferredLocalization(); int candidate = 0; for (int i = 0; i < streams.size(); i++) { final Locale streamLocale = streams.get(i).getLocale(); - final boolean languageEquals = streamLocale.getLanguage() != null && preferredLocalization.getLanguageCode() != null && - streamLocale.getLanguage().equals(new Locale(preferredLocalization.getLanguageCode()).getLanguage()); - final boolean countryEquals = streamLocale.getCountry() != null && streamLocale.getCountry().equals(preferredLocalization.getCountryCode()); + final boolean languageEquals = streamLocale.getLanguage() != null + && preferredLocalization.getLanguageCode() != null + && streamLocale.getLanguage() + .equals(new Locale(preferredLocalization.getLanguageCode()).getLanguage()); + final boolean countryEquals = streamLocale.getCountry() != null + && streamLocale.getCountry().equals(preferredLocalization.getCountryCode()); if (languageEquals) { - if (countryEquals) return i; + if (countryEquals) { + return i; + } candidate = i; } @@ -509,35 +563,30 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck return candidate; } - StoredDirectoryHelper mainStorageAudio = null; - StoredDirectoryHelper mainStorageVideo = null; - DownloadManager downloadManager = null; - ActionMenuItemView okButton = null; - Context context; - boolean askForSavePath; - private String getNameEditText() { String str = nameEditText.getText().toString().trim(); return FilenameUtils.createFilename(context, str.isEmpty() ? currentInfo.getName() : str); } - private void showFailedDialog(@StringRes int msg) { + private void showFailedDialog(@StringRes final int msg) { + assureCorrectAppLanguage(getContext()); new AlertDialog.Builder(context) .setTitle(R.string.general_error) .setMessage(msg) - .setNegativeButton(android.R.string.ok, null) + .setNegativeButton(getString(R.string.finish), null) .create() .show(); } - private void showErrorActivity(Exception e) { + private void showErrorActivity(final Exception e) { ErrorActivity.reportError( context, Collections.singletonList(e), null, null, - ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "-", "-", R.string.general_error) + ErrorActivity.ErrorInfo + .make(UserAction.SOMETHING_ELSE, "-", "-", R.string.general_error) ); } @@ -555,8 +604,16 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck case R.id.audio_button: mainStorage = mainStorageAudio; format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat(); - mime = format.mimeType; - filename += format.suffix; + switch (format) { + case WEBMA_OPUS: + mime = "audio/ogg"; + filename += "opus"; + break; + default: + mime = format.mimeType; + filename += format.suffix; + break; + } break; case R.id.video_button: mainStorage = mainStorageVideo; @@ -565,7 +622,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck filename += format.suffix; break; case R.id.subtitle_button: - mainStorage = mainStorageVideo;// subtitle & video files go together + mainStorage = mainStorageVideo; // subtitle & video files go together format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat(); mime = format.mimeType; filename += format == MediaFormat.TTML ? MediaFormat.SRT.suffix : format.suffix; @@ -580,23 +637,25 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck // * save path not defined (via download settings) // * the user checked the "ask where to download" option - if (!askForSavePath) - Toast.makeText(context, getString(R.string.no_available_dir), Toast.LENGTH_LONG).show(); + if (!askForSavePath) { + Toast.makeText(context, getString(R.string.no_available_dir), + Toast.LENGTH_LONG).show(); + } if (NewPipeSettings.useStorageAccessFramework(context)) { - StoredFileHelper.requestSafWithFileCreation(this, REQUEST_DOWNLOAD_SAVE_AS, filename, mime); + StoredFileHelper.requestSafWithFileCreation(this, REQUEST_DOWNLOAD_SAVE_AS, + filename, mime); } else { File initialSavePath; - if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.audio_button) + if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.audio_button) { initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MUSIC); - else + } else { initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MOVIES); + } initialSavePath = new File(initialSavePath, filename); - startActivityForResult( - FilePickerActivityHelper.chooseFileToSave(context, initialSavePath.getAbsolutePath()), - REQUEST_DOWNLOAD_SAVE_AS - ); + startActivityForResult(FilePickerActivityHelper.chooseFileToSave(context, + initialSavePath.getAbsolutePath()), REQUEST_DOWNLOAD_SAVE_AS); } return; @@ -606,7 +665,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck checkSelectedDownload(mainStorage, mainStorage.findFile(filename), filename, mime); } - private void checkSelectedDownload(StoredDirectoryHelper mainStorage, Uri targetFile, String filename, String mime) { + private void checkSelectedDownload(final StoredDirectoryHelper mainStorage, + final Uri targetFile, final String filename, + final String mime) { StoredFileHelper storage; try { @@ -615,10 +676,12 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck storage = new StoredFileHelper(context, null, targetFile, ""); } else if (targetFile == null) { // the file does not exist, but it is probably used in a pending download - storage = new StoredFileHelper(mainStorage.getUri(), filename, mime, mainStorage.getTag()); + storage = new StoredFileHelper(mainStorage.getUri(), filename, mime, + mainStorage.getTag()); } else { // the target filename is already use, attempt to use it - storage = new StoredFileHelper(context, mainStorage.getUri(), targetFile, mainStorage.getTag()); + storage = new StoredFileHelper(context, mainStorage.getUri(), targetFile, + mainStorage.getTag()); } } catch (Exception e) { showErrorActivity(e); @@ -722,24 +785,28 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck } else { try { // try take (or steal) the file - storageNew = new StoredFileHelper(context, mainStorage.getUri(), targetFile, mainStorage.getTag()); + storageNew = new StoredFileHelper(context, mainStorage.getUri(), + targetFile, mainStorage.getTag()); } catch (IOException e) { - Log.e(TAG, "Failed to take (or steal) the file in " + targetFile.toString()); + Log.e(TAG, "Failed to take (or steal) the file in " + + targetFile.toString()); storageNew = null; } } - if (storageNew != null && storageNew.canWrite()) + if (storageNew != null && storageNew.canWrite()) { continueSelectedDownload(storageNew); - else + } else { showFailedDialog(R.string.error_file_creation); + } break; case PendingRunning: storageNew = mainStorage.createUniqueFile(filename, mime); - if (storageNew == null) + if (storageNew == null) { showFailedDialog(R.string.error_file_creation); - else + } else { continueSelectedDownload(storageNew); + } break; } }); @@ -747,7 +814,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck askDialog.create().show(); } - private void continueSelectedDownload(@NonNull StoredFileHelper storage) { + private void continueSelectedDownload(@NonNull final StoredFileHelper storage) { if (!storage.canWrite()) { showFailedDialog(R.string.permission_denied); return; @@ -755,7 +822,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck // check if the selected file has to be overwritten, by simply checking its length try { - if (storage.length() > 0) storage.truncate(); + if (storage.length() > 0) { + storage.truncate(); + } } catch (IOException e) { Log.e(TAG, "failed to truncate the file: " + storage.getUri().toString(), e); showFailedDialog(R.string.overwrite_failed); @@ -795,13 +864,15 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck if (secondary != null) { secondaryStream = secondary.getStream(); - if (selectedStream.getFormat() == MediaFormat.MPEG_4) + if (selectedStream.getFormat() == MediaFormat.MPEG_4) { psName = Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER; - else + } else { psName = Postprocessing.ALGORITHM_WEBM_MUXER; + } psArgs = null; - long videoSize = wrappedVideoStreams.getSizeInBytes((VideoStream) selectedStream); + long videoSize = wrappedVideoStreams + .getSizeInBytes((VideoStream) selectedStream); // set nearLength, only, if both sizes are fetched or known. This probably // does not work on slow networks but is later updated in the downloader @@ -811,7 +882,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck } break; case R.id.subtitle_button: - threads = 1;// use unique thread for subtitles due small file size + threads = 1; // use unique thread for subtitles due small file size kind = 's'; selectedStream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex); @@ -819,8 +890,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck psName = Postprocessing.ALGORITHM_TTML_CONVERTER; psArgs = new String[]{ selectedStream.getFormat().getSuffix(), - "false",// ignore empty frames - "false",// detect youtube duplicate lines + "false" // ignore empty frames }; } break; @@ -839,14 +909,12 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck urls = new String[]{ selectedStream.getUrl(), secondaryStream.getUrl() }; - recoveryInfo = new MissionRecoveryInfo[]{ - new MissionRecoveryInfo(selectedStream), new MissionRecoveryInfo(secondaryStream) - }; + recoveryInfo = new MissionRecoveryInfo[]{new MissionRecoveryInfo(selectedStream), + new MissionRecoveryInfo(secondaryStream)}; } - DownloadManagerService.startMission( - context, urls, storage, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength, recoveryInfo - ); + DownloadManagerService.startMission(context, urls, storage, kind, threads, + currentInfo.getUrl(), psName, psArgs, nearLength, recoveryInfo); dismiss(); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/BackPressable.java b/app/src/main/java/org/schabi/newpipe/fragments/BackPressable.java index 737db784b..6add5eb09 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/BackPressable.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/BackPressable.java @@ -1,11 +1,11 @@ package org.schabi.newpipe.fragments; /** - * Indicates that the current fragment can handle back presses + * Indicates that the current fragment can handle back presses. */ public interface BackPressable { /** - * A back press was delegated to this fragment + * A back press was delegated to this fragment. * * @return if the back press was handled */ diff --git a/app/src/main/java/org/schabi/newpipe/fragments/BaseStateFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/BaseStateFragment.java index 8e328266e..255841857 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/BaseStateFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/BaseStateFragment.java @@ -1,9 +1,8 @@ package org.schabi.newpipe.fragments; +import android.content.Context; import android.content.Intent; import android.os.Bundle; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; import android.util.Log; import android.view.View; import android.widget.Button; @@ -11,19 +10,23 @@ import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; + import com.jakewharton.rxbinding2.view.RxView; import org.schabi.newpipe.BaseFragment; import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; import org.schabi.newpipe.ReCaptchaActivity; +import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; +import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException; import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; -import org.schabi.newpipe.util.ExtractorHelper; +import org.schabi.newpipe.util.ExceptionUtils; import org.schabi.newpipe.util.InfoCache; -import java.io.IOException; import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; @@ -35,22 +38,21 @@ import io.reactivex.android.schedulers.AndroidSchedulers; import static org.schabi.newpipe.util.AnimationUtils.animateView; public abstract class BaseStateFragment extends BaseFragment implements ViewContract { - @State protected AtomicBoolean wasLoading = new AtomicBoolean(); protected AtomicBoolean isLoading = new AtomicBoolean(); @Nullable - protected View emptyStateView; + private View emptyStateView; @Nullable - protected ProgressBar loadingProgressBar; + private ProgressBar loadingProgressBar; protected View errorPanelRoot; - protected Button errorButtonRetry; - protected TextView errorTextView; + private Button errorButtonRetry; + private TextView errorTextView; @Override - public void onViewCreated(View rootView, Bundle savedInstanceState) { + public void onViewCreated(final View rootView, final Bundle savedInstanceState) { super.onViewCreated(rootView, savedInstanceState); doInitialLoadLogic(); } @@ -61,14 +63,12 @@ public abstract class BaseStateFragment extends BaseFragment implements ViewC wasLoading.set(isLoading.get()); } - /*////////////////////////////////////////////////////////////////////////// // Init //////////////////////////////////////////////////////////////////////////*/ - @Override - protected void initViews(View rootView, Bundle savedInstanceState) { + protected void initViews(final View rootView, final Bundle savedInstanceState) { super.initViews(rootView, savedInstanceState); emptyStateView = rootView.findViewById(R.id.empty_state_view); @@ -104,8 +104,10 @@ public abstract class BaseStateFragment extends BaseFragment implements ViewC startLoading(true); } - protected void startLoading(boolean forceLoad) { - if (DEBUG) Log.d(TAG, "startLoading() called with: forceLoad = [" + forceLoad + "]"); + protected void startLoading(final boolean forceLoad) { + if (DEBUG) { + Log.d(TAG, "startLoading() called with: forceLoad = [" + forceLoad + "]"); + } showLoading(); isLoading.set(true); } @@ -116,42 +118,62 @@ public abstract class BaseStateFragment extends BaseFragment implements ViewC @Override public void showLoading() { - if (emptyStateView != null) animateView(emptyStateView, false, 150); - if (loadingProgressBar != null) animateView(loadingProgressBar, true, 400); + if (emptyStateView != null) { + animateView(emptyStateView, false, 150); + } + if (loadingProgressBar != null) { + animateView(loadingProgressBar, true, 400); + } animateView(errorPanelRoot, false, 150); } @Override public void hideLoading() { - if (emptyStateView != null) animateView(emptyStateView, false, 150); - if (loadingProgressBar != null) animateView(loadingProgressBar, false, 0); + if (emptyStateView != null) { + animateView(emptyStateView, false, 150); + } + if (loadingProgressBar != null) { + animateView(loadingProgressBar, false, 0); + } animateView(errorPanelRoot, false, 150); } @Override public void showEmptyState() { isLoading.set(false); - if (emptyStateView != null) animateView(emptyStateView, true, 200); - if (loadingProgressBar != null) animateView(loadingProgressBar, false, 0); + if (emptyStateView != null) { + animateView(emptyStateView, true, 200); + } + if (loadingProgressBar != null) { + animateView(loadingProgressBar, false, 0); + } animateView(errorPanelRoot, false, 150); } @Override - public void showError(String message, boolean showRetryButton) { - if (DEBUG) Log.d(TAG, "showError() called with: message = [" + message + "], showRetryButton = [" + showRetryButton + "]"); + public void showError(final String message, final boolean showRetryButton) { + if (DEBUG) { + Log.d(TAG, "showError() called with: " + + "message = [" + message + "], showRetryButton = [" + showRetryButton + "]"); + } isLoading.set(false); InfoCache.getInstance().clearCache(); hideLoading(); errorTextView.setText(message); - if (showRetryButton) animateView(errorButtonRetry, true, 600); - else animateView(errorButtonRetry, false, 0); + if (showRetryButton) { + animateView(errorButtonRetry, true, 600); + } else { + animateView(errorButtonRetry, false, 0); + } animateView(errorPanelRoot, true, 300); } @Override - public void handleResult(I result) { - if (DEBUG) Log.d(TAG, "handleResult() called with: result = [" + result + "]"); + public void handleResult(final I result) { + if (DEBUG) { + Log.d(TAG, "handleResult() called with: result = [" + result + "]"); + } hideLoading(); } @@ -160,37 +182,52 @@ public abstract class BaseStateFragment extends BaseFragment implements ViewC //////////////////////////////////////////////////////////////////////////*/ /** - * Default implementation handles some general exceptions + * Default implementation handles some general exceptions. * - * @return if the exception was handled + * @param exception The exception that should be handled + * @return If the exception was handled */ - protected boolean onError(Throwable exception) { - if (DEBUG) Log.d(TAG, "onError() called with: exception = [" + exception + "]"); + protected boolean onError(final Throwable exception) { + if (DEBUG) { + Log.d(TAG, "onError() called with: exception = [" + exception + "]"); + } isLoading.set(false); if (isDetached() || isRemoving()) { - if (DEBUG) Log.w(TAG, "onError() is detached or removing = [" + exception + "]"); + if (DEBUG) { + Log.w(TAG, "onError() is detached or removing = [" + exception + "]"); + } return true; } - if (ExtractorHelper.isInterruptedCaused(exception)) { - if (DEBUG) Log.w(TAG, "onError() isInterruptedCaused! = [" + exception + "]"); + if (ExceptionUtils.isInterruptedCaused(exception)) { + if (DEBUG) { + Log.w(TAG, "onError() isInterruptedCaused! = [" + exception + "]"); + } return true; } if (exception instanceof ReCaptchaException) { onReCaptchaException((ReCaptchaException) exception); return true; - } else if (exception instanceof IOException) { + } else if (exception instanceof ContentNotAvailableException) { + showError(getString(R.string.content_not_available), false); + return true; + } else if (ExceptionUtils.isNetworkRelated(exception)) { showError(getString(R.string.network_error), true); return true; + } else if (exception instanceof ContentNotSupportedException) { + showError(getString(R.string.content_not_supported), false); + return true; } return false; } - public void onReCaptchaException(ReCaptchaException exception) { - if (DEBUG) Log.d(TAG, "onReCaptchaException() called"); + public void onReCaptchaException(final ReCaptchaException exception) { + if (DEBUG) { + Log.d(TAG, "onReCaptchaException() called"); + } Toast.makeText(activity, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show(); // Starting ReCaptcha Challenge Activity Intent intent = new Intent(activity, ReCaptchaActivity.class); @@ -200,33 +237,58 @@ public abstract class BaseStateFragment extends BaseFragment implements ViewC showError(getString(R.string.recaptcha_request_toast), false); } - public void onUnrecoverableError(Throwable exception, UserAction userAction, String serviceName, String request, @StringRes int errorId) { - onUnrecoverableError(Collections.singletonList(exception), userAction, serviceName, request, errorId); + public void onUnrecoverableError(final Throwable exception, final UserAction userAction, + final String serviceName, final String request, + @StringRes final int errorId) { + onUnrecoverableError(Collections.singletonList(exception), userAction, serviceName, + request, errorId); } - public void onUnrecoverableError(List exception, UserAction userAction, String serviceName, String request, @StringRes int errorId) { - if (DEBUG) Log.d(TAG, "onUnrecoverableError() called with: exception = [" + exception + "]"); + public void onUnrecoverableError(final List exception, final UserAction userAction, + final String serviceName, final String request, + @StringRes final int errorId) { + if (DEBUG) { + Log.d(TAG, "onUnrecoverableError() called with: exception = [" + exception + "]"); + } - if (serviceName == null) serviceName = "none"; - if (request == null) request = "none"; - - ErrorActivity.reportError(getContext(), exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(userAction, serviceName, request, errorId)); + ErrorActivity.reportError(getContext(), exception, MainActivity.class, null, + ErrorActivity.ErrorInfo.make(userAction, serviceName == null ? "none" : serviceName, + request == null ? "none" : request, errorId)); } - public void showSnackBarError(Throwable exception, UserAction userAction, String serviceName, String request, @StringRes int errorId) { - showSnackBarError(Collections.singletonList(exception), userAction, serviceName, request, errorId); + public void showSnackBarError(final Throwable exception, final UserAction userAction, + final String serviceName, final String request, + @StringRes final int errorId) { + showSnackBarError(Collections.singletonList(exception), userAction, serviceName, request, + errorId); } /** - * Show a SnackBar and only call ErrorActivity#reportError IF we a find a valid view (otherwise the error screen appears) + * Show a SnackBar and only call + * {@link ErrorActivity#reportError(Context, List, Class, View, ErrorActivity.ErrorInfo)} + * IF we a find a valid view (otherwise the error screen appears). + * + * @param exception List of the exceptions to show + * @param userAction The user action that caused the exception + * @param serviceName The service where the exception happened + * @param request The page that was requested + * @param errorId The ID of the error */ - public void showSnackBarError(List exception, UserAction userAction, String serviceName, String request, @StringRes int errorId) { + public void showSnackBarError(final List exception, final UserAction userAction, + final String serviceName, final String request, + @StringRes final int errorId) { if (DEBUG) { - Log.d(TAG, "showSnackBarError() called with: exception = [" + exception + "], userAction = [" + userAction + "], request = [" + request + "], errorId = [" + errorId + "]"); + Log.d(TAG, "showSnackBarError() called with: " + + "exception = [" + exception + "], userAction = [" + userAction + "], " + + "request = [" + request + "], errorId = [" + errorId + "]"); } View rootView = activity != null ? activity.findViewById(android.R.id.content) : null; - if (rootView == null && getView() != null) rootView = getView(); - if (rootView == null) return; + if (rootView == null && getView() != null) { + rootView = getView(); + } + if (rootView == null) { + return; + } ErrorActivity.reportError(getContext(), exception, MainActivity.class, rootView, ErrorActivity.ErrorInfo.make(userAction, serviceName, request, errorId)); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/BlankFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/BlankFragment.java index 1e284c711..0cccfa4fe 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/BlankFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/BlankFragment.java @@ -1,24 +1,26 @@ package org.schabi.newpipe.fragments; import android.os.Bundle; -import androidx.annotation.Nullable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import androidx.annotation.Nullable; + import org.schabi.newpipe.BaseFragment; import org.schabi.newpipe.R; public class BlankFragment extends BaseFragment { @Nullable @Override - public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { + public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container, + final Bundle savedInstanceState) { setTitle("NewPipe"); return inflater.inflate(R.layout.fragment_blank, container, false); } @Override - public void setUserVisibleHint(boolean isVisibleToUser) { + public void setUserVisibleHint(final boolean isVisibleToUser) { super.setUserVisibleHint(isVisibleToUser); setTitle("NewPipe"); // leave this inline. Will make it harder for copy cats. diff --git a/app/src/main/java/org/schabi/newpipe/fragments/EmptyFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/EmptyFragment.java index de9716f28..62f823c73 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/EmptyFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/EmptyFragment.java @@ -1,17 +1,19 @@ package org.schabi.newpipe.fragments; import android.os.Bundle; -import androidx.annotation.Nullable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import androidx.annotation.Nullable; + import org.schabi.newpipe.BaseFragment; import org.schabi.newpipe.R; public class EmptyFragment extends BaseFragment { @Override - public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { + public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container, + final Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_empty, container, false); } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java index 720e0f216..709dac368 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java @@ -1,7 +1,9 @@ package org.schabi.newpipe.fragments; import android.content.Context; +import android.content.res.ColorStateList; import android.os.Bundle; +import android.preference.PreferenceManager; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; @@ -16,7 +18,7 @@ import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentStatePagerAdapter; +import androidx.fragment.app.FragmentStatePagerAdapterMenuWorkaround; import androidx.viewpager.widget.ViewPager; import com.google.android.material.tabs.TabLayout; @@ -30,6 +32,8 @@ import org.schabi.newpipe.settings.tabs.Tab; import org.schabi.newpipe.settings.tabs.TabsManager; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.ServiceHelper; +import org.schabi.newpipe.util.ThemeHelper; +import org.schabi.newpipe.views.ScrollableTabLayout; import java.util.ArrayList; import java.util.List; @@ -37,26 +41,29 @@ import java.util.List; public class MainFragment extends BaseFragment implements TabLayout.OnTabSelectedListener { private ViewPager viewPager; private SelectedTabsPagerAdapter pagerAdapter; - private TabLayout tabLayout; + private ScrollableTabLayout tabLayout; private List tabsList = new ArrayList<>(); private TabsManager tabsManager; private boolean hasTabsChanged = false; + private boolean previousYoutubeRestrictedModeEnabled; + private String youtubeRestrictedModeEnabledKey; + /*////////////////////////////////////////////////////////////////////////// // Fragment's LifeCycle //////////////////////////////////////////////////////////////////////////*/ @Override - public void onCreate(Bundle savedInstanceState) { + public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); - tabsManager = TabsManager.getManager(activity); tabsManager.setSavedTabsListener(() -> { if (DEBUG) { - Log.d(TAG, "TabsManager.SavedTabsChangeListener: onTabsChanged called, isResumed = " + isResumed()); + Log.d(TAG, "TabsManager.SavedTabsChangeListener: " + + "onTabsChanged called, isResumed = " + isResumed()); } if (isResumed()) { setupTabs(); @@ -64,20 +71,29 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte hasTabsChanged = true; } }); + + youtubeRestrictedModeEnabledKey = getString(R.string.youtube_restricted_mode_enabled); + previousYoutubeRestrictedModeEnabled = + PreferenceManager.getDefaultSharedPreferences(getContext()) + .getBoolean(youtubeRestrictedModeEnabledKey, false); } @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + public View onCreateView(@NonNull final LayoutInflater inflater, + @Nullable final ViewGroup container, + @Nullable final Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_main, container, false); } @Override - protected void initViews(View rootView, Bundle savedInstanceState) { + protected void initViews(final View rootView, final Bundle savedInstanceState) { super.initViews(rootView, savedInstanceState); tabLayout = rootView.findViewById(R.id.main_tab_layout); viewPager = rootView.findViewById(R.id.pager); + tabLayout.setTabIconTint(ColorStateList.valueOf( + ThemeHelper.resolveColorFromAttr(requireContext(), R.attr.colorAccent))); tabLayout.setupWithViewPager(viewPager); tabLayout.addOnTabSelectedListener(this); @@ -88,14 +104,24 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte public void onResume() { super.onResume(); - if (hasTabsChanged) setupTabs(); + boolean youtubeRestrictedModeEnabled = + PreferenceManager.getDefaultSharedPreferences(getContext()) + .getBoolean(youtubeRestrictedModeEnabledKey, false); + if (previousYoutubeRestrictedModeEnabled != youtubeRestrictedModeEnabled) { + previousYoutubeRestrictedModeEnabled = youtubeRestrictedModeEnabled; + setupTabs(); + } else if (hasTabsChanged) { + setupTabs(); + } } @Override public void onDestroy() { super.onDestroy(); tabsManager.unsetSavedTabsListener(); - if (viewPager != null) viewPager.setAdapter(null); + if (viewPager != null) { + viewPager.setAdapter(null); + } } /*////////////////////////////////////////////////////////////////////////// @@ -103,9 +129,12 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte //////////////////////////////////////////////////////////////////////////*/ @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); - if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]"); + if (DEBUG) { + Log.d(TAG, "onCreateOptionsMenu() called with: " + + "menu = [" + menu + "], inflater = [" + inflater + "]"); + } inflater.inflate(R.menu.main_fragment_menu, menu); ActionBar supportActionBar = activity.getSupportActionBar(); @@ -115,7 +144,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte } @Override - public boolean onOptionsItemSelected(MenuItem item) { + public boolean onOptionsItemSelected(final MenuItem item) { switch (item.getItemId()) { case R.id.action_search: try { @@ -135,16 +164,18 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte // Tabs //////////////////////////////////////////////////////////////////////////*/ - public void setupTabs() { + private void setupTabs() { tabsList.clear(); tabsList.addAll(tabsManager.getTabs()); if (pagerAdapter == null || !pagerAdapter.sameTabs(tabsList)) { - pagerAdapter = new SelectedTabsPagerAdapter(requireContext(), getChildFragmentManager(), tabsList); + pagerAdapter = new SelectedTabsPagerAdapter(requireContext(), + getChildFragmentManager(), tabsList); } - // Clear previous tabs/fragments and set new adapter - viewPager.setAdapter(pagerAdapter); + + viewPager.setAdapter(null); viewPager.setOffscreenPageLimit(tabsList.size()); + viewPager.setAdapter(pagerAdapter); updateTabsIconAndDescription(); updateTitleForTab(viewPager.getCurrentItem()); @@ -163,38 +194,45 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte } } - private void updateTitleForTab(int tabPosition) { + private void updateTitleForTab(final int tabPosition) { setTitle(tabsList.get(tabPosition).getTabName(requireContext())); } @Override - public void onTabSelected(TabLayout.Tab selectedTab) { - if (DEBUG) Log.d(TAG, "onTabSelected() called with: selectedTab = [" + selectedTab + "]"); + public void onTabSelected(final TabLayout.Tab selectedTab) { + if (DEBUG) { + Log.d(TAG, "onTabSelected() called with: selectedTab = [" + selectedTab + "]"); + } updateTitleForTab(selectedTab.getPosition()); } @Override - public void onTabUnselected(TabLayout.Tab tab) { - } + public void onTabUnselected(final TabLayout.Tab tab) { } @Override - public void onTabReselected(TabLayout.Tab tab) { - if (DEBUG) Log.d(TAG, "onTabReselected() called with: tab = [" + tab + "]"); + public void onTabReselected(final TabLayout.Tab tab) { + if (DEBUG) { + Log.d(TAG, "onTabReselected() called with: tab = [" + tab + "]"); + } updateTitleForTab(tab.getPosition()); } - private static class SelectedTabsPagerAdapter extends FragmentStatePagerAdapter { + private static final class SelectedTabsPagerAdapter + extends FragmentStatePagerAdapterMenuWorkaround { private final Context context; private final List internalTabsList; - private SelectedTabsPagerAdapter(Context context, FragmentManager fragmentManager, List tabsList) { + private SelectedTabsPagerAdapter(final Context context, + final FragmentManager fragmentManager, + final List tabsList) { super(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); this.context = context; this.internalTabsList = new ArrayList<>(tabsList); } + @NonNull @Override - public Fragment getItem(int position) { + public Fragment getItem(final int position) { final Tab tab = internalTabsList.get(position); Throwable throwable = null; @@ -206,8 +244,8 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte } if (throwable != null) { - ErrorActivity.reportError(context, throwable, null, null, - ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash)); + ErrorActivity.reportError(context, throwable, null, null, ErrorActivity.ErrorInfo + .make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash)); return new BlankFragment(); } @@ -219,7 +257,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte } @Override - public int getItemPosition(Object object) { + public int getItemPosition(final Object object) { // Causes adapter to reload all Fragments when // notifyDataSetChanged is called return POSITION_NONE; @@ -230,7 +268,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte return internalTabsList.size(); } - public boolean sameTabs(List tabsToCompare) { + public boolean sameTabs(final List tabsToCompare) { return internalTabsList.equals(tabsToCompare); } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/OnScrollBelowItemsListener.java b/app/src/main/java/org/schabi/newpipe/fragments/OnScrollBelowItemsListener.java index 887097679..28ce91f55 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/OnScrollBelowItemsListener.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/OnScrollBelowItemsListener.java @@ -9,12 +9,13 @@ import androidx.recyclerview.widget.StaggeredGridLayoutManager; * if the view is scrolled below the last item. */ public abstract class OnScrollBelowItemsListener extends RecyclerView.OnScrollListener { - @Override - public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + public void onScrolled(final RecyclerView recyclerView, final int dx, final int dy) { super.onScrolled(recyclerView, dx, dy); if (dy > 0) { - int pastVisibleItems = 0, visibleItemCount, totalItemCount; + int pastVisibleItems = 0; + int visibleItemCount; + int totalItemCount; RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); visibleItemCount = layoutManager.getChildCount(); @@ -22,10 +23,14 @@ public abstract class OnScrollBelowItemsListener extends RecyclerView.OnScrollLi // Already covers the GridLayoutManager case if (layoutManager instanceof LinearLayoutManager) { - pastVisibleItems = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition(); + pastVisibleItems = ((LinearLayoutManager) layoutManager) + .findFirstVisibleItemPosition(); } else if (layoutManager instanceof StaggeredGridLayoutManager) { - int[] positions = ((StaggeredGridLayoutManager) layoutManager).findFirstVisibleItemPositions(null); - if (positions != null && positions.length > 0) pastVisibleItems = positions[0]; + int[] positions = ((StaggeredGridLayoutManager) layoutManager) + .findFirstVisibleItemPositions(null); + if (positions != null && positions.length > 0) { + pastVisibleItems = positions[0]; + } } if ((visibleItemCount + pastVisibleItems) >= totalItemCount) { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/ViewContract.java b/app/src/main/java/org/schabi/newpipe/fragments/ViewContract.java index 4ce09b000..bb980ac64 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/ViewContract.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/ViewContract.java @@ -2,8 +2,11 @@ package org.schabi.newpipe.fragments; public interface ViewContract { void showLoading(); + void hideLoading(); + void showEmptyState(); + void showError(String message, boolean showRetryButton); void handleResult(I result); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/StackItem.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/StackItem.java index ca96b5f96..1fbc21bf9 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/StackItem.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/StackItem.java @@ -6,8 +6,8 @@ import java.io.Serializable; class StackItem implements Serializable { private final int serviceId; - private String title; private String url; + private String title; private PlayQueue playQueue; StackItem(final int serviceId, final String url, final String title, final PlayQueue playQueue) { @@ -17,10 +17,6 @@ class StackItem implements Serializable { this.playQueue = playQueue; } - public void setTitle(String title) { - this.title = title; - } - public void setUrl(String url) { this.url = url; } @@ -37,6 +33,10 @@ class StackItem implements Serializable { return title; } + public void setTitle(final String title) { + this.title = title; + } + public String getUrl() { return url; } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/TabAdaptor.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/TabAdaptor.java index d86226e92..38f013200 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/TabAdaptor.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/TabAdaptor.java @@ -1,27 +1,27 @@ package org.schabi.newpipe.fragments.detail; +import android.view.ViewGroup; + import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentPagerAdapter; -import android.view.ViewGroup; import java.util.ArrayList; import java.util.List; public class TabAdaptor extends FragmentPagerAdapter { - private final List mFragmentList = new ArrayList<>(); private final List mFragmentTitleList = new ArrayList<>(); private final FragmentManager fragmentManager; - public TabAdaptor(FragmentManager fm) { + public TabAdaptor(final FragmentManager fm) { super(fm); this.fragmentManager = fm; } @Override - public Fragment getItem(int position) { + public Fragment getItem(final int position) { return mFragmentList.get(position); } @@ -30,7 +30,7 @@ public class TabAdaptor extends FragmentPagerAdapter { return mFragmentList.size(); } - public void addFragment(Fragment fragment, String title) { + public void addFragment(final Fragment fragment, final String title) { mFragmentList.add(fragment); mFragmentTitleList.add(title); } @@ -40,46 +40,49 @@ public class TabAdaptor extends FragmentPagerAdapter { mFragmentTitleList.clear(); } - public void removeItem(int position){ + public void removeItem(final int position) { mFragmentList.remove(position == 0 ? 0 : position - 1); mFragmentTitleList.remove(position == 0 ? 0 : position - 1); } - public void updateItem(int position, Fragment fragment){ + public void updateItem(final int position, final Fragment fragment) { mFragmentList.set(position, fragment); } - public void updateItem(String title, Fragment fragment){ + public void updateItem(final String title, final Fragment fragment) { int index = mFragmentTitleList.indexOf(title); - if(index != -1){ + if (index != -1) { updateItem(index, fragment); } } @Override - public int getItemPosition(Object object) { - if (mFragmentList.contains(object)) return mFragmentList.indexOf(object); - else return POSITION_NONE; + public int getItemPosition(final Object object) { + if (mFragmentList.contains(object)) { + return mFragmentList.indexOf(object); + } else { + return POSITION_NONE; + } } - public int getItemPositionByTitle(String title) { + public int getItemPositionByTitle(final String title) { return mFragmentTitleList.indexOf(title); } @Nullable - public String getItemTitle(int position) { + public String getItemTitle(final int position) { if (position < 0 || position >= mFragmentTitleList.size()) { return null; } return mFragmentTitleList.get(position); } - public void notifyDataSetUpdate(){ + public void notifyDataSetUpdate() { notifyDataSetChanged(); } @Override - public void destroyItem(ViewGroup container, int position, Object object) { + public void destroyItem(final ViewGroup container, final int position, final Object object) { fragmentManager.beginTransaction().remove((Fragment) object).commitNowAllowingStateLoss(); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 060f95b68..ce6bd72f5 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -5,6 +5,7 @@ import android.app.Activity; import android.content.*; import android.content.pm.ActivityInfo; import android.database.ContentObserver; +import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Bundle; import android.os.Handler; @@ -31,11 +32,13 @@ import androidx.viewpager.widget.ViewPager; import android.text.Html; import android.text.Spanned; import android.text.TextUtils; -import android.text.method.LinkMovementMethod; import android.text.util.Linkify; import android.util.DisplayMetrics; import android.util.Log; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.content.res.AppCompatResources; + import com.nostra13.universalimageloader.core.assist.FailReason; import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener; @@ -46,11 +49,10 @@ import org.schabi.newpipe.download.DownloadDialog; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.ServiceList; -import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; import org.schabi.newpipe.extractor.exceptions.ExtractionException; -import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor; import org.schabi.newpipe.extractor.stream.AudioStream; +import org.schabi.newpipe.extractor.stream.Description; import org.schabi.newpipe.extractor.stream.Stream; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamType; @@ -63,20 +65,34 @@ import org.schabi.newpipe.fragments.list.videos.RelatedVideosFragment; import org.schabi.newpipe.local.dialog.PlaylistAppendDialog; import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.player.*; +import org.schabi.newpipe.player.event.OnKeyDownListener; import org.schabi.newpipe.player.event.PlayerEventListener; import org.schabi.newpipe.player.event.PlayerServiceEventListener; import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.playqueue.*; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; -import org.schabi.newpipe.util.*; +import org.schabi.newpipe.util.AndroidTvUtils; +import org.schabi.newpipe.util.Constants; +import org.schabi.newpipe.util.ExtractorHelper; +import org.schabi.newpipe.util.ImageDisplayConstants; +import org.schabi.newpipe.util.InfoCache; +import org.schabi.newpipe.util.ListHelper; +import org.schabi.newpipe.util.Localization; +import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.PermissionHelper; +import org.schabi.newpipe.util.ShareUtils; +import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.views.AnimatedProgressBar; +import org.schabi.newpipe.views.LargeTextMovementMethod; import java.io.Serializable; import java.util.*; import java.util.concurrent.TimeUnit; import icepick.State; +import io.noties.markwon.Markwon; +import io.noties.markwon.linkify.LinkifyPlugin; import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; @@ -86,6 +102,7 @@ import io.reactivex.schedulers.Schedulers; import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS; import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired; import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET; +import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT; import static org.schabi.newpipe.util.AnimationUtils.animateView; public class VideoDetailFragment @@ -95,7 +112,8 @@ public class VideoDetailFragment View.OnClickListener, View.OnLongClickListener, PlayerEventListener, - PlayerServiceEventListener { + PlayerServiceEventListener, + OnKeyDownListener { public static final String AUTO_PLAY = "auto_play"; private static final int RELATED_STREAMS_UPDATE_FLAG = 0x1; @@ -112,6 +130,9 @@ public class VideoDetailFragment private static final String RELATED_TAB_TAG = "NEXT VIDEO"; private static final String EMPTY_TAB_TAG = "EMPTY TAB"; + private static final String INFO_KEY = "info_key"; + private static final String STACK_KEY = "stack_key"; + private boolean showRelatedStreams; private boolean showComments; private String selectedTabTag; @@ -174,6 +195,8 @@ public class VideoDetailFragment private View uploaderRootLayout; private TextView uploaderTextView; private ImageView uploaderThumb; + private TextView subChannelTextView; + private ImageView subChannelThumb; private TextView thumbsUpTextView; private ImageView thumbsUpImageView; @@ -300,13 +323,13 @@ public class VideoDetailFragment return instance; } + /*////////////////////////////////////////////////////////////////////////// // Fragment's Lifecycle //////////////////////////////////////////////////////////////////////////*/ @Override - public void - onCreate(Bundle savedInstanceState) { + public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); showRelatedStreams = PreferenceManager.getDefaultSharedPreferences(activity) @@ -336,19 +359,22 @@ public class VideoDetailFragment } @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container, + final Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_video_detail, container, false); } @Override public void onPause() { super.onPause(); - if (currentWorker != null) currentWorker.dispose(); - + if (currentWorker != null) { + currentWorker.dispose(); + } setupBrightness(true); PreferenceManager.getDefaultSharedPreferences(getContext()) .edit() - .putString(getString(R.string.stream_info_selected_tab_key), pageAdapter.getItemTitle(viewPager.getCurrentItem())) + .putString(getString(R.string.stream_info_selected_tab_key), + pageAdapter.getItemTitle(viewPager.getCurrentItem())) .apply(); } @@ -395,8 +421,12 @@ public class VideoDetailFragment activity.unregisterReceiver(broadcastReceiver); activity.getContentResolver().unregisterContentObserver(settingsContentObserver); - if (positionSubscriber != null) positionSubscriber.dispose(); - if (currentWorker != null) currentWorker.dispose(); + if (positionSubscriber != null) { + positionSubscriber.dispose(); + } + if (currentWorker != null) { + currentWorker.dispose(); + } disposables.clear(); positionSubscriber = null; currentWorker = null; @@ -404,13 +434,16 @@ public class VideoDetailFragment } @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { + public void onActivityResult(final int requestCode, final int resultCode, final Intent data) { super.onActivityResult(requestCode, resultCode, data); switch (requestCode) { case ReCaptchaActivity.RECAPTCHA_REQUEST: if (resultCode == Activity.RESULT_OK) { - NavigationHelper.openVideoDetailFragment(getFragmentManager(), serviceId, url, name); - } else Log.e(TAG, "ReCaptcha failed"); + NavigationHelper + .openVideoDetailFragment(getFragmentManager(), serviceId, url, name); + } else { + Log.e(TAG, "ReCaptcha failed"); + } break; default: Log.e(TAG, "Request code from activity not supported [" + requestCode + "]"); @@ -419,7 +452,8 @@ public class VideoDetailFragment } @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, + final String key) { if (key.equals(getString(R.string.show_next_video_key))) { showRelatedStreams = sharedPreferences.getBoolean(key, true); updateFlags |= RELATED_STREAMS_UPDATE_FLAG; @@ -433,11 +467,8 @@ public class VideoDetailFragment // State Saving //////////////////////////////////////////////////////////////////////////*/ - private static final String INFO_KEY = "info_key"; - private static final String STACK_KEY = "stack_key"; - @Override - public void onSaveInstanceState(Bundle outState) { + public void onSaveInstanceState(final Bundle outState) { super.onSaveInstanceState(outState); // Check if the next video label and video is visible, @@ -453,7 +484,7 @@ public class VideoDetailFragment } @Override - protected void onRestoreInstanceState(@NonNull Bundle savedState) { + protected void onRestoreInstanceState(@NonNull final Bundle savedState) { super.onRestoreInstanceState(savedState); Serializable serializable = savedState.getSerializable(INFO_KEY); @@ -476,8 +507,10 @@ public class VideoDetailFragment //////////////////////////////////////////////////////////////////////////*/ @Override - public void onClick(View v) { - if (isLoading.get() || currentInfo == null) return; + public void onClick(final View v) { + if (isLoading.get() || currentInfo == null) { + return; + } switch (v.getId()) { case R.id.detail_controls_background: @@ -499,7 +532,18 @@ public class VideoDetailFragment } break; case R.id.detail_uploader_root_layout: - openChannel(); + if (TextUtils.isEmpty(currentInfo.getSubChannelUrl())) { + if (!TextUtils.isEmpty(currentInfo.getUploaderUrl())) { + openChannel(currentInfo.getUploaderUrl(), currentInfo.getUploaderName()); + } + + if (DEBUG) { + Log.i(TAG, "Can't open sub-channel because we got no channel URL"); + } + } else { + openChannel(currentInfo.getSubChannelUrl(), + currentInfo.getSubChannelName()); + } break; case R.id.detail_thumbnail_root_layout: openVideoPlayer(); @@ -527,9 +571,23 @@ public class VideoDetailFragment } } + private void openChannel(final String subChannelUrl, final String subChannelName) { + try { + NavigationHelper.openChannelFragment( + getFragmentManager(), + currentInfo.getServiceId(), + subChannelUrl, + subChannelName); + } catch (Exception e) { + ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); + } + } + @Override - public boolean onLongClick(View v) { - if (isLoading.get() || currentInfo == null) return false; + public boolean onLongClick(final View v) { + if (isLoading.get() || currentInfo == null) { + return false; + } switch (v.getId()) { case R.id.detail_controls_background: @@ -543,7 +601,19 @@ public class VideoDetailFragment break; case R.id.overlay_thumbnail: case R.id.overlay_metadata_layout: - openChannel(); + if (currentInfo != null) + openChannel(currentInfo.getUploaderUrl(), currentInfo.getUploaderName()); + break; + case R.id.detail_uploader_root_layout: + if (TextUtils.isEmpty(currentInfo.getSubChannelUrl())) { + Log.w(TAG, + "Can't open parent channel because we got no parent channel URL"); + } else { + openChannel(currentInfo.getUploaderUrl(), currentInfo.getUploaderName()); + } + break; + case R.id.detail_title_root_layout: + ShareUtils.copyToClipboard(getContext(), videoTitleTextView.getText().toString()); break; } @@ -554,11 +624,16 @@ public class VideoDetailFragment if (videoDescriptionRootLayout.getVisibility() == View.VISIBLE) { videoTitleTextView.setMaxLines(1); videoDescriptionRootLayout.setVisibility(View.GONE); - videoTitleToggleArrow.setImageResource(R.drawable.arrow_down); + videoDescriptionView.setFocusable(false); + videoTitleToggleArrow.setImageResource( + ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_expand_more)); } else { videoTitleTextView.setMaxLines(10); videoDescriptionRootLayout.setVisibility(View.VISIBLE); - videoTitleToggleArrow.setImageResource(R.drawable.arrow_up); + videoDescriptionView.setFocusable(true); + videoDescriptionView.setMovementMethod(new LargeTextMovementMethod()); + videoTitleToggleArrow.setImageResource( + ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_expand_less)); } } @@ -567,7 +642,7 @@ public class VideoDetailFragment //////////////////////////////////////////////////////////////////////////*/ @Override - protected void initViews(View rootView, Bundle savedInstanceState) { + protected void initViews(final View rootView, final Bundle savedInstanceState) { super.initViews(rootView, savedInstanceState); thumbnailBackgroundButton = rootView.findViewById(R.id.detail_thumbnail_root_layout); thumbnailImageView = rootView.findViewById(R.id.detail_thumbnail_image_view); @@ -592,8 +667,6 @@ public class VideoDetailFragment videoDescriptionRootLayout = rootView.findViewById(R.id.detail_description_root_layout); videoUploadDateView = rootView.findViewById(R.id.detail_upload_date_view); videoDescriptionView = rootView.findViewById(R.id.detail_description_view); - videoDescriptionView.setMovementMethod(LinkMovementMethod.getInstance()); - videoDescriptionView.setAutoLinkMask(Linkify.WEB_URLS); thumbsUpTextView = rootView.findViewById(R.id.detail_thumbs_up_count_view); thumbsUpImageView = rootView.findViewById(R.id.detail_thumbs_up_img_view); @@ -604,6 +677,8 @@ public class VideoDetailFragment uploaderRootLayout = rootView.findViewById(R.id.detail_uploader_root_layout); uploaderTextView = rootView.findViewById(R.id.detail_uploader_text_view); uploaderThumb = rootView.findViewById(R.id.detail_uploader_thumbnail_view); + subChannelTextView = rootView.findViewById(R.id.detail_sub_channel_text_view); + subChannelThumb = rootView.findViewById(R.id.detail_sub_channel_thumbnail_view); overlay = rootView.findViewById(R.id.overlay_layout); overlayMetadata = rootView.findViewById(R.id.overlay_metadata_layout); @@ -624,14 +699,28 @@ public class VideoDetailFragment relatedStreamsLayout = rootView.findViewById(R.id.relatedStreamsLayout); setHeightThumbnail(); + + thumbnailBackgroundButton.requestFocus(); + + if (AndroidTvUtils.isTv(getContext())) { + // remove ripple effects from detail controls + final int transparent = getResources().getColor(R.color.transparent_background_color); + detailControlsAddToPlaylist.setBackgroundColor(transparent); + detailControlsBackground.setBackgroundColor(transparent); + detailControlsPopup.setBackgroundColor(transparent); + detailControlsDownload.setBackgroundColor(transparent); + } + } @Override protected void initListeners() { super.initListeners(); - videoTitleRoot.setOnClickListener(this); + videoTitleRoot.setOnLongClickListener(this); uploaderRootLayout.setOnClickListener(this); + uploaderRootLayout.setOnLongClickListener(this); + videoTitleRoot.setOnClickListener(this); thumbnailBackgroundButton.setOnClickListener(this); detailControlsBackground.setOnClickListener(this); detailControlsPopup.setOnClickListener(this); @@ -674,25 +763,31 @@ public class VideoDetailFragment }; } - private void initThumbnailViews(@NonNull StreamInfo info) { + private void initThumbnailViews(@NonNull final StreamInfo info) { thumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark); if (!TextUtils.isEmpty(info.getThumbnailUrl())) { final String infoServiceName = NewPipe.getNameOfService(info.getServiceId()); - final ImageLoadingListener loadingListener = new SimpleImageLoadingListener() { + final ImageLoadingListener onFailListener = new SimpleImageLoadingListener() { @Override - public void onLoadingFailed(String imageUri, View view, FailReason failReason) { + public void onLoadingFailed(final String imageUri, final View view, + final FailReason failReason) { showSnackBarError(failReason.getCause(), UserAction.LOAD_IMAGE, infoServiceName, imageUri, R.string.could_not_load_thumbnails); } }; - imageLoader.displayImage(info.getThumbnailUrl(), thumbnailImageView, - ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, loadingListener); + IMAGE_LOADER.displayImage(info.getThumbnailUrl(), thumbnailImageView, + ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, onFailListener); + } + + if (!TextUtils.isEmpty(info.getSubChannelAvatarUrl())) { + IMAGE_LOADER.displayImage(info.getSubChannelAvatarUrl(), subChannelThumb, + ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS); } if (!TextUtils.isEmpty(info.getUploaderAvatarUrl())) { - imageLoader.displayImage(info.getUploaderAvatarUrl(), uploaderThumb, + IMAGE_LOADER.displayImage(info.getUploaderAvatarUrl(), uploaderThumb, ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS); } } @@ -707,20 +802,16 @@ public class VideoDetailFragment */ protected final LinkedList stack = new LinkedList<>(); - /*public void setTitleToUrl(int serviceId, String videoUrl, String name) { - if (name != null && !name.isEmpty()) { - for (StackItem stackItem : stack) { - if (stack.peek().getServiceId() == serviceId - && stackItem.getUrl().equals(videoUrl)) { - stackItem.setTitle(name); - } - } - } - }*/ + @Override + public boolean onKeyDown(final int keyCode) { + return player != null && player.onKeyDown(keyCode); + } @Override public boolean onBackPressed() { - if (DEBUG) Log.d(TAG, "onBackPressed() called"); + if (DEBUG) { + Log.d(TAG, "onBackPressed() called"); + } // If we are in fullscreen mode just exit from it via first back press if (player != null && player.isFullscreen()) { @@ -782,23 +873,28 @@ public class VideoDetailFragment protected void doInitialLoadLogic() { if (wasCleared()) return; - if (currentInfo == null) prepareAndLoadInfo(); - else prepareAndHandleInfo(currentInfo, false); + if (currentInfo == null) { + prepareAndLoadInfo(); + } else { + prepareAndHandleInfo(currentInfo, false); + } } - public void selectAndLoadVideo(final int serviceId, final String videoUrl, final String name, final PlayQueue playQueue) { + public void selectAndLoadVideo(final int sid, final String videoUrl, final String title, final PlayQueue playQueue) { // Situation when user switches from players to main player. All needed data is here, we can start watching if (this.playQueue != null && this.playQueue.equals(playQueue)) { openVideoPlayer(); return; } - setInitialData(serviceId, videoUrl, name, playQueue); + setInitialData(sid, videoUrl, title, playQueue); startLoading(false, true); } - public void prepareAndHandleInfo(final StreamInfo info, final boolean scrollToTop) { - if (DEBUG) Log.d(TAG, "prepareAndHandleInfo() called with: info = [" - + info + "], scrollToTop = [" + scrollToTop + "]"); + private void prepareAndHandleInfo(final StreamInfo info, final boolean scrollToTop) { + if (DEBUG) { + Log.d(TAG, "prepareAndHandleInfo() called with: " + + "info = [" + info + "], scrollToTop = [" + scrollToTop + "]"); + } showLoading(); initTabs(); @@ -820,7 +916,11 @@ public class VideoDetailFragment initTabs(); currentInfo = null; - if (currentWorker != null) currentWorker.dispose(); + if (currentWorker != null) { + currentWorker.dispose(); + } + + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity); runWorker(forceLoad, stack.isEmpty()); } @@ -836,20 +936,27 @@ public class VideoDetailFragment } private void runWorker(final boolean forceLoad, final boolean addToBackStack) { + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity); currentWorker = ExtractorHelper.getStreamInfo(serviceId, url, forceLoad) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe((@NonNull StreamInfo result) -> { + .subscribe((@NonNull final StreamInfo result) -> { isLoading.set(false); hideMainPlayer(); - handleResult(result); - showContent(); - if (addToBackStack) { - if (playQueue == null) playQueue = new SinglePlayQueue(result); - stack.push(new StackItem(serviceId, url, name, playQueue)); + if (result.getAgeLimit() != NO_AGE_LIMIT && !prefs.getBoolean( + getString(R.string.show_age_restricted_content), false)) { + hideAgeRestrictedContent(); + } else { + currentInfo = result; + handleResult(result); + showContent(); + if (addToBackStack) { + if (playQueue == null) playQueue = new SinglePlayQueue(result); + stack.push(new StackItem(serviceId, url, name, playQueue)); + } + if (isAutoplayEnabled()) openVideoPlayer(); } - if (isAutoplayEnabled()) openVideoPlayer(); - }, (@NonNull Throwable throwable) -> { + }, (@NonNull final Throwable throwable) -> { isLoading.set(false); onError(throwable); }); @@ -879,8 +986,10 @@ public class VideoDetailFragment if (pageAdapter.getCount() < 2) { tabLayout.setVisibility(View.GONE); } else { - final int position = pageAdapter.getItemPositionByTitle(selectedTabTag); - if (position != -1) viewPager.setCurrentItem(position); + int position = pageAdapter.getItemPositionByTitle(selectedTabTag); + if (position != -1) { + viewPager.setCurrentItem(position); + } tabLayout.setVisibility(View.VISIBLE); } } @@ -1001,22 +1110,6 @@ public class VideoDetailFragment return queue; } - private void openChannel() { - if (TextUtils.isEmpty(currentInfo.getUploaderUrl())) { - Log.w(TAG, "Can't open channel because we got no channel URL"); - } else { - try { - NavigationHelper.openChannelFragment( - getFragmentManager(), - currentInfo.getServiceId(), - currentInfo.getUploaderUrl(), - currentInfo.getUploaderName()); - } catch (Exception e) { - ErrorActivity.reportUiError(activity, e); - } - } - } - /*////////////////////////////////////////////////////////////////////////// // Utils //////////////////////////////////////////////////////////////////////////*/ @@ -1029,12 +1122,12 @@ public class VideoDetailFragment @NonNull final StreamInfo info, @NonNull final Stream selectedStream) { NavigationHelper.playOnExternalPlayer(context, currentInfo.getName(), - currentInfo.getUploaderName(), selectedStream); + currentInfo.getSubChannelName(), selectedStream); final HistoryRecordManager recordManager = new HistoryRecordManager(requireContext()); disposables.add(recordManager.onViewed(info).onErrorComplete() .subscribe( - ignored -> {/* successful */}, + ignored -> { /* successful */ }, error -> Log.e(TAG, "Register view failure: ", error) )); } @@ -1101,28 +1194,42 @@ public class VideoDetailFragment return sortedVideoStreams != null ? sortedVideoStreams.get(selectedVideoStreamIndex) : null; } - private void prepareDescription(final String descriptionHtml) { - if (TextUtils.isEmpty(descriptionHtml)) { + private void prepareDescription(final Description description) { + if (TextUtils.isEmpty(description.getContent()) + || description == Description.emptyDescription) { return; } - disposables.add(Single.just(descriptionHtml) - .map((@io.reactivex.annotations.NonNull String description) -> { - final Spanned parsedDescription; - if (Build.VERSION.SDK_INT >= 24) { - parsedDescription = Html.fromHtml(description, 0); - } else { - //noinspection deprecation - parsedDescription = Html.fromHtml(description); - } - return parsedDescription; - }) - .subscribeOn(Schedulers.computation()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe((@io.reactivex.annotations.NonNull Spanned spanned) -> { - videoDescriptionView.setText(spanned); - videoDescriptionView.setVisibility(View.VISIBLE); - })); + if (description.getType() == Description.HTML) { + disposables.add(Single.just(description.getContent()) + .map((@io.reactivex.annotations.NonNull String descriptionText) -> { + Spanned parsedDescription; + if (Build.VERSION.SDK_INT >= 24) { + parsedDescription = Html.fromHtml(descriptionText, 0); + } else { + //noinspection deprecation + parsedDescription = Html.fromHtml(descriptionText); + } + return parsedDescription; + }) + .subscribeOn(Schedulers.computation()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe((@io.reactivex.annotations.NonNull Spanned spanned) -> { + videoDescriptionView.setText(spanned); + videoDescriptionView.setVisibility(View.VISIBLE); + })); + } else if (description.getType() == Description.MARKDOWN) { + final Markwon markwon = Markwon.builder(getContext()) + .usePlugin(LinkifyPlugin.create()) + .build(); + markwon.setMarkdown(videoDescriptionView, description.getContent()); + videoDescriptionView.setVisibility(View.VISIBLE); + } else { + //== Description.PLAIN_TEXT + videoDescriptionView.setAutoLinkMask(Linkify.WEB_URLS); + videoDescriptionView.setText(description.getContent(), TextView.BufferType.SPANNABLE); + videoDescriptionView.setVisibility(View.VISIBLE); + } } /** @@ -1154,27 +1261,31 @@ public class VideoDetailFragment contentRootLayoutHiding.setVisibility(View.VISIBLE); } - protected void setInitialData(final int serviceId, final String url, final String name, final PlayQueue playQueue) { - this.serviceId = serviceId; - this.url = url; + protected void setInitialData(final int sid, final String u, final String name, final PlayQueue playQueue) { + this.serviceId = sid; + this.url = u; this.name = !TextUtils.isEmpty(name) ? name : ""; this.playQueue = playQueue; } private void setErrorImage(final int imageResource) { - if (thumbnailImageView == null || activity == null) return; + if (thumbnailImageView == null || activity == null) { + return; + } - thumbnailImageView.setImageDrawable(ContextCompat.getDrawable(activity, imageResource)); + thumbnailImageView.setImageDrawable( + AppCompatResources.getDrawable(requireContext(), imageResource)); animateView(thumbnailImageView, false, 0, 0, () -> animateView(thumbnailImageView, true, 500)); } @Override - public void showError(String message, boolean showRetryButton) { + public void showError(final String message, final boolean showRetryButton) { showError(message, showRetryButton, R.drawable.not_available_monkey); } - protected void showError(String message, boolean showRetryButton, @DrawableRes int imageError) { + protected void showError(final String message, final boolean showRetryButton, + @DrawableRes final int imageError) { super.showError(message, showRetryButton); setErrorImage(imageError); } @@ -1236,7 +1347,6 @@ public class VideoDetailFragment animateView(videoTitleTextView, true, 0); videoDescriptionRootLayout.setVisibility(View.GONE); - videoTitleToggleArrow.setImageResource(R.drawable.arrow_down); videoTitleToggleArrow.setVisibility(View.GONE); videoTitleRoot.setClickable(false); @@ -1248,14 +1358,14 @@ public class VideoDetailFragment } } - imageLoader.cancelDisplayTask(thumbnailImageView); - imageLoader.cancelDisplayTask(uploaderThumb); + IMAGE_LOADER.cancelDisplayTask(thumbnailImageView); + IMAGE_LOADER.cancelDisplayTask(subChannelThumb); thumbnailImageView.setImageBitmap(null); - uploaderThumb.setImageBitmap(null); + subChannelThumb.setImageBitmap(null); } @Override - public void handleResult(@NonNull StreamInfo info) { + public void handleResult(@NonNull final StreamInfo info) { super.handleResult(info); currentInfo = info; @@ -1263,11 +1373,13 @@ public class VideoDetailFragment if (showRelatedStreams) { if (null == relatedStreamsLayout) { //phone - pageAdapter.updateItem(RELATED_TAB_TAG, RelatedVideosFragment.getInstance(info)); + pageAdapter.updateItem(RELATED_TAB_TAG, + RelatedVideosFragment.getInstance(info)); pageAdapter.notifyDataSetUpdate(); } else { //tablet getChildFragmentManager().beginTransaction() - .replace(R.id.relatedStreamsLayout, RelatedVideosFragment.getInstance(info)) + .replace(R.id.relatedStreamsLayout, + RelatedVideosFragment.getInstance(info)) .commitNow(); relatedStreamsLayout.setVisibility(player != null && player.isFullscreen() ? View.GONE : View.VISIBLE); } @@ -1276,22 +1388,28 @@ public class VideoDetailFragment animateView(thumbnailPlayButton, true, 200); videoTitleTextView.setText(name); - if (!TextUtils.isEmpty(info.getUploaderName())) { - uploaderTextView.setText(info.getUploaderName()); - uploaderTextView.setVisibility(View.VISIBLE); - uploaderTextView.setSelected(true); + if (!TextUtils.isEmpty(info.getSubChannelName())) { + displayBothUploaderAndSubChannel(info); + } else if (!TextUtils.isEmpty(info.getUploaderName())) { + displayUploaderAsSubChannel(info); } else { uploaderTextView.setVisibility(View.GONE); + uploaderThumb.setVisibility(View.GONE); } - uploaderThumb.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.buddy)); + + Drawable buddyDrawable = AppCompatResources.getDrawable(activity, R.drawable.buddy); + subChannelThumb.setImageDrawable(buddyDrawable); + uploaderThumb.setImageDrawable(buddyDrawable); if (info.getViewCount() >= 0) { if (info.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) { videoCountView.setText(Localization.listeningCount(activity, info.getViewCount())); } else if (info.getStreamType().equals(StreamType.LIVE_STREAM)) { - videoCountView.setText(Localization.watchingCount(activity, info.getViewCount())); + videoCountView.setText(Localization + .localizeWatchingCount(activity, info.getViewCount())); } else { - videoCountView.setText(Localization.localizeViewCount(activity, info.getViewCount())); + videoCountView.setText(Localization + .localizeViewCount(activity, info.getViewCount())); } videoCountView.setVisibility(View.VISIBLE); } else { @@ -1307,7 +1425,8 @@ public class VideoDetailFragment thumbsDisabledTextView.setVisibility(View.VISIBLE); } else { if (info.getDislikeCount() >= 0) { - thumbsDownTextView.setText(Localization.shortCount(activity, info.getDislikeCount())); + thumbsDownTextView.setText(Localization + .shortCount(activity, info.getDislikeCount())); thumbsDownTextView.setVisibility(View.VISIBLE); thumbsDownImageView.setVisibility(View.VISIBLE); } else { @@ -1342,12 +1461,14 @@ public class VideoDetailFragment videoDescriptionView.setVisibility(View.GONE); videoTitleRoot.setClickable(true); + videoTitleToggleArrow.setImageResource( + ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_expand_more)); videoTitleToggleArrow.setVisibility(View.VISIBLE); - videoTitleToggleArrow.setImageResource(R.drawable.arrow_down); videoDescriptionRootLayout.setVisibility(View.GONE); if (info.getUploadDate() != null) { - videoUploadDateView.setText(Localization.localizeUploadDate(activity, info.getUploadDate().date().getTime())); + videoUploadDateView.setText(Localization + .localizeUploadDate(activity, info.getUploadDate().date().getTime())); videoUploadDateView.setVisibility(View.VISIBLE); } else { videoUploadDateView.setText(null); @@ -1382,16 +1503,54 @@ public class VideoDetailFragment detailControlsDownload.setVisibility(View.GONE); break; default: - if (info.getAudioStreams().isEmpty()) detailControlsBackground.setVisibility(View.GONE); - if (!info.getVideoStreams().isEmpty() - || !info.getVideoOnlyStreams().isEmpty()) break; + if (info.getAudioStreams().isEmpty()) { + detailControlsBackground.setVisibility(View.GONE); + } + if (!info.getVideoStreams().isEmpty() || !info.getVideoOnlyStreams().isEmpty()) { + break; + } detailControlsPopup.setVisibility(View.GONE); - thumbnailPlayButton.setImageResource(R.drawable.ic_headset_white_24dp); + thumbnailPlayButton.setImageResource(R.drawable.ic_headset_shadow); break; } } + private void hideAgeRestrictedContent() { + showError(getString(R.string.restricted_video), false); + + if (relatedStreamsLayout != null) { // tablet + relatedStreamsLayout.setVisibility(View.INVISIBLE); + } + + viewPager.setVisibility(View.GONE); + tabLayout.setVisibility(View.GONE); + } + + private void displayUploaderAsSubChannel(final StreamInfo info) { + subChannelTextView.setText(info.getUploaderName()); + subChannelTextView.setVisibility(View.VISIBLE); + subChannelTextView.setSelected(true); + uploaderTextView.setVisibility(View.GONE); + } + + private void displayBothUploaderAndSubChannel(final StreamInfo info) { + subChannelTextView.setText(info.getSubChannelName()); + subChannelTextView.setVisibility(View.VISIBLE); + subChannelTextView.setSelected(true); + + subChannelThumb.setVisibility(View.VISIBLE); + + if (!TextUtils.isEmpty(info.getUploaderName())) { + uploaderTextView.setText( + String.format(getString(R.string.video_detail_by), info.getUploaderName())); + uploaderTextView.setVisibility(View.VISIBLE); + uploaderTextView.setSelected(true); + } else { + uploaderTextView.setVisibility(View.GONE); + } + } + public void openDownloadDialog() { try { @@ -1423,24 +1582,20 @@ public class VideoDetailFragment //////////////////////////////////////////////////////////////////////////*/ @Override - protected boolean onError(Throwable exception) { - if (super.onError(exception)) return true; - - else if (exception instanceof ContentNotAvailableException) { - showError(getString(R.string.content_not_available), false); - } else { - final int errorId = exception instanceof YoutubeStreamExtractor.DecryptException - ? R.string.youtube_signature_decryption_error - : exception instanceof ParsingException - ? R.string.parsing_error - : R.string.general_error; - onUnrecoverableError(exception, - UserAction.REQUESTED_STREAM, - NewPipe.getNameOfService(serviceId), - url, - errorId); + protected boolean onError(final Throwable exception) { + if (super.onError(exception)) { + return true; } + int errorId = exception instanceof YoutubeStreamExtractor.DecryptException + ? R.string.youtube_signature_decryption_error + : exception instanceof ExtractionException + ? R.string.parsing_error + : R.string.general_error; + + onUnrecoverableError(exception, UserAction.REQUESTED_STREAM, + NewPipe.getNameOfService(serviceId), url, errorId); + return true; } @@ -1449,9 +1604,9 @@ public class VideoDetailFragment positionSubscriber.dispose(); } final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity); - final boolean playbackResumeEnabled = - prefs.getBoolean(activity.getString(R.string.enable_watch_history_key), true) - && prefs.getBoolean(activity.getString(R.string.enable_playback_resume_key), true); + final boolean playbackResumeEnabled = prefs + .getBoolean(activity.getString(R.string.enable_watch_history_key), true) + && prefs.getBoolean(activity.getString(R.string.enable_playback_resume_key), true); final boolean showPlaybackPosition = prefs.getBoolean( activity.getString(R.string.enable_playback_state_lists_key), true); if (!playbackResumeEnabled) { @@ -1459,6 +1614,12 @@ public class VideoDetailFragment || playQueue.getItem().getRecoveryPosition() == RECOVERY_UNSET || !showPlaybackPosition) { positionView.setVisibility(View.INVISIBLE); detailPositionView.setVisibility(View.GONE); + // TODO: Remove this check when separation of concerns is done. + // (live streams weren't getting updated because they are mixed) + if (!info.getStreamType().equals(StreamType.LIVE_STREAM) + && !info.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) { + return; + } } else { // Show saved position from backStack if user allows it showPlaybackProgress(playQueue.getItem().getRecoveryPosition(), @@ -1466,9 +1627,12 @@ public class VideoDetailFragment animateView(positionView, true, 500); animateView(detailPositionView, true, 500); } - return; - } + return; + } final HistoryRecordManager recordManager = new HistoryRecordManager(requireContext()); + + // TODO: Separate concerns when updating database data. + // (move the updating part to when the loading happens) positionSubscriber = recordManager.loadStreamState(info) .subscribeOn(Schedulers.io()) .onErrorComplete() @@ -1478,7 +1642,9 @@ public class VideoDetailFragment animateView(positionView, true, 500); animateView(detailPositionView, true, 500); }, e -> { - if (DEBUG) e.printStackTrace(); + if (DEBUG) { + e.printStackTrace(); + } }, () -> { positionView.setVisibility(View.GONE); detailPositionView.setVisibility(View.GONE); @@ -1489,7 +1655,7 @@ public class VideoDetailFragment final int progressSeconds = (int) TimeUnit.MILLISECONDS.toSeconds(progress); final int durationSeconds = (int) TimeUnit.MILLISECONDS.toSeconds(duration); positionView.setMax(durationSeconds); - positionView.setProgress(progressSeconds); + positionView.setProgressAnimated(progressSeconds); detailPositionView.setText(Localization.getDurationString(progressSeconds)); if (positionView.getVisibility() != View.VISIBLE) { animateView(positionView, true, 100); @@ -1862,12 +2028,12 @@ public class VideoDetailFragment overlayChannelTextView.setText(TextUtils.isEmpty(uploader) ? "" : uploader); overlayThumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark); if (!TextUtils.isEmpty(thumbnailUrl)) - imageLoader.displayImage(thumbnailUrl, overlayThumbnailImageView, + IMAGE_LOADER.displayImage(thumbnailUrl, overlayThumbnailImageView, ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, null); } private void setOverlayPlayPauseImage() { - final int attr = player != null && player.getPlayer().getPlayWhenReady() ? R.attr.pause : R.attr.play; + final int attr = player != null && player.getPlayer().getPlayWhenReady() ? R.attr.ic_pause : R.attr.ic_play_arrow; overlayPlayPauseButton.setImageResource(ThemeHelper.resolveResourceIdFromAttr(activity, attr)); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java index d6fd1dd00..9ce62a0df 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java @@ -7,17 +7,17 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.os.Bundle; import android.preference.PreferenceManager; -import androidx.annotation.NonNull; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AppCompatActivity; -import androidx.recyclerview.widget.GridLayoutManager; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.View; +import androidx.annotation.NonNull; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.channel.ChannelInfoItem; @@ -34,13 +34,21 @@ import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.StateSaver; import org.schabi.newpipe.util.StreamDialogEntry; +import org.schabi.newpipe.views.SuperScrollLayoutManager; import java.util.List; import java.util.Queue; import static org.schabi.newpipe.util.AnimationUtils.animateView; -public abstract class BaseListFragment extends BaseStateFragment implements ListViewContract, StateSaver.WriteRead, SharedPreferences.OnSharedPreferenceChangeListener { +public abstract class BaseListFragment extends BaseStateFragment + implements ListViewContract, StateSaver.WriteRead, + SharedPreferences.OnSharedPreferenceChangeListener { + private static final int LIST_MODE_UPDATE_FLAG = 0x32; + protected StateSaver.SavedState savedState; + + private boolean useDefaultStateSaving = true; + private int updateFlags = 0; /*////////////////////////////////////////////////////////////////////////// // Views @@ -48,18 +56,19 @@ public abstract class BaseListFragment extends BaseStateFragment implem protected InfoListAdapter infoListAdapter; protected RecyclerView itemsList; - private int updateFlags = 0; - - private static final int LIST_MODE_UPDATE_FLAG = 0x32; + private int focusedPosition = -1; /*////////////////////////////////////////////////////////////////////////// // LifeCycle //////////////////////////////////////////////////////////////////////////*/ @Override - public void onAttach(Context context) { + public void onAttach(final Context context) { super.onAttach(context); - infoListAdapter = new InfoListAdapter(activity); + + if (infoListAdapter == null) { + infoListAdapter = new InfoListAdapter(activity); + } } @Override @@ -68,7 +77,7 @@ public abstract class BaseListFragment extends BaseStateFragment implem } @Override - public void onCreate(Bundle savedInstanceState) { + public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); PreferenceManager.getDefaultSharedPreferences(activity) @@ -78,7 +87,9 @@ public abstract class BaseListFragment extends BaseStateFragment implem @Override public void onDestroy() { super.onDestroy(); - StateSaver.onDestroy(savedState); + if (useDefaultStateSaving) { + StateSaver.onDestroy(savedState); + } PreferenceManager.getDefaultSharedPreferences(activity) .unregisterOnSharedPreferenceChangeListener(this); } @@ -90,8 +101,9 @@ public abstract class BaseListFragment extends BaseStateFragment implem if (updateFlags != 0) { if ((updateFlags & LIST_MODE_UPDATE_FLAG) != 0) { final boolean useGrid = isGridLayout(); - itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager()); - infoListAdapter.setGridItemVariants(useGrid); + itemsList.setLayoutManager(useGrid + ? getGridLayoutManager() : getListLayoutManager()); + infoListAdapter.setUseGridVariant(useGrid); infoListAdapter.notifyDataSetChanged(); } updateFlags = 0; @@ -102,7 +114,15 @@ public abstract class BaseListFragment extends BaseStateFragment implem // State Saving //////////////////////////////////////////////////////////////////////////*/ - protected StateSaver.SavedState savedState; + /** + * If the default implementation of {@link StateSaver.WriteRead} should be used. + * + * @see StateSaver + * @param useDefaultStateSaving Whether the default implementation should be used + */ + public void setUseDefaultStateSaving(final boolean useDefaultStateSaving) { + this.useDefaultStateSaving = useDefaultStateSaving; + } @Override public String generateSuffix() { @@ -110,28 +130,81 @@ public abstract class BaseListFragment extends BaseStateFragment implem return "." + infoListAdapter.getItemsList().size() + ".list"; } + private int getFocusedPosition() { + try { + final View focusedItem = itemsList.getFocusedChild(); + final RecyclerView.ViewHolder itemHolder = + itemsList.findContainingViewHolder(focusedItem); + return itemHolder.getAdapterPosition(); + } catch (NullPointerException e) { + return -1; + } + } + @Override - public void writeTo(Queue objectsToSave) { + public void writeTo(final Queue objectsToSave) { + if (!useDefaultStateSaving) { + return; + } + objectsToSave.add(infoListAdapter.getItemsList()); + objectsToSave.add(getFocusedPosition()); } @Override @SuppressWarnings("unchecked") - public void readFrom(@NonNull Queue savedObjects) throws Exception { + public void readFrom(@NonNull final Queue savedObjects) throws Exception { + if (!useDefaultStateSaving) { + return; + } + infoListAdapter.getItemsList().clear(); infoListAdapter.getItemsList().addAll((List) savedObjects.poll()); + restoreFocus((Integer) savedObjects.poll()); + } + + private void restoreFocus(final Integer position) { + if (position == null || position < 0) { + return; + } + + itemsList.post(() -> { + RecyclerView.ViewHolder focusedHolder = + itemsList.findViewHolderForAdapterPosition(position); + + if (focusedHolder != null) { + focusedHolder.itemView.requestFocus(); + } + }); } @Override - public void onSaveInstanceState(Bundle bundle) { + public void onSaveInstanceState(final Bundle bundle) { super.onSaveInstanceState(bundle); - savedState = StateSaver.tryToSave(activity.isChangingConfigurations(), savedState, bundle, this); + if (useDefaultStateSaving) { + savedState = StateSaver + .tryToSave(activity.isChangingConfigurations(), savedState, bundle, this); + } } @Override - protected void onRestoreInstanceState(@NonNull Bundle bundle) { + protected void onRestoreInstanceState(@NonNull final Bundle bundle) { super.onRestoreInstanceState(bundle); - savedState = StateSaver.tryToRestore(bundle, this); + if (useDefaultStateSaving) { + savedState = StateSaver.tryToRestore(bundle, this); + } + } + + @Override + public void onStop() { + focusedPosition = getFocusedPosition(); + super.onStop(); + } + + @Override + public void onStart() { + super.onStart(); + restoreFocus(focusedPosition); } /*////////////////////////////////////////////////////////////////////////// @@ -147,36 +220,39 @@ public abstract class BaseListFragment extends BaseStateFragment implem } protected RecyclerView.LayoutManager getListLayoutManager() { - return new LinearLayoutManager(activity); + return new SuperScrollLayoutManager(activity); } protected RecyclerView.LayoutManager getGridLayoutManager() { final Resources resources = activity.getResources(); int width = resources.getDimensionPixelSize(R.dimen.video_item_grid_thumbnail_image_width); width += (24 * resources.getDisplayMetrics().density); - final int spanCount = (int) Math.floor(resources.getDisplayMetrics().widthPixels / (double)width); + final int spanCount = (int) Math.floor(resources.getDisplayMetrics().widthPixels + / (double) width); final GridLayoutManager lm = new GridLayoutManager(activity, spanCount); lm.setSpanSizeLookup(infoListAdapter.getSpanSizeLookup(spanCount)); return lm; } @Override - protected void initViews(View rootView, Bundle savedInstanceState) { + protected void initViews(final View rootView, final Bundle savedInstanceState) { super.initViews(rootView, savedInstanceState); final boolean useGrid = isGridLayout(); itemsList = rootView.findViewById(R.id.items_list); itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager()); - infoListAdapter.setGridItemVariants(useGrid); + infoListAdapter.setUseGridVariant(useGrid); infoListAdapter.setFooter(getListFooter()); infoListAdapter.setHeader(getListHeader()); itemsList.setAdapter(infoListAdapter); } - protected void onItemSelected(InfoItem selectedItem) { - if (DEBUG) Log.d(TAG, "onItemSelected() called with: selectedItem = [" + selectedItem + "]"); + protected void onItemSelected(final InfoItem selectedItem) { + if (DEBUG) { + Log.d(TAG, "onItemSelected() called with: selectedItem = [" + selectedItem + "]"); + } } @Override @@ -184,19 +260,19 @@ public abstract class BaseListFragment extends BaseStateFragment implem super.initListeners(); infoListAdapter.setOnStreamSelectedListener(new OnClickGesture() { @Override - public void selected(StreamInfoItem selectedItem) { + public void selected(final StreamInfoItem selectedItem) { onStreamSelected(selectedItem); } @Override - public void held(StreamInfoItem selectedItem) { + public void held(final StreamInfoItem selectedItem) { showStreamDialog(selectedItem); } }); infoListAdapter.setOnChannelSelectedListener(new OnClickGesture() { @Override - public void selected(ChannelInfoItem selectedItem) { + public void selected(final ChannelInfoItem selectedItem) { try { onItemSelected(selectedItem); NavigationHelper.openChannelFragment(getFM(), @@ -211,7 +287,7 @@ public abstract class BaseListFragment extends BaseStateFragment implem infoListAdapter.setOnPlaylistSelectedListener(new OnClickGesture() { @Override - public void selected(PlaylistInfoItem selectedItem) { + public void selected(final PlaylistInfoItem selectedItem) { try { onItemSelected(selectedItem); NavigationHelper.openPlaylistFragment(getFM(), @@ -226,7 +302,7 @@ public abstract class BaseListFragment extends BaseStateFragment implem infoListAdapter.setOnCommentsSelectedListener(new OnClickGesture() { @Override - public void selected(CommentsInfoItem selectedItem) { + public void selected(final CommentsInfoItem selectedItem) { onItemSelected(selectedItem); } }); @@ -234,13 +310,13 @@ public abstract class BaseListFragment extends BaseStateFragment implem itemsList.clearOnScrollListeners(); itemsList.addOnScrollListener(new OnScrollBelowItemsListener() { @Override - public void onScrolledDown(RecyclerView recyclerView) { + public void onScrolledDown(final RecyclerView recyclerView) { onScrollToBottom(); } }); } - private void onStreamSelected(StreamInfoItem selectedItem) { + private void onStreamSelected(final StreamInfoItem selectedItem) { onItemSelected(selectedItem); NavigationHelper.openVideoDetailFragment(getFM(), selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName()); @@ -253,12 +329,12 @@ public abstract class BaseListFragment extends BaseStateFragment implem } - - protected void showStreamDialog(final StreamInfoItem item) { final Context context = getContext(); final Activity activity = getActivity(); - if (context == null || context.getResources() == null || activity == null) return; + if (context == null || context.getResources() == null || activity == null) { + return; + } if (item.getStreamType() == StreamType.AUDIO_STREAM) { StreamDialogEntry.setEnabledEntries( @@ -276,8 +352,8 @@ public abstract class BaseListFragment extends BaseStateFragment implem StreamDialogEntry.share); } - new InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context), (dialog, which) -> - StreamDialogEntry.clickOn(which, this, item)).show(); + new InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context), + (dialog, which) -> StreamDialogEntry.clickOn(which, this, item)).show(); } /*////////////////////////////////////////////////////////////////////////// @@ -285,8 +361,11 @@ public abstract class BaseListFragment extends BaseStateFragment implem //////////////////////////////////////////////////////////////////////////*/ @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]"); + public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { + if (DEBUG) { + Log.d(TAG, "onCreateOptionsMenu() called with: " + + "menu = [" + menu + "], inflater = [" + inflater + "]"); + } super.onCreateOptionsMenu(menu, inflater); ActionBar supportActionBar = activity.getSupportActionBar(); if (supportActionBar != null) { @@ -324,7 +403,7 @@ public abstract class BaseListFragment extends BaseStateFragment implem } @Override - public void showError(String message, boolean showRetryButton) { + public void showError(final String message, final boolean showRetryButton) { super.showError(message, showRetryButton); showListFooter(false); animateView(itemsList, false, 200); @@ -346,25 +425,28 @@ public abstract class BaseListFragment extends BaseStateFragment implem } @Override - public void handleNextItems(N result) { + public void handleNextItems(final N result) { isLoading.set(false); } @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, + final String key) { if (key.equals(getString(R.string.list_view_mode_key))) { updateFlags |= LIST_MODE_UPDATE_FLAG; } } protected boolean isGridLayout() { - final String list_mode = PreferenceManager.getDefaultSharedPreferences(activity).getString(getString(R.string.list_view_mode_key), getString(R.string.list_view_mode_value)); - if ("auto".equals(list_mode)) { + final String listMode = PreferenceManager.getDefaultSharedPreferences(activity) + .getString(getString(R.string.list_view_mode_key), + getString(R.string.list_view_mode_value)); + if ("auto".equals(listMode)) { final Configuration configuration = getResources().getConfiguration(); return configuration.orientation == Configuration.ORIENTATION_LANDSCAPE && configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE); } else { - return "grid".equals(list_mode); + return "grid".equals(listMode); } } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java index 9a8e1fd17..82b1d18ed 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java @@ -9,7 +9,9 @@ import androidx.annotation.NonNull; import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.ListInfo; +import org.schabi.newpipe.extractor.Page; import org.schabi.newpipe.util.Constants; +import org.schabi.newpipe.views.NewPipeRecyclerView; import java.util.Queue; @@ -21,7 +23,6 @@ import io.reactivex.schedulers.Schedulers; public abstract class BaseListInfoFragment extends BaseListFragment { - @State protected int serviceId = Constants.NO_SERVICE_ID; @State @@ -30,11 +31,11 @@ public abstract class BaseListInfoFragment protected String url; protected I currentInfo; - protected String currentNextPageUrl; + protected Page currentNextPage; protected Disposable currentWorker; @Override - protected void initViews(View rootView, Bundle savedInstanceState) { + protected void initViews(final View rootView, final Bundle savedInstanceState) { super.initViews(rootView, savedInstanceState); setTitle(name); showListFooter(hasMoreItems()); @@ -43,7 +44,9 @@ public abstract class BaseListInfoFragment @Override public void onPause() { super.onPause(); - if (currentWorker != null) currentWorker.dispose(); + if (currentWorker != null) { + currentWorker.dispose(); + } } @Override @@ -73,18 +76,18 @@ public abstract class BaseListInfoFragment //////////////////////////////////////////////////////////////////////////*/ @Override - public void writeTo(Queue objectsToSave) { + public void writeTo(final Queue objectsToSave) { super.writeTo(objectsToSave); objectsToSave.add(currentInfo); - objectsToSave.add(currentNextPageUrl); + objectsToSave.add(currentNextPage); } @Override @SuppressWarnings("unchecked") - public void readFrom(@NonNull Queue savedObjects) throws Exception { + public void readFrom(@NonNull final Queue savedObjects) throws Exception { super.readFrom(savedObjects); currentInfo = (I) savedObjects.poll(); - currentNextPageUrl = (String) savedObjects.poll(); + currentNextPage = (Page) savedObjects.poll(); } /*////////////////////////////////////////////////////////////////////////// @@ -92,10 +95,14 @@ public abstract class BaseListInfoFragment //////////////////////////////////////////////////////////////////////////*/ protected void doInitialLoadLogic() { - if (DEBUG) Log.d(TAG, "doInitialLoadLogic() called"); + if (DEBUG) { + Log.d(TAG, "doInitialLoadLogic() called"); + } if (currentInfo == null) { startLoading(false); - } else handleResult(currentInfo); + } else { + handleResult(currentInfo); + } } /** @@ -103,43 +110,56 @@ public abstract class BaseListInfoFragment * You can use the default implementations from {@link org.schabi.newpipe.util.ExtractorHelper}. * * @param forceLoad allow or disallow the result to come from the cache + * @return Rx {@link Single} containing the {@link ListInfo} */ protected abstract Single loadResult(boolean forceLoad); @Override - public void startLoading(boolean forceLoad) { + public void startLoading(final boolean forceLoad) { super.startLoading(forceLoad); showListFooter(false); infoListAdapter.clearStreamItemList(); currentInfo = null; - if (currentWorker != null) currentWorker.dispose(); + if (currentWorker != null) { + currentWorker.dispose(); + } currentWorker = loadResult(forceLoad) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe((@NonNull I result) -> { isLoading.set(false); currentInfo = result; - currentNextPageUrl = result.getNextPageUrl(); + currentNextPage = result.getNextPage(); handleResult(result); }, (@NonNull Throwable throwable) -> onError(throwable)); } /** - * Implement the logic to load more items
- * You can use the default implementations from {@link org.schabi.newpipe.util.ExtractorHelper} + * Implement the logic to load more items. + *

You can use the default implementations + * from {@link org.schabi.newpipe.util.ExtractorHelper}.

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

* Copyright (C) Christian Schabesberger 2017 * KioskFragment.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 . + * along with NewPipe. If not, see . + *

*/ public class KioskFragment extends BaseListInfoFragment { - @State - protected String kioskId = ""; - protected String kioskTranslatedName; + String kioskId = ""; + String kioskTranslatedName; @State - protected ContentCountry contentCountry; - + ContentCountry contentCountry; /*////////////////////////////////////////////////////////////////////////// // Views //////////////////////////////////////////////////////////////////////////*/ - public static KioskFragment getInstance(int serviceId) - throws ExtractionException { + public static KioskFragment getInstance(final int serviceId) throws ExtractionException { return getInstance(serviceId, NewPipe.getService(serviceId) - .getKioskList() - .getDefaultKioskId()); + .getKioskList().getDefaultKioskId()); } - public static KioskFragment getInstance(int serviceId, String kioskId) + public static KioskFragment getInstance(final int serviceId, final String kioskId) throws ExtractionException { KioskFragment instance = new KioskFragment(); StreamingService service = NewPipe.getService(serviceId); @@ -88,7 +87,7 @@ public class KioskFragment extends BaseListInfoFragment { //////////////////////////////////////////////////////////////////////////*/ @Override - public void onCreate(Bundle savedInstanceState) { + public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); kioskTranslatedName = KioskTranslator.getTranslatedKioskName(kioskId, activity); @@ -97,9 +96,9 @@ public class KioskFragment extends BaseListInfoFragment { } @Override - public void setUserVisibleHint(boolean isVisibleToUser) { + public void setUserVisibleHint(final boolean isVisibleToUser) { super.setUserVisibleHint(isVisibleToUser); - if(useAsFrontPage && isVisibleToUser && activity != null) { + if (useAsFrontPage && isVisibleToUser && activity != null) { try { setTitle(kioskTranslatedName); } catch (Exception e) { @@ -111,7 +110,9 @@ public class KioskFragment extends BaseListInfoFragment { } @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + public View onCreateView(@NonNull final LayoutInflater inflater, + @Nullable final ViewGroup container, + @Nullable final Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_kiosk, container, false); } @@ -129,7 +130,7 @@ public class KioskFragment extends BaseListInfoFragment { //////////////////////////////////////////////////////////////////////////*/ @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); ActionBar supportActionBar = activity.getSupportActionBar(); if (supportActionBar != null && useAsFrontPage) { @@ -142,18 +143,14 @@ public class KioskFragment extends BaseListInfoFragment { //////////////////////////////////////////////////////////////////////////*/ @Override - public Single loadResult(boolean forceReload) { + public Single loadResult(final boolean forceReload) { contentCountry = Localization.getPreferredContentCountry(requireContext()); - return ExtractorHelper.getKioskInfo(serviceId, - url, - forceReload); + return ExtractorHelper.getKioskInfo(serviceId, url, forceReload); } @Override public Single loadMoreItemsLogic() { - return ExtractorHelper.getMoreKioskItems(serviceId, - url, - currentNextPageUrl); + return ExtractorHelper.getMoreKioskItems(serviceId, url, currentNextPage); } /*////////////////////////////////////////////////////////////////////////// @@ -181,13 +178,13 @@ public class KioskFragment extends BaseListInfoFragment { } @Override - public void handleNextItems(ListExtractor.InfoItemsPage result) { + public void handleNextItems(final ListExtractor.InfoItemsPage result) { super.handleNextItems(result); if (!result.getErrors().isEmpty()) { showSnackBarError(result.getErrors(), - UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(serviceId) - , "Get next page of: " + url, 0); + UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(serviceId), + "Get next page of: " + url, 0); } } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java index f3f14f746..48ec7b505 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java @@ -3,9 +3,6 @@ package org.schabi.newpipe.fragments.list.playlist; import android.app.Activity; import android.content.Context; import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; @@ -17,6 +14,10 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; + import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.schabi.newpipe.NewPipeDatabase; @@ -38,6 +39,7 @@ import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ImageDisplayConstants; +import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.ShareUtils; import org.schabi.newpipe.util.StreamDialogEntry; @@ -57,7 +59,6 @@ import io.reactivex.disposables.Disposables; import static org.schabi.newpipe.util.AnimationUtils.animateView; public class PlaylistFragment extends BaseListInfoFragment { - private CompositeDisposable disposables; private Subscription bookmarkReactor; private AtomicBoolean isBookmarkButtonReady; @@ -82,7 +83,8 @@ public class PlaylistFragment extends BaseListInfoFragment { private MenuItem playlistBookmarkButton; - public static PlaylistFragment getInstance(int serviceId, String url, String name) { + public static PlaylistFragment getInstance(final int serviceId, final String url, + final String name) { PlaylistFragment instance = new PlaylistFragment(); instance.setInitialData(serviceId, url, name); return instance; @@ -93,17 +95,18 @@ public class PlaylistFragment extends BaseListInfoFragment { //////////////////////////////////////////////////////////////////////////*/ @Override - public void onCreate(Bundle savedInstanceState) { + public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); disposables = new CompositeDisposable(); isBookmarkButtonReady = new AtomicBoolean(false); - remotePlaylistManager = new RemotePlaylistManager(NewPipeDatabase.getInstance( - requireContext())); + remotePlaylistManager = new RemotePlaylistManager(NewPipeDatabase + .getInstance(requireContext())); } @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { + public View onCreateView(@NonNull final LayoutInflater inflater, + @Nullable final ViewGroup container, + @Nullable final Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_playlist, container, false); } @@ -112,7 +115,8 @@ public class PlaylistFragment extends BaseListInfoFragment { //////////////////////////////////////////////////////////////////////////*/ protected View getListHeader() { - headerRootLayout = activity.getLayoutInflater().inflate(R.layout.playlist_header, itemsList, false); + headerRootLayout = activity.getLayoutInflater() + .inflate(R.layout.playlist_header, itemsList, false); headerTitleView = headerRootLayout.findViewById(R.id.playlist_title_view); headerUploaderLayout = headerRootLayout.findViewById(R.id.uploader_layout); headerUploaderName = headerRootLayout.findViewById(R.id.uploader_name); @@ -129,21 +133,23 @@ public class PlaylistFragment extends BaseListInfoFragment { } @Override - protected void initViews(View rootView, Bundle savedInstanceState) { + protected void initViews(final View rootView, final Bundle savedInstanceState) { super.initViews(rootView, savedInstanceState); - infoListAdapter.useMiniItemVariants(true); + infoListAdapter.setUseMiniVariant(true); } - private PlayQueue getPlayQueueStartingAt(StreamInfoItem infoItem) { + private PlayQueue getPlayQueueStartingAt(final StreamInfoItem infoItem) { return getPlayQueue(Math.max(infoListAdapter.getItemsList().indexOf(infoItem), 0)); } @Override - protected void showStreamDialog(StreamInfoItem item) { + protected void showStreamDialog(final StreamInfoItem item) { final Context context = getContext(); final Activity activity = getActivity(); - if (context == null || context.getResources() == null || activity == null) return; + if (context == null || context.getResources() == null || activity == null) { + return; + } if (item.getStreamType() == StreamType.AUDIO_STREAM) { StreamDialogEntry.setEnabledEntries( @@ -160,21 +166,25 @@ public class PlaylistFragment extends BaseListInfoFragment { StreamDialogEntry.append_playlist, StreamDialogEntry.share); - StreamDialogEntry.start_here_on_popup.setCustomAction( - (fragment, infoItem) -> NavigationHelper.playOnPopupPlayer(context, getPlayQueueStartingAt(infoItem), true)); + StreamDialogEntry.start_here_on_popup.setCustomAction((fragment, infoItem) -> + NavigationHelper.playOnPopupPlayer(context, + getPlayQueueStartingAt(infoItem), true)); } - StreamDialogEntry.start_here_on_background.setCustomAction( - (fragment, infoItem) -> NavigationHelper.playOnBackgroundPlayer(context, getPlayQueueStartingAt(infoItem), true)); + StreamDialogEntry.start_here_on_background.setCustomAction((fragment, infoItem) -> + NavigationHelper.playOnBackgroundPlayer(context, + getPlayQueueStartingAt(infoItem), true)); - new InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context), (dialog, which) -> - StreamDialogEntry.clickOn(which, this, item)).show(); + new InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context), + (dialog, which) -> StreamDialogEntry.clickOn(which, this, item)).show(); } @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + - "], inflater = [" + inflater + "]"); + public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { + if (DEBUG) { + Log.d(TAG, "onCreateOptionsMenu() called with: " + + "menu = [" + menu + "], inflater = [" + inflater + "]"); + } super.onCreateOptionsMenu(menu, inflater); inflater.inflate(R.menu.menu_playlist, menu); @@ -185,10 +195,16 @@ public class PlaylistFragment extends BaseListInfoFragment { @Override public void onDestroyView() { super.onDestroyView(); - if (isBookmarkButtonReady != null) isBookmarkButtonReady.set(false); + if (isBookmarkButtonReady != null) { + isBookmarkButtonReady.set(false); + } - if (disposables != null) disposables.clear(); - if (bookmarkReactor != null) bookmarkReactor.cancel(); + if (disposables != null) { + disposables.clear(); + } + if (bookmarkReactor != null) { + bookmarkReactor.cancel(); + } bookmarkReactor = null; } @@ -197,7 +213,9 @@ public class PlaylistFragment extends BaseListInfoFragment { public void onDestroy() { super.onDestroy(); - if (disposables != null) disposables.dispose(); + if (disposables != null) { + disposables.dispose(); + } disposables = null; remotePlaylistManager = null; @@ -211,22 +229,25 @@ public class PlaylistFragment extends BaseListInfoFragment { @Override protected Single loadMoreItemsLogic() { - return ExtractorHelper.getMorePlaylistItems(serviceId, url, currentNextPageUrl); + return ExtractorHelper.getMorePlaylistItems(serviceId, url, currentNextPage); } @Override - protected Single loadResult(boolean forceLoad) { + protected Single loadResult(final boolean forceLoad) { return ExtractorHelper.getPlaylistInfo(serviceId, url, forceLoad); } @Override - public boolean onOptionsItemSelected(MenuItem item) { + public boolean onOptionsItemSelected(final MenuItem item) { switch (item.getItemId()) { + case R.id.action_settings: + NavigationHelper.openSettings(requireContext()); + break; case R.id.menu_item_openInBrowser: - ShareUtils.openUrlInBrowser(this.getContext(), url); + ShareUtils.openUrlInBrowser(requireContext(), url); break; case R.id.menu_item_share: - ShareUtils.shareUrl(this.getContext(), name, url); + ShareUtils.shareUrl(requireContext(), name, url); break; case R.id.menu_item_bookmark: onBookmarkClicked(); @@ -248,7 +269,7 @@ public class PlaylistFragment extends BaseListInfoFragment { animateView(headerRootLayout, false, 200); animateView(itemsList, false, 100); - imageLoader.cancelDisplayTask(headerUploaderAvatar); + IMAGE_LOADER.cancelDisplayTask(headerUploaderAvatar); animateView(headerUploaderLayout, false, 200); } @@ -259,7 +280,8 @@ public class PlaylistFragment extends BaseListInfoFragment { animateView(headerRootLayout, true, 100); animateView(headerUploaderLayout, true, 300); headerUploaderLayout.setOnClickListener(null); - if (!TextUtils.isEmpty(result.getUploaderName())) { // If we have an uploader : Put them into the ui + // If we have an uploader put them into the UI + if (!TextUtils.isEmpty(result.getUploaderName())) { headerUploaderName.setText(result.getUploaderName()); if (!TextUtils.isEmpty(result.getUploaderUrl())) { headerUploaderLayout.setOnClickListener(v -> { @@ -273,19 +295,20 @@ public class PlaylistFragment extends BaseListInfoFragment { } }); } - } else { // Else : say we have no uploader + } else { // Otherwise say we have no uploader headerUploaderName.setText(R.string.playlist_no_uploader); } playlistCtrl.setVisibility(View.VISIBLE); - imageLoader.displayImage(result.getUploaderAvatarUrl(), headerUploaderAvatar, + IMAGE_LOADER.displayImage(result.getUploaderAvatarUrl(), headerUploaderAvatar, ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS); - headerStreamCount.setText(getResources().getQuantityString(R.plurals.videos, - (int) result.getStreamCount(), (int) result.getStreamCount())); + headerStreamCount.setText(Localization + .localizeStreamCount(getContext(), result.getStreamCount())); if (!result.getErrors().isEmpty()) { - showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0); + showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST, + NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0); } remotePlaylistManager.getPlaylist(result) @@ -318,27 +341,27 @@ public class PlaylistFragment extends BaseListInfoFragment { private PlayQueue getPlayQueue(final int index) { final List infoItems = new ArrayList<>(); - for(InfoItem i : infoListAdapter.getItemsList()) { - if(i instanceof StreamInfoItem) { + for (InfoItem i : infoListAdapter.getItemsList()) { + if (i instanceof StreamInfoItem) { infoItems.add((StreamInfoItem) i); } } return new PlaylistPlayQueue( currentInfo.getServiceId(), currentInfo.getUrl(), - currentInfo.getNextPageUrl(), + currentInfo.getNextPage(), infoItems, index ); } @Override - public void handleNextItems(ListExtractor.InfoItemsPage result) { + public void handleNextItems(final ListExtractor.InfoItemsPage result) { super.handleNextItems(result); if (!result.getErrors().isEmpty()) { - showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(serviceId) - , "Get next page of: " + url, 0); + showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST, + NewPipe.getNameOfService(serviceId), "Get next page of: " + url, 0); } } @@ -347,15 +370,15 @@ public class PlaylistFragment extends BaseListInfoFragment { //////////////////////////////////////////////////////////////////////////*/ @Override - protected boolean onError(Throwable exception) { - if (super.onError(exception)) return true; + protected boolean onError(final Throwable exception) { + if (super.onError(exception)) { + return true; + } - int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error; - onUnrecoverableError(exception, - UserAction.REQUESTED_PLAYLIST, - NewPipe.getNameOfService(serviceId), - url, - errorId); + int errorId = exception instanceof ExtractionException + ? R.string.parsing_error : R.string.general_error; + onUnrecoverableError(exception, UserAction.REQUESTED_PLAYLIST, + NewPipe.getNameOfService(serviceId), url, errorId); return true; } @@ -363,13 +386,18 @@ public class PlaylistFragment extends BaseListInfoFragment { // Utils //////////////////////////////////////////////////////////////////////////*/ - private Flowable getUpdateProcessor(@NonNull List playlists, - @NonNull PlaylistInfo result) { + private Flowable getUpdateProcessor( + @NonNull final List playlists, + @NonNull final PlaylistInfo result) { final Flowable noItemToUpdate = Flowable.just(/*noItemToUpdate=*/-1); - if (playlists.isEmpty()) return noItemToUpdate; + if (playlists.isEmpty()) { + return noItemToUpdate; + } - final PlaylistRemoteEntity playlistEntity = playlists.get(0); - if (playlistEntity.isIdenticalTo(result)) return noItemToUpdate; + final PlaylistRemoteEntity playlistRemoteEntity = playlists.get(0); + if (playlistRemoteEntity.isIdenticalTo(result)) { + return noItemToUpdate; + } return remotePlaylistManager.onUpdate(playlists.get(0).getUid(), result).toFlowable(); } @@ -377,56 +405,59 @@ public class PlaylistFragment extends BaseListInfoFragment { private Subscriber> getPlaylistBookmarkSubscriber() { return new Subscriber>() { @Override - public void onSubscribe(Subscription s) { - if (bookmarkReactor != null) bookmarkReactor.cancel(); + public void onSubscribe(final Subscription s) { + if (bookmarkReactor != null) { + bookmarkReactor.cancel(); + } bookmarkReactor = s; bookmarkReactor.request(1); } @Override - public void onNext(List playlist) { + public void onNext(final List playlist) { playlistEntity = playlist.isEmpty() ? null : playlist.get(0); updateBookmarkButtons(); isBookmarkButtonReady.set(true); - if (bookmarkReactor != null) bookmarkReactor.request(1); + if (bookmarkReactor != null) { + bookmarkReactor.request(1); + } } @Override - public void onError(Throwable t) { + public void onError(final Throwable t) { PlaylistFragment.this.onError(t); } @Override - public void onComplete() { - - } + public void onComplete() { } }; } @Override - public void setTitle(String title) { + public void setTitle(final String title) { super.setTitle(title); headerTitleView.setText(title); } private void onBookmarkClicked() { - if (isBookmarkButtonReady == null || !isBookmarkButtonReady.get() || - remotePlaylistManager == null) + if (isBookmarkButtonReady == null || !isBookmarkButtonReady.get() + || remotePlaylistManager == null) { return; + } final Disposable action; if (currentInfo != null && playlistEntity == null) { action = remotePlaylistManager.onBookmark(currentInfo) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(ignored -> {/* Do nothing */}, this::onError); + .subscribe(ignored -> { /* Do nothing */ }, this::onError); } else if (playlistEntity != null) { action = remotePlaylistManager.deletePlaylist(playlistEntity.getUid()) .observeOn(AndroidSchedulers.mainThread()) .doFinally(() -> playlistEntity = null) - .subscribe(ignored -> {/* Do nothing */}, this::onError); + .subscribe(ignored -> { /* Do nothing */ }, this::onError); } else { action = Disposables.empty(); } @@ -435,13 +466,15 @@ public class PlaylistFragment extends BaseListInfoFragment { } private void updateBookmarkButtons() { - if (playlistBookmarkButton == null || activity == null) return; + if (playlistBookmarkButton == null || activity == null) { + return; + } - final int iconAttr = playlistEntity == null ? - R.attr.ic_playlist_add : R.attr.ic_playlist_check; + final int iconAttr = playlistEntity == null + ? R.attr.ic_playlist_add : R.attr.ic_playlist_check; - final int titleRes = playlistEntity == null ? - R.string.bookmark_playlist : R.string.unbookmark_playlist; + final int titleRes = playlistEntity == null + ? R.string.bookmark_playlist : R.string.unbookmark_playlist; playlistBookmarkButton.setIcon(ThemeHelper.resolveResourceIdFromAttr(activity, iconAttr)); playlistBookmarkButton.setTitle(titleRes); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java index 5fd470745..33ab001c7 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java @@ -6,14 +6,8 @@ import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.preference.PreferenceManager; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AlertDialog; -import androidx.recyclerview.widget.RecyclerView; -import androidx.appcompat.widget.TooltipCompat; -import androidx.recyclerview.widget.ItemTouchHelper; import android.text.Editable; +import android.text.Html; import android.text.TextUtils; import android.text.TextWatcher; import android.util.Log; @@ -30,31 +24,37 @@ import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.TooltipCompat; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.RecyclerView; + import org.schabi.newpipe.R; import org.schabi.newpipe.ReCaptchaActivity; import org.schabi.newpipe.database.history.model.SearchHistoryEntry; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.Page; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.search.SearchExtractor; import org.schabi.newpipe.extractor.search.SearchInfo; -import org.schabi.newpipe.util.FireTvUtils; import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.list.BaseListFragment; import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; +import org.schabi.newpipe.util.AndroidTvUtils; import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.ServiceHelper; -import java.io.IOException; -import java.io.InterruptedIOException; -import java.net.SocketException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -73,14 +73,13 @@ import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; import io.reactivex.subjects.PublishSubject; +import static android.text.Html.escapeHtml; import static androidx.recyclerview.widget.ItemTouchHelper.Callback.makeMovementFlags; import static java.util.Arrays.asList; import static org.schabi.newpipe.util.AnimationUtils.animateView; -public class SearchFragment - extends BaseListFragment +public class SearchFragment extends BaseListFragment implements BackPressable { - /*////////////////////////////////////////////////////////////////////////// // Search //////////////////////////////////////////////////////////////////////////*/ @@ -92,44 +91,51 @@ public class SearchFragment private static final int THRESHOLD_NETWORK_SUGGESTION = 1; /** - * How much time have to pass without emitting a item (i.e. the user stop typing) to fetch/show the suggestions, in milliseconds. + * How much time have to pass without emitting a item (i.e. the user stop typing) + * to fetch/show the suggestions, in milliseconds. */ private static final int SUGGESTIONS_DEBOUNCE = 120; //ms + private final PublishSubject suggestionPublisher = PublishSubject.create(); @State - protected int filterItemCheckedId = -1; + int filterItemCheckedId = -1; @State protected int serviceId = Constants.NO_SERVICE_ID; - - // this three represet the current search query + + // these three represents the current search query @State - protected String searchString; + String searchString; /** - * No content filter should add like contentfilter = all + * No content filter should add like contentFilter = all * be aware of this when implementing an extractor. */ @State - protected String[] contentFilter = new String[0]; + String[] contentFilter = new String[0]; + @State - protected String sortFilter; - - // these represtent the last search + String sortFilter; + + // these represents the last search @State - protected String lastSearchedString; - + String lastSearchedString; + @State - protected boolean wasSearchFocused = false; + String searchSuggestion; + + @State + boolean isCorrectedSearch; + + @State + boolean wasSearchFocused = false; private Map menuItemToFilterName; private StreamingService service; - private String currentPageUrl; - private String nextPageUrl; + private Page nextPage; private String contentCountry; private boolean isSuggestionsEnabled = true; - private final PublishSubject suggestionPublisher = PublishSubject.create(); private Disposable searchDisposable; private Disposable suggestionDisposable; private final CompositeDisposable disposables = new CompositeDisposable(); @@ -145,12 +151,16 @@ public class SearchFragment private EditText searchEditText; private View searchClear; + private TextView correctSuggestion; + private View suggestionsPanel; private RecyclerView suggestionsRecyclerView; /*////////////////////////////////////////////////////////////////////////*/ - public static SearchFragment getInstance(int serviceId, String searchString) { + private TextWatcher textWatcher; + + public static SearchFragment getInstance(final int serviceId, final String searchString) { SearchFragment searchFragment = new SearchFragment(); searchFragment.setQuery(serviceId, searchString, new String[0], ""); @@ -173,33 +183,37 @@ public class SearchFragment //////////////////////////////////////////////////////////////////////////*/ @Override - public void onAttach(Context context) { + public void onAttach(final Context context) { super.onAttach(context); suggestionListAdapter = new SuggestionListAdapter(activity); SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity); - boolean isSearchHistoryEnabled = preferences.getBoolean(getString(R.string.enable_search_history_key), true); + boolean isSearchHistoryEnabled = preferences + .getBoolean(getString(R.string.enable_search_history_key), true); suggestionListAdapter.setShowSuggestionHistory(isSearchHistoryEnabled); historyRecordManager = new HistoryRecordManager(context); } @Override - public void onCreate(Bundle savedInstanceState) { + public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity); - isSuggestionsEnabled = preferences.getBoolean(getString(R.string.show_search_suggestions_key), true); - contentCountry = preferences.getString(getString(R.string.content_country_key), getString(R.string.default_country_value)); + isSuggestionsEnabled = preferences + .getBoolean(getString(R.string.show_search_suggestions_key), true); + contentCountry = preferences.getString(getString(R.string.content_country_key), + getString(R.string.default_localization_key)); } @Override - public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container, + @Nullable final Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_search, container, false); } @Override - public void onViewCreated(View rootView, Bundle savedInstanceState) { + public void onViewCreated(final View rootView, final Bundle savedInstanceState) { super.onViewCreated(rootView, savedInstanceState); showSearchOnStart(); initSearchListeners(); @@ -211,15 +225,23 @@ public class SearchFragment wasSearchFocused = searchEditText.hasFocus(); - if (searchDisposable != null) searchDisposable.dispose(); - if (suggestionDisposable != null) suggestionDisposable.dispose(); - if (disposables != null) disposables.clear(); + if (searchDisposable != null) { + searchDisposable.dispose(); + } + if (suggestionDisposable != null) { + suggestionDisposable.dispose(); + } + if (disposables != null) { + disposables.clear(); + } hideKeyboardSearch(); } @Override public void onResume() { - if (DEBUG) Log.d(TAG, "onResume() called"); + if (DEBUG) { + Log.d(TAG, "onResume() called"); + } super.onResume(); try { @@ -245,7 +267,11 @@ public class SearchFragment } } - if (suggestionDisposable == null || suggestionDisposable.isDisposed()) initSuggestionObserver(); + handleSearchSuggestion(); + + if (suggestionDisposable == null || suggestionDisposable.isDisposed()) { + initSuggestionObserver(); + } if (TextUtils.isEmpty(searchString) || wasSearchFocused) { showKeyboardSearch(); @@ -259,7 +285,9 @@ public class SearchFragment @Override public void onDestroyView() { - if (DEBUG) Log.d(TAG, "onDestroyView() called"); + if (DEBUG) { + Log.d(TAG, "onDestroyView() called"); + } unsetSearchListeners(); super.onDestroyView(); } @@ -267,19 +295,27 @@ public class SearchFragment @Override public void onDestroy() { super.onDestroy(); - if (searchDisposable != null) searchDisposable.dispose(); - if (suggestionDisposable != null) suggestionDisposable.dispose(); - if (disposables != null) disposables.clear(); + if (searchDisposable != null) { + searchDisposable.dispose(); + } + if (suggestionDisposable != null) { + suggestionDisposable.dispose(); + } + if (disposables != null) { + disposables.clear(); + } } @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { + public void onActivityResult(final int requestCode, final int resultCode, final Intent data) { switch (requestCode) { case ReCaptchaActivity.RECAPTCHA_REQUEST: if (resultCode == Activity.RESULT_OK && !TextUtils.isEmpty(searchString)) { search(searchString, contentFilter, sortFilter); - } else Log.e(TAG, "ReCaptcha failed"); + } else { + Log.e(TAG, "ReCaptcha failed"); + } break; default: @@ -293,25 +329,27 @@ public class SearchFragment //////////////////////////////////////////////////////////////////////////*/ @Override - protected void initViews(View rootView, Bundle savedInstanceState) { + protected void initViews(final View rootView, final Bundle savedInstanceState) { super.initViews(rootView, savedInstanceState); suggestionsPanel = rootView.findViewById(R.id.suggestions_panel); suggestionsRecyclerView = rootView.findViewById(R.id.suggestions_list); suggestionsRecyclerView.setAdapter(suggestionListAdapter); new ItemTouchHelper(new ItemTouchHelper.Callback() { @Override - public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { + public int getMovementFlags(@NonNull final RecyclerView recyclerView, + @NonNull final RecyclerView.ViewHolder viewHolder) { return getSuggestionMovementFlags(recyclerView, viewHolder); } @Override - public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, - @NonNull RecyclerView.ViewHolder viewHolder1) { + public boolean onMove(@NonNull final RecyclerView recyclerView, + @NonNull final RecyclerView.ViewHolder viewHolder, + @NonNull final RecyclerView.ViewHolder viewHolder1) { return false; } @Override - public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int i) { + public void onSwiped(@NonNull final RecyclerView.ViewHolder viewHolder, final int i) { onSuggestionItemSwiped(viewHolder, i); } }).attachToRecyclerView(suggestionsRecyclerView); @@ -319,6 +357,8 @@ public class SearchFragment searchToolbarContainer = activity.findViewById(R.id.toolbar_search_container); searchEditText = searchToolbarContainer.findViewById(R.id.toolbar_search_edit_text); searchClear = searchToolbarContainer.findViewById(R.id.toolbar_search_clear); + + correctSuggestion = rootView.findViewById(R.id.correct_suggestion); } /*////////////////////////////////////////////////////////////////////////// @@ -326,21 +366,19 @@ public class SearchFragment //////////////////////////////////////////////////////////////////////////*/ @Override - public void writeTo(Queue objectsToSave) { + public void writeTo(final Queue objectsToSave) { super.writeTo(objectsToSave); - objectsToSave.add(currentPageUrl); - objectsToSave.add(nextPageUrl); + objectsToSave.add(nextPage); } @Override - public void readFrom(@NonNull Queue savedObjects) throws Exception { + public void readFrom(@NonNull final Queue savedObjects) throws Exception { super.readFrom(savedObjects); - currentPageUrl = (String) savedObjects.poll(); - nextPageUrl = (String) savedObjects.poll(); + nextPage = (Page) savedObjects.poll(); } @Override - public void onSaveInstanceState(Bundle bundle) { + public void onSaveInstanceState(final Bundle bundle) { searchString = searchEditText != null ? searchEditText.getText().toString() : searchString; @@ -372,7 +410,7 @@ public class SearchFragment //////////////////////////////////////////////////////////////////////////*/ @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); ActionBar supportActionBar = activity.getSupportActionBar(); @@ -386,13 +424,20 @@ public class SearchFragment int itemId = 0; boolean isFirstItem = true; final Context c = getContext(); - for(String filter : service.getSearchQHFactory().getAvailableContentFilter()) { + for (String filter : service.getSearchQHFactory().getAvailableContentFilter()) { + if (filter.equals("music_songs")) { + MenuItem musicItem = menu.add(2, + itemId++, + 0, + "YouTube Music"); + musicItem.setEnabled(false); + } menuItemToFilterName.put(itemId, filter); MenuItem item = menu.add(1, itemId++, 0, ServiceHelper.getTranslatedFilterString(filter, c)); - if(isFirstItem) { + if (isFirstItem) { item.setChecked(true); isFirstItem = false; } @@ -403,19 +448,20 @@ public class SearchFragment } @Override - public boolean onOptionsItemSelected(MenuItem item) { - - List contentFilter = new ArrayList<>(1); - contentFilter.add(menuItemToFilterName.get(item.getItemId())); - changeContentFilter(item, contentFilter); + public boolean onOptionsItemSelected(final MenuItem item) { + List cf = new ArrayList<>(1); + cf.add(menuItemToFilterName.get(item.getItemId())); + changeContentFilter(item, cf); return true; } - private void restoreFilterChecked(Menu menu, int itemId) { + private void restoreFilterChecked(final Menu menu, final int itemId) { if (itemId != -1) { MenuItem item = menu.findItem(itemId); - if (item == null) return; + if (item == null) { + return; + } item.setChecked(true); } @@ -425,13 +471,13 @@ public class SearchFragment // Search //////////////////////////////////////////////////////////////////////////*/ - private TextWatcher textWatcher; - private void showSearchOnStart() { - if (DEBUG) Log.d(TAG, "showSearchOnStart() called, searchQuery → " - + searchString - + ", lastSearchedQuery → " - + lastSearchedString); + if (DEBUG) { + Log.d(TAG, "showSearchOnStart() called, searchQuery → " + + searchString + + ", lastSearchedQuery → " + + lastSearchedString); + } searchEditText.setText(searchString); if (TextUtils.isEmpty(searchString) || TextUtils.isEmpty(searchEditText.getText())) { @@ -451,14 +497,20 @@ public class SearchFragment } private void initSearchListeners() { - if (DEBUG) Log.d(TAG, "initSearchListeners() called"); + if (DEBUG) { + Log.d(TAG, "initSearchListeners() called"); + } searchClear.setOnClickListener(v -> { - if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]"); + if (DEBUG) { + Log.d(TAG, "onClick() called with: v = [" + v + "]"); + } if (TextUtils.isEmpty(searchEditText.getText())) { NavigationHelper.gotoMainFragment(getFragmentManager()); return; } + correctSuggestion.setVisibility(View.GONE); + searchEditText.setText(""); suggestionListAdapter.setItems(new ArrayList<>()); showKeyboardSearch(); @@ -467,53 +519,65 @@ public class SearchFragment TooltipCompat.setTooltipText(searchClear, getString(R.string.clear)); searchEditText.setOnClickListener(v -> { - if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]"); + if (DEBUG) { + Log.d(TAG, "onClick() called with: v = [" + v + "]"); + } if (isSuggestionsEnabled && errorPanelRoot.getVisibility() != View.VISIBLE) { showSuggestionsPanel(); } - if(FireTvUtils.isFireTv()){ + if (AndroidTvUtils.isTv(getContext())) { showKeyboardSearch(); } }); searchEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> { - if (DEBUG) Log.d(TAG, "onFocusChange() called with: v = [" + v + "], hasFocus = [" + hasFocus + "]"); - if (isSuggestionsEnabled && hasFocus && errorPanelRoot.getVisibility() != View.VISIBLE) { + if (DEBUG) { + Log.d(TAG, "onFocusChange() called with: " + + "v = [" + v + "], hasFocus = [" + hasFocus + "]"); + } + if (isSuggestionsEnabled && hasFocus + && errorPanelRoot.getVisibility() != View.VISIBLE) { showSuggestionsPanel(); } }); suggestionListAdapter.setListener(new SuggestionListAdapter.OnSuggestionItemSelected() { @Override - public void onSuggestionItemSelected(SuggestionItem item) { + public void onSuggestionItemSelected(final SuggestionItem item) { search(item.query, new String[0], ""); searchEditText.setText(item.query); } @Override - public void onSuggestionItemInserted(SuggestionItem item) { + public void onSuggestionItemInserted(final SuggestionItem item) { searchEditText.setText(item.query); searchEditText.setSelection(searchEditText.getText().length()); } @Override - public void onSuggestionItemLongClick(SuggestionItem item) { - if (item.fromHistory) showDeleteSuggestionDialog(item); + public void onSuggestionItemLongClick(final SuggestionItem item) { + if (item.fromHistory) { + showDeleteSuggestionDialog(item); + } } }); - if (textWatcher != null) searchEditText.removeTextChangedListener(textWatcher); + if (textWatcher != null) { + searchEditText.removeTextChangedListener(textWatcher); + } textWatcher = new TextWatcher() { @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { + public void beforeTextChanged(final CharSequence s, final int start, + final int count, final int after) { } @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { + public void onTextChanged(final CharSequence s, final int start, + final int before, final int count) { } @Override - public void afterTextChanged(Editable s) { + public void afterTextChanged(final Editable s) { String newText = searchEditText.getText().toString(); suggestionPublisher.onNext(newText); } @@ -522,48 +586,62 @@ public class SearchFragment searchEditText.setOnEditorActionListener( (TextView v, int actionId, KeyEvent event) -> { if (DEBUG) { - Log.d(TAG, "onEditorAction() called with: v = [" + v + "], actionId = [" + actionId + "], event = [" + event + "]"); + Log.d(TAG, "onEditorAction() called with: v = [" + v + "], " + + "actionId = [" + actionId + "], event = [" + event + "]"); } - if(actionId == EditorInfo.IME_ACTION_PREVIOUS){ + if (actionId == EditorInfo.IME_ACTION_PREVIOUS) { hideKeyboardSearch(); } else if (event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER - || event.getAction() == EditorInfo.IME_ACTION_SEARCH)) { + || event.getAction() == EditorInfo.IME_ACTION_SEARCH)) { search(searchEditText.getText().toString(), new String[0], ""); return true; } return false; }); - if (suggestionDisposable == null || suggestionDisposable.isDisposed()) + if (suggestionDisposable == null || suggestionDisposable.isDisposed()) { initSuggestionObserver(); + } } private void unsetSearchListeners() { - if (DEBUG) Log.d(TAG, "unsetSearchListeners() called"); + if (DEBUG) { + Log.d(TAG, "unsetSearchListeners() called"); + } searchClear.setOnClickListener(null); searchClear.setOnLongClickListener(null); searchEditText.setOnClickListener(null); searchEditText.setOnFocusChangeListener(null); searchEditText.setOnEditorActionListener(null); - if (textWatcher != null) searchEditText.removeTextChangedListener(textWatcher); + if (textWatcher != null) { + searchEditText.removeTextChangedListener(textWatcher); + } textWatcher = null; } private void showSuggestionsPanel() { - if (DEBUG) Log.d(TAG, "showSuggestionsPanel() called"); + if (DEBUG) { + Log.d(TAG, "showSuggestionsPanel() called"); + } animateView(suggestionsPanel, AnimationUtils.Type.LIGHT_SLIDE_AND_ALPHA, true, 200); } private void hideSuggestionsPanel() { - if (DEBUG) Log.d(TAG, "hideSuggestionsPanel() called"); + if (DEBUG) { + Log.d(TAG, "hideSuggestionsPanel() called"); + } animateView(suggestionsPanel, AnimationUtils.Type.LIGHT_SLIDE_AND_ALPHA, false, 200); } private void showKeyboardSearch() { - if (DEBUG) Log.d(TAG, "showKeyboardSearch() called"); - if (searchEditText == null) return; + if (DEBUG) { + Log.d(TAG, "showKeyboardSearch() called"); + } + if (searchEditText == null) { + return; + } if (searchEditText.requestFocus()) { InputMethodManager imm = (InputMethodManager) activity.getSystemService( @@ -573,19 +651,26 @@ public class SearchFragment } private void hideKeyboardSearch() { - if (DEBUG) Log.d(TAG, "hideKeyboardSearch() called"); - if (searchEditText == null) return; + if (DEBUG) { + Log.d(TAG, "hideKeyboardSearch() called"); + } + if (searchEditText == null) { + return; + } - InputMethodManager imm = (InputMethodManager) activity.getSystemService( - Context.INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(searchEditText.getWindowToken(), InputMethodManager.RESULT_UNCHANGED_SHOWN); + InputMethodManager imm = (InputMethodManager) activity + .getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(searchEditText.getWindowToken(), + InputMethodManager.RESULT_UNCHANGED_SHOWN); searchEditText.clearFocus(); } private void showDeleteSuggestionDialog(final SuggestionItem item) { - if (activity == null || historyRecordManager == null || suggestionPublisher == null || - searchEditText == null || disposables == null) return; + if (activity == null || historyRecordManager == null || suggestionPublisher == null + || searchEditText == null || disposables == null) { + return; + } final String query = item.query; new AlertDialog.Builder(activity) .setTitle(query) @@ -619,20 +704,20 @@ public class SearchFragment return false; } - public void giveSearchEditTextFocus() { - showKeyboardSearch(); - } - private void initSuggestionObserver() { - if (DEBUG) Log.d(TAG, "initSuggestionObserver() called"); - if (suggestionDisposable != null) suggestionDisposable.dispose(); + if (DEBUG) { + Log.d(TAG, "initSuggestionObserver() called"); + } + if (suggestionDisposable != null) { + suggestionDisposable.dispose(); + } final Observable observable = suggestionPublisher .debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS) .startWith(searchString != null ? searchString : "") - .filter(searchString -> isSuggestionsEnabled); + .filter(ss -> isSuggestionsEnabled); suggestionDisposable = observable .switchMap(query -> { @@ -641,13 +726,15 @@ public class SearchFragment final Observable> local = flowable.toObservable() .map(searchHistoryEntries -> { List result = new ArrayList<>(); - for (SearchHistoryEntry entry : searchHistoryEntries) + for (SearchHistoryEntry entry : searchHistoryEntries) { result.add(new SuggestionItem(true, entry.getSearch())); + } return result; }); if (query.length() < THRESHOLD_NETWORK_SUGGESTION) { - // Only pass through if the query length is equal or greater than THRESHOLD_NETWORK_SUGGESTION + // Only pass through if the query length + // is equal or greater than THRESHOLD_NETWORK_SUGGESTION return local.materialize(); } @@ -664,7 +751,9 @@ public class SearchFragment return Observable.zip(local, network, (localResult, networkResult) -> { List result = new ArrayList<>(); - if (localResult.size() > 0) result.addAll(localResult); + if (localResult.size() > 0) { + result.addAll(localResult); + } // Remove duplicates final Iterator iterator = networkResult.iterator(); @@ -678,7 +767,9 @@ public class SearchFragment } } - if (networkResult.size() > 0) result.addAll(networkResult); + if (networkResult.size() > 0) { + result.addAll(networkResult); + } return result; }).materialize(); }) @@ -688,12 +779,7 @@ public class SearchFragment if (listNotification.isOnNext()) { handleSuggestions(listNotification.getValue()); } else if (listNotification.isOnError()) { - Throwable error = listNotification.getError(); - if (!ExtractorHelper.hasAssignableCauseThrowable(error, - IOException.class, SocketException.class, - InterruptedException.class, InterruptedIOException.class)) { - onSuggestionError(error); - } + onSuggestionError(listNotification.getError()); } }); } @@ -703,17 +789,21 @@ public class SearchFragment // no-op } - private void search(final String searchString, String[] contentFilter, String sortFilter) { - if (DEBUG) Log.d(TAG, "search() called with: query = [" + searchString + "]"); - if (searchString.isEmpty()) return; + private void search(final String ss, final String[] cf, final String sf) { + if (DEBUG) { + Log.d(TAG, "search() called with: query = [" + ss + "]"); + } + if (ss.isEmpty()) { + return; + } try { - final StreamingService service = NewPipe.getServiceByUrl(searchString); - if (service != null) { + final StreamingService streamingService = NewPipe.getServiceByUrl(ss); + if (streamingService != null) { showLoading(); disposables.add(Observable .fromCallable(() -> - NavigationHelper.getIntentByLink(activity, service, searchString)) + NavigationHelper.getIntentByLink(activity, streamingService, ss)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(intent -> { @@ -723,36 +813,41 @@ public class SearchFragment showError(getString(R.string.url_not_supported_toast), false))); return; } - } catch (Exception e) { + } catch (Exception ignored) { // Exception occurred, it's not a url } lastSearchedString = this.searchString; - this.searchString = searchString; + this.searchString = ss; infoListAdapter.clearStreamItemList(); hideSuggestionsPanel(); hideKeyboardSearch(); - historyRecordManager.onSearched(serviceId, searchString) + historyRecordManager.onSearched(serviceId, ss) .observeOn(AndroidSchedulers.mainThread()) .subscribe( - ignored -> {}, + ignored -> { + }, error -> showSnackBarError(error, UserAction.SEARCHED, - NewPipe.getNameOfService(serviceId), searchString, 0) + NewPipe.getNameOfService(serviceId), ss, 0) ); - suggestionPublisher.onNext(searchString); + suggestionPublisher.onNext(ss); startLoading(false); } @Override - public void startLoading(boolean forceLoad) { + public void startLoading(final boolean forceLoad) { super.startLoading(forceLoad); - if (disposables != null) disposables.clear(); - if (searchDisposable != null) searchDisposable.dispose(); + if (disposables != null) { + disposables.clear(); + } + if (searchDisposable != null) { + searchDisposable.dispose(); + } searchDisposable = ExtractorHelper.searchFor(serviceId, - searchString, - Arrays.asList(contentFilter), - sortFilter) + searchString, + Arrays.asList(contentFilter), + sortFilter) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnEvent((searchResult, throwable) -> isLoading.set(false)) @@ -762,16 +857,20 @@ public class SearchFragment @Override protected void loadMoreItems() { - if(nextPageUrl == null || nextPageUrl.isEmpty()) return; + if (!Page.isValid(nextPage)) { + return; + } isLoading.set(true); showListFooter(true); - if (searchDisposable != null) searchDisposable.dispose(); + if (searchDisposable != null) { + searchDisposable.dispose(); + } searchDisposable = ExtractorHelper.getMoreSearchItems( - serviceId, - searchString, - asList(contentFilter), - sortFilter, - nextPageUrl) + serviceId, + searchString, + asList(contentFilter), + sortFilter, + nextPage) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnEvent((nextItemsResult, throwable) -> isLoading.set(false)) @@ -785,7 +884,7 @@ public class SearchFragment } @Override - protected void onItemSelected(InfoItem selectedItem) { + protected void onItemSelected(final InfoItem selectedItem) { super.onItemSelected(selectedItem); hideKeyboardSearch(); } @@ -794,22 +893,22 @@ public class SearchFragment // Utils //////////////////////////////////////////////////////////////////////////*/ - private void changeContentFilter(MenuItem item, List contentFilter) { + private void changeContentFilter(final MenuItem item, final List cf) { this.filterItemCheckedId = item.getItemId(); item.setChecked(true); - this.contentFilter = new String[] {contentFilter.get(0)}; + this.contentFilter = new String[]{cf.get(0)}; if (!TextUtils.isEmpty(searchString)) { search(searchString, this.contentFilter, sortFilter); } } - private void setQuery(int serviceId, String searchString, String[] contentfilter, String sortFilter) { - this.serviceId = serviceId; + private void setQuery(final int sid, final String ss, final String[] cf, final String sf) { + this.serviceId = sid; this.searchString = searchString; - this.contentFilter = contentfilter; - this.sortFilter = sortFilter; + this.contentFilter = cf; + this.sortFilter = sf; } /*////////////////////////////////////////////////////////////////////////// @@ -817,7 +916,9 @@ public class SearchFragment //////////////////////////////////////////////////////////////////////////*/ public void handleSuggestions(@NonNull final List suggestions) { - if (DEBUG) Log.d(TAG, "handleSuggestions() called with: suggestions = [" + suggestions + "]"); + if (DEBUG) { + Log.d(TAG, "handleSuggestions() called with: suggestions = [" + suggestions + "]"); + } suggestionsRecyclerView.smoothScrollToPosition(0); suggestionsRecyclerView.post(() -> suggestionListAdapter.setItems(suggestions)); @@ -826,9 +927,13 @@ public class SearchFragment } } - public void onSuggestionError(Throwable exception) { - if (DEBUG) Log.d(TAG, "onSuggestionError() called with: exception = [" + exception + "]"); - if (super.onError(exception)) return; + public void onSuggestionError(final Throwable exception) { + if (DEBUG) { + Log.d(TAG, "onSuggestionError() called with: exception = [" + exception + "]"); + } + if (super.onError(exception)) { + return; + } int errorId = exception instanceof ParsingException ? R.string.parsing_error @@ -848,7 +953,7 @@ public class SearchFragment } @Override - public void showError(String message, boolean showRetryButton) { + public void showError(final String message, final boolean showRetryButton) { super.showError(message, showRetryButton); hideSuggestionsPanel(); hideKeyboardSearch(); @@ -859,18 +964,22 @@ public class SearchFragment //////////////////////////////////////////////////////////////////////////*/ @Override - public void handleResult(@NonNull SearchInfo result) { + public void handleResult(@NonNull final SearchInfo result) { final List exceptions = result.getErrors(); if (!exceptions.isEmpty() - && !(exceptions.size() == 1 - && exceptions.get(0) instanceof SearchExtractor.NothingFoundException)){ + && !(exceptions.size() == 1 + && exceptions.get(0) instanceof SearchExtractor.NothingFoundException)) { showSnackBarError(result.getErrors(), UserAction.SEARCHED, NewPipe.getNameOfService(serviceId), searchString, 0); } + searchSuggestion = result.getSearchSuggestion(); + isCorrectedSearch = result.isCorrectedSearch(); + + handleSearchSuggestion(); + lastSearchedString = searchString; - nextPageUrl = result.getNextPageUrl(); - currentPageUrl = result.getUrl(); + nextPage = result.getNextPage(); if (infoListAdapter.getItemsList().size() == 0) { if (!result.getRelatedItems().isEmpty()) { @@ -885,24 +994,58 @@ public class SearchFragment super.handleResult(result); } + private void handleSearchSuggestion() { + if (TextUtils.isEmpty(searchSuggestion)) { + correctSuggestion.setVisibility(View.GONE); + } else { + final String helperText = getString(isCorrectedSearch + ? R.string.search_showing_result_for + : R.string.did_you_mean); + + final String highlightedSearchSuggestion = + "" + escapeHtml(searchSuggestion) + ""; + correctSuggestion.setText( + Html.fromHtml(String.format(helperText, highlightedSearchSuggestion))); + + + correctSuggestion.setOnClickListener(v -> { + correctSuggestion.setVisibility(View.GONE); + search(searchSuggestion, contentFilter, sortFilter); + searchEditText.setText(searchSuggestion); + }); + + correctSuggestion.setOnLongClickListener(v -> { + searchEditText.setText(searchSuggestion); + searchEditText.setSelection(searchSuggestion.length()); + showKeyboardSearch(); + return true; + }); + + correctSuggestion.setVisibility(View.VISIBLE); + } + } + @Override - public void handleNextItems(ListExtractor.InfoItemsPage result) { + public void handleNextItems(final ListExtractor.InfoItemsPage result) { showListFooter(false); - currentPageUrl = result.getNextPageUrl(); infoListAdapter.addInfoItemList(result.getItems()); - nextPageUrl = result.getNextPageUrl(); + nextPage = result.getNextPage(); if (!result.getErrors().isEmpty()) { showSnackBarError(result.getErrors(), UserAction.SEARCHED, - NewPipe.getNameOfService(serviceId) - , "\"" + searchString + "\" → page: " + nextPageUrl, 0); + NewPipe.getNameOfService(serviceId), + "\"" + searchString + "\" → pageUrl: " + nextPage.getUrl() + ", " + + "pageIds: " + nextPage.getIds() + ", " + + "pageCookies: " + nextPage.getCookies(), 0); } super.handleNextItems(result); } @Override - protected boolean onError(Throwable exception) { - if (super.onError(exception)) return true; + protected boolean onError(final Throwable exception) { + if (super.onError(exception)) { + return true; + } if (exception instanceof SearchExtractor.NothingFoundException) { infoListAdapter.clearStreamItemList(); @@ -922,13 +1065,20 @@ public class SearchFragment // Suggestion item touch helper //////////////////////////////////////////////////////////////////////////*/ - public int getSuggestionMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { + public int getSuggestionMovementFlags(@NonNull final RecyclerView recyclerView, + @NonNull final RecyclerView.ViewHolder viewHolder) { final int position = viewHolder.getAdapterPosition(); + if (position == RecyclerView.NO_POSITION) { + return 0; + } + final SuggestionItem item = suggestionListAdapter.getItem(position); - return item.fromHistory ? makeMovementFlags(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) : 0; + return item.fromHistory ? makeMovementFlags(0, + ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) : 0; } - public void onSuggestionItemSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int i) { + public void onSuggestionItemSwiped(@NonNull final RecyclerView.ViewHolder viewHolder, + final int i) { final int position = viewHolder.getAdapterPosition(); final String query = suggestionListAdapter.getItem(position).query; final Disposable onDelete = historyRecordManager.deleteSearchHistory(query) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionItem.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionItem.java index 722638926..5aa927ed3 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionItem.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionItem.java @@ -1,10 +1,10 @@ package org.schabi.newpipe.fragments.list.search; public class SuggestionItem { - public final boolean fromHistory; + final boolean fromHistory; public final String query; - public SuggestionItem(boolean fromHistory, String query) { + public SuggestionItem(final boolean fromHistory, final String query) { this.fromHistory = fromHistory; this.query = query; } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java index d46f4bb31..608ea77d2 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java @@ -2,36 +2,32 @@ package org.schabi.newpipe.fragments.list.search; import android.content.Context; import android.content.res.TypedArray; -import androidx.annotation.AttrRes; -import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.AttrRes; +import androidx.recyclerview.widget.RecyclerView; + import org.schabi.newpipe.R; import java.util.ArrayList; import java.util.List; -public class SuggestionListAdapter extends RecyclerView.Adapter { +public class SuggestionListAdapter + extends RecyclerView.Adapter { private final ArrayList items = new ArrayList<>(); private final Context context; private OnSuggestionItemSelected listener; private boolean showSuggestionHistory = true; - public interface OnSuggestionItemSelected { - void onSuggestionItemSelected(SuggestionItem item); - void onSuggestionItemInserted(SuggestionItem item); - void onSuggestionItemLongClick(SuggestionItem item); - } - - public SuggestionListAdapter(Context context) { + public SuggestionListAdapter(final Context context) { this.context = context; } - public void setItems(List items) { + public void setItems(final List items) { this.items.clear(); if (showSuggestionHistory) { this.items.addAll(items); @@ -46,36 +42,43 @@ public class SuggestionListAdapter extends RecyclerView.Adapter { - if (listener != null) listener.onSuggestionItemSelected(currentItem); + if (listener != null) { + listener.onSuggestionItemSelected(currentItem); + } }); holder.queryView.setOnLongClickListener(v -> { - if (listener != null) listener.onSuggestionItemLongClick(currentItem); - return true; + if (listener != null) { + listener.onSuggestionItemLongClick(currentItem); + } + return true; }); holder.insertView.setOnClickListener(v -> { - if (listener != null) listener.onSuggestionItemInserted(currentItem); + if (listener != null) { + listener.onSuggestionItemInserted(currentItem); + } }); } - SuggestionItem getItem(int position) { + SuggestionItem getItem(final int position) { return items.get(position); } @@ -88,7 +91,15 @@ public class SuggestionListAdapter extends RecyclerView.Adapter implements SharedPreferences.OnSharedPreferenceChangeListener{ - +public class RelatedVideosFragment extends BaseListInfoFragment + implements SharedPreferences.OnSharedPreferenceChangeListener { + private static final String INFO_KEY = "related_info_key"; private CompositeDisposable disposables = new CompositeDisposable(); private RelatedStreamInfo relatedStreamInfo; + /*////////////////////////////////////////////////////////////////////////// // Views //////////////////////////////////////////////////////////////////////////*/ + private View headerRootLayout; private Switch aSwitch; private boolean mIsVisibleToUser = false; - public static RelatedVideosFragment getInstance(StreamInfo info) { + public static RelatedVideosFragment getInstance(final StreamInfo info) { RelatedVideosFragment instance = new RelatedVideosFragment(); instance.setInitialData(info); return instance; } + @Override + public void setUserVisibleHint(final boolean isVisibleToUser) { + super.setUserVisibleHint(isVisibleToUser); + mIsVisibleToUser = isVisibleToUser; + } + /*////////////////////////////////////////////////////////////////////////// // LifeCycle //////////////////////////////////////////////////////////////////////////*/ @Override - public void setUserVisibleHint(boolean isVisibleToUser) { - super.setUserVisibleHint(isVisibleToUser); - mIsVisibleToUser = isVisibleToUser; - } - - @Override - public void onAttach(Context context) { + public void onAttach(final Context context) { super.onAttach(context); } @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + public View onCreateView(@NonNull final LayoutInflater inflater, + @Nullable final ViewGroup container, + @Nullable final Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_related_streams, container, false); } @Override public void onDestroy() { super.onDestroy(); - if (disposables != null) disposables.clear(); + if (disposables != null) { + disposables.clear(); + } } - protected View getListHeader(){ - if(relatedStreamInfo != null && relatedStreamInfo.getNextStream() != null){ - headerRootLayout = activity.getLayoutInflater().inflate(R.layout.related_streams_header, itemsList, false); + protected View getListHeader() { + if (relatedStreamInfo != null && relatedStreamInfo.getNextStream() != null) { + headerRootLayout = activity.getLayoutInflater() + .inflate(R.layout.related_streams_header, itemsList, false); aSwitch = headerRootLayout.findViewById(R.id.autoplay_switch); SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getContext()); @@ -82,14 +91,14 @@ public class RelatedVideosFragment extends BaseListInfoFragment ListExtractor.InfoItemsPage.emptyPage()); } - @Override - protected Single loadResult(boolean forceLoad) { - return Single.fromCallable(() -> relatedStreamInfo); - } - /*////////////////////////////////////////////////////////////////////////// // Contract //////////////////////////////////////////////////////////////////////////*/ + @Override + protected Single loadResult(final boolean forceLoad) { + return Single.fromCallable(() -> relatedStreamInfo); + } + @Override public void showLoading() { super.showLoading(); - if(null != headerRootLayout) headerRootLayout.setVisibility(View.INVISIBLE); + if (headerRootLayout != null) { + headerRootLayout.setVisibility(View.INVISIBLE); + } } @Override - public void handleResult(@NonNull RelatedStreamInfo result) { - + public void handleResult(@NonNull final RelatedStreamInfo result) { super.handleResult(result); - if(null != headerRootLayout) headerRootLayout.setVisibility(View.VISIBLE); - AnimationUtils.slideUp(getView(),120, 96, 0.06f); + if (headerRootLayout != null) { + headerRootLayout.setVisibility(View.VISIBLE); + } + AnimationUtils.slideUp(getView(), 120, 96, 0.06f); if (!result.getErrors().isEmpty()) { - showSnackBarError(result.getErrors(), UserAction.REQUESTED_STREAM, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0); + showSnackBarError(result.getErrors(), UserAction.REQUESTED_STREAM, + NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0); } - if (disposables != null) disposables.clear(); + if (disposables != null) { + disposables.clear(); + } } @Override - public void handleNextItems(ListExtractor.InfoItemsPage result) { + public void handleNextItems(final ListExtractor.InfoItemsPage result) { super.handleNextItems(result); if (!result.getErrors().isEmpty()) { @@ -147,11 +162,14 @@ public class RelatedVideosFragment extends BaseListInfoFragment * Copyright (C) Christian Schabesberger 2016 * InfoItemBuilder.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 . + *

*/ public class InfoItemBuilder { - private static final String TAG = InfoItemBuilder.class.toString(); - private final Context context; private final ImageLoader imageLoader = ImageLoader.getInstance(); @@ -55,31 +58,39 @@ public class InfoItemBuilder { private OnClickGesture onPlaylistSelectedListener; private OnClickGesture onCommentsSelectedListener; - public InfoItemBuilder(Context context) { + public InfoItemBuilder(final Context context) { this.context = context; } - public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem, final HistoryRecordManager historyRecordManager) { + public View buildView(@NonNull final ViewGroup parent, @NonNull final InfoItem infoItem, + final HistoryRecordManager historyRecordManager) { return buildView(parent, infoItem, historyRecordManager, false); } - public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem, - final HistoryRecordManager historyRecordManager, boolean useMiniVariant) { + public View buildView(@NonNull final ViewGroup parent, @NonNull final InfoItem infoItem, + final HistoryRecordManager historyRecordManager, + final boolean useMiniVariant) { InfoItemHolder holder = holderFromInfoType(parent, infoItem.getInfoType(), useMiniVariant); holder.updateFromItem(infoItem, historyRecordManager); return holder.itemView; } - private InfoItemHolder holderFromInfoType(@NonNull ViewGroup parent, @NonNull InfoItem.InfoType infoType, boolean useMiniVariant) { + private InfoItemHolder holderFromInfoType(@NonNull final ViewGroup parent, + @NonNull final InfoItem.InfoType infoType, + final boolean useMiniVariant) { switch (infoType) { case STREAM: - return useMiniVariant ? new StreamMiniInfoItemHolder(this, parent) : new StreamInfoItemHolder(this, parent); + return useMiniVariant ? new StreamMiniInfoItemHolder(this, parent) + : new StreamInfoItemHolder(this, parent); case CHANNEL: - return useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent) : new ChannelInfoItemHolder(this, parent); + return useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent) + : new ChannelInfoItemHolder(this, parent); case PLAYLIST: - return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent) : new PlaylistInfoItemHolder(this, parent); + return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent) + : new PlaylistInfoItemHolder(this, parent); case COMMENT: - return useMiniVariant ? new CommentsMiniInfoItemHolder(this, parent) : new CommentsInfoItemHolder(this, parent); + return useMiniVariant ? new CommentsMiniInfoItemHolder(this, parent) + : new CommentsInfoItemHolder(this, parent); default: throw new RuntimeException("InfoType not expected = " + infoType.name()); } @@ -97,7 +108,7 @@ public class InfoItemBuilder { return onStreamSelectedListener; } - public void setOnStreamSelectedListener(OnClickGesture listener) { + public void setOnStreamSelectedListener(final OnClickGesture listener) { this.onStreamSelectedListener = listener; } @@ -105,7 +116,7 @@ public class InfoItemBuilder { return onChannelSelectedListener; } - public void setOnChannelSelectedListener(OnClickGesture listener) { + public void setOnChannelSelectedListener(final OnClickGesture listener) { this.onChannelSelectedListener = listener; } @@ -113,7 +124,7 @@ public class InfoItemBuilder { return onPlaylistSelectedListener; } - public void setOnPlaylistSelectedListener(OnClickGesture listener) { + public void setOnPlaylistSelectedListener(final OnClickGesture listener) { this.onPlaylistSelectedListener = listener; } @@ -121,8 +132,8 @@ public class InfoItemBuilder { return onCommentsSelectedListener; } - public void setOnCommentsSelectedListener(OnClickGesture onCommentsSelectedListener) { + public void setOnCommentsSelectedListener( + final OnClickGesture onCommentsSelectedListener) { this.onCommentsSelectedListener = onCommentsSelectedListener; } - } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemDialog.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemDialog.java index a7f961e7d..4ff56306e 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemDialog.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemDialog.java @@ -3,11 +3,12 @@ package org.schabi.newpipe.info_list; import android.app.Activity; import android.app.AlertDialog; import android.content.DialogInterface; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import android.view.View; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.stream.StreamInfoItem; diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java index 594ec81af..eb4b2c2c0 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java @@ -1,13 +1,14 @@ package org.schabi.newpipe.info_list; import android.content.Context; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import android.util.Log; -import android.view.View; -import android.view.ViewGroup; import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.extractor.InfoItem; @@ -83,92 +84,101 @@ public class InfoListAdapter extends RecyclerView.Adapter(); } - public void setOnStreamSelectedListener(OnClickGesture listener) { + public void setOnStreamSelectedListener(final OnClickGesture listener) { infoItemBuilder.setOnStreamSelectedListener(listener); } - public void setOnChannelSelectedListener(OnClickGesture listener) { + public void setOnChannelSelectedListener(final OnClickGesture listener) { infoItemBuilder.setOnChannelSelectedListener(listener); } - public void setOnPlaylistSelectedListener(OnClickGesture listener) { + public void setOnPlaylistSelectedListener(final OnClickGesture listener) { infoItemBuilder.setOnPlaylistSelectedListener(listener); } - public void setOnCommentsSelectedListener(OnClickGesture listener) { + public void setOnCommentsSelectedListener(final OnClickGesture listener) { infoItemBuilder.setOnCommentsSelectedListener(listener); } - public void useMiniItemVariants(boolean useMiniVariant) { + public void setUseMiniVariant(final boolean useMiniVariant) { this.useMiniVariant = useMiniVariant; } - public void setGridItemVariants(boolean useGridVariant) { + public void setUseGridVariant(final boolean useGridVariant) { this.useGridVariant = useGridVariant; } - public void addInfoItemList(@Nullable final List data) { + public void addInfoItemList(@Nullable final List data) { if (data == null) { return; } - if (DEBUG) Log.d(TAG, "addInfoItemList() before > infoItemList.size() = " + - infoItemList.size() + ", data.size() = " + data.size()); + if (DEBUG) { + Log.d(TAG, "addInfoItemList() before > infoItemList.size() = " + + infoItemList.size() + ", data.size() = " + data.size()); + } int offsetStart = sizeConsideringHeaderOffset(); infoItemList.addAll(data); - if (DEBUG) Log.d(TAG, "addInfoItemList() after > offsetStart = " + offsetStart + - ", infoItemList.size() = " + infoItemList.size() + - ", header = " + header + ", footer = " + footer + - ", showFooter = " + showFooter); + if (DEBUG) { + Log.d(TAG, "addInfoItemList() after > offsetStart = " + offsetStart + ", " + + "infoItemList.size() = " + infoItemList.size() + ", " + + "header = " + header + ", footer = " + footer + ", " + + "showFooter = " + showFooter); + } notifyItemRangeInserted(offsetStart, data.size()); if (footer != null && showFooter) { int footerNow = sizeConsideringHeaderOffset(); notifyItemMoved(offsetStart, footerNow); - if (DEBUG) Log.d(TAG, "addInfoItemList() footer from " + offsetStart + - " to " + footerNow); + if (DEBUG) { + Log.d(TAG, "addInfoItemList() footer from " + offsetStart + + " to " + footerNow); + } } } - public void addInfoItem(@Nullable InfoItem data) { + public void setInfoItemList(final List data) { + infoItemList.clear(); + infoItemList.addAll(data); + notifyDataSetChanged(); + } + + public void addInfoItem(@Nullable final InfoItem data) { if (data == null) { return; } - if (DEBUG) Log.d(TAG, "addInfoItem() before > infoItemList.size() = " + - infoItemList.size() + ", thread = " + Thread.currentThread()); + if (DEBUG) { + Log.d(TAG, "addInfoItem() before > infoItemList.size() = " + + infoItemList.size() + ", thread = " + Thread.currentThread()); + } int positionInserted = sizeConsideringHeaderOffset(); infoItemList.add(data); - if (DEBUG) Log.d(TAG, "addInfoItem() after > position = " + positionInserted + - ", infoItemList.size() = " + infoItemList.size() + - ", header = " + header + ", footer = " + footer + - ", showFooter = " + showFooter); + if (DEBUG) { + Log.d(TAG, "addInfoItem() after > position = " + positionInserted + ", " + + "infoItemList.size() = " + infoItemList.size() + ", " + + "header = " + header + ", footer = " + footer + ", " + + "showFooter = " + showFooter); + } notifyItemInserted(positionInserted); if (footer != null && showFooter) { int footerNow = sizeConsideringHeaderOffset(); notifyItemMoved(positionInserted, footerNow); - if (DEBUG) Log.d(TAG, "addInfoItem() footer from " + positionInserted + - " to " + footerNow); + if (DEBUG) { + Log.d(TAG, "addInfoItem() footer from " + positionInserted + + " to " + footerNow); + } } } @@ -180,29 +190,39 @@ public class InfoListAdapter extends RecyclerView.Adapter payloads) { + public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position, + @NonNull final List payloads) { if (!payloads.isEmpty() && holder instanceof InfoItemHolder) { for (Object payload : payloads) { if (payload instanceof StreamStateEntity) { - ((InfoItemHolder) holder).updateState(infoItemList.get(header == null ? position : position - 1), recordManager); + ((InfoItemHolder) holder).updateState(infoItemList + .get(header == null ? position : position - 1), recordManager); } else if (payload instanceof Boolean) { - ((InfoItemHolder) holder).updateState(infoItemList.get(header == null ? position : position - 1), recordManager); + ((InfoItemHolder) holder).updateState(infoItemList + .get(header == null ? position : position - 1), recordManager); } } } else { @@ -319,10 +364,19 @@ public class InfoListAdapter extends RecyclerView.Adapter { @@ -120,36 +123,102 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder { itemBuilder.getOnCommentsSelectedListener().selected(item); } }); + + + itemView.setOnLongClickListener(view -> { + if (AndroidTvUtils.isTv(itemBuilder.getContext())) { + openCommentAuthor(item); + } else { + ShareUtils.copyToClipboard(itemBuilder.getContext(), commentText); + } + return true; + }); + } + + private void openCommentAuthor(final CommentsInfoItem item) { + if (TextUtils.isEmpty(item.getUploaderUrl())) { + return; + } + try { + final AppCompatActivity activity = (AppCompatActivity) itemBuilder.getContext(); + NavigationHelper.openChannelFragment( + activity.getSupportFragmentManager(), + item.getServiceId(), + item.getUploaderUrl(), + item.getUploaderName()); + } catch (Exception e) { + ErrorActivity.reportUiError((AppCompatActivity) itemBuilder.getContext(), e); + } + } + + private void allowLinkFocus() { + itemContentView.setMovementMethod(LinkMovementMethod.getInstance()); + } + + private void denyLinkFocus() { + itemContentView.setMovementMethod(null); + } + + private boolean shouldFocusLinks() { + if (itemView.isInTouchMode()) { + return false; + } + + URLSpan[] urls = itemContentView.getUrls(); + + return urls != null && urls.length != 0; + } + + private void determineLinkFocus() { + if (shouldFocusLinks()) { + allowLinkFocus(); + } else { + denyLinkFocus(); + } } private void ellipsize() { - if (itemContentView.getLineCount() > commentDefaultLines){ - int endOfLastLine = itemContentView.getLayout().getLineEnd(commentDefaultLines - 1); - int end = itemContentView.getText().toString().lastIndexOf(' ', endOfLastLine -2); - if(end == -1) end = Math.max(endOfLastLine -2, 0); + boolean hasEllipsis = false; + + if (itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) { + int endOfLastLine = itemContentView.getLayout().getLineEnd(COMMENT_DEFAULT_LINES - 1); + int end = itemContentView.getText().toString().lastIndexOf(' ', endOfLastLine - 2); + if (end == -1) { + end = Math.max(endOfLastLine - 2, 0); + } String newVal = itemContentView.getText().subSequence(0, end) + " …"; itemContentView.setText(newVal); + hasEllipsis = true; } + linkify(); + + if (hasEllipsis) { + denyLinkFocus(); + } else { + determineLinkFocus(); + } } private void toggleEllipsize() { if (itemContentView.getText().toString().equals(commentText)) { - if (itemContentView.getLineCount() > commentDefaultLines) ellipsize(); + if (itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) { + ellipsize(); + } } else { expand(); } } private void expand() { - itemContentView.setMaxLines(commentExpandedLines); + itemContentView.setMaxLines(COMMENT_EXPANDED_LINES); itemContentView.setText(commentText); linkify(); + determineLinkFocus(); } - private void linkify(){ + private void linkify() { Linkify.addLinks(itemContentView, Linkify.WEB_URLS); - Linkify.addLinks(itemContentView, pattern, null, null, timestampLink); - itemContentView.setMovementMethod(null); + Linkify.addLinks(itemContentView, PATTERN, null, null, timestampLink); } } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/InfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/InfoItemHolder.java index 1b97e2d27..9e1561786 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/InfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/InfoItemHolder.java @@ -1,9 +1,10 @@ package org.schabi.newpipe.info_list.holder; -import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; import android.view.ViewGroup; +import androidx.recyclerview.widget.RecyclerView; + import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.local.history.HistoryRecordManager; @@ -31,13 +32,15 @@ import org.schabi.newpipe.local.history.HistoryRecordManager; public abstract class InfoItemHolder extends RecyclerView.ViewHolder { protected final InfoItemBuilder itemBuilder; - public InfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) { + public InfoItemHolder(final InfoItemBuilder infoItemBuilder, final int layoutId, + final ViewGroup parent) { super(LayoutInflater.from(infoItemBuilder.getContext()).inflate(layoutId, parent, false)); this.itemBuilder = infoItemBuilder; } - public abstract void updateFromItem(final InfoItem infoItem, final HistoryRecordManager historyRecordManager); + public abstract void updateFromItem(InfoItem infoItem, + HistoryRecordManager historyRecordManager); - public void updateState(final InfoItem infoItem, final HistoryRecordManager historyRecordManager) { - } + public void updateState(final InfoItem infoItem, + final HistoryRecordManager historyRecordManager) { } } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistGridInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistGridInfoItemHolder.java index 96b9c90a7..1cb69208b 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistGridInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistGridInfoItemHolder.java @@ -6,8 +6,8 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.info_list.InfoItemBuilder; public class PlaylistGridInfoItemHolder extends PlaylistMiniInfoItemHolder { - - public PlaylistGridInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) { - super(infoItemBuilder, R.layout.list_playlist_grid_item, parent); - } -} \ No newline at end of file + public PlaylistGridInfoItemHolder(final InfoItemBuilder infoItemBuilder, + final ViewGroup parent) { + super(infoItemBuilder, R.layout.list_playlist_grid_item, parent); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistInfoItemHolder.java index 252d05e09..7691a377d 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistInfoItemHolder.java @@ -6,8 +6,7 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.info_list.InfoItemBuilder; public class PlaylistInfoItemHolder extends PlaylistMiniInfoItemHolder { - - public PlaylistInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) { + public PlaylistInfoItemHolder(final InfoItemBuilder infoItemBuilder, final ViewGroup parent) { super(infoItemBuilder, R.layout.list_playlist_item, parent); } } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java index b73f22d93..d4af63062 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java @@ -10,14 +10,16 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.util.ImageDisplayConstants; +import org.schabi.newpipe.util.Localization; public class PlaylistMiniInfoItemHolder extends InfoItemHolder { public final ImageView itemThumbnailView; - public final TextView itemStreamCountView; + private final TextView itemStreamCountView; public final TextView itemTitleView; public final TextView itemUploaderView; - public PlaylistMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) { + public PlaylistMiniInfoItemHolder(final InfoItemBuilder infoItemBuilder, final int layoutId, + final ViewGroup parent) { super(infoItemBuilder, layoutId, parent); itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView); @@ -26,22 +28,27 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder { itemUploaderView = itemView.findViewById(R.id.itemUploaderView); } - public PlaylistMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) { + public PlaylistMiniInfoItemHolder(final InfoItemBuilder infoItemBuilder, + final ViewGroup parent) { this(infoItemBuilder, R.layout.list_playlist_mini_item, parent); } @Override - public void updateFromItem(final InfoItem infoItem, final HistoryRecordManager historyRecordManager) { - if (!(infoItem instanceof PlaylistInfoItem)) return; + public void updateFromItem(final InfoItem infoItem, + final HistoryRecordManager historyRecordManager) { + if (!(infoItem instanceof PlaylistInfoItem)) { + return; + } final PlaylistInfoItem item = (PlaylistInfoItem) infoItem; itemTitleView.setText(item.getName()); - itemStreamCountView.setText(String.valueOf(item.getStreamCount())); + itemStreamCountView.setText(Localization + .localizeStreamCountMini(itemStreamCountView.getContext(), item.getStreamCount())); itemUploaderView.setText(item.getUploaderName()); itemBuilder.getImageLoader() .displayImage(item.getThumbnailUrl(), itemThumbnailView, - ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS); + ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS); itemView.setOnClickListener(view -> { if (itemBuilder.getOnPlaylistSelectedListener() != null) { diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamGridInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamGridInfoItemHolder.java index a2e585857..8e4a1914e 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamGridInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamGridInfoItemHolder.java @@ -5,9 +5,8 @@ import android.view.ViewGroup; import org.schabi.newpipe.R; import org.schabi.newpipe.info_list.InfoItemBuilder; -public class StreamGridInfoItemHolder extends StreamMiniInfoItemHolder { - - public StreamGridInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) { - super(infoItemBuilder, R.layout.list_stream_grid_item, parent); - } +public class StreamGridInfoItemHolder extends StreamInfoItemHolder { + public StreamGridInfoItemHolder(final InfoItemBuilder infoItemBuilder, final ViewGroup parent) { + super(infoItemBuilder, R.layout.list_stream_grid_item, parent); + } } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamInfoItemHolder.java index c48934d10..5fa0904de 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamInfoItemHolder.java @@ -20,35 +20,46 @@ import static org.schabi.newpipe.MainActivity.DEBUG; *

* Copyright (C) Christian Schabesberger 2016 * StreamInfoItemHolder.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 . + * along with NewPipe. If not, see . + *

*/ public class StreamInfoItemHolder extends StreamMiniInfoItemHolder { - public final TextView itemAdditionalDetails; - public StreamInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) { - super(infoItemBuilder, R.layout.list_stream_item, parent); + public StreamInfoItemHolder(final InfoItemBuilder infoItemBuilder, final ViewGroup parent) { + this(infoItemBuilder, R.layout.list_stream_item, parent); + } + + public StreamInfoItemHolder(final InfoItemBuilder infoItemBuilder, final int layoutId, + final ViewGroup parent) { + super(infoItemBuilder, layoutId, parent); itemAdditionalDetails = itemView.findViewById(R.id.itemAdditionalDetails); } @Override - public void updateFromItem(final InfoItem infoItem, final HistoryRecordManager historyRecordManager) { + public void updateFromItem(final InfoItem infoItem, + final HistoryRecordManager historyRecordManager) { super.updateFromItem(infoItem, historyRecordManager); - if (!(infoItem instanceof StreamInfoItem)) return; + if (!(infoItem instanceof StreamInfoItem)) { + return; + } final StreamInfoItem item = (StreamInfoItem) infoItem; itemAdditionalDetails.setText(getStreamInfoDetailLine(item)); @@ -58,11 +69,14 @@ public class StreamInfoItemHolder extends StreamMiniInfoItemHolder { String viewsAndDate = ""; if (infoItem.getViewCount() >= 0) { if (infoItem.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) { - viewsAndDate = Localization.listeningCount(itemBuilder.getContext(), infoItem.getViewCount()); + viewsAndDate = Localization + .listeningCount(itemBuilder.getContext(), infoItem.getViewCount()); } else if (infoItem.getStreamType().equals(StreamType.LIVE_STREAM)) { - viewsAndDate = Localization.watchingCount(itemBuilder.getContext(), infoItem.getViewCount()); + viewsAndDate = Localization + .shortWatchingCount(itemBuilder.getContext(), infoItem.getViewCount()); } else { - viewsAndDate = Localization.shortViewCount(itemBuilder.getContext(), infoItem.getViewCount()); + viewsAndDate = Localization + .shortViewCount(itemBuilder.getContext(), infoItem.getViewCount()); } } @@ -80,10 +94,12 @@ public class StreamInfoItemHolder extends StreamMiniInfoItemHolder { private String getFormattedRelativeUploadDate(final StreamInfoItem infoItem) { if (infoItem.getUploadDate() != null) { - String formattedRelativeTime = Localization.relativeTime(infoItem.getUploadDate().date()); + String formattedRelativeTime = Localization + .relativeTime(infoItem.getUploadDate().date()); if (DEBUG && PreferenceManager.getDefaultSharedPreferences(itemBuilder.getContext()) - .getBoolean(itemBuilder.getContext().getString(R.string.show_original_time_ago_key), false)) { + .getBoolean(itemBuilder.getContext() + .getString(R.string.show_original_time_ago_key), false)) { formattedRelativeTime += " (" + infoItem.getTextualUploadDate() + ")"; } return formattedRelativeTime; diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java index 6173e53f9..da6c9e82f 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java @@ -1,11 +1,12 @@ package org.schabi.newpipe.info_list.holder; -import androidx.core.content.ContextCompat; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; +import androidx.core.content.ContextCompat; + import org.schabi.newpipe.R; import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.extractor.InfoItem; @@ -21,14 +22,14 @@ import org.schabi.newpipe.views.AnimatedProgressBar; import java.util.concurrent.TimeUnit; public class StreamMiniInfoItemHolder extends InfoItemHolder { - public final ImageView itemThumbnailView; public final TextView itemVideoTitleView; public final TextView itemUploaderView; public final TextView itemDurationView; - public final AnimatedProgressBar itemProgressView; + private final AnimatedProgressBar itemProgressView; - StreamMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) { + StreamMiniInfoItemHolder(final InfoItemBuilder infoItemBuilder, final int layoutId, + final ViewGroup parent) { super(infoItemBuilder, layoutId, parent); itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView); @@ -38,13 +39,16 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder { itemProgressView = itemView.findViewById(R.id.itemProgressView); } - public StreamMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) { + public StreamMiniInfoItemHolder(final InfoItemBuilder infoItemBuilder, final ViewGroup parent) { this(infoItemBuilder, R.layout.list_stream_mini_item, parent); } @Override - public void updateFromItem(final InfoItem infoItem, final HistoryRecordManager historyRecordManager) { - if (!(infoItem instanceof StreamInfoItem)) return; + public void updateFromItem(final InfoItem infoItem, + final HistoryRecordManager historyRecordManager) { + if (!(infoItem instanceof StreamInfoItem)) { + return; + } final StreamInfoItem item = (StreamInfoItem) infoItem; itemVideoTitleView.setText(item.getName()); @@ -56,11 +60,13 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder { R.color.duration_background_color)); itemDurationView.setVisibility(View.VISIBLE); - StreamStateEntity state2 = historyRecordManager.loadStreamState(infoItem).blockingGet()[0]; + StreamStateEntity state2 = historyRecordManager.loadStreamState(infoItem) + .blockingGet()[0]; if (state2 != null) { itemProgressView.setVisibility(View.VISIBLE); itemProgressView.setMax((int) item.getDuration()); - itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state2.getProgressTime())); + itemProgressView.setProgress((int) TimeUnit.MILLISECONDS + .toSeconds(state2.getProgressTime())); } else { itemProgressView.setVisibility(View.GONE); } @@ -103,16 +109,20 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder { } @Override - public void updateState(final InfoItem infoItem, final HistoryRecordManager historyRecordManager) { + public void updateState(final InfoItem infoItem, + final HistoryRecordManager historyRecordManager) { final StreamInfoItem item = (StreamInfoItem) infoItem; StreamStateEntity state = historyRecordManager.loadStreamState(infoItem).blockingGet()[0]; - if (state != null && item.getDuration() > 0 && item.getStreamType() != StreamType.LIVE_STREAM) { + if (state != null && item.getDuration() > 0 + && item.getStreamType() != StreamType.LIVE_STREAM) { itemProgressView.setMax((int) item.getDuration()); if (itemProgressView.getVisibility() == View.VISIBLE) { - itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime())); + itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS + .toSeconds(state.getProgressTime())); } else { - itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime())); + itemProgressView.setProgress((int) TimeUnit.MILLISECONDS + .toSeconds(state.getProgressTime())); AnimationUtils.animateView(itemProgressView, true, 500); } } else if (itemProgressView.getVisibility() == View.VISIBLE) { @@ -134,4 +144,4 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder { itemView.setLongClickable(false); itemView.setOnLongClickListener(null); } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java b/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java index 414a9b6b5..650953bea 100644 --- a/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java @@ -5,16 +5,17 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.os.Bundle; import android.preference.PreferenceManager; -import androidx.fragment.app.Fragment; -import androidx.appcompat.app.ActionBar; -import androidx.recyclerview.widget.GridLayoutManager; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.View; +import androidx.appcompat.app.ActionBar; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + import org.schabi.newpipe.R; import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.fragments.list.ListViewContract; @@ -25,10 +26,14 @@ import static org.schabi.newpipe.util.AnimationUtils.animateView; * This fragment is design to be used with persistent data such as * {@link org.schabi.newpipe.database.LocalItem}, and does not cache the data contained * in the list adapter to avoid extra writes when the it exits or re-enters its lifecycle. - * + *

* This fragment destroys its adapter and views when {@link Fragment#onDestroyView()} is * called and is memory efficient when in backstack. - * */ + *

+ * + * @param List of {@link org.schabi.newpipe.database.LocalItem}s + * @param {@link Void} + */ public abstract class BaseLocalListFragment extends BaseStateFragment implements ListViewContract, SharedPreferences.OnSharedPreferenceChangeListener { @@ -36,21 +41,19 @@ public abstract class BaseLocalListFragment extends BaseStateFragment // Views //////////////////////////////////////////////////////////////////////////*/ - protected View headerRootView; - protected View footerRootView; - + private static final int LIST_MODE_UPDATE_FLAG = 0x32; + private View headerRootView; + private View footerRootView; protected LocalItemListAdapter itemListAdapter; protected RecyclerView itemsList; private int updateFlags = 0; - private static final int LIST_MODE_UPDATE_FLAG = 0x32; - /*////////////////////////////////////////////////////////////////////////// // Lifecycle - Creation //////////////////////////////////////////////////////////////////////////*/ @Override - public void onCreate(Bundle savedInstanceState) { + public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); PreferenceManager.getDefaultSharedPreferences(activity) @@ -70,8 +73,9 @@ public abstract class BaseLocalListFragment extends BaseStateFragment if (updateFlags != 0) { if ((updateFlags & LIST_MODE_UPDATE_FLAG) != 0) { final boolean useGrid = isGridLayout(); - itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager()); - itemListAdapter.setGridItemVariants(useGrid); + itemsList.setLayoutManager( + useGrid ? getGridLayoutManager() : getListLayoutManager()); + itemListAdapter.setUseGridVariant(useGrid); itemListAdapter.notifyDataSetChanged(); } updateFlags = 0; @@ -94,7 +98,8 @@ public abstract class BaseLocalListFragment extends BaseStateFragment final Resources resources = activity.getResources(); int width = resources.getDimensionPixelSize(R.dimen.video_item_grid_thumbnail_image_width); width += (24 * resources.getDisplayMetrics().density); - final int spanCount = (int) Math.floor(resources.getDisplayMetrics().widthPixels / (double)width); + final int spanCount = (int) Math.floor(resources.getDisplayMetrics().widthPixels + / (double) width); final GridLayoutManager lm = new GridLayoutManager(activity, spanCount); lm.setSpanSizeLookup(itemListAdapter.getSpanSizeLookup(spanCount)); return lm; @@ -105,7 +110,7 @@ public abstract class BaseLocalListFragment extends BaseStateFragment } @Override - protected void initViews(View rootView, Bundle savedInstanceState) { + protected void initViews(final View rootView, final Bundle savedInstanceState) { super.initViews(rootView, savedInstanceState); itemListAdapter = new LocalItemListAdapter(activity); @@ -114,9 +119,11 @@ public abstract class BaseLocalListFragment extends BaseStateFragment itemsList = rootView.findViewById(R.id.items_list); itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager()); - itemListAdapter.setGridItemVariants(useGrid); - itemListAdapter.setHeader(headerRootView = getListHeader()); - itemListAdapter.setFooter(footerRootView = getListFooter()); + itemListAdapter.setUseGridVariant(useGrid); + headerRootView = getListHeader(); + itemListAdapter.setHeader(headerRootView); + footerRootView = getListFooter(); + itemListAdapter.setFooter(footerRootView); itemsList.setAdapter(itemListAdapter); } @@ -131,13 +138,17 @@ public abstract class BaseLocalListFragment extends BaseStateFragment //////////////////////////////////////////////////////////////////////////*/ @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); - if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + - "], inflater = [" + inflater + "]"); + if (DEBUG) { + Log.d(TAG, "onCreateOptionsMenu() called with: " + + "menu = [" + menu + "], inflater = [" + inflater + "]"); + } final ActionBar supportActionBar = activity.getSupportActionBar(); - if (supportActionBar == null) return; + if (supportActionBar == null) { + return; + } supportActionBar.setDisplayShowTitleEnabled(true); } @@ -158,7 +169,7 @@ public abstract class BaseLocalListFragment extends BaseStateFragment //////////////////////////////////////////////////////////////////////////*/ @Override - public void startLoading(boolean forceLoad) { + public void startLoading(final boolean forceLoad) { super.startLoading(forceLoad); resetFragment(); } @@ -166,24 +177,36 @@ public abstract class BaseLocalListFragment extends BaseStateFragment @Override public void showLoading() { super.showLoading(); - if (itemsList != null) animateView(itemsList, false, 200); - if (headerRootView != null) animateView(headerRootView, false, 200); + if (itemsList != null) { + animateView(itemsList, false, 200); + } + if (headerRootView != null) { + animateView(headerRootView, false, 200); + } } @Override public void hideLoading() { super.hideLoading(); - if (itemsList != null) animateView(itemsList, true, 200); - if (headerRootView != null) animateView(headerRootView, true, 200); + if (itemsList != null) { + animateView(itemsList, true, 200); + } + if (headerRootView != null) { + animateView(headerRootView, true, 200); + } } @Override - public void showError(String message, boolean showRetryButton) { + public void showError(final String message, final boolean showRetryButton) { super.showError(message, showRetryButton); showListFooter(false); - if (itemsList != null) animateView(itemsList, false, 200); - if (headerRootView != null) animateView(headerRootView, false, 200); + if (itemsList != null) { + animateView(itemsList, false, 200); + } + if (headerRootView != null) { + animateView(headerRootView, false, 200); + } } @Override @@ -194,14 +217,18 @@ public abstract class BaseLocalListFragment extends BaseStateFragment @Override public void showListFooter(final boolean show) { - if (itemsList == null) return; + if (itemsList == null) { + return; + } itemsList.post(() -> { - if (itemListAdapter != null) itemListAdapter.showFooter(show); + if (itemListAdapter != null) { + itemListAdapter.showFooter(show); + } }); } @Override - public void handleNextItems(N result) { + public void handleNextItems(final N result) { isLoading.set(false); } @@ -210,30 +237,35 @@ public abstract class BaseLocalListFragment extends BaseStateFragment //////////////////////////////////////////////////////////////////////////*/ protected void resetFragment() { - if (itemListAdapter != null) itemListAdapter.clearStreamItemList(); + if (itemListAdapter != null) { + itemListAdapter.clearStreamItemList(); + } } @Override - protected boolean onError(Throwable exception) { + protected boolean onError(final Throwable exception) { resetFragment(); return super.onError(exception); } @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, + final String key) { if (key.equals(getString(R.string.list_view_mode_key))) { updateFlags |= LIST_MODE_UPDATE_FLAG; } } protected boolean isGridLayout() { - final String list_mode = PreferenceManager.getDefaultSharedPreferences(activity).getString(getString(R.string.list_view_mode_key), getString(R.string.list_view_mode_value)); - if ("auto".equals(list_mode)) { + final String listMode = PreferenceManager.getDefaultSharedPreferences(activity) + .getString(getString(R.string.list_view_mode_key), + getString(R.string.list_view_mode_value)); + if ("auto".equals(listMode)) { final Configuration configuration = getResources().getConfiguration(); return configuration.orientation == Configuration.ORIENTATION_LANDSCAPE && configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE); } else { - return "grid".equals(list_mode); + return "grid".equals(listMode); } } } diff --git a/app/src/main/java/org/schabi/newpipe/local/HeaderFooterHolder.java b/app/src/main/java/org/schabi/newpipe/local/HeaderFooterHolder.java index 9ee33b3c4..5aac75119 100644 --- a/app/src/main/java/org/schabi/newpipe/local/HeaderFooterHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/HeaderFooterHolder.java @@ -1,13 +1,14 @@ package org.schabi.newpipe.local; -import androidx.recyclerview.widget.RecyclerView; import android.view.View; +import androidx.recyclerview.widget.RecyclerView; + public class HeaderFooterHolder extends RecyclerView.ViewHolder { public View view; - public HeaderFooterHolder(View v) { + public HeaderFooterHolder(final View v) { super(v); view = v; } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/schabi/newpipe/local/LocalItemBuilder.java b/app/src/main/java/org/schabi/newpipe/local/LocalItemBuilder.java index 0fbab0398..d7aaddcc4 100644 --- a/app/src/main/java/org/schabi/newpipe/local/LocalItemBuilder.java +++ b/app/src/main/java/org/schabi/newpipe/local/LocalItemBuilder.java @@ -30,14 +30,12 @@ import org.schabi.newpipe.util.OnClickGesture; */ public class LocalItemBuilder { - private static final String TAG = LocalItemBuilder.class.toString(); - private final Context context; private final ImageLoader imageLoader = ImageLoader.getInstance(); private OnClickGesture onSelectedListener; - public LocalItemBuilder(Context context) { + public LocalItemBuilder(final Context context) { this.context = context; } @@ -54,7 +52,7 @@ public class LocalItemBuilder { return onSelectedListener; } - public void setOnItemSelectedListener(OnClickGesture listener) { + public void setOnItemSelectedListener(final OnClickGesture listener) { this.onSelectedListener = listener; } } diff --git a/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java index 89c1267c8..ad0524f92 100644 --- a/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java @@ -1,13 +1,14 @@ package org.schabi.newpipe.local; import android.content.Context; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import android.util.Log; -import android.view.View; -import android.view.ViewGroup; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.stream.model.StreamStateEntity; @@ -50,7 +51,6 @@ import java.util.List; */ public class LocalItemListAdapter extends RecyclerView.Adapter { - private static final String TAG = LocalItemListAdapter.class.getSimpleName(); private static final boolean DEBUG = false; @@ -63,8 +63,8 @@ public class LocalItemListAdapter extends RecyclerView.Adapter localItems; @@ -76,7 +76,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter(); @@ -84,7 +84,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter listener) { + public void setSelectedListener(final OnClickGesture listener) { localItemBuilder.setOnItemSelectedListener(listener); } @@ -92,28 +92,34 @@ public class LocalItemListAdapter extends RecyclerView.Adapter data) { + public void addItems(@Nullable final List data) { if (data == null) { return; } - if (DEBUG) Log.d(TAG, "addItems() before > localItems.size() = " + - localItems.size() + ", data.size() = " + data.size()); + if (DEBUG) { + Log.d(TAG, "addItems() before > localItems.size() = " + + localItems.size() + ", data.size() = " + data.size()); + } int offsetStart = sizeConsideringHeader(); localItems.addAll(data); - if (DEBUG) Log.d(TAG, "addItems() after > offsetStart = " + offsetStart + - ", localItems.size() = " + localItems.size() + - ", header = " + header + ", footer = " + footer + - ", showFooter = " + showFooter); + if (DEBUG) { + Log.d(TAG, "addItems() after > offsetStart = " + offsetStart + ", " + + "localItems.size() = " + localItems.size() + ", " + + "header = " + header + ", footer = " + footer + ", " + + "showFooter = " + showFooter); + } notifyItemRangeInserted(offsetStart, data.size()); if (footer != null && showFooter) { int footerNow = sizeConsideringHeader(); notifyItemMoved(offsetStart, footerNow); - if (DEBUG) Log.d(TAG, "addItems() footer from " + offsetStart + - " to " + footerNow); + if (DEBUG) { + Log.d(TAG, "addItems() footer from " + offsetStart + + " to " + footerNow); + } } } @@ -123,12 +129,16 @@ public class LocalItemListAdapter extends RecyclerView.Adapter= localItems.size() || actualTo >= localItems.size()) return false; + if (actualFrom < 0 || actualTo < 0) { + return false; + } + if (actualFrom >= localItems.size() || actualTo >= localItems.size()) { + return false; + } localItems.add(actualTo, localItems.remove(actualFrom)); notifyItemMoved(fromAdapterPosition, toAdapterPosition); @@ -143,27 +153,36 @@ public class LocalItemListAdapter extends RecyclerView.Adapter payloads) { + public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position, + @NonNull final List payloads) { if (!payloads.isEmpty() && holder instanceof LocalItemHolder) { for (Object payload : payloads) { if (payload instanceof StreamStateEntity) { - ((LocalItemHolder) holder).updateState(localItems.get(header == null ? position : position - 1), recordManager); + ((LocalItemHolder) holder).updateState(localItems + .get(header == null ? position : position - 1), recordManager); } else if (payload instanceof Boolean) { - ((LocalItemHolder) holder).updateState(localItems.get(header == null ? position : position - 1), recordManager); + ((LocalItemHolder) holder).updateState(localItems + .get(header == null ? position : position - 1), recordManager); } } } else { @@ -288,7 +333,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter, Void> { - +public final class BookmarkFragment extends BaseLocalListFragment, Void> { @State protected Parcelable itemsListState; @@ -52,9 +53,11 @@ public final class BookmarkFragment /////////////////////////////////////////////////////////////////////////// @Override - public void onCreate(Bundle savedInstanceState) { + public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if (activity == null) return; + if (activity == null) { + return; + } final AppDatabase database = NewPipeDatabase.getInstance(activity); localPlaylistManager = new LocalPlaylistManager(database); remotePlaylistManager = new RemotePlaylistManager(database); @@ -63,19 +66,18 @@ public final class BookmarkFragment @Nullable @Override - public View onCreateView(@NonNull LayoutInflater inflater, - @Nullable ViewGroup container, - Bundle savedInstanceState) { + public View onCreateView(@NonNull final LayoutInflater inflater, + @Nullable final ViewGroup container, + final Bundle savedInstanceState) { - if(!useAsFrontPage) { + if (!useAsFrontPage) { setTitle(activity.getString(R.string.tab_bookmarks)); } return inflater.inflate(R.layout.fragment_bookmarks, container, false); } - @Override - public void setUserVisibleHint(boolean isVisibleToUser) { + public void setUserVisibleHint(final boolean isVisibleToUser) { super.setUserVisibleHint(isVisibleToUser); if (activity != null && isVisibleToUser) { setTitle(activity.getString(R.string.tab_bookmarks)); @@ -87,7 +89,7 @@ public final class BookmarkFragment /////////////////////////////////////////////////////////////////////////// @Override - protected void initViews(View rootView, Bundle savedInstanceState) { + protected void initViews(final View rootView, final Bundle savedInstanceState) { super.initViews(rootView, savedInstanceState); } @@ -97,7 +99,7 @@ public final class BookmarkFragment itemListAdapter.setSelectedListener(new OnClickGesture() { @Override - public void selected(LocalItem selectedItem) { + public void selected(final LocalItem selectedItem) { final FragmentManager fragmentManager = getFM(); if (selectedItem instanceof PlaylistMetadataEntry) { @@ -116,10 +118,9 @@ public final class BookmarkFragment } @Override - public void held(LocalItem selectedItem) { + public void held(final LocalItem selectedItem) { if (selectedItem instanceof PlaylistMetadataEntry) { - showLocalDeleteDialog((PlaylistMetadataEntry) selectedItem); - + showLocalDialog((PlaylistMetadataEntry) selectedItem); } else if (selectedItem instanceof PlaylistRemoteEntity) { showRemoteDeleteDialog((PlaylistRemoteEntity) selectedItem); } @@ -132,16 +133,14 @@ public final class BookmarkFragment /////////////////////////////////////////////////////////////////////////// @Override - public void startLoading(boolean forceLoad) { + public void startLoading(final boolean forceLoad) { super.startLoading(forceLoad); - Flowable.combineLatest( - localPlaylistManager.getPlaylists(), - remotePlaylistManager.getPlaylists(), - BookmarkFragment::merge - ).onBackpressureLatest() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(getPlaylistsSubscriber()); + Flowable.combineLatest(localPlaylistManager.getPlaylists(), + remotePlaylistManager.getPlaylists(), PlaylistLocalItem::merge) + .onBackpressureLatest() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(getPlaylistsSubscriber()); } /////////////////////////////////////////////////////////////////////////// @@ -158,8 +157,12 @@ public final class BookmarkFragment public void onDestroyView() { super.onDestroyView(); - if (disposables != null) disposables.clear(); - if (databaseSubscription != null) databaseSubscription.cancel(); + if (disposables != null) { + disposables.clear(); + } + if (databaseSubscription != null) { + databaseSubscription.cancel(); + } databaseSubscription = null; } @@ -167,7 +170,9 @@ public final class BookmarkFragment @Override public void onDestroy() { super.onDestroy(); - if (disposables != null) disposables.dispose(); + if (disposables != null) { + disposables.dispose(); + } disposables = null; localPlaylistManager = null; @@ -182,32 +187,35 @@ public final class BookmarkFragment private Subscriber> getPlaylistsSubscriber() { return new Subscriber>() { @Override - public void onSubscribe(Subscription s) { + public void onSubscribe(final Subscription s) { showLoading(); - if (databaseSubscription != null) databaseSubscription.cancel(); + if (databaseSubscription != null) { + databaseSubscription.cancel(); + } databaseSubscription = s; databaseSubscription.request(1); } @Override - public void onNext(List subscriptions) { + public void onNext(final List subscriptions) { handleResult(subscriptions); - if (databaseSubscription != null) databaseSubscription.request(1); + if (databaseSubscription != null) { + databaseSubscription.request(1); + } } @Override - public void onError(Throwable exception) { + public void onError(final Throwable exception) { BookmarkFragment.this.onError(exception); } @Override - public void onComplete() { - } + public void onComplete() { } }; } @Override - public void handleResult(@NonNull List result) { + public void handleResult(@NonNull final List result) { super.handleResult(result); itemListAdapter.clearStreamItemList(); @@ -224,13 +232,16 @@ public final class BookmarkFragment } hideLoading(); } + /////////////////////////////////////////////////////////////////////////// // Fragment Error Handling /////////////////////////////////////////////////////////////////////////// @Override - protected boolean onError(Throwable exception) { - if (super.onError(exception)) return true; + protected boolean onError(final Throwable exception) { + if (super.onError(exception)) { + return true; + } onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, "none", "Bookmark", R.string.general_error); @@ -240,23 +251,43 @@ public final class BookmarkFragment @Override protected void resetFragment() { super.resetFragment(); - if (disposables != null) disposables.clear(); + if (disposables != null) { + disposables.clear(); + } } /////////////////////////////////////////////////////////////////////////// // Utils /////////////////////////////////////////////////////////////////////////// - private void showLocalDeleteDialog(final PlaylistMetadataEntry item) { - showDeleteDialog(item.name, localPlaylistManager.deletePlaylist(item.uid)); - } - private void showRemoteDeleteDialog(final PlaylistRemoteEntity item) { showDeleteDialog(item.getName(), remotePlaylistManager.deletePlaylist(item.getUid())); } + private void showLocalDialog(final PlaylistMetadataEntry selectedItem) { + View dialogView = View.inflate(getContext(), R.layout.dialog_bookmark, null); + EditText editText = dialogView.findViewById(R.id.playlist_name_edit_text); + editText.setText(selectedItem.name); + + Builder builder = new AlertDialog.Builder(activity); + builder.setView(dialogView) + .setPositiveButton(R.string.rename_playlist, (dialog, which) -> { + changeLocalPlaylistName(selectedItem.uid, editText.getText().toString()); + }) + .setNegativeButton(R.string.cancel, null) + .setNeutralButton(R.string.delete, (dialog, which) -> { + showDeleteDialog(selectedItem.name, + localPlaylistManager.deletePlaylist(selectedItem.uid)); + dialog.dismiss(); + }) + .create() + .show(); + } + private void showDeleteDialog(final String name, final Single deleteReactor) { - if (activity == null || disposables == null) return; + if (activity == null || disposables == null) { + return; + } new AlertDialog.Builder(activity) .setTitle(name) @@ -265,23 +296,27 @@ public final class BookmarkFragment .setPositiveButton(R.string.delete, (dialog, i) -> disposables.add(deleteReactor .observeOn(AndroidSchedulers.mainThread()) - .subscribe(ignored -> {/*Do nothing on success*/}, this::onError)) + .subscribe(ignored -> { /*Do nothing on success*/ }, this::onError)) ) .setNegativeButton(R.string.cancel, null) .show(); } - private static List merge(final List localPlaylists, - final List remotePlaylists) { - List items = new ArrayList<>( - localPlaylists.size() + remotePlaylists.size()); - items.addAll(localPlaylists); - items.addAll(remotePlaylists); + private void changeLocalPlaylistName(final long id, final String name) { + if (localPlaylistManager == null) { + return; + } - Collections.sort(items, (left, right) -> - left.getOrderingName().compareToIgnoreCase(right.getOrderingName())); + if (DEBUG) { + Log.d(TAG, "Updating playlist id=[" + id + "] " + + "with new name=[" + name + "] items"); + } - return items; + localPlaylistManager.renamePlaylist(id, name); + final Disposable disposable = localPlaylistManager.renamePlaylist(id, name) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(longs -> { /*Do nothing on success*/ }, this::onError); + disposables.add(disposable); } } diff --git a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistAppendDialog.java b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistAppendDialog.java index ac02b0b37..4eb97bbbf 100644 --- a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistAppendDialog.java +++ b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistAppendDialog.java @@ -1,15 +1,16 @@ package org.schabi.newpipe.local.dialog; import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; @@ -68,13 +69,13 @@ public final class PlaylistAppendDialog extends PlaylistDialog { //////////////////////////////////////////////////////////////////////////*/ @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container, + final Bundle savedInstanceState) { return inflater.inflate(R.layout.dialog_playlists, container); } @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); final LocalPlaylistManager playlistManager = @@ -83,9 +84,10 @@ public final class PlaylistAppendDialog extends PlaylistDialog { playlistAdapter = new LocalItemListAdapter(getActivity()); playlistAdapter.setSelectedListener(new OnClickGesture() { @Override - public void selected(LocalItem selectedItem) { - if (!(selectedItem instanceof PlaylistMetadataEntry) || getStreams() == null) + public void selected(final LocalItem selectedItem) { + if (!(selectedItem instanceof PlaylistMetadataEntry) || getStreams() == null) { return; + } onPlaylistSelected(playlistManager, (PlaylistMetadataEntry) selectedItem, getStreams()); } @@ -125,7 +127,9 @@ public final class PlaylistAppendDialog extends PlaylistDialog { //////////////////////////////////////////////////////////////////////////*/ public void openCreatePlaylistDialog() { - if (getStreams() == null || getFragmentManager() == null) return; + if (getStreams() == null || getFragmentManager() == null) { + return; + } PlaylistCreationDialog.newInstance(getStreams()).show(getFragmentManager(), TAG); getDialog().dismiss(); @@ -144,14 +148,23 @@ public final class PlaylistAppendDialog extends PlaylistDialog { } } - private void onPlaylistSelected(@NonNull LocalPlaylistManager manager, - @NonNull PlaylistMetadataEntry playlist, - @NonNull List streams) { - if (getStreams() == null) return; + private void onPlaylistSelected(@NonNull final LocalPlaylistManager manager, + @NonNull final PlaylistMetadataEntry playlist, + @NonNull final List streams) { + if (getStreams() == null) { + return; + } final Toast successToast = Toast.makeText(getContext(), R.string.playlist_add_stream_success, Toast.LENGTH_SHORT); + if (playlist.thumbnailUrl.equals("drawable://" + R.drawable.dummy_thumbnail_playlist)) { + playlistDisposables.add(manager + .changePlaylistThumbnail(playlist.uid, streams.get(0).getThumbnailUrl()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(ignored -> successToast.show())); + } + playlistDisposables.add(manager.appendToPlaylist(playlist.uid, streams) .observeOn(AndroidSchedulers.mainThread()) .subscribe(ignored -> successToast.show())); diff --git a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistCreationDialog.java b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistCreationDialog.java index 0507d3dd0..b25ec7288 100644 --- a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistCreationDialog.java +++ b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistCreationDialog.java @@ -3,12 +3,13 @@ package org.schabi.newpipe.local.dialog; import android.app.AlertDialog; import android.app.Dialog; import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import android.view.View; import android.widget.EditText; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.database.stream.model.StreamEntity; @@ -19,8 +20,6 @@ import java.util.List; import io.reactivex.android.schedulers.AndroidSchedulers; public final class PlaylistCreationDialog extends PlaylistDialog { - private static final String TAG = PlaylistCreationDialog.class.getCanonicalName(); - public static PlaylistCreationDialog newInstance(final List streams) { PlaylistCreationDialog dialog = new PlaylistCreationDialog(); dialog.setInfo(streams); @@ -33,8 +32,10 @@ public final class PlaylistCreationDialog extends PlaylistDialog { @NonNull @Override - public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { - if (getStreams() == null) return super.onCreateDialog(savedInstanceState); + public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) { + if (getStreams() == null) { + return super.onCreateDialog(savedInstanceState); + } View dialogView = View.inflate(getContext(), R.layout.dialog_playlist_name, null); EditText nameInput = dialogView.findViewById(R.id.playlist_name); diff --git a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistDialog.java b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistDialog.java index 12e57808e..9ca8733cc 100644 --- a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistDialog.java +++ b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistDialog.java @@ -2,10 +2,11 @@ package org.schabi.newpipe.local.dialog; import android.app.Dialog; import android.os.Bundle; +import android.view.Window; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.DialogFragment; -import android.view.Window; import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.util.StateSaver; @@ -14,7 +15,6 @@ import java.util.List; import java.util.Queue; public abstract class PlaylistDialog extends DialogFragment implements StateSaver.WriteRead { - private List streamEntities; private StateSaver.SavedState savedState; @@ -32,7 +32,7 @@ public abstract class PlaylistDialog extends DialogFragment implements StateSave //////////////////////////////////////////////////////////////////////////*/ @Override - public void onCreate(@Nullable Bundle savedInstanceState) { + public void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); savedState = StateSaver.tryToRestore(savedInstanceState, this); } @@ -45,7 +45,7 @@ public abstract class PlaylistDialog extends DialogFragment implements StateSave @NonNull @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { + public Dialog onCreateDialog(final Bundle savedInstanceState) { final Dialog dialog = super.onCreateDialog(savedInstanceState); //remove title final Window window = dialog.getWindow(); @@ -66,18 +66,18 @@ public abstract class PlaylistDialog extends DialogFragment implements StateSave } @Override - public void writeTo(Queue objectsToSave) { + public void writeTo(final Queue objectsToSave) { objectsToSave.add(streamEntities); } @Override @SuppressWarnings("unchecked") - public void readFrom(@NonNull Queue savedObjects) { + public void readFrom(@NonNull final Queue savedObjects) { streamEntities = (List) savedObjects.poll(); } @Override - public void onSaveInstanceState(Bundle outState) { + public void onSaveInstanceState(final Bundle outState) { super.onSaveInstanceState(outState); if (getActivity() != null) { savedState = StateSaver.tryToSave(getActivity().isChangingConfigurations(), diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt new file mode 100644 index 000000000..d319c9fa3 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt @@ -0,0 +1,168 @@ +package org.schabi.newpipe.local.feed + +import android.content.Context +import android.util.Log +import io.reactivex.Completable +import io.reactivex.Flowable +import io.reactivex.Maybe +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import java.util.Calendar +import java.util.Date +import org.schabi.newpipe.MainActivity.DEBUG +import org.schabi.newpipe.NewPipeDatabase +import org.schabi.newpipe.database.feed.model.FeedEntity +import org.schabi.newpipe.database.feed.model.FeedGroupEntity +import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity +import org.schabi.newpipe.database.stream.model.StreamEntity +import org.schabi.newpipe.extractor.stream.StreamInfoItem +import org.schabi.newpipe.extractor.stream.StreamType +import org.schabi.newpipe.local.subscription.FeedGroupIcon + +class FeedDatabaseManager(context: Context) { + private val database = NewPipeDatabase.getInstance(context) + private val feedTable = database.feedDAO() + private val feedGroupTable = database.feedGroupDAO() + private val streamTable = database.streamDAO() + + companion object { + /** + * Only items that are newer than this will be saved. + */ + val FEED_OLDEST_ALLOWED_DATE: Calendar = Calendar.getInstance().apply { + add(Calendar.WEEK_OF_YEAR, -13) + set(Calendar.HOUR_OF_DAY, 0) + set(Calendar.MINUTE, 0) + set(Calendar.SECOND, 0) + set(Calendar.MILLISECOND, 0) + } + } + + fun groups() = feedGroupTable.getAll() + + fun database() = database + + fun asStreamItems(groupId: Long = FeedGroupEntity.GROUP_ALL_ID): Flowable> { + val streams = when (groupId) { + FeedGroupEntity.GROUP_ALL_ID -> feedTable.getAllStreams() + else -> feedTable.getAllStreamsFromGroup(groupId) + } + + return streams.map> { + val items = ArrayList(it.size) + for (streamEntity in it) items.add(streamEntity.toStreamInfoItem()) + return@map items + } + } + + fun outdatedSubscriptions(outdatedThreshold: Date) = feedTable.getAllOutdated(outdatedThreshold) + + fun notLoadedCount(groupId: Long = FeedGroupEntity.GROUP_ALL_ID): Flowable { + return when (groupId) { + FeedGroupEntity.GROUP_ALL_ID -> feedTable.notLoadedCount() + else -> feedTable.notLoadedCountForGroup(groupId) + } + } + + fun outdatedSubscriptionsForGroup(groupId: Long = FeedGroupEntity.GROUP_ALL_ID, outdatedThreshold: Date) = + feedTable.getAllOutdatedForGroup(groupId, outdatedThreshold) + + fun markAsOutdated(subscriptionId: Long) = feedTable + .setLastUpdatedForSubscription(FeedLastUpdatedEntity(subscriptionId, null)) + + fun upsertAll( + subscriptionId: Long, + items: List, + oldestAllowedDate: Date = FEED_OLDEST_ALLOWED_DATE.time + ) { + val itemsToInsert = ArrayList() + loop@ for (streamItem in items) { + val uploadDate = streamItem.uploadDate + + itemsToInsert += when { + uploadDate == null && streamItem.streamType == StreamType.LIVE_STREAM -> streamItem + uploadDate != null && uploadDate.date().time >= oldestAllowedDate -> streamItem + else -> continue@loop + } + } + + feedTable.unlinkOldLivestreams(subscriptionId) + + if (itemsToInsert.isNotEmpty()) { + val streamEntities = itemsToInsert.map { StreamEntity(it) } + val streamIds = streamTable.upsertAll(streamEntities) + val feedEntities = streamIds.map { FeedEntity(it, subscriptionId) } + + feedTable.insertAll(feedEntities) + } + + feedTable.setLastUpdatedForSubscription(FeedLastUpdatedEntity(subscriptionId, Calendar.getInstance().time)) + } + + fun removeOrphansOrOlderStreams(oldestAllowedDate: Date = FEED_OLDEST_ALLOWED_DATE.time) { + feedTable.unlinkStreamsOlderThan(oldestAllowedDate) + streamTable.deleteOrphans() + } + + fun clear() { + feedTable.deleteAll() + val deletedOrphans = streamTable.deleteOrphans() + if (DEBUG) Log.d(this::class.java.simpleName, "clear() → streamTable.deleteOrphans() → $deletedOrphans") + } + + // ///////////////////////////////////////////////////////////////////////// + // Feed Groups + // ///////////////////////////////////////////////////////////////////////// + + fun subscriptionIdsForGroup(groupId: Long): Flowable> { + return feedGroupTable.getSubscriptionIdsFor(groupId) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + } + + fun updateSubscriptionsForGroup(groupId: Long, subscriptionIds: List): Completable { + return Completable.fromCallable { feedGroupTable.updateSubscriptionsForGroup(groupId, subscriptionIds) } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + } + + fun createGroup(name: String, icon: FeedGroupIcon): Maybe { + return Maybe.fromCallable { feedGroupTable.insert(FeedGroupEntity(0, name, icon)) } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + } + + fun getGroup(groupId: Long): Maybe { + return feedGroupTable.getGroup(groupId) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + } + + fun updateGroup(feedGroupEntity: FeedGroupEntity): Completable { + return Completable.fromCallable { feedGroupTable.update(feedGroupEntity) } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + } + + fun deleteGroup(groupId: Long): Completable { + return Completable.fromCallable { feedGroupTable.delete(groupId) } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + } + + fun updateGroupsOrder(groupIdList: List): Completable { + var index = 0L + val orderMap = groupIdList.associateBy({ it }, { index++ }) + + return Completable.fromCallable { feedGroupTable.updateOrder(orderMap) } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + } + + fun oldestSubscriptionUpdate(groupId: Long): Flowable> { + return when (groupId) { + FeedGroupEntity.GROUP_ALL_ID -> feedTable.oldestSubscriptionUpdateFromAll() + else -> feedTable.oldestSubscriptionUpdate(groupId) + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.java b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.java deleted file mode 100644 index 04406c3da..000000000 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.java +++ /dev/null @@ -1,444 +0,0 @@ -package org.schabi.newpipe.local.feed; - -import android.os.Bundle; -import android.os.Handler; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.ActionBar; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.View; -import android.view.ViewGroup; - -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; -import org.schabi.newpipe.R; -import org.schabi.newpipe.database.subscription.SubscriptionEntity; -import org.schabi.newpipe.extractor.InfoItem; -import org.schabi.newpipe.extractor.NewPipe; -import org.schabi.newpipe.extractor.channel.ChannelInfo; -import org.schabi.newpipe.extractor.exceptions.ExtractionException; -import org.schabi.newpipe.fragments.list.BaseListFragment; -import org.schabi.newpipe.local.subscription.SubscriptionService; -import org.schabi.newpipe.report.UserAction; - -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Queue; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; - -import io.reactivex.Flowable; -import io.reactivex.MaybeObserver; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.disposables.Disposable; - -public class FeedFragment extends BaseListFragment, Void> { - - private static final int OFF_SCREEN_ITEMS_COUNT = 3; - private static final int MIN_ITEMS_INITIAL_LOAD = 8; - private int FEED_LOAD_COUNT = MIN_ITEMS_INITIAL_LOAD; - - private int subscriptionPoolSize; - - private SubscriptionService subscriptionService; - - private AtomicBoolean allItemsLoaded = new AtomicBoolean(false); - private HashSet itemsLoaded = new HashSet<>(); - private final AtomicInteger requestLoadedAtomic = new AtomicInteger(); - - private CompositeDisposable compositeDisposable = new CompositeDisposable(); - private Disposable subscriptionObserver; - private Subscription feedSubscriber; - - /*////////////////////////////////////////////////////////////////////////// - // Fragment LifeCycle - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - subscriptionService = SubscriptionService.getInstance(activity); - - FEED_LOAD_COUNT = howManyItemsToLoad(); - } - - @Override - public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { - - if(!useAsFrontPage) { - setTitle(activity.getString(R.string.fragment_whats_new)); - } - return inflater.inflate(R.layout.fragment_feed, container, false); - } - - @Override - public void onPause() { - super.onPause(); - disposeEverything(); - } - - @Override - public void onResume() { - super.onResume(); - if (wasLoading.get()) doInitialLoadLogic(); - } - - @Override - public void onDestroy() { - super.onDestroy(); - - disposeEverything(); - subscriptionService = null; - compositeDisposable = null; - subscriptionObserver = null; - feedSubscriber = null; - } - - @Override - public void onDestroyView() { - // Do not monitor for updates when user is not viewing the feed fragment. - // This is a waste of bandwidth. - disposeEverything(); - super.onDestroyView(); - } - - @Override - public void setUserVisibleHint(boolean isVisibleToUser) { - super.setUserVisibleHint(isVisibleToUser); - if (activity != null && isVisibleToUser) { - setTitle(activity.getString(R.string.fragment_whats_new)); - } - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - super.onCreateOptionsMenu(menu, inflater); - - ActionBar supportActionBar = activity.getSupportActionBar(); - - if(useAsFrontPage) { - supportActionBar.setDisplayShowTitleEnabled(true); - //supportActionBar.setDisplayShowTitleEnabled(false); - } - } - - @Override - public void reloadContent() { - resetFragment(); - super.reloadContent(); - } - - /*////////////////////////////////////////////////////////////////////////// - // StateSaving - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void writeTo(Queue objectsToSave) { - super.writeTo(objectsToSave); - objectsToSave.add(allItemsLoaded); - objectsToSave.add(itemsLoaded); - } - - @Override - @SuppressWarnings("unchecked") - public void readFrom(@NonNull Queue savedObjects) throws Exception { - super.readFrom(savedObjects); - allItemsLoaded = (AtomicBoolean) savedObjects.poll(); - itemsLoaded = (HashSet) savedObjects.poll(); - } - - /*////////////////////////////////////////////////////////////////////////// - // Feed Loader - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void startLoading(boolean forceLoad) { - if (DEBUG) Log.d(TAG, "startLoading() called with: forceLoad = [" + forceLoad + "]"); - if (subscriptionObserver != null) subscriptionObserver.dispose(); - - if (allItemsLoaded.get()) { - if (infoListAdapter.getItemsList().size() == 0) { - showEmptyState(); - } else { - showListFooter(false); - hideLoading(); - } - - isLoading.set(false); - return; - } - - isLoading.set(true); - showLoading(); - showListFooter(true); - subscriptionObserver = subscriptionService.getSubscription() - .onErrorReturnItem(Collections.emptyList()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(this::handleResult, this::onError); - } - - @Override - public void handleResult(@androidx.annotation.NonNull List result) { - super.handleResult(result); - - if (result.isEmpty()) { - infoListAdapter.clearStreamItemList(); - showEmptyState(); - return; - } - - subscriptionPoolSize = result.size(); - Flowable.fromIterable(result) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(getSubscriptionObserver()); - } - - /** - * Responsible for reacting to user pulling request and starting a request for new feed stream. - *

- * On initialization, it automatically requests the amount of feed needed to display - * a minimum amount required (FEED_LOAD_SIZE). - *

- * Upon receiving a user pull, it creates a Single Observer to fetch the ChannelInfo - * containing the feed streams. - **/ - private Subscriber getSubscriptionObserver() { - return new Subscriber() { - @Override - public void onSubscribe(Subscription s) { - if (feedSubscriber != null) feedSubscriber.cancel(); - feedSubscriber = s; - - int requestSize = FEED_LOAD_COUNT - infoListAdapter.getItemsList().size(); - if (wasLoading.getAndSet(false)) requestSize = FEED_LOAD_COUNT; - - boolean hasToLoad = requestSize > 0; - if (hasToLoad) { - requestLoadedAtomic.set(infoListAdapter.getItemsList().size()); - requestFeed(requestSize); - } - isLoading.set(hasToLoad); - } - - @Override - public void onNext(SubscriptionEntity subscriptionEntity) { - if (!itemsLoaded.contains(subscriptionEntity.getServiceId() + subscriptionEntity.getUrl())) { - subscriptionService.getChannelInfo(subscriptionEntity) - .observeOn(AndroidSchedulers.mainThread()) - .onErrorComplete( - (@io.reactivex.annotations.NonNull Throwable throwable) -> - FeedFragment.super.onError(throwable)) - .subscribe( - getChannelInfoObserver(subscriptionEntity.getServiceId(), - subscriptionEntity.getUrl())); - } else { - requestFeed(1); - } - } - - @Override - public void onError(Throwable exception) { - FeedFragment.this.onError(exception); - } - - @Override - public void onComplete() { - if (DEBUG) Log.d(TAG, "getSubscriptionObserver > onComplete() called"); - } - }; - } - - /** - * On each request, a subscription item from the updated table is transformed - * into a ChannelInfo, containing the latest streams from the channel. - *

- * Currently, the feed uses the first into from the list of streams. - *

- * If chosen feed already displayed, then we request another feed from another - * subscription, until the subscription table runs out of new items. - *

- * This Observer is self-contained and will close itself when complete. However, this - * does not obey the fragment lifecycle and may continue running in the background - * until it is complete. This is done due to RxJava2 no longer propagate errors once - * an observer is unsubscribed while the thread process is still running. - *

- * To solve the above issue, we can either set a global RxJava Error Handler, or - * manage exceptions case by case. This should be done if the current implementation is - * too costly when dealing with larger subscription sets. - * - * @param url + serviceId to put in {@link #allItemsLoaded} to signal that this specific entity has been loaded. - */ - private MaybeObserver getChannelInfoObserver(final int serviceId, final String url) { - return new MaybeObserver() { - private Disposable observer; - - @Override - public void onSubscribe(Disposable d) { - observer = d; - compositeDisposable.add(d); - isLoading.set(true); - } - - // Called only when response is non-empty - @Override - public void onSuccess(final ChannelInfo channelInfo) { - if (infoListAdapter == null || channelInfo.getRelatedItems().isEmpty()) { - onDone(); - return; - } - - final InfoItem item = channelInfo.getRelatedItems().get(0); - // Keep requesting new items if the current one already exists - boolean itemExists = doesItemExist(infoListAdapter.getItemsList(), item); - if (!itemExists) { - infoListAdapter.addInfoItem(item); - //updateSubscription(channelInfo); - } else { - requestFeed(1); - } - onDone(); - } - - @Override - public void onError(Throwable exception) { - showSnackBarError(exception, - UserAction.SUBSCRIPTION, - NewPipe.getNameOfService(serviceId), - url, 0); - requestFeed(1); - onDone(); - } - - // Called only when response is empty - @Override - public void onComplete() { - onDone(); - } - - private void onDone() { - if (observer.isDisposed()) { - return; - } - - itemsLoaded.add(serviceId + url); - compositeDisposable.remove(observer); - - int loaded = requestLoadedAtomic.incrementAndGet(); - if (loaded >= Math.min(FEED_LOAD_COUNT, subscriptionPoolSize)) { - requestLoadedAtomic.set(0); - isLoading.set(false); - } - - if (itemsLoaded.size() == subscriptionPoolSize) { - if (DEBUG) Log.d(TAG, "getChannelInfoObserver > All Items Loaded"); - allItemsLoaded.set(true); - showListFooter(false); - isLoading.set(false); - hideLoading(); - if (infoListAdapter.getItemsList().size() == 0) { - showEmptyState(); - } - } - } - }; - } - - @Override - protected void loadMoreItems() { - isLoading.set(true); - delayHandler.removeCallbacksAndMessages(null); - // Add a little of a delay when requesting more items because the cache is so fast, - // that the view seems stuck to the user when he scroll to the bottom - delayHandler.postDelayed(() -> requestFeed(FEED_LOAD_COUNT), 300); - } - - @Override - protected boolean hasMoreItems() { - return !allItemsLoaded.get(); - } - - private final Handler delayHandler = new Handler(); - - private void requestFeed(final int count) { - if (DEBUG) Log.d(TAG, "requestFeed() called with: count = [" + count + "], feedSubscriber = [" + feedSubscriber + "]"); - if (feedSubscriber == null) return; - - isLoading.set(true); - delayHandler.removeCallbacksAndMessages(null); - feedSubscriber.request(count); - } - - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ - - private void resetFragment() { - if (DEBUG) Log.d(TAG, "resetFragment() called"); - if (subscriptionObserver != null) subscriptionObserver.dispose(); - if (compositeDisposable != null) compositeDisposable.clear(); - if (infoListAdapter != null) infoListAdapter.clearStreamItemList(); - - delayHandler.removeCallbacksAndMessages(null); - requestLoadedAtomic.set(0); - allItemsLoaded.set(false); - showListFooter(false); - itemsLoaded.clear(); - } - - private void disposeEverything() { - if (subscriptionObserver != null) subscriptionObserver.dispose(); - if (compositeDisposable != null) compositeDisposable.clear(); - if (feedSubscriber != null) feedSubscriber.cancel(); - delayHandler.removeCallbacksAndMessages(null); - } - - private boolean doesItemExist(final List items, final InfoItem item) { - for (final InfoItem existingItem : items) { - if (existingItem.getInfoType() == item.getInfoType() && - existingItem.getServiceId() == item.getServiceId() && - existingItem.getName().equals(item.getName()) && - existingItem.getUrl().equals(item.getUrl())) return true; - } - return false; - } - - private int howManyItemsToLoad() { - int heightPixels = getResources().getDisplayMetrics().heightPixels; - int itemHeightPixels = activity.getResources().getDimensionPixelSize(R.dimen.video_item_search_height); - - int items = itemHeightPixels > 0 - ? heightPixels / itemHeightPixels + OFF_SCREEN_ITEMS_COUNT - : MIN_ITEMS_INITIAL_LOAD; - return Math.max(MIN_ITEMS_INITIAL_LOAD, items); - } - - /*////////////////////////////////////////////////////////////////////////// - // Fragment Error Handling - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void showError(String message, boolean showRetryButton) { - resetFragment(); - super.showError(message, showRetryButton); - } - - @Override - protected boolean onError(Throwable exception) { - if (super.onError(exception)) return true; - - int errorId = exception instanceof ExtractionException - ? R.string.parsing_error - : R.string.general_error; - onUnrecoverableError(exception, - UserAction.SOMETHING_ELSE, - "none", - "Requesting feed", - errorId); - return true; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt new file mode 100644 index 000000000..8018e2cd8 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt @@ -0,0 +1,342 @@ +/* + * Copyright 2019 Mauricio Colli + * FeedFragment.kt is part of NewPipe + * + * License: GPL-3.0+ + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.schabi.newpipe.local.feed + +import android.content.Intent +import android.os.Bundle +import android.os.Parcelable +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 androidx.appcompat.app.AlertDialog +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import androidx.preference.PreferenceManager +import icepick.State +import java.util.Calendar +import kotlinx.android.synthetic.main.error_retry.error_button_retry +import kotlinx.android.synthetic.main.error_retry.error_message_view +import kotlinx.android.synthetic.main.fragment_feed.empty_state_view +import kotlinx.android.synthetic.main.fragment_feed.error_panel +import kotlinx.android.synthetic.main.fragment_feed.items_list +import kotlinx.android.synthetic.main.fragment_feed.loading_progress_bar +import kotlinx.android.synthetic.main.fragment_feed.loading_progress_text +import kotlinx.android.synthetic.main.fragment_feed.refresh_root_view +import kotlinx.android.synthetic.main.fragment_feed.refresh_subtitle_text +import kotlinx.android.synthetic.main.fragment_feed.refresh_text +import org.schabi.newpipe.R +import org.schabi.newpipe.database.feed.model.FeedGroupEntity +import org.schabi.newpipe.fragments.list.BaseListFragment +import org.schabi.newpipe.local.feed.service.FeedLoadService +import org.schabi.newpipe.report.UserAction +import org.schabi.newpipe.util.AnimationUtils.animateView +import org.schabi.newpipe.util.Localization + +class FeedFragment : BaseListFragment() { + private lateinit var viewModel: FeedViewModel + @State + @JvmField + var listState: Parcelable? = null + + private var groupId = FeedGroupEntity.GROUP_ALL_ID + private var groupName = "" + private var oldestSubscriptionUpdate: Calendar? = null + + init { + setHasOptionsMenu(true) + setUseDefaultStateSaving(false) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + groupId = arguments?.getLong(KEY_GROUP_ID, FeedGroupEntity.GROUP_ALL_ID) + ?: FeedGroupEntity.GROUP_ALL_ID + groupName = arguments?.getString(KEY_GROUP_NAME) ?: "" + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_feed, container, false) + } + + override fun onViewCreated(rootView: View, savedInstanceState: Bundle?) { + super.onViewCreated(rootView, savedInstanceState) + + viewModel = ViewModelProviders.of(this, FeedViewModel.Factory(requireContext(), groupId)).get(FeedViewModel::class.java) + viewModel.stateLiveData.observe(viewLifecycleOwner, Observer { it?.let(::handleResult) }) + } + + override fun onPause() { + super.onPause() + listState = items_list?.layoutManager?.onSaveInstanceState() + } + + override fun onResume() { + super.onResume() + updateRelativeTimeViews() + } + + override fun setUserVisibleHint(isVisibleToUser: Boolean) { + super.setUserVisibleHint(isVisibleToUser) + + if (!isVisibleToUser && view != null) { + updateRelativeTimeViews() + } + } + + override fun initListeners() { + super.initListeners() + refresh_root_view.setOnClickListener { + triggerUpdate() + } + } + + // ///////////////////////////////////////////////////////////////////////// + // Menu + // ///////////////////////////////////////////////////////////////////////// + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + super.onCreateOptionsMenu(menu, inflater) + activity.supportActionBar?.setTitle(R.string.fragment_feed_title) + activity.supportActionBar?.subtitle = groupName + + inflater.inflate(R.menu.menu_feed_fragment, menu) + + if (useAsFrontPage) { + menu.findItem(R.id.menu_item_feed_help).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER) + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == R.id.menu_item_feed_help) { + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) + + val usingDedicatedMethod = sharedPreferences.getBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), false) + val enableDisableButtonText = when { + usingDedicatedMethod -> R.string.feed_use_dedicated_fetch_method_disable_button + else -> R.string.feed_use_dedicated_fetch_method_enable_button + } + + AlertDialog.Builder(requireContext()) + .setMessage(R.string.feed_use_dedicated_fetch_method_help_text) + .setNeutralButton(enableDisableButtonText) { _, _ -> + sharedPreferences.edit() + .putBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), !usingDedicatedMethod) + .apply() + } + .setPositiveButton(resources.getString(R.string.finish), null) + .create() + .show() + return true + } + + return super.onOptionsItemSelected(item) + } + + override fun onDestroyOptionsMenu() { + super.onDestroyOptionsMenu() + activity?.supportActionBar?.subtitle = null + } + + override fun onDestroy() { + super.onDestroy() + activity?.supportActionBar?.subtitle = null + } + + // ///////////////////////////////////////////////////////////////////////// + // Handling + // ///////////////////////////////////////////////////////////////////////// + + override fun showLoading() { + animateView(refresh_root_view, false, 0) + animateView(items_list, false, 0) + + animateView(loading_progress_bar, true, 200) + animateView(loading_progress_text, true, 200) + + empty_state_view?.let { animateView(it, false, 0) } + animateView(error_panel, false, 0) + } + + override fun hideLoading() { + animateView(refresh_root_view, true, 200) + animateView(items_list, true, 300) + + animateView(loading_progress_bar, false, 0) + animateView(loading_progress_text, false, 0) + + empty_state_view?.let { animateView(it, false, 0) } + animateView(error_panel, false, 0) + } + + override fun showEmptyState() { + animateView(refresh_root_view, true, 200) + animateView(items_list, false, 0) + + animateView(loading_progress_bar, false, 0) + animateView(loading_progress_text, false, 0) + + empty_state_view?.let { animateView(it, true, 800) } + animateView(error_panel, false, 0) + } + + override fun showError(message: String, showRetryButton: Boolean) { + infoListAdapter.clearStreamItemList() + animateView(refresh_root_view, false, 120) + animateView(items_list, false, 120) + + animateView(loading_progress_bar, false, 120) + animateView(loading_progress_text, false, 120) + + error_message_view.text = message + animateView(error_button_retry, showRetryButton, if (showRetryButton) 600 else 0) + animateView(error_panel, true, 300) + } + + override fun handleResult(result: FeedState) { + when (result) { + is FeedState.ProgressState -> handleProgressState(result) + is FeedState.LoadedState -> handleLoadedState(result) + is FeedState.ErrorState -> if (handleErrorState(result)) return + } + + updateRefreshViewState() + } + + private fun handleProgressState(progressState: FeedState.ProgressState) { + showLoading() + + val isIndeterminate = progressState.currentProgress == -1 && + progressState.maxProgress == -1 + + if (!isIndeterminate) { + loading_progress_text.text = "${progressState.currentProgress}/${progressState.maxProgress}" + } else if (progressState.progressMessage > 0) { + loading_progress_text?.setText(progressState.progressMessage) + } else { + loading_progress_text?.text = "∞/∞" + } + + loading_progress_bar.isIndeterminate = isIndeterminate || + (progressState.maxProgress > 0 && progressState.currentProgress == 0) + loading_progress_bar.progress = progressState.currentProgress + + loading_progress_bar.max = progressState.maxProgress + } + + private fun handleLoadedState(loadedState: FeedState.LoadedState) { + infoListAdapter.setInfoItemList(loadedState.items) + listState?.run { + items_list.layoutManager?.onRestoreInstanceState(listState) + listState = null + } + + oldestSubscriptionUpdate = loadedState.oldestUpdate + + if (loadedState.notLoadedCount > 0) { + refresh_subtitle_text.visibility = View.VISIBLE + refresh_subtitle_text.text = getString(R.string.feed_subscription_not_loaded_count, loadedState.notLoadedCount) + } else { + refresh_subtitle_text.visibility = View.GONE + } + + if (loadedState.itemsErrors.isNotEmpty()) { + showSnackBarError(loadedState.itemsErrors, UserAction.REQUESTED_FEED, + "none", "Loading feed", R.string.general_error) + } + + if (loadedState.items.isEmpty()) { + showEmptyState() + } else { + hideLoading() + } + } + + private fun handleErrorState(errorState: FeedState.ErrorState): Boolean { + hideLoading() + errorState.error?.let { + onError(errorState.error) + return true + } + return false + } + + private fun updateRelativeTimeViews() { + updateRefreshViewState() + infoListAdapter.notifyDataSetChanged() + } + + private fun updateRefreshViewState() { + val oldestSubscriptionUpdateText = when { + oldestSubscriptionUpdate != null -> Localization.relativeTime(oldestSubscriptionUpdate!!) + else -> "—" + } + + refresh_text?.text = getString(R.string.feed_oldest_subscription_update, oldestSubscriptionUpdateText) + } + + // ///////////////////////////////////////////////////////////////////////// + // Load Service Handling + // ///////////////////////////////////////////////////////////////////////// + + override fun doInitialLoadLogic() {} + override fun reloadContent() = triggerUpdate() + override fun loadMoreItems() {} + override fun hasMoreItems() = false + + private fun triggerUpdate() { + getActivity()?.startService(Intent(requireContext(), FeedLoadService::class.java).apply { + putExtra(FeedLoadService.EXTRA_GROUP_ID, groupId) + }) + listState = null + } + + override fun onError(exception: Throwable): Boolean { + if (super.onError(exception)) return true + + if (useAsFrontPage) { + showSnackBarError(exception, UserAction.REQUESTED_FEED, "none", "Loading Feed", 0) + return true + } + + onUnrecoverableError(exception, UserAction.REQUESTED_FEED, "none", "Loading Feed", 0) + return true + } + + companion object { + const val KEY_GROUP_ID = "ARG_GROUP_ID" + const val KEY_GROUP_NAME = "ARG_GROUP_NAME" + + @JvmStatic + fun newInstance(groupId: Long = FeedGroupEntity.GROUP_ALL_ID, groupName: String? = null): FeedFragment { + val feedFragment = FeedFragment() + + feedFragment.arguments = Bundle().apply { + putLong(KEY_GROUP_ID, groupId) + putString(KEY_GROUP_NAME, groupName) + } + + return feedFragment + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedState.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedState.kt new file mode 100644 index 000000000..de3dd3113 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedState.kt @@ -0,0 +1,24 @@ +package org.schabi.newpipe.local.feed + +import androidx.annotation.StringRes +import java.util.Calendar +import org.schabi.newpipe.extractor.stream.StreamInfoItem + +sealed class FeedState { + data class ProgressState( + val currentProgress: Int = -1, + val maxProgress: Int = -1, + @StringRes val progressMessage: Int = 0 + ) : FeedState() + + data class LoadedState( + val items: List, + val oldestUpdate: Calendar? = null, + val notLoadedCount: Long, + val itemsErrors: List = emptyList() + ) : FeedState() + + data class ErrorState( + val error: Throwable? = null + ) : FeedState() +} diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt new file mode 100644 index 000000000..da2b5ffa4 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt @@ -0,0 +1,75 @@ +package org.schabi.newpipe.local.feed + +import android.content.Context +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import io.reactivex.Flowable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.functions.Function4 +import io.reactivex.schedulers.Schedulers +import java.util.Calendar +import java.util.Date +import java.util.concurrent.TimeUnit +import org.schabi.newpipe.database.feed.model.FeedGroupEntity +import org.schabi.newpipe.extractor.stream.StreamInfoItem +import org.schabi.newpipe.local.feed.service.FeedEventManager +import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ErrorResultEvent +import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.IdleEvent +import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ProgressEvent +import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.SuccessResultEvent +import org.schabi.newpipe.util.DEFAULT_THROTTLE_TIMEOUT + +class FeedViewModel(applicationContext: Context, val groupId: Long = FeedGroupEntity.GROUP_ALL_ID) : ViewModel() { + class Factory(val context: Context, val groupId: Long = FeedGroupEntity.GROUP_ALL_ID) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return FeedViewModel(context.applicationContext, groupId) as T + } + } + + private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(applicationContext) + + private val mutableStateLiveData = MutableLiveData() + val stateLiveData: LiveData = mutableStateLiveData + + private var combineDisposable = Flowable + .combineLatest( + FeedEventManager.events(), + feedDatabaseManager.asStreamItems(groupId), + feedDatabaseManager.notLoadedCount(groupId), + feedDatabaseManager.oldestSubscriptionUpdate(groupId), + + Function4 { t1: FeedEventManager.Event, t2: List, t3: Long, t4: List -> + return@Function4 CombineResultHolder(t1, t2, t3, t4.firstOrNull()) + } + ) + .throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + val (event, listFromDB, notLoadedCount, oldestUpdate) = it + + val oldestUpdateCalendar = + oldestUpdate?.let { Calendar.getInstance().apply { time = it } } + + mutableStateLiveData.postValue(when (event) { + is IdleEvent -> FeedState.LoadedState(listFromDB, oldestUpdateCalendar, notLoadedCount) + is ProgressEvent -> FeedState.ProgressState(event.currentProgress, event.maxProgress, event.progressMessage) + is SuccessResultEvent -> FeedState.LoadedState(listFromDB, oldestUpdateCalendar, notLoadedCount, event.itemsErrors) + is ErrorResultEvent -> FeedState.ErrorState(event.error) + }) + + if (event is ErrorResultEvent || event is SuccessResultEvent) { + FeedEventManager.reset() + } + } + + override fun onCleared() { + super.onCleared() + combineDisposable.dispose() + } + + private data class CombineResultHolder(val t1: FeedEventManager.Event, val t2: List, val t3: Long, val t4: Date?) +} diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedEventManager.kt b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedEventManager.kt new file mode 100644 index 000000000..b72098345 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedEventManager.kt @@ -0,0 +1,37 @@ +package org.schabi.newpipe.local.feed.service + +import androidx.annotation.StringRes +import io.reactivex.Flowable +import io.reactivex.processors.BehaviorProcessor +import java.util.concurrent.atomic.AtomicBoolean +import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.IdleEvent + +object FeedEventManager { + private var processor: BehaviorProcessor = BehaviorProcessor.create() + private var ignoreUpstream = AtomicBoolean() + private var eventsFlowable = processor.startWith(IdleEvent) + + fun postEvent(event: Event) { + processor.onNext(event) + } + + fun events(): Flowable { + return eventsFlowable.filter { !ignoreUpstream.get() } + } + + fun reset() { + ignoreUpstream.set(true) + postEvent(IdleEvent) + ignoreUpstream.set(false) + } + + sealed class Event { + object IdleEvent : Event() + data class ProgressEvent(val currentProgress: Int = -1, val maxProgress: Int = -1, @StringRes val progressMessage: Int = 0) : Event() { + constructor(@StringRes progressMessage: Int) : this(-1, -1, progressMessage) + } + + data class SuccessResultEvent(val itemsErrors: List = emptyList()) : Event() + data class ErrorResultEvent(val error: Throwable) : Event() + } +} diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt new file mode 100644 index 000000000..65860096c --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt @@ -0,0 +1,466 @@ +/* + * Copyright 2019 Mauricio Colli + * FeedLoadService.kt is part of NewPipe + * + * License: GPL-3.0+ + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.schabi.newpipe.local.feed.service + +import android.app.PendingIntent +import android.app.Service +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Build +import android.os.IBinder +import android.preference.PreferenceManager +import android.util.Log +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import io.reactivex.Flowable +import io.reactivex.Notification +import io.reactivex.Single +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.functions.Consumer +import io.reactivex.functions.Function +import io.reactivex.processors.PublishProcessor +import io.reactivex.schedulers.Schedulers +import java.io.IOException +import java.util.Calendar +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger +import org.reactivestreams.Subscriber +import org.reactivestreams.Subscription +import org.schabi.newpipe.MainActivity.DEBUG +import org.schabi.newpipe.R +import org.schabi.newpipe.database.feed.model.FeedGroupEntity +import org.schabi.newpipe.extractor.ListInfo +import org.schabi.newpipe.extractor.exceptions.ReCaptchaException +import org.schabi.newpipe.extractor.stream.StreamInfoItem +import org.schabi.newpipe.local.feed.FeedDatabaseManager +import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ErrorResultEvent +import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.IdleEvent +import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ProgressEvent +import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.SuccessResultEvent +import org.schabi.newpipe.local.feed.service.FeedEventManager.postEvent +import org.schabi.newpipe.local.subscription.SubscriptionManager +import org.schabi.newpipe.util.ExceptionUtils +import org.schabi.newpipe.util.ExtractorHelper + +class FeedLoadService : Service() { + companion object { + private val TAG = FeedLoadService::class.java.simpleName + private const val NOTIFICATION_ID = 7293450 + private const val ACTION_CANCEL = "org.schabi.newpipe.local.feed.service.FeedLoadService.CANCEL" + + /** + * How often the notification will be updated. + */ + private const val NOTIFICATION_SAMPLING_PERIOD = 1500 + + /** + * How many extractions will be running in parallel. + */ + private const val PARALLEL_EXTRACTIONS = 6 + + /** + * Number of items to buffer to mass-insert in the database. + */ + private const val BUFFER_COUNT_BEFORE_INSERT = 20 + + const val EXTRA_GROUP_ID: String = "FeedLoadService.EXTRA_GROUP_ID" + } + + private var loadingSubscription: Subscription? = null + private lateinit var subscriptionManager: SubscriptionManager + + private lateinit var feedDatabaseManager: FeedDatabaseManager + private lateinit var feedResultsHolder: ResultsHolder + + private var disposables = CompositeDisposable() + private var notificationUpdater = PublishProcessor.create() + + // ///////////////////////////////////////////////////////////////////////// + // Lifecycle + // ///////////////////////////////////////////////////////////////////////// + + override fun onCreate() { + super.onCreate() + subscriptionManager = SubscriptionManager(this) + feedDatabaseManager = FeedDatabaseManager(this) + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + if (DEBUG) { + Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "]," + + " flags = [" + flags + "], startId = [" + startId + "]") + } + + if (intent == null || loadingSubscription != null) { + return START_NOT_STICKY + } + + setupNotification() + setupBroadcastReceiver() + val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) + + val groupId = intent.getLongExtra(EXTRA_GROUP_ID, FeedGroupEntity.GROUP_ALL_ID) + val useFeedExtractor = defaultSharedPreferences + .getBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), false) + + val thresholdOutdatedSecondsString = defaultSharedPreferences + .getString(getString(R.string.feed_update_threshold_key), getString(R.string.feed_update_threshold_default_value)) + val thresholdOutdatedSeconds = thresholdOutdatedSecondsString!!.toInt() + + startLoading(groupId, useFeedExtractor, thresholdOutdatedSeconds) + + return START_NOT_STICKY + } + + private fun disposeAll() { + unregisterReceiver(broadcastReceiver) + + loadingSubscription?.cancel() + loadingSubscription = null + + disposables.dispose() + } + + private fun stopService() { + disposeAll() + stopForeground(true) + notificationManager.cancel(NOTIFICATION_ID) + stopSelf() + } + + override fun onBind(intent: Intent): IBinder? { + return null + } + + // ///////////////////////////////////////////////////////////////////////// + // Loading & Handling + // ///////////////////////////////////////////////////////////////////////// + + private class RequestException(val subscriptionId: Long, message: String, cause: Throwable) : Exception(message, cause) { + companion object { + fun wrapList(subscriptionId: Long, info: ListInfo): List { + val toReturn = ArrayList(info.errors.size) + for (error in info.errors) { + toReturn.add(RequestException(subscriptionId, info.serviceId.toString() + ":" + info.url, error)) + } + return toReturn + } + } + } + + private fun startLoading(groupId: Long = FeedGroupEntity.GROUP_ALL_ID, useFeedExtractor: Boolean, thresholdOutdatedSeconds: Int) { + feedResultsHolder = ResultsHolder() + + val outdatedThreshold = Calendar.getInstance().apply { + add(Calendar.SECOND, -thresholdOutdatedSeconds) + }.time + + val subscriptions = when (groupId) { + FeedGroupEntity.GROUP_ALL_ID -> feedDatabaseManager.outdatedSubscriptions(outdatedThreshold) + else -> feedDatabaseManager.outdatedSubscriptionsForGroup(groupId, outdatedThreshold) + } + + subscriptions + .limit(1) + + .doOnNext { + currentProgress.set(0) + maxProgress.set(it.size) + } + .filter { it.isNotEmpty() } + + .observeOn(AndroidSchedulers.mainThread()) + .doOnNext { + startForeground(NOTIFICATION_ID, notificationBuilder.build()) + updateNotificationProgress(null) + broadcastProgress() + } + + .observeOn(Schedulers.io()) + .flatMap { Flowable.fromIterable(it) } + .takeWhile { !cancelSignal.get() } + + .parallel(PARALLEL_EXTRACTIONS, PARALLEL_EXTRACTIONS * 2) + .runOn(Schedulers.io(), PARALLEL_EXTRACTIONS * 2) + .filter { !cancelSignal.get() } + + .map { subscriptionEntity -> + try { + val listInfo = if (useFeedExtractor) { + ExtractorHelper + .getFeedInfoFallbackToChannelInfo(subscriptionEntity.serviceId, subscriptionEntity.url) + .blockingGet() + } else { + ExtractorHelper + .getChannelInfo(subscriptionEntity.serviceId, subscriptionEntity.url, true) + .blockingGet() + } as ListInfo + + return@map Notification.createOnNext(Pair(subscriptionEntity.uid, listInfo)) + } catch (e: Throwable) { + val request = "${subscriptionEntity.serviceId}:${subscriptionEntity.url}" + val wrapper = RequestException(subscriptionEntity.uid, request, e) + return@map Notification.createOnError>>(wrapper) + } + } + .sequential() + + .observeOn(AndroidSchedulers.mainThread()) + .doOnNext(errorHandlingConsumer) + + .observeOn(AndroidSchedulers.mainThread()) + .doOnNext(notificationsConsumer) + + .observeOn(Schedulers.io()) + .buffer(BUFFER_COUNT_BEFORE_INSERT) + .doOnNext(databaseConsumer) + + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(resultSubscriber) + } + + private fun broadcastProgress() { + postEvent(ProgressEvent(currentProgress.get(), maxProgress.get())) + } + + private val resultSubscriber + get() = object : Subscriber>>>> { + + override fun onSubscribe(s: Subscription) { + loadingSubscription = s + s.request(java.lang.Long.MAX_VALUE) + } + + override fun onNext(notification: List>>>) { + if (DEBUG) Log.v(TAG, "onNext() → $notification") + } + + override fun onError(error: Throwable) { + handleError(error) + } + + override fun onComplete() { + if (maxProgress.get() == 0) { + postEvent(IdleEvent) + stopService() + + return + } + + currentProgress.set(-1) + maxProgress.set(-1) + + notificationUpdater.onNext(getString(R.string.feed_processing_message)) + postEvent(ProgressEvent(R.string.feed_processing_message)) + + disposables.add(Single + .fromCallable { + feedResultsHolder.ready() + + postEvent(ProgressEvent(R.string.feed_processing_message)) + feedDatabaseManager.removeOrphansOrOlderStreams() + + postEvent(SuccessResultEvent(feedResultsHolder.itemsErrors)) + true + } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { _, throwable -> + if (throwable != null) { + Log.e(TAG, "Error while storing result", throwable) + handleError(throwable) + return@subscribe + } + stopService() + }) + } + } + + private val databaseConsumer: Consumer>>>> + get() = Consumer { + feedDatabaseManager.database().runInTransaction { + for (notification in it) { + + if (notification.isOnNext) { + val subscriptionId = notification.value!!.first + val info = notification.value!!.second + + feedDatabaseManager.upsertAll(subscriptionId, info.relatedItems) + subscriptionManager.updateFromInfo(subscriptionId, info) + + if (info.errors.isNotEmpty()) { + feedResultsHolder.addErrors(RequestException.wrapList(subscriptionId, info)) + feedDatabaseManager.markAsOutdated(subscriptionId) + } + } else if (notification.isOnError) { + val error = notification.error!! + feedResultsHolder.addError(error) + + if (error is RequestException) { + feedDatabaseManager.markAsOutdated(error.subscriptionId) + } + } + } + } + } + + private val errorHandlingConsumer: Consumer>>> + get() = Consumer { + if (it.isOnError) { + var error = it.error!! + if (error is RequestException) error = error.cause!! + val cause = error.cause + + when { + error is ReCaptchaException -> throw error + cause is ReCaptchaException -> throw cause + + error is IOException -> throw error + cause is IOException -> throw cause + ExceptionUtils.isNetworkRelated(error) -> throw IOException(error) + } + } + } + + private val notificationsConsumer: Consumer>>> + get() = Consumer { onItemCompleted(it.value?.second?.name) } + + private fun onItemCompleted(updateDescription: String?) { + currentProgress.incrementAndGet() + notificationUpdater.onNext(updateDescription ?: "") + + broadcastProgress() + } + + // ///////////////////////////////////////////////////////////////////////// + // Notification + // ///////////////////////////////////////////////////////////////////////// + + private lateinit var notificationManager: NotificationManagerCompat + private lateinit var notificationBuilder: NotificationCompat.Builder + + private var currentProgress = AtomicInteger(-1) + private var maxProgress = AtomicInteger(-1) + + private fun createNotification(): NotificationCompat.Builder { + val cancelActionIntent = PendingIntent.getBroadcast(this, + NOTIFICATION_ID, Intent(ACTION_CANCEL), 0) + + return NotificationCompat.Builder(this, getString(R.string.notification_channel_id)) + .setOngoing(true) + .setProgress(-1, -1, true) + .setSmallIcon(R.drawable.ic_newpipe_triangle_white) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .addAction(0, getString(R.string.cancel), cancelActionIntent) + .setContentTitle(getString(R.string.feed_notification_loading)) + } + + private fun setupNotification() { + notificationManager = NotificationManagerCompat.from(this) + notificationBuilder = createNotification() + + val throttleAfterFirstEmission = Function { flow: Flowable -> + flow.limit(1).concatWith(flow.skip(1).throttleLatest(NOTIFICATION_SAMPLING_PERIOD.toLong(), TimeUnit.MILLISECONDS)) + } + + disposables.add(notificationUpdater + .publish(throttleAfterFirstEmission) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::updateNotificationProgress)) + } + + private fun updateNotificationProgress(updateDescription: String?) { + notificationBuilder.setProgress(maxProgress.get(), currentProgress.get(), maxProgress.get() == -1) + + if (maxProgress.get() == -1) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) notificationBuilder.setContentInfo(null) + if (!updateDescription.isNullOrEmpty()) notificationBuilder.setContentText(updateDescription) + notificationBuilder.setContentText(updateDescription) + } else { + val progressText = this.currentProgress.toString() + "/" + maxProgress + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (!updateDescription.isNullOrEmpty()) notificationBuilder.setContentText("$updateDescription ($progressText)") + } else { + notificationBuilder.setContentInfo(progressText) + if (!updateDescription.isNullOrEmpty()) notificationBuilder.setContentText(updateDescription) + } + } + + notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()) + } + + // ///////////////////////////////////////////////////////////////////////// + // Notification Actions + // ///////////////////////////////////////////////////////////////////////// + + private lateinit var broadcastReceiver: BroadcastReceiver + private val cancelSignal = AtomicBoolean() + + private fun setupBroadcastReceiver() { + broadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + if (intent?.action == ACTION_CANCEL) { + cancelSignal.set(true) + } + } + } + registerReceiver(broadcastReceiver, IntentFilter(ACTION_CANCEL)) + } + + // ///////////////////////////////////////////////////////////////////////// + // Error handling + // ///////////////////////////////////////////////////////////////////////// + + private fun handleError(error: Throwable) { + postEvent(ErrorResultEvent(error)) + stopService() + } + + // ///////////////////////////////////////////////////////////////////////// + // Results Holder + // ///////////////////////////////////////////////////////////////////////// + + class ResultsHolder { + /** + * List of errors that may have happen during loading. + */ + internal lateinit var itemsErrors: List + + private val itemsErrorsHolder: MutableList = ArrayList() + + fun addError(error: Throwable) { + itemsErrorsHolder.add(error) + } + + fun addErrors(errors: List) { + itemsErrorsHolder.addAll(errors) + } + + fun ready() { + itemsErrors = itemsErrorsHolder.toList() + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/local/history/HistoryEntryAdapter.java b/app/src/main/java/org/schabi/newpipe/local/history/HistoryEntryAdapter.java index c4ca08a0a..e7ccd07d2 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/HistoryEntryAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryEntryAdapter.java @@ -1,6 +1,7 @@ package org.schabi.newpipe.local.history; import android.content.Context; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; @@ -14,19 +15,19 @@ import java.util.Date; /** - * Adapter for history entries - * @param the type of the entries + * This is an adapter for history entries. + * + * @param the type of the entries * @param the type of the view holder */ -public abstract class HistoryEntryAdapter extends RecyclerView.Adapter { - +public abstract class HistoryEntryAdapter + extends RecyclerView.Adapter { private final ArrayList mEntries; private final DateFormat mDateFormat; private final Context mContext; private OnHistoryItemClickListener onHistoryItemClickListener = null; - - public HistoryEntryAdapter(Context context) { + public HistoryEntryAdapter(final Context context) { super(); mContext = context; mEntries = new ArrayList<>(); @@ -34,7 +35,7 @@ public abstract class HistoryEntryAdapter Localization.getPreferredLocale(context)); } - public void setEntries(@NonNull Collection historyEntries) { + public void setEntries(@NonNull final Collection historyEntries) { mEntries.clear(); mEntries.addAll(historyEntries); notifyDataSetChanged(); @@ -49,7 +50,7 @@ public abstract class HistoryEntryAdapter notifyDataSetChanged(); } - protected String getFormattedDate(Date date) { + protected String getFormattedDate(final Date date) { return mDateFormat.format(date); } @@ -63,10 +64,10 @@ public abstract class HistoryEntryAdapter } @Override - public void onBindViewHolder(VH holder, int position) { + public void onBindViewHolder(final VH holder, final int position) { final E entry = mEntries.get(position); holder.itemView.setOnClickListener(v -> { - if(onHistoryItemClickListener != null) { + if (onHistoryItemClickListener != null) { onHistoryItemClickListener.onHistoryItemClick(entry); } }); @@ -83,14 +84,15 @@ public abstract class HistoryEntryAdapter } @Override - public void onViewRecycled(VH holder) { + public void onViewRecycled(final VH holder) { super.onViewRecycled(holder); holder.itemView.setOnClickListener(null); } abstract void onBindViewHolder(VH holder, E entry, int position); - public void setOnHistoryItemClickListener(@Nullable OnHistoryItemClickListener onHistoryItemClickListener) { + public void setOnHistoryItemClickListener( + @Nullable final OnHistoryItemClickListener onHistoryItemClickListener) { this.onHistoryItemClickListener = onHistoryItemClickListener; } @@ -100,6 +102,7 @@ public abstract class HistoryEntryAdapter public interface OnHistoryItemClickListener { void onHistoryItemClick(E item); + void onHistoryItemLongClick(E item); } } diff --git a/app/src/main/java/org/schabi/newpipe/local/history/HistoryListener.java b/app/src/main/java/org/schabi/newpipe/local/history/HistoryListener.java deleted file mode 100644 index fc039f770..000000000 --- a/app/src/main/java/org/schabi/newpipe/local/history/HistoryListener.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.schabi.newpipe.local.history; - -import androidx.annotation.Nullable; - -import org.schabi.newpipe.extractor.stream.AudioStream; -import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.extractor.stream.VideoStream; - -public interface HistoryListener { - /** - * Called when a video is played - * - * @param streamInfo the stream info - * @param videoStream the video stream that is played. Can be null if it's not sure what - * quality was viewed (e.g. with Kodi). - */ - void onVideoPlayed(StreamInfo streamInfo, @Nullable VideoStream videoStream); - - /** - * Called when the audio is played in the background - * - * @param streamInfo the stream info - * @param audioStream the audio stream that is played - */ - void onAudioPlayed(StreamInfo streamInfo, AudioStream audioStream); - - /** - * Called when the user searched for something - * - * @param serviceId which service the search was done - * @param query what the user searched for - */ - void onSearch(int serviceId, String query); -} diff --git a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java index d84fe0195..96a385ca8 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java +++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java @@ -21,6 +21,7 @@ package org.schabi.newpipe.local.history; import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; + import androidx.annotation.NonNull; import org.schabi.newpipe.NewPipeDatabase; @@ -55,7 +56,6 @@ import io.reactivex.Single; import io.reactivex.schedulers.Schedulers; public class HistoryRecordManager { - private final AppDatabase database; private final StreamDAO streamTable; private final StreamHistoryDAO streamHistoryTable; @@ -81,7 +81,9 @@ public class HistoryRecordManager { /////////////////////////////////////////////////////// public Maybe onViewed(final StreamInfo info) { - if (!isStreamHistoryEnabled()) return Maybe.empty(); + if (!isStreamHistoryEnabled()) { + return Maybe.empty(); + } final Date currentTime = new Date(); return Maybe.fromCallable(() -> database.runInTransaction(() -> { @@ -118,6 +120,10 @@ public class HistoryRecordManager { return streamHistoryTable.getHistory().subscribeOn(Schedulers.io()); } + public Flowable> getStreamHistorySortedById() { + return streamHistoryTable.getHistorySortedById().subscribeOn(Schedulers.io()); + } + public Flowable> getStreamStatistics() { return streamHistoryTable.getStatistics().subscribeOn(Schedulers.io()); } @@ -149,7 +155,9 @@ public class HistoryRecordManager { /////////////////////////////////////////////////////// public Maybe onSearched(final int serviceId, final String search) { - if (!isSearchHistoryEnabled()) return Maybe.empty(); + if (!isSearchHistoryEnabled()) { + return Maybe.empty(); + } final Date currentTime = new Date(); final SearchHistoryEntry newEntry = new SearchHistoryEntry(currentTime, serviceId, search); @@ -231,11 +239,13 @@ public class HistoryRecordManager { public Single loadStreamState(final InfoItem info) { return Single.fromCallable(() -> { - final List entities = streamTable.getStream(info.getServiceId(), info.getUrl()).blockingFirst(); + final List entities = streamTable + .getStream(info.getServiceId(), info.getUrl()).blockingFirst(); if (entities.isEmpty()) { return new StreamStateEntity[]{null}; } - final List states = streamStateTable.getState(entities.get(0).getUid()).blockingFirst(); + final List states = streamStateTable + .getState(entities.get(0).getUid()).blockingFirst(); if (states.isEmpty()) { return new StreamStateEntity[]{null}; } @@ -247,12 +257,14 @@ public class HistoryRecordManager { return Single.fromCallable(() -> { final List result = new ArrayList<>(infos.size()); for (InfoItem info : infos) { - final List entities = streamTable.getStream(info.getServiceId(), info.getUrl()).blockingFirst(); + final List entities = streamTable + .getStream(info.getServiceId(), info.getUrl()).blockingFirst(); if (entities.isEmpty()) { result.add(null); continue; } - final List states = streamStateTable.getState(entities.get(0).getUid()).blockingFirst(); + final List states = streamStateTable + .getState(entities.get(0).getUid()).blockingFirst(); if (states.isEmpty()) { result.add(null); continue; @@ -263,22 +275,24 @@ public class HistoryRecordManager { }).subscribeOn(Schedulers.io()); } - public Single> loadLocalStreamStateBatch(final List items) { + public Single> loadLocalStreamStateBatch( + final List items) { return Single.fromCallable(() -> { final List result = new ArrayList<>(items.size()); for (LocalItem item : items) { long streamId; if (item instanceof StreamStatisticsEntry) { - streamId = ((StreamStatisticsEntry) item).streamId; + streamId = ((StreamStatisticsEntry) item).getStreamId(); } else if (item instanceof PlaylistStreamEntity) { streamId = ((PlaylistStreamEntity) item).getStreamUid(); } else if (item instanceof PlaylistStreamEntry) { - streamId = ((PlaylistStreamEntry) item).streamId; + streamId = ((PlaylistStreamEntry) item).getStreamId(); } else { result.add(null); continue; } - final List states = streamStateTable.getState(streamId).blockingFirst(); + final List states = streamStateTable.getState(streamId) + .blockingFirst(); if (states.isEmpty()) { result.add(null); continue; diff --git a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java index e40549b88..4b5ad31a3 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java @@ -4,10 +4,6 @@ import android.app.Activity; import android.content.Context; import android.os.Bundle; import android.os.Parcelable; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.google.android.material.snackbar.Snackbar; -import androidx.appcompat.app.AlertDialog; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -18,6 +14,12 @@ import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; + +import com.google.android.material.snackbar.Snackbar; + import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.schabi.newpipe.R; @@ -48,7 +50,10 @@ import io.reactivex.disposables.Disposable; public class StatisticsPlaylistFragment extends BaseLocalListFragment, Void> { - + private final CompositeDisposable disposables = new CompositeDisposable(); + @State + Parcelable itemsListState; + private StatisticSortMode sortMode = StatisticSortMode.LAST_PLAYED; private View headerPlayAllButton; private View headerPopupButton; private View headerBackgroundButton; @@ -56,33 +61,22 @@ public class StatisticsPlaylistFragment private View sortButton; private ImageView sortButtonIcon; private TextView sortButtonText; - - @State - protected Parcelable itemsListState; - /* Used for independent events */ private Subscription databaseSubscription; private HistoryRecordManager recordManager; - private final CompositeDisposable disposables = new CompositeDisposable(); - private enum StatisticSortMode { - LAST_PLAYED, - MOST_PLAYED, - } - - StatisticSortMode sortMode = StatisticSortMode.LAST_PLAYED; - - protected List processResult(final List results) { + private List processResult(final List results) { switch (sortMode) { case LAST_PLAYED: Collections.sort(results, (left, right) -> - right.latestAccessDate.compareTo(left.latestAccessDate)); + right.getLatestAccessDate().compareTo(left.getLatestAccessDate())); return results; case MOST_PLAYED: Collections.sort(results, (left, right) -> - Long.compare(right.watchCount, left.watchCount)); + Long.compare(right.getWatchCount(), left.getWatchCount())); return results; - default: return null; + default: + return null; } } @@ -91,20 +85,20 @@ public class StatisticsPlaylistFragment /////////////////////////////////////////////////////////////////////////// @Override - public void onCreate(Bundle savedInstanceState) { + public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); recordManager = new HistoryRecordManager(getContext()); } @Override - public View onCreateView(@NonNull LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { + public View onCreateView(@NonNull final LayoutInflater inflater, + @Nullable final ViewGroup container, + @Nullable final Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_playlist, container, false); } @Override - public void setUserVisibleHint(boolean isVisibleToUser) { + public void setUserVisibleHint(final boolean isVisibleToUser) { super.setUserVisibleHint(isVisibleToUser); if (activity != null && isVisibleToUser) { setTitle(activity.getString(R.string.title_activity_history)); @@ -112,7 +106,7 @@ public class StatisticsPlaylistFragment } @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); inflater.inflate(R.menu.menu_history, menu); } @@ -122,17 +116,17 @@ public class StatisticsPlaylistFragment /////////////////////////////////////////////////////////////////////////// @Override - protected void initViews(View rootView, Bundle savedInstanceState) { + protected void initViews(final View rootView, final Bundle savedInstanceState) { super.initViews(rootView, savedInstanceState); - if(!useAsFrontPage) { + if (!useAsFrontPage) { setTitle(getString(R.string.title_last_played)); } } @Override protected View getListHeader() { - final View headerRootLayout = activity.getLayoutInflater().inflate(R.layout.statistic_playlist_control, - itemsList, false); + final View headerRootLayout = activity.getLayoutInflater() + .inflate(R.layout.statistic_playlist_control, itemsList, false); playlistCtrl = headerRootLayout.findViewById(R.id.playlist_control); headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_all_button); headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button); @@ -149,18 +143,18 @@ public class StatisticsPlaylistFragment itemListAdapter.setSelectedListener(new OnClickGesture() { @Override - public void selected(LocalItem selectedItem) { + public void selected(final LocalItem selectedItem) { if (selectedItem instanceof StreamStatisticsEntry) { final StreamStatisticsEntry item = (StreamStatisticsEntry) selectedItem; NavigationHelper.openVideoDetailFragment(getFM(), - item.serviceId, - item.url, - item.title); + item.getStreamEntity().getServiceId(), + item.getStreamEntity().getUrl(), + item.getStreamEntity().getTitle()); } } @Override - public void held(LocalItem selectedItem) { + public void held(final LocalItem selectedItem) { if (selectedItem instanceof StreamStatisticsEntry) { showStreamDialog((StreamStatisticsEntry) selectedItem); } @@ -169,7 +163,7 @@ public class StatisticsPlaylistFragment } @Override - public boolean onOptionsItemSelected(MenuItem item) { + public boolean onOptionsItemSelected(final MenuItem item) { switch (item.getItemId()) { case R.id.action_history_clear: new AlertDialog.Builder(activity) @@ -194,7 +188,8 @@ public class StatisticsPlaylistFragment final Disposable onClearOrphans = recordManager.removeOrphanedRecords() .observeOn(AndroidSchedulers.mainThread()) .subscribe( - howManyDeleted -> {}, + howManyDeleted -> { + }, throwable -> ErrorActivity.reportError(getContext(), throwable, SettingsActivity.class, null, @@ -220,7 +215,7 @@ public class StatisticsPlaylistFragment /////////////////////////////////////////////////////////////////////////// @Override - public void startLoading(boolean forceLoad) { + public void startLoading(final boolean forceLoad) { super.startLoading(forceLoad); recordManager.getStreamStatistics() .observeOn(AndroidSchedulers.mainThread()) @@ -241,12 +236,22 @@ public class StatisticsPlaylistFragment public void onDestroyView() { super.onDestroyView(); - if (itemListAdapter != null) itemListAdapter.unsetSelectedListener(); - if (headerBackgroundButton != null) headerBackgroundButton.setOnClickListener(null); - if (headerPlayAllButton != null) headerPlayAllButton.setOnClickListener(null); - if (headerPopupButton != null) headerPopupButton.setOnClickListener(null); + if (itemListAdapter != null) { + itemListAdapter.unsetSelectedListener(); + } + if (headerBackgroundButton != null) { + headerBackgroundButton.setOnClickListener(null); + } + if (headerPlayAllButton != null) { + headerPlayAllButton.setOnClickListener(null); + } + if (headerPopupButton != null) { + headerPopupButton.setOnClickListener(null); + } - if (databaseSubscription != null) databaseSubscription.cancel(); + if (databaseSubscription != null) { + databaseSubscription.cancel(); + } databaseSubscription = null; } @@ -264,22 +269,26 @@ public class StatisticsPlaylistFragment private Subscriber> getHistoryObserver() { return new Subscriber>() { @Override - public void onSubscribe(Subscription s) { + public void onSubscribe(final Subscription s) { showLoading(); - if (databaseSubscription != null) databaseSubscription.cancel(); + if (databaseSubscription != null) { + databaseSubscription.cancel(); + } databaseSubscription = s; databaseSubscription.request(1); } @Override - public void onNext(List streams) { + public void onNext(final List streams) { handleResult(streams); - if (databaseSubscription != null) databaseSubscription.request(1); + if (databaseSubscription != null) { + databaseSubscription.request(1); + } } @Override - public void onError(Throwable exception) { + public void onError(final Throwable exception) { StatisticsPlaylistFragment.this.onError(exception); } @@ -290,9 +299,11 @@ public class StatisticsPlaylistFragment } @Override - public void handleResult(@NonNull List result) { + public void handleResult(@NonNull final List result) { super.handleResult(result); - if (itemListAdapter == null) return; + if (itemListAdapter == null) { + return; + } playlistCtrl.setVisibility(View.VISIBLE); @@ -319,6 +330,7 @@ public class StatisticsPlaylistFragment hideLoading(); } + /////////////////////////////////////////////////////////////////////////// // Fragment Error Handling /////////////////////////////////////////////////////////////////////////// @@ -326,12 +338,16 @@ public class StatisticsPlaylistFragment @Override protected void resetFragment() { super.resetFragment(); - if (databaseSubscription != null) databaseSubscription.cancel(); + if (databaseSubscription != null) { + databaseSubscription.cancel(); + } } @Override - protected boolean onError(Throwable exception) { - if (super.onError(exception)) return true; + protected boolean onError(final Throwable exception) { + if (super.onError(exception)) { + return true; + } onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, "none", "History Statistics", R.string.general_error); @@ -343,28 +359,32 @@ public class StatisticsPlaylistFragment //////////////////////////////////////////////////////////////////////////*/ private void toggleSortMode() { - if(sortMode == StatisticSortMode.LAST_PLAYED) { + if (sortMode == StatisticSortMode.LAST_PLAYED) { sortMode = StatisticSortMode.MOST_PLAYED; setTitle(getString(R.string.title_most_played)); - sortButtonIcon.setImageResource(ThemeHelper.getIconByAttr(R.attr.history, getContext())); + sortButtonIcon.setImageResource( + ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_history)); sortButtonText.setText(R.string.title_last_played); } else { sortMode = StatisticSortMode.LAST_PLAYED; setTitle(getString(R.string.title_last_played)); - sortButtonIcon.setImageResource(ThemeHelper.getIconByAttr(R.attr.filter, getContext())); + sortButtonIcon.setImageResource( + ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_filter_list)); sortButtonText.setText(R.string.title_most_played); } startLoading(true); } - private PlayQueue getPlayQueueStartingAt(StreamStatisticsEntry infoItem) { + private PlayQueue getPlayQueueStartingAt(final StreamStatisticsEntry infoItem) { return getPlayQueue(Math.max(itemListAdapter.getItemsList().indexOf(infoItem), 0)); } private void showStreamDialog(final StreamStatisticsEntry item) { final Context context = getContext(); final Activity activity = getActivity(); - if (context == null || context.getResources() == null || activity == null) return; + if (context == null || context.getResources() == null || activity == null) { + return; + } final StreamInfoItem infoItem = item.toStreamInfoItem(); if (infoItem.getStreamType() == StreamType.AUDIO_STREAM) { @@ -384,29 +404,31 @@ public class StatisticsPlaylistFragment StreamDialogEntry.append_playlist, StreamDialogEntry.share); - StreamDialogEntry.start_here_on_popup.setCustomAction( - (fragment, infoItemDuplicate) -> NavigationHelper.playOnPopupPlayer(context, getPlayQueueStartingAt(item), true)); + StreamDialogEntry.start_here_on_popup.setCustomAction((fragment, infoItemDuplicate) -> + NavigationHelper + .playOnPopupPlayer(context, getPlayQueueStartingAt(item), true)); } - StreamDialogEntry.start_here_on_background.setCustomAction( - (fragment, infoItemDuplicate) -> NavigationHelper.playOnBackgroundPlayer(context, getPlayQueueStartingAt(item), true)); + StreamDialogEntry.start_here_on_background.setCustomAction((fragment, infoItemDuplicate) -> + NavigationHelper + .playOnBackgroundPlayer(context, getPlayQueueStartingAt(item), true)); StreamDialogEntry.delete.setCustomAction((fragment, infoItemDuplicate) -> - deleteEntry(Math.max(itemListAdapter.getItemsList().indexOf(item), 0))); + deleteEntry(Math.max(itemListAdapter.getItemsList().indexOf(item), 0))); - new InfoItemDialog(activity, infoItem, StreamDialogEntry.getCommands(context), (dialog, which) -> - StreamDialogEntry.clickOn(which, this, infoItem)).show(); + new InfoItemDialog(activity, infoItem, StreamDialogEntry.getCommands(context), + (dialog, which) -> StreamDialogEntry.clickOn(which, this, infoItem)).show(); } private void deleteEntry(final int index) { final LocalItem infoItem = itemListAdapter.getItemsList() .get(index); - if(infoItem instanceof StreamStatisticsEntry) { + if (infoItem instanceof StreamStatisticsEntry) { final StreamStatisticsEntry entry = (StreamStatisticsEntry) infoItem; - final Disposable onDelete = recordManager.deleteStreamHistory(entry.streamId) + final Disposable onDelete = recordManager.deleteStreamHistory(entry.getStreamId()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( howManyDeleted -> { - if(getView() != null) { + if (getView() != null) { Snackbar.make(getView(), R.string.one_item_deleted, Snackbar.LENGTH_SHORT).show(); } else { @@ -441,5 +463,10 @@ public class StatisticsPlaylistFragment } return new SinglePlayQueue(streamInfoItems, index); } + + private enum StatisticSortMode { + LAST_PLAYED, + MOST_PLAYED, + } } diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalItemHolder.java index f9da969a5..c4307fcde 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalItemHolder.java @@ -1,9 +1,10 @@ package org.schabi.newpipe.local.holder; -import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; import android.view.ViewGroup; +import androidx.recyclerview.widget.RecyclerView; + import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.local.LocalItemBuilder; import org.schabi.newpipe.local.history.HistoryRecordManager; @@ -33,14 +34,15 @@ import java.text.DateFormat; public abstract class LocalItemHolder extends RecyclerView.ViewHolder { protected final LocalItemBuilder itemBuilder; - public LocalItemHolder(LocalItemBuilder itemBuilder, int layoutId, ViewGroup parent) { - super(LayoutInflater.from(itemBuilder.getContext()) - .inflate(layoutId, parent, false)); + public LocalItemHolder(final LocalItemBuilder itemBuilder, final int layoutId, + final ViewGroup parent) { + super(LayoutInflater.from(itemBuilder.getContext()).inflate(layoutId, parent, false)); this.itemBuilder = itemBuilder; } - public abstract void updateFromItem(final LocalItem item, HistoryRecordManager historyRecordManager, final DateFormat dateFormat); + public abstract void updateFromItem(LocalItem item, HistoryRecordManager historyRecordManager, + DateFormat dateFormat); - public void updateState(final LocalItem localItem, HistoryRecordManager historyRecordManager) { - } + public void updateState(final LocalItem localItem, + final HistoryRecordManager historyRecordManager) { } } diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistGridItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistGridItemHolder.java index 4276cf721..2b493f4ee 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistGridItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistGridItemHolder.java @@ -6,8 +6,8 @@ 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); - } + public LocalPlaylistGridItemHolder(final LocalItemBuilder infoItemBuilder, + final ViewGroup parent) { + super(infoItemBuilder, R.layout.list_playlist_grid_item, parent); + } } diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java index 1366bd02e..458b3c30e 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java @@ -8,26 +8,32 @@ import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; import org.schabi.newpipe.local.LocalItemBuilder; import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.util.ImageDisplayConstants; +import org.schabi.newpipe.util.Localization; import java.text.DateFormat; public class LocalPlaylistItemHolder extends PlaylistItemHolder { - - public LocalPlaylistItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) { + public LocalPlaylistItemHolder(final LocalItemBuilder infoItemBuilder, final ViewGroup parent) { super(infoItemBuilder, parent); } - LocalPlaylistItemHolder(LocalItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) { + LocalPlaylistItemHolder(final LocalItemBuilder infoItemBuilder, final int layoutId, + final ViewGroup parent) { super(infoItemBuilder, layoutId, parent); } @Override - public void updateFromItem(final LocalItem localItem, HistoryRecordManager historyRecordManager, final DateFormat dateFormat) { - if (!(localItem instanceof PlaylistMetadataEntry)) return; + public void updateFromItem(final LocalItem localItem, + final HistoryRecordManager historyRecordManager, + final DateFormat dateFormat) { + if (!(localItem instanceof PlaylistMetadataEntry)) { + return; + } final PlaylistMetadataEntry item = (PlaylistMetadataEntry) localItem; itemTitleView.setText(item.name); - itemStreamCountView.setText(String.valueOf(item.streamCount)); + itemStreamCountView.setText(Localization.localizeStreamCountMini( + itemStreamCountView.getContext(), item.streamCount)); itemUploaderView.setVisibility(View.INVISIBLE); itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamGridItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamGridItemHolder.java index 6986713bb..e2f936792 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamGridItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamGridItemHolder.java @@ -6,8 +6,8 @@ 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 - } + public LocalPlaylistStreamGridItemHolder(final LocalItemBuilder infoItemBuilder, + final ViewGroup parent) { + super(infoItemBuilder, R.layout.list_stream_playlist_grid_item, parent); // TODO + } } diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java index 30cc6de32..ece5f0994 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java @@ -1,12 +1,13 @@ package org.schabi.newpipe.local.holder; -import androidx.core.content.ContextCompat; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; +import androidx.core.content.ContextCompat; + import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; @@ -24,15 +25,15 @@ import java.util.ArrayList; import java.util.concurrent.TimeUnit; public class LocalPlaylistStreamItemHolder extends LocalItemHolder { - public final ImageView itemThumbnailView; public final TextView itemVideoTitleView; - public final TextView itemAdditionalDetailsView; + private final TextView itemAdditionalDetailsView; public final TextView itemDurationView; - public final View itemHandleView; - public final AnimatedProgressBar itemProgressView; + private final View itemHandleView; + private final AnimatedProgressBar itemProgressView; - LocalPlaylistStreamItemHolder(LocalItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) { + LocalPlaylistStreamItemHolder(final LocalItemBuilder infoItemBuilder, final int layoutId, + final ViewGroup parent) { super(infoItemBuilder, layoutId, parent); itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView); @@ -43,30 +44,41 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder { itemProgressView = itemView.findViewById(R.id.itemProgressView); } - public LocalPlaylistStreamItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) { + public LocalPlaylistStreamItemHolder(final LocalItemBuilder infoItemBuilder, + final ViewGroup parent) { this(infoItemBuilder, R.layout.list_stream_playlist_item, parent); } @Override - public void updateFromItem(final LocalItem localItem, HistoryRecordManager historyRecordManager, final DateFormat dateFormat) { - if (!(localItem instanceof PlaylistStreamEntry)) return; + public void updateFromItem(final LocalItem localItem, + final HistoryRecordManager historyRecordManager, + final DateFormat dateFormat) { + if (!(localItem instanceof PlaylistStreamEntry)) { + return; + } final PlaylistStreamEntry item = (PlaylistStreamEntry) localItem; - itemVideoTitleView.setText(item.title); - itemAdditionalDetailsView.setText(Localization.concatenateStrings(item.uploader, - NewPipe.getNameOfService(item.serviceId))); + itemVideoTitleView.setText(item.getStreamEntity().getTitle()); + itemAdditionalDetailsView.setText(Localization + .concatenateStrings(item.getStreamEntity().getUploader(), + NewPipe.getNameOfService(item.getStreamEntity().getServiceId()))); - if (item.duration > 0) { - itemDurationView.setText(Localization.getDurationString(item.duration)); + if (item.getStreamEntity().getDuration() > 0) { + itemDurationView.setText(Localization + .getDurationString(item.getStreamEntity().getDuration())); itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), R.color.duration_background_color)); itemDurationView.setVisibility(View.VISIBLE); - StreamStateEntity state = historyRecordManager.loadLocalStreamStateBatch(new ArrayList() {{ add(localItem); }}).blockingGet().get(0); + StreamStateEntity state = historyRecordManager + .loadLocalStreamStateBatch(new ArrayList() {{ + add(localItem); + }}).blockingGet().get(0); if (state != null) { itemProgressView.setVisibility(View.VISIBLE); - itemProgressView.setMax((int) item.duration); - itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime())); + itemProgressView.setMax((int) item.getStreamEntity().getDuration()); + itemProgressView.setProgress((int) TimeUnit.MILLISECONDS + .toSeconds(state.getProgressTime())); } else { itemProgressView.setVisibility(View.GONE); } @@ -75,7 +87,7 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder { } // Default thumbnail is shown on error, while loading and if the url is empty - itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, + itemBuilder.displayImage(item.getStreamEntity().getThumbnailUrl(), itemThumbnailView, ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS); itemView.setOnClickListener(view -> { @@ -97,17 +109,25 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder { } @Override - public void updateState(LocalItem localItem, HistoryRecordManager historyRecordManager) { - if (!(localItem instanceof PlaylistStreamEntry)) return; + public void updateState(final LocalItem localItem, + final HistoryRecordManager historyRecordManager) { + if (!(localItem instanceof PlaylistStreamEntry)) { + return; + } final PlaylistStreamEntry item = (PlaylistStreamEntry) localItem; - StreamStateEntity state = historyRecordManager.loadLocalStreamStateBatch(new ArrayList() {{ add(localItem); }}).blockingGet().get(0); - if (state != null && item.duration > 0) { - itemProgressView.setMax((int) item.duration); + StreamStateEntity state = historyRecordManager + .loadLocalStreamStateBatch(new ArrayList() {{ + add(localItem); + }}).blockingGet().get(0); + if (state != null && item.getStreamEntity().getDuration() > 0) { + itemProgressView.setMax((int) item.getStreamEntity().getDuration()); if (itemProgressView.getVisibility() == View.VISIBLE) { - itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime())); + itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS + .toSeconds(state.getProgressTime())); } else { - itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime())); + itemProgressView.setProgress((int) TimeUnit.MILLISECONDS + .toSeconds(state.getProgressTime())); AnimationUtils.animateView(itemProgressView, true, 500); } } else if (itemProgressView.getVisibility() == View.VISIBLE) { @@ -118,8 +138,8 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder { private View.OnTouchListener getOnTouchListener(final PlaylistStreamEntry item) { return (view, motionEvent) -> { view.performClick(); - if (itemBuilder != null && itemBuilder.getOnItemSelectedListener() != null && - motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) { + if (itemBuilder != null && itemBuilder.getOnItemSelectedListener() != null + && motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) { itemBuilder.getOnItemSelectedListener().drag(item, LocalPlaylistStreamItemHolder.this); } diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamGridItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamGridItemHolder.java index 792ad92f0..39a43b034 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamGridItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamGridItemHolder.java @@ -6,8 +6,8 @@ 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); - } + public LocalStatisticStreamGridItemHolder(final LocalItemBuilder infoItemBuilder, + final ViewGroup parent) { + super(infoItemBuilder, R.layout.list_stream_grid_item, parent); + } } diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java index 75fbf13ea..a83c6ba67 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java @@ -1,12 +1,13 @@ package org.schabi.newpipe.local.holder; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; + import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.stream.StreamStatisticsEntry; @@ -44,20 +45,21 @@ import java.util.concurrent.TimeUnit; */ public class LocalStatisticStreamItemHolder extends LocalItemHolder { - public final ImageView itemThumbnailView; public final TextView itemVideoTitleView; public final TextView itemUploaderView; public final TextView itemDurationView; @Nullable public final TextView itemAdditionalDetails; - public final AnimatedProgressBar itemProgressView; + private final AnimatedProgressBar itemProgressView; - public LocalStatisticStreamItemHolder(LocalItemBuilder itemBuilder, ViewGroup parent) { + public LocalStatisticStreamItemHolder(final LocalItemBuilder itemBuilder, + final ViewGroup parent) { this(itemBuilder, R.layout.list_stream_item, parent); } - LocalStatisticStreamItemHolder(LocalItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) { + LocalStatisticStreamItemHolder(final LocalItemBuilder infoItemBuilder, final int layoutId, + final ViewGroup parent) { super(infoItemBuilder, layoutId, parent); itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView); @@ -70,32 +72,41 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder { private String getStreamInfoDetailLine(final StreamStatisticsEntry entry, final DateFormat dateFormat) { - final String watchCount = Localization.shortViewCount(itemBuilder.getContext(), - entry.watchCount); - final String uploadDate = dateFormat.format(entry.latestAccessDate); - final String serviceName = NewPipe.getNameOfService(entry.serviceId); + final String watchCount = Localization + .shortViewCount(itemBuilder.getContext(), entry.getWatchCount()); + final String uploadDate = dateFormat.format(entry.getLatestAccessDate()); + final String serviceName = NewPipe.getNameOfService(entry.getStreamEntity().getServiceId()); return Localization.concatenateStrings(watchCount, uploadDate, serviceName); } @Override - public void updateFromItem(final LocalItem localItem, HistoryRecordManager historyRecordManager, final DateFormat dateFormat) { - if (!(localItem instanceof StreamStatisticsEntry)) return; + public void updateFromItem(final LocalItem localItem, + final HistoryRecordManager historyRecordManager, + final DateFormat dateFormat) { + if (!(localItem instanceof StreamStatisticsEntry)) { + return; + } final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem; - itemVideoTitleView.setText(item.title); - itemUploaderView.setText(item.uploader); + itemVideoTitleView.setText(item.getStreamEntity().getTitle()); + itemUploaderView.setText(item.getStreamEntity().getUploader()); - if (item.duration > 0) { - itemDurationView.setText(Localization.getDurationString(item.duration)); + if (item.getStreamEntity().getDuration() > 0) { + itemDurationView. + setText(Localization.getDurationString(item.getStreamEntity().getDuration())); itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), R.color.duration_background_color)); itemDurationView.setVisibility(View.VISIBLE); - StreamStateEntity state = historyRecordManager.loadLocalStreamStateBatch(new ArrayList() {{ add(localItem); }}).blockingGet().get(0); + StreamStateEntity state = historyRecordManager + .loadLocalStreamStateBatch(new ArrayList() {{ + add(localItem); + }}).blockingGet().get(0); if (state != null) { itemProgressView.setVisibility(View.VISIBLE); - itemProgressView.setMax((int) item.duration); - itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime())); + itemProgressView.setMax((int) item.getStreamEntity().getDuration()); + itemProgressView.setProgress((int) TimeUnit.MILLISECONDS + .toSeconds(state.getProgressTime())); } else { itemProgressView.setVisibility(View.GONE); } @@ -109,7 +120,7 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder { } // Default thumbnail is shown on error, while loading and if the url is empty - itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, + itemBuilder.displayImage(item.getStreamEntity().getThumbnailUrl(), itemThumbnailView, ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS); itemView.setOnClickListener(view -> { @@ -128,17 +139,25 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder { } @Override - public void updateState(LocalItem localItem, HistoryRecordManager historyRecordManager) { - if (!(localItem instanceof StreamStatisticsEntry)) return; + public void updateState(final LocalItem localItem, + final HistoryRecordManager historyRecordManager) { + if (!(localItem instanceof StreamStatisticsEntry)) { + return; + } final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem; - StreamStateEntity state = historyRecordManager.loadLocalStreamStateBatch(new ArrayList() {{ add(localItem); }}).blockingGet().get(0); - if (state != null && item.duration > 0) { - itemProgressView.setMax((int) item.duration); + StreamStateEntity state = historyRecordManager + .loadLocalStreamStateBatch(new ArrayList() {{ + add(localItem); + }}).blockingGet().get(0); + if (state != null && item.getStreamEntity().getDuration() > 0) { + itemProgressView.setMax((int) item.getStreamEntity().getDuration()); if (itemProgressView.getVisibility() == View.VISIBLE) { - itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime())); + itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS + .toSeconds(state.getProgressTime())); } else { - itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime())); + itemProgressView.setProgress((int) TimeUnit.MILLISECONDS + .toSeconds(state.getProgressTime())); AnimationUtils.animateView(itemProgressView, true, 500); } } else if (itemProgressView.getVisibility() == View.VISIBLE) { diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/PlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/PlaylistItemHolder.java index c5f1813c7..11e3deb67 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/PlaylistItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/PlaylistItemHolder.java @@ -13,12 +13,12 @@ import java.text.DateFormat; public abstract class PlaylistItemHolder extends LocalItemHolder { public final ImageView itemThumbnailView; - public final TextView itemStreamCountView; + final TextView itemStreamCountView; public final TextView itemTitleView; public final TextView itemUploaderView; - public PlaylistItemHolder(LocalItemBuilder infoItemBuilder, - int layoutId, ViewGroup parent) { + public PlaylistItemHolder(final LocalItemBuilder infoItemBuilder, final int layoutId, + final ViewGroup parent) { super(infoItemBuilder, layoutId, parent); itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView); @@ -27,12 +27,14 @@ public abstract class PlaylistItemHolder extends LocalItemHolder { itemUploaderView = itemView.findViewById(R.id.itemUploaderView); } - public PlaylistItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) { + public PlaylistItemHolder(final LocalItemBuilder infoItemBuilder, final ViewGroup parent) { this(infoItemBuilder, R.layout.list_playlist_mini_item, parent); } @Override - public void updateFromItem(final LocalItem localItem, HistoryRecordManager historyRecordManager, final DateFormat dateFormat) { + public void updateFromItem(final LocalItem localItem, + final HistoryRecordManager historyRecordManager, + final DateFormat dateFormat) { itemView.setOnClickListener(view -> { if (itemBuilder.getOnItemSelectedListener() != null) { itemBuilder.getOnItemSelectedListener().selected(localItem); diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistGridItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistGridItemHolder.java index 5ac18fccb..00dcefbda 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistGridItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistGridItemHolder.java @@ -6,8 +6,8 @@ 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); - } + public RemotePlaylistGridItemHolder(final LocalItemBuilder infoItemBuilder, + final ViewGroup parent) { + super(infoItemBuilder, R.layout.list_playlist_grid_item, parent); + } } diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java index 8bb16c318..a47d61d2f 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java @@ -1,5 +1,6 @@ package org.schabi.newpipe.local.holder; +import android.text.TextUtils; import android.view.ViewGroup; import org.schabi.newpipe.database.LocalItem; @@ -10,30 +11,35 @@ import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.Localization; -import android.text.TextUtils; - import java.text.DateFormat; public class RemotePlaylistItemHolder extends PlaylistItemHolder { - public RemotePlaylistItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) { + public RemotePlaylistItemHolder(final LocalItemBuilder infoItemBuilder, + final ViewGroup parent) { super(infoItemBuilder, parent); } - RemotePlaylistItemHolder(LocalItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) { + RemotePlaylistItemHolder(final LocalItemBuilder infoItemBuilder, final int layoutId, + final ViewGroup parent) { super(infoItemBuilder, layoutId, parent); } @Override - public void updateFromItem(final LocalItem localItem, HistoryRecordManager historyRecordManager, final DateFormat dateFormat) { - if (!(localItem instanceof PlaylistRemoteEntity)) return; + public void updateFromItem(final LocalItem localItem, + final HistoryRecordManager historyRecordManager, + final DateFormat dateFormat) { + if (!(localItem instanceof PlaylistRemoteEntity)) { + return; + } final PlaylistRemoteEntity item = (PlaylistRemoteEntity) localItem; itemTitleView.setText(item.getName()); - itemStreamCountView.setText(String.valueOf(item.getStreamCount())); + itemStreamCountView.setText(Localization.localizeStreamCountMini( + itemStreamCountView.getContext(), item.getStreamCount())); // Here is where the uploader name is set in the bookmarked playlists library if (!TextUtils.isEmpty(item.getUploader())) { itemUploaderView.setText(Localization.concatenateStrings(item.getUploader(), - NewPipe.getNameOfService(item.getServiceId()))); + NewPipe.getNameOfService(item.getServiceId()))); } else { itemUploaderView.setText(NewPipe.getNameOfService(item.getServiceId())); } diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java index 33f98614c..96056bd39 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java @@ -2,32 +2,40 @@ package org.schabi.newpipe.local.playlist; import android.app.Activity; import android.content.Context; +import android.content.DialogInterface; import android.os.Bundle; import android.os.Parcelable; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import androidx.recyclerview.widget.RecyclerView; -import androidx.recyclerview.widget.ItemTouchHelper; 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.EditText; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.RecyclerView; + import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.database.history.model.StreamHistoryEntry; import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.local.BaseLocalListFragment; +import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.report.UserAction; @@ -38,40 +46,41 @@ import org.schabi.newpipe.util.StreamDialogEntry; import java.util.ArrayList; import java.util.Collections; +import java.util.Iterator; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import icepick.State; +import io.reactivex.Flowable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; import io.reactivex.disposables.Disposables; +import io.reactivex.schedulers.Schedulers; import io.reactivex.subjects.PublishSubject; import static org.schabi.newpipe.util.AnimationUtils.animateView; public class LocalPlaylistFragment extends BaseLocalListFragment, Void> { - // Save the list 10 seconds after the last change occurred private static final long SAVE_DEBOUNCE_MILLIS = 10000; private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12; - private View headerRootLayout; - private TextView headerTitleView; - private TextView headerStreamCount; - - private View playlistControl; - private View headerPlayAllButton; - private View headerPopupButton; - private View headerBackgroundButton; - @State protected Long playlistId; @State protected String name; @State - protected Parcelable itemsListState; + Parcelable itemsListState; + + private View headerRootLayout; + private TextView headerTitleView; + private TextView headerStreamCount; + private View playlistControl; + private View headerPlayAllButton; + private View headerPopupButton; + private View headerBackgroundButton; private ItemTouchHelper itemTouchHelper; @@ -85,8 +94,10 @@ public class LocalPlaylistFragment extends BaseLocalListFragment() { @Override - public void selected(LocalItem selectedItem) { + public void selected(final LocalItem selectedItem) { if (selectedItem instanceof PlaylistStreamEntry) { final PlaylistStreamEntry item = (PlaylistStreamEntry) selectedItem; NavigationHelper.openVideoDetailFragment(getFragmentManager(), - item.serviceId, item.url, item.title); + item.getStreamEntity().getServiceId(), item.getStreamEntity().getUrl(), + item.getStreamEntity().getTitle()); } } @Override - public void held(LocalItem selectedItem) { + public void held(final LocalItem selectedItem) { if (selectedItem instanceof PlaylistStreamEntry) { showStreamItemDialog((PlaylistStreamEntry) selectedItem); } } @Override - public void drag(LocalItem selectedItem, RecyclerView.ViewHolder viewHolder) { - if (itemTouchHelper != null) itemTouchHelper.startDrag(viewHolder); + public void drag(final LocalItem selectedItem, + final RecyclerView.ViewHolder viewHolder) { + if (itemTouchHelper != null) { + itemTouchHelper.startDrag(viewHolder); + } } }); } @@ -192,22 +207,32 @@ public class LocalPlaylistFragment extends BaseLocalListFragment> getPlaylistObserver() { return new Subscriber>() { @Override - public void onSubscribe(Subscription s) { + public void onSubscribe(final Subscription s) { showLoading(); isLoadingComplete.set(false); - if (databaseSubscription != null) databaseSubscription.cancel(); + if (databaseSubscription != null) { + databaseSubscription.cancel(); + } databaseSubscription = s; databaseSubscription.request(1); } @Override - public void onNext(List streams) { + public void onNext(final List streams) { // Skip handling the result after it has been modified if (isModified == null || !isModified.get()) { handleResult(streams); isLoadingComplete.set(true); } - if (databaseSubscription != null) databaseSubscription.request(1); + if (databaseSubscription != null) { + databaseSubscription.request(1); + } } @Override - public void onError(Throwable exception) { + public void onError(final Throwable exception) { LocalPlaylistFragment.this.onError(exception); } @Override - public void onComplete() {} + public void onComplete() { } }; } @Override - public void handleResult(@NonNull List result) { + public boolean onOptionsItemSelected(final MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_item_remove_watched: + if (!isRemovingWatched) { + new AlertDialog.Builder(requireContext()) + .setMessage(R.string.remove_watched_popup_warning) + .setTitle(R.string.remove_watched_popup_title) + .setPositiveButton(R.string.yes, + (DialogInterface d, int id) -> removeWatchedStreams(false)) + .setNeutralButton( + R.string.remove_watched_popup_yes_and_partially_watched_videos, + (DialogInterface d, int id) -> removeWatchedStreams(true)) + .setNegativeButton(R.string.cancel, + (DialogInterface d, int id) -> d.cancel()) + .create() + .show(); + } + break; + default: + return super.onOptionsItemSelected(item); + } + return true; + } + + public void removeWatchedStreams(final boolean removePartiallyWatched) { + if (isRemovingWatched) { + return; + } + isRemovingWatched = true; + showLoading(); + + disposables.add(playlistManager.getPlaylistStreams(playlistId) + .subscribeOn(Schedulers.io()) + .map((List playlist) -> { + // Playlist data + final Iterator playlistIter = playlist.iterator(); + + // History data + final HistoryRecordManager recordManager + = new HistoryRecordManager(getContext()); + final Iterator historyIter = recordManager + .getStreamHistorySortedById().blockingFirst().iterator(); + + // Remove Watched, Functionality data + final List notWatchedItems = new ArrayList<>(); + boolean thumbnailVideoRemoved = false; + + // already sorted by ^ getStreamHistorySortedById(), binary search can be used + final ArrayList historyStreamIds = new ArrayList<>(); + while (historyIter.hasNext()) { + historyStreamIds.add(historyIter.next().getStreamId()); + } + + if (removePartiallyWatched) { + while (playlistIter.hasNext()) { + final PlaylistStreamEntry playlistItem = playlistIter.next(); + int indexInHistory = Collections.binarySearch(historyStreamIds, + playlistItem.getStreamId()); + + if (indexInHistory < 0) { + notWatchedItems.add(playlistItem); + } else if (!thumbnailVideoRemoved + && playlistManager.getPlaylistThumbnail(playlistId) + .equals(playlistItem.getStreamEntity().getThumbnailUrl())) { + thumbnailVideoRemoved = true; + } + } + } else { + final Iterator streamStatesIter = recordManager + .loadLocalStreamStateBatch(playlist).blockingGet().iterator(); + + while (playlistIter.hasNext()) { + PlaylistStreamEntry playlistItem = playlistIter.next(); + final int indexInHistory = Collections.binarySearch(historyStreamIds, + playlistItem.getStreamId()); + + final boolean hasState = streamStatesIter.next() != null; + if (indexInHistory < 0 || hasState) { + notWatchedItems.add(playlistItem); + } else if (!thumbnailVideoRemoved + && playlistManager.getPlaylistThumbnail(playlistId) + .equals(playlistItem.getStreamEntity().getThumbnailUrl())) { + thumbnailVideoRemoved = true; + } + } + } + + return Flowable.just(notWatchedItems, thumbnailVideoRemoved); + }) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(flow -> { + final List notWatchedItems = + (List) flow.blockingFirst(); + final boolean thumbnailVideoRemoved = (Boolean) flow.blockingLast(); + + itemListAdapter.clearStreamItemList(); + itemListAdapter.addItems(notWatchedItems); + saveChanges(); + + + if (thumbnailVideoRemoved) { + updateThumbnailUrl(); + } + + final long videoCount = itemListAdapter.getItemsList().size(); + setVideoCount(videoCount); + if (videoCount == 0) { + showEmptyState(); + } + + hideLoading(); + isRemovingWatched = false; + }, this::onError)); + } + + @Override + public void handleResult(@NonNull final List result) { super.handleResult(result); - if (itemListAdapter == null) return; + if (itemListAdapter == null) { + return; + } itemListAdapter.clearStreamItemList(); @@ -325,6 +498,16 @@ public class LocalPlaylistFragment extends BaseLocalListFragment NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false)); + headerPopupButton.setOnLongClickListener(view -> { + NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue(), true); + return true; + }); + + headerBackgroundButton.setOnLongClickListener(view -> { + NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue(), true); + return true; + }); + hideLoading(); } @@ -335,12 +518,16 @@ public class LocalPlaylistFragment extends BaseLocalListFragment {/*Do nothing on success*/}, this::onError); + .subscribe(longs -> { /*Do nothing on success*/ }, this::onError); disposables.add(disposable); } private void changeThumbnailUrl(final String thumbnailUrl) { - if (playlistManager == null) return; + if (playlistManager == null) { + return; + } final Toast successToast = Toast.makeText(getActivity(), R.string.playlist_thumbnail_change_success, Toast.LENGTH_SHORT); - Log.d(TAG, "Updating playlist id=[" + playlistId + - "] with new thumbnail url=[" + thumbnailUrl + "]"); + if (DEBUG) { + Log.d(TAG, "Updating playlist id=[" + playlistId + "] " + + "with new thumbnail url=[" + thumbnailUrl + "]"); + } final Disposable disposable = playlistManager .changePlaylistThumbnail(playlistId, thumbnailUrl) @@ -403,23 +600,47 @@ public class LocalPlaylistFragment extends BaseLocalListFragment streamIds = new ArrayList<>(items.size()); for (final LocalItem item : items) { if (item instanceof PlaylistStreamEntry) { - streamIds.add(((PlaylistStreamEntry) item).streamId); + streamIds.add(((PlaylistStreamEntry) item).getStreamId()); } } - Log.d(TAG, "Updating playlist id=[" + playlistId + - "] with [" + streamIds.size() + "] items"); + if (DEBUG) { + Log.d(TAG, "Updating playlist id=[" + playlistId + "] " + + "with [" + streamIds.size() + "] items"); + } final Disposable disposable = playlistManager.updateJoin(playlistId, streamIds) .observeOn(AndroidSchedulers.mainThread()) .subscribe( - () -> { if (isModified != null) isModified.set(false); }, + () -> { + if (isModified != null) { + isModified.set(false); + } + }, this::onError ); disposables.add(disposable); @@ -467,28 +696,33 @@ public class LocalPlaylistFragment extends BaseLocalListFragment NavigationHelper.playOnPopupPlayer(context, getPlayQueueStartingAt(item), true)); + (fragment, infoItemDuplicate) -> NavigationHelper. + playOnPopupPlayer(context, getPlayQueueStartingAt(item), true)); } - StreamDialogEntry.start_here_on_background.setCustomAction( - (fragment, infoItemDuplicate) -> NavigationHelper.playOnBackgroundPlayer(context, getPlayQueueStartingAt(item), true)); + StreamDialogEntry.start_here_on_background.setCustomAction((fragment, infoItemDuplicate) -> + NavigationHelper.playOnBackgroundPlayer(context, + getPlayQueueStartingAt(item), true)); StreamDialogEntry.set_as_playlist_thumbnail.setCustomAction( - (fragment, infoItemDuplicate) -> changeThumbnailUrl(item.thumbnailUrl)); - StreamDialogEntry.delete.setCustomAction( - (fragment, infoItemDuplicate) -> deleteItem(item)); + (fragment, infoItemDuplicate) -> + changeThumbnailUrl(item.getStreamEntity().getThumbnailUrl())); + StreamDialogEntry.delete.setCustomAction((fragment, infoItemDuplicate) -> + deleteItem(item)); - new InfoItemDialog(activity, infoItem, StreamDialogEntry.getCommands(context), (dialog, which) -> - StreamDialogEntry.clickOn(which, this, infoItem)).show(); + new InfoItemDialog(activity, infoItem, StreamDialogEntry.getCommands(context), + (dialog, which) -> StreamDialogEntry.clickOn(which, this, infoItem)).show(); } - private void setInitialData(long playlistId, String name) { - this.playlistId = playlistId; - this.name = !TextUtils.isEmpty(name) ? name : ""; + private void setInitialData(final long pid, final String title) { + this.playlistId = pid; + this.name = !TextUtils.isEmpty(title) ? title : ""; } private void setVideoCount(final long count) { diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistManager.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistManager.java index c025b360a..21164497a 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistManager.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistManager.java @@ -22,7 +22,6 @@ import io.reactivex.Single; import io.reactivex.schedulers.Schedulers; public class LocalPlaylistManager { - private final AppDatabase database; private final StreamDAO streamTable; private final PlaylistDAO playlistTable; @@ -37,7 +36,9 @@ public class LocalPlaylistManager { public Maybe> createPlaylist(final String name, final List streams) { // Disallow creation of empty playlists - if (streams.isEmpty()) return Maybe.empty(); + if (streams.isEmpty()) { + return Maybe.empty(); + } final StreamEntity defaultStream = streams.get(0); final PlaylistEntity newPlaylist = new PlaylistEntity(name, defaultStream.getThumbnailUrl()); @@ -103,6 +104,10 @@ public class LocalPlaylistManager { return modifyPlaylist(playlistId, null, thumbnailUrl); } + public String getPlaylistThumbnail(final long playlistId) { + return playlistTable.getPlaylist(playlistId).blockingFirst().get(0).getThumbnailUrl(); + } + private Maybe modifyPlaylist(final long playlistId, @Nullable final String name, @Nullable final String thumbnailUrl) { @@ -111,8 +116,12 @@ public class LocalPlaylistManager { .filter(playlistEntities -> !playlistEntities.isEmpty()) .map(playlistEntities -> { PlaylistEntity playlist = playlistEntities.get(0); - if (name != null) playlist.setName(name); - if (thumbnailUrl != null) playlist.setThumbnailUrl(thumbnailUrl); + if (name != null) { + playlist.setName(name); + } + if (thumbnailUrl != null) { + playlist.setThumbnailUrl(thumbnailUrl); + } return playlistTable.update(playlist); }).subscribeOn(Schedulers.io()); } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/FeedGroupIcon.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/FeedGroupIcon.kt new file mode 100644 index 000000000..19038be93 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/FeedGroupIcon.kt @@ -0,0 +1,63 @@ +package org.schabi.newpipe.local.subscription + +import android.content.Context +import androidx.annotation.AttrRes +import androidx.annotation.DrawableRes +import org.schabi.newpipe.R +import org.schabi.newpipe.util.ThemeHelper + +enum class FeedGroupIcon( + /** + * The id that will be used to store and retrieve icons from some persistent storage (e.g. DB). + */ + val id: Int, + + /** + * The attribute that points to a drawable resource. "R.attr" is used here to support multiple themes. + */ + @AttrRes val drawableResourceAttr: Int +) { + ALL(0, R.attr.ic_asterisk), + MUSIC(1, R.attr.ic_music_note), + EDUCATION(2, R.attr.ic_school), + FITNESS(3, R.attr.ic_fitness_center), + SPACE(4, R.attr.ic_telescope), + COMPUTER(5, R.attr.ic_computer), + GAMING(6, R.attr.ic_videogame_asset), + SPORTS(7, R.attr.ic_sports), + NEWS(8, R.attr.ic_megaphone), + FAVORITES(9, R.attr.ic_heart), + CAR(10, R.attr.ic_car), + MOTORCYCLE(11, R.attr.ic_motorcycle), + TREND(12, R.attr.ic_trending_up), + MOVIE(13, R.attr.ic_movie), + BACKUP(14, R.attr.ic_backup), + ART(15, R.attr.ic_palette), + PERSON(16, R.attr.ic_person), + PEOPLE(17, R.attr.ic_people), + MONEY(18, R.attr.ic_money), + KIDS(19, R.attr.ic_child_care), + FOOD(20, R.attr.ic_fastfood), + SMILE(21, R.attr.ic_smile), + EXPLORE(22, R.attr.ic_explore), + RESTAURANT(23, R.attr.ic_restaurant), + MIC(24, R.attr.ic_mic), + HEADSET(25, R.attr.ic_headset), + RADIO(26, R.attr.ic_radio), + SHOPPING_CART(27, R.attr.ic_shopping_cart), + WATCH_LATER(28, R.attr.ic_watch_later), + WORK(29, R.attr.ic_work), + HOT(30, R.attr.ic_kiosk_hot), + CHANNEL(31, R.attr.ic_channel), + BOOKMARK(32, R.attr.ic_bookmark), + PETS(33, R.attr.ic_pets), + WORLD(34, R.attr.ic_world), + STAR(35, R.attr.ic_stars), + SUN(36, R.attr.ic_sunny), + RSS(37, R.attr.ic_rss); + + @DrawableRes + fun getDrawableRes(context: Context): Int { + return ThemeHelper.resolveResourceIdFromAttr(context, drawableResourceAttr) + } +} diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/ImportConfirmationDialog.java b/app/src/main/java/org/schabi/newpipe/local/subscription/ImportConfirmationDialog.java index fbcf5d70e..17ae7b1c0 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/ImportConfirmationDialog.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/ImportConfirmationDialog.java @@ -4,6 +4,7 @@ import android.app.AlertDialog; import android.app.Dialog; import android.content.Intent; import android.os.Bundle; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.DialogFragment; @@ -15,30 +16,36 @@ import org.schabi.newpipe.util.ThemeHelper; import icepick.Icepick; import icepick.State; +import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; + public class ImportConfirmationDialog extends DialogFragment { @State protected Intent resultServiceIntent; - public void setResultServiceIntent(Intent resultServiceIntent) { - this.resultServiceIntent = resultServiceIntent; - } - - public static void show(@NonNull Fragment fragment, @NonNull Intent resultServiceIntent) { - if (fragment.getFragmentManager() == null) return; + public static void show(@NonNull final Fragment fragment, + @NonNull final Intent resultServiceIntent) { + if (fragment.getFragmentManager() == null) { + return; + } final ImportConfirmationDialog confirmationDialog = new ImportConfirmationDialog(); confirmationDialog.setResultServiceIntent(resultServiceIntent); confirmationDialog.show(fragment.getFragmentManager(), null); } + public void setResultServiceIntent(final Intent resultServiceIntent) { + this.resultServiceIntent = resultServiceIntent; + } + @NonNull @Override - public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) { + assureCorrectAppLanguage(getContext()); return new AlertDialog.Builder(getContext(), ThemeHelper.getDialogTheme(getContext())) .setMessage(R.string.import_network_expensive_warning) .setCancelable(true) .setNegativeButton(R.string.cancel, null) - .setPositiveButton(android.R.string.ok, (dialogInterface, i) -> { + .setPositiveButton(R.string.finish, (dialogInterface, i) -> { if (resultServiceIntent != null && getContext() != null) { getContext().startService(resultServiceIntent); } @@ -48,16 +55,18 @@ public class ImportConfirmationDialog extends DialogFragment { } @Override - public void onCreate(@Nullable Bundle savedInstanceState) { + public void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if (resultServiceIntent == null) throw new IllegalStateException("Result intent is null"); + if (resultServiceIntent == null) { + throw new IllegalStateException("Result intent is null"); + } Icepick.restoreInstanceState(this, savedInstanceState); } @Override - public void onSaveInstanceState(Bundle outState) { + public void onSaveInstanceState(final Bundle outState) { super.onSaveInstanceState(outState); Icepick.saveInstanceState(this, outState); } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java deleted file mode 100644 index bff6c1b3a..000000000 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java +++ /dev/null @@ -1,595 +0,0 @@ -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 androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.FragmentManager; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import androidx.appcompat.app.ActionBar; -import androidx.recyclerview.widget.GridLayoutManager; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; -import android.widget.Toast; - -import com.nononsenseapps.filepicker.Utils; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.database.subscription.SubscriptionEntity; -import org.schabi.newpipe.extractor.InfoItem; -import org.schabi.newpipe.extractor.NewPipe; -import org.schabi.newpipe.extractor.StreamingService; -import org.schabi.newpipe.extractor.channel.ChannelInfoItem; -import org.schabi.newpipe.extractor.exceptions.ExtractionException; -import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor; -import org.schabi.newpipe.fragments.BaseStateFragment; -import org.schabi.newpipe.info_list.InfoListAdapter; -import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService; -import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService; -import org.schabi.newpipe.report.UserAction; -import org.schabi.newpipe.util.FilePickerActivityHelper; -import org.schabi.newpipe.util.NavigationHelper; -import org.schabi.newpipe.util.OnClickGesture; -import org.schabi.newpipe.util.ServiceHelper; -import org.schabi.newpipe.util.ShareUtils; -import org.schabi.newpipe.util.ThemeHelper; -import org.schabi.newpipe.views.CollapsibleView; - -import java.io.File; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.List; -import java.util.Locale; - -import icepick.State; -import io.reactivex.Observer; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; - -import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_MODE; -import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_VALUE; -import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.PREVIOUS_EXPORT_MODE; -import static org.schabi.newpipe.util.AnimationUtils.animateRotation; -import static org.schabi.newpipe.util.AnimationUtils.animateView; - -public class SubscriptionFragment extends BaseStateFragment> implements SharedPreferences.OnSharedPreferenceChangeListener { - private static final int REQUEST_EXPORT_CODE = 666; - private static final int REQUEST_IMPORT_CODE = 667; - - private RecyclerView itemsList; - @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; - - @State - protected Parcelable importExportOptionsState; - private CollapsibleView importExportOptions; - - private CompositeDisposable disposables = new CompositeDisposable(); - private SubscriptionService subscriptionService; - - /////////////////////////////////////////////////////////////////////////// - // Fragment LifeCycle - /////////////////////////////////////////////////////////////////////////// - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setHasOptionsMenu(true); - PreferenceManager.getDefaultSharedPreferences(activity) - .registerOnSharedPreferenceChangeListener(this); - } - - @Override - public void setUserVisibleHint(boolean isVisibleToUser) { - super.setUserVisibleHint(isVisibleToUser); - if (activity != null && isVisibleToUser) { - setTitle(activity.getString(R.string.tab_subscriptions)); - } - } - - @Override - public void onAttach(Context context) { - super.onAttach(context); - infoListAdapter = new InfoListAdapter(activity); - subscriptionService = SubscriptionService.getInstance(activity); - } - - @Override - public void onDetach() { - super.onDetach(); - } - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_subscription, container, false); - } - - @Override - 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 - public void onPause() { - super.onPause(); - itemsListState = itemsList.getLayoutManager().onSaveInstanceState(); - importExportOptionsState = importExportOptions.onSaveInstanceState(); - - if (subscriptionBroadcastReceiver != null && activity != null) { - LocalBroadcastManager.getInstance(activity).unregisterReceiver(subscriptionBroadcastReceiver); - } - } - - @Override - public void onDestroyView() { - if (disposables != null) disposables.clear(); - - super.onDestroyView(); - } - - @Override - public void onDestroy() { - if (disposables != null) disposables.dispose(); - 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 - /////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - super.onCreateOptionsMenu(menu, inflater); - - ActionBar supportActionBar = activity.getSupportActionBar(); - if (supportActionBar != null) { - supportActionBar.setDisplayShowTitleEnabled(true); - setTitle(getString(R.string.tab_subscriptions)); - } - } - - /*////////////////////////////////////////////////////////////////////////// - // Subscriptions import/export - //////////////////////////////////////////////////////////////////////////*/ - - private BroadcastReceiver subscriptionBroadcastReceiver; - - private void setupBroadcastReceiver() { - if (activity == null) return; - - if (subscriptionBroadcastReceiver != null) { - LocalBroadcastManager.getInstance(activity).unregisterReceiver(subscriptionBroadcastReceiver); - } - - final IntentFilter filters = new IntentFilter(); - filters.addAction(SubscriptionsExportService.EXPORT_COMPLETE_ACTION); - filters.addAction(SubscriptionsImportService.IMPORT_COMPLETE_ACTION); - subscriptionBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (importExportOptions != null) importExportOptions.collapse(); - } - }; - - LocalBroadcastManager.getInstance(activity).registerReceiver(subscriptionBroadcastReceiver, filters); - } - - private View addItemView(final String title, @DrawableRes final int icon, ViewGroup container) { - final View itemRoot = View.inflate(getContext(), R.layout.subscription_import_export_item, null); - final TextView titleView = itemRoot.findViewById(android.R.id.text1); - final ImageView iconView = itemRoot.findViewById(android.R.id.icon1); - - titleView.setText(title); - iconView.setImageResource(icon); - - container.addView(itemRoot); - return itemRoot; - } - - private void setupImportFromItems(final ViewGroup listHolder) { - final View previousBackupItem = addItemView(getString(R.string.previous_export), - ThemeHelper.resolveResourceIdFromAttr(getContext(), R.attr.ic_backup), listHolder); - previousBackupItem.setOnClickListener(item -> onImportPreviousSelected()); - - final int iconColor = ThemeHelper.isLightThemeSelected(getContext()) ? Color.BLACK : Color.WHITE; - final String[] services = getResources().getStringArray(R.array.service_list); - for (String serviceName : services) { - try { - final StreamingService service = NewPipe.getService(serviceName); - - final SubscriptionExtractor subscriptionExtractor = service.getSubscriptionExtractor(); - if (subscriptionExtractor == null) continue; - - final List supportedSources = subscriptionExtractor.getSupportedSources(); - if (supportedSources.isEmpty()) continue; - - final View itemView = addItemView(serviceName, ServiceHelper.getIcon(service.getServiceId()), listHolder); - final ImageView iconView = itemView.findViewById(android.R.id.icon1); - iconView.setColorFilter(iconColor, PorterDuff.Mode.SRC_IN); - - itemView.setOnClickListener(selectedItem -> onImportFromServiceSelected(service.getServiceId())); - } catch (ExtractionException e) { - throw new RuntimeException("Services array contains an entry that it's not a valid service name (" + serviceName + ")", e); - } - } - } - - private void setupExportToItems(final ViewGroup listHolder) { - final View previousBackupItem = addItemView(getString(R.string.file), ThemeHelper.resolveResourceIdFromAttr(getContext(), R.attr.ic_save), listHolder); - previousBackupItem.setOnClickListener(item -> onExportSelected()); - } - - private void onImportFromServiceSelected(int serviceId) { - FragmentManager fragmentManager = getFM(); - NavigationHelper.openSubscriptionsImportFragment(fragmentManager, serviceId); - } - - private void onImportPreviousSelected() { - startActivityForResult(FilePickerActivityHelper.chooseSingleFile(activity), REQUEST_IMPORT_CODE); - } - - private void onExportSelected() { - final String date = new SimpleDateFormat("yyyyMMddHHmm", Locale.ENGLISH).format(new Date()); - final String exportName = "newpipe_subscriptions_" + date + ".json"; - final File exportFile = new File(Environment.getExternalStorageDirectory(), exportName); - - startActivityForResult(FilePickerActivityHelper.chooseFileToSave(activity, exportFile.getAbsolutePath()), REQUEST_EXPORT_CODE); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (data != null && data.getData() != null && resultCode == Activity.RESULT_OK) { - if (requestCode == REQUEST_EXPORT_CODE) { - final File exportFile = Utils.getFileForUri(data.getData()); - if (!exportFile.getParentFile().canWrite() || !exportFile.getParentFile().canRead()) { - Toast.makeText(activity, R.string.invalid_directory, Toast.LENGTH_SHORT).show(); - } else { - activity.startService(new Intent(activity, SubscriptionsExportService.class) - .putExtra(SubscriptionsExportService.KEY_FILE_PATH, exportFile.getAbsolutePath())); - } - } else if (requestCode == REQUEST_IMPORT_CODE) { - final String path = Utils.getFileForUri(data.getData()).getAbsolutePath(); - ImportConfirmationDialog.show(this, new Intent(activity, SubscriptionsImportService.class) - .putExtra(KEY_MODE, PREVIOUS_EXPORT_MODE) - .putExtra(KEY_VALUE, path)); - } - } - } - /*///////////////////////////////////////////////////////////////////////// - // Fragment Views - /////////////////////////////////////////////////////////////////////////*/ - - @Override - 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(useGrid ? getGridLayoutManager() : getListLayoutManager()); - - View headerRootLayout; - infoListAdapter.setHeader(headerRootLayout = activity.getLayoutInflater().inflate(R.layout.subscription_header, itemsList, false)); - whatsNewItemListHeader = headerRootLayout.findViewById(R.id.whats_new); - importExportListHeader = headerRootLayout.findViewById(R.id.import_export); - 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)); - setupExportToItems(headerRootLayout.findViewById(R.id.export_to_options)); - - if (importExportOptionsState != null) { - importExportOptions.onRestoreInstanceState(importExportOptionsState); - importExportOptionsState = null; - } - - importExportOptions.addListener(getExpandIconSyncListener(headerRootLayout.findViewById(R.id.import_export_expand_icon))); - importExportOptions.ready(); - } - - private CollapsibleView.StateListener getExpandIconSyncListener(final ImageView iconView) { - return newState -> animateRotation(iconView, 250, newState == CollapsibleView.COLLAPSED ? 0 : 180); - } - - @Override - protected void initListeners() { - super.initListeners(); - - infoListAdapter.setOnChannelSelectedListener(new OnClickGesture() { - - public void selected(ChannelInfoItem selectedItem) { - final FragmentManager fragmentManager = getFM(); - NavigationHelper.openChannelFragment(fragmentManager, - selectedItem.getServiceId(), - selectedItem.getUrl(), - selectedItem.getName()); - } - - public void held(ChannelInfoItem selectedItem) { - showLongTapDialog(selectedItem); - } - - }); - - whatsNewItemListHeader.setOnClickListener(v -> { - FragmentManager fragmentManager = getFM(); - NavigationHelper.openWhatsNewFragment(fragmentManager); - }); - 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.unsubscribe), - context.getResources().getString(R.string.share) - }; - - final DialogInterface.OnClickListener actions = (dialogInterface, i) -> { - switch (i) { - case 0: - deleteChannel(selectedItem); - break; - case 1: - shareChannel(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) { - ShareUtils.shareUrl(getContext(), 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> getDeleteObserver() { - return new Observer>() { - @Override - public void onSubscribe(Disposable d) { - disposables.add(d); - } - - @Override - public void onNext(List 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(); - } - - /////////////////////////////////////////////////////////////////////////// - // Subscriptions Loader - /////////////////////////////////////////////////////////////////////////// - - @Override - public void startLoading(boolean forceLoad) { - super.startLoading(forceLoad); - resetFragment(); - - subscriptionService.getSubscription().toObservable() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(getSubscriptionObserver()); - } - - private Observer> getSubscriptionObserver() { - return new Observer>() { - @Override - public void onSubscribe(Disposable d) { - showLoading(); - disposables.add(d); - } - - @Override - public void onNext(List subscriptions) { - handleResult(subscriptions); - } - - @Override - public void onError(Throwable exception) { - SubscriptionFragment.this.onError(exception); - } - - @Override - public void onComplete() { - } - }; - } - - @Override - public void handleResult(@NonNull List result) { - super.handleResult(result); - - infoListAdapter.clearStreamItemList(); - - if (result.isEmpty()) { - whatsNewItemListHeader.setVisibility(View.GONE); - showEmptyState(); - } else { - infoListAdapter.addInfoItemList(getSubscriptionItems(result)); - if (itemsListState != null) { - itemsList.getLayoutManager().onRestoreInstanceState(itemsListState); - itemsListState = null; - } - whatsNewItemListHeader.setVisibility(View.VISIBLE); - hideLoading(); - } - } - - - private List getSubscriptionItems(List subscriptions) { - List items = new ArrayList<>(); - for (final SubscriptionEntity subscription : subscriptions) { - items.add(subscription.toChannelInfoItem()); - } - - Collections.sort(items, - (InfoItem o1, InfoItem o2) -> - o1.getName().compareToIgnoreCase(o2.getName())); - return items; - } - - /*////////////////////////////////////////////////////////////////////////// - // Contract - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void showLoading() { - super.showLoading(); - animateView(itemsList, false, 100); - } - - @Override - public void hideLoading() { - super.hideLoading(); - animateView(itemsList, true, 200); - } - - /////////////////////////////////////////////////////////////////////////// - // Fragment Error Handling - /////////////////////////////////////////////////////////////////////////// - - @Override - protected boolean onError(Throwable exception) { - resetFragment(); - if (super.onError(exception)) return true; - - onUnrecoverableError(exception, - UserAction.SOMETHING_ELSE, - "none", - "Subscriptions", - R.string.general_error); - return true; - } - - @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); - } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt new file mode 100644 index 000000000..7fea3b5d8 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt @@ -0,0 +1,446 @@ +package org.schabi.newpipe.local.subscription + +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.res.Configuration +import android.os.Bundle +import android.os.Environment +import android.os.Parcelable +import android.preference.PreferenceManager +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.lifecycle.ViewModelProviders +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import androidx.recyclerview.widget.GridLayoutManager +import com.nononsenseapps.filepicker.Utils +import com.xwray.groupie.Group +import com.xwray.groupie.GroupAdapter +import com.xwray.groupie.Item +import com.xwray.groupie.Section +import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder +import icepick.State +import io.reactivex.disposables.CompositeDisposable +import java.io.File +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import kotlin.math.floor +import kotlin.math.max +import kotlinx.android.synthetic.main.dialog_title.view.itemAdditionalDetails +import kotlinx.android.synthetic.main.dialog_title.view.itemTitleView +import kotlinx.android.synthetic.main.fragment_subscription.items_list +import org.schabi.newpipe.R +import org.schabi.newpipe.database.feed.model.FeedGroupEntity +import org.schabi.newpipe.extractor.channel.ChannelInfoItem +import org.schabi.newpipe.fragments.BaseStateFragment +import org.schabi.newpipe.local.subscription.SubscriptionViewModel.SubscriptionState +import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialog +import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialog +import org.schabi.newpipe.local.subscription.item.ChannelItem +import org.schabi.newpipe.local.subscription.item.EmptyPlaceholderItem +import org.schabi.newpipe.local.subscription.item.FeedGroupAddItem +import org.schabi.newpipe.local.subscription.item.FeedGroupCardItem +import org.schabi.newpipe.local.subscription.item.FeedGroupCarouselItem +import org.schabi.newpipe.local.subscription.item.FeedImportExportItem +import org.schabi.newpipe.local.subscription.item.HeaderWithMenuItem +import org.schabi.newpipe.local.subscription.item.HeaderWithMenuItem.Companion.PAYLOAD_UPDATE_VISIBILITY_MENU_ITEM +import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService +import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService.EXPORT_COMPLETE_ACTION +import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService.KEY_FILE_PATH +import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService +import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.IMPORT_COMPLETE_ACTION +import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_MODE +import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_VALUE +import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.PREVIOUS_EXPORT_MODE +import org.schabi.newpipe.report.UserAction +import org.schabi.newpipe.util.AnimationUtils.animateView +import org.schabi.newpipe.util.FilePickerActivityHelper +import org.schabi.newpipe.util.NavigationHelper +import org.schabi.newpipe.util.OnClickGesture +import org.schabi.newpipe.util.ShareUtils +import org.schabi.newpipe.util.ThemeHelper + +class SubscriptionFragment : BaseStateFragment() { + private lateinit var viewModel: SubscriptionViewModel + private lateinit var subscriptionManager: SubscriptionManager + private val disposables: CompositeDisposable = CompositeDisposable() + + private var subscriptionBroadcastReceiver: BroadcastReceiver? = null + + private val groupAdapter = GroupAdapter() + private val feedGroupsSection = Section() + private var feedGroupsCarousel: FeedGroupCarouselItem? = null + private lateinit var importExportItem: FeedImportExportItem + private lateinit var feedGroupsSortMenuItem: HeaderWithMenuItem + private val subscriptionsSection = Section() + + @State + @JvmField + var itemsListState: Parcelable? = null + @State + @JvmField + var feedGroupsListState: Parcelable? = null + @State + @JvmField + var importExportItemExpandedState: Boolean? = null + + init { + setHasOptionsMenu(true) + } + + // ///////////////////////////////////////////////////////////////////////// + // Fragment LifeCycle + // ///////////////////////////////////////////////////////////////////////// + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setupInitialLayout() + } + + override fun setUserVisibleHint(isVisibleToUser: Boolean) { + super.setUserVisibleHint(isVisibleToUser) + if (activity != null && isVisibleToUser) { + setTitle(activity.getString(R.string.tab_subscriptions)) + } + } + + override fun onAttach(context: Context) { + super.onAttach(context) + subscriptionManager = SubscriptionManager(requireContext()) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_subscription, container, false) + } + + override fun onResume() { + super.onResume() + setupBroadcastReceiver() + } + + override fun onPause() { + super.onPause() + itemsListState = items_list.layoutManager?.onSaveInstanceState() + feedGroupsListState = feedGroupsCarousel?.onSaveInstanceState() + importExportItemExpandedState = importExportItem.isExpanded + + if (subscriptionBroadcastReceiver != null && activity != null) { + LocalBroadcastManager.getInstance(activity).unregisterReceiver(subscriptionBroadcastReceiver!!) + } + } + + override fun onDestroy() { + super.onDestroy() + disposables.dispose() + } + + // //////////////////////////////////////////////////////////////////////// + // Menu + // //////////////////////////////////////////////////////////////////////// + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + super.onCreateOptionsMenu(menu, inflater) + + val supportActionBar = activity.supportActionBar + if (supportActionBar != null) { + supportActionBar.setDisplayShowTitleEnabled(true) + setTitle(getString(R.string.tab_subscriptions)) + } + } + + private fun setupBroadcastReceiver() { + if (activity == null) return + + if (subscriptionBroadcastReceiver != null) { + LocalBroadcastManager.getInstance(activity).unregisterReceiver(subscriptionBroadcastReceiver!!) + } + + val filters = IntentFilter() + filters.addAction(EXPORT_COMPLETE_ACTION) + filters.addAction(IMPORT_COMPLETE_ACTION) + subscriptionBroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + items_list?.post { + importExportItem.isExpanded = false + importExportItem.notifyChanged(FeedImportExportItem.REFRESH_EXPANDED_STATUS) + } + } + } + + LocalBroadcastManager.getInstance(activity).registerReceiver(subscriptionBroadcastReceiver!!, filters) + } + + private fun onImportFromServiceSelected(serviceId: Int) { + val fragmentManager = fm + NavigationHelper.openSubscriptionsImportFragment(fragmentManager, serviceId) + } + + private fun onImportPreviousSelected() { + startActivityForResult(FilePickerActivityHelper.chooseSingleFile(activity), REQUEST_IMPORT_CODE) + } + + private fun onExportSelected() { + val date = SimpleDateFormat("yyyyMMddHHmm", Locale.ENGLISH).format(Date()) + val exportName = "newpipe_subscriptions_$date.json" + val exportFile = File(Environment.getExternalStorageDirectory(), exportName) + + startActivityForResult(FilePickerActivityHelper.chooseFileToSave(activity, exportFile.absolutePath), REQUEST_EXPORT_CODE) + } + + private fun openReorderDialog() { + FeedGroupReorderDialog().show(requireFragmentManager(), null) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (data != null && data.data != null && resultCode == Activity.RESULT_OK) { + if (requestCode == REQUEST_EXPORT_CODE) { + val exportFile = Utils.getFileForUri(data.data!!) + if (!exportFile.parentFile.canWrite() || !exportFile.parentFile.canRead()) { + Toast.makeText(activity, R.string.invalid_directory, Toast.LENGTH_SHORT).show() + } else { + activity.startService(Intent(activity, SubscriptionsExportService::class.java) + .putExtra(KEY_FILE_PATH, exportFile.absolutePath)) + } + } else if (requestCode == REQUEST_IMPORT_CODE) { + val path = Utils.getFileForUri(data.data!!).absolutePath + ImportConfirmationDialog.show(this, Intent(activity, SubscriptionsImportService::class.java) + .putExtra(KEY_MODE, PREVIOUS_EXPORT_MODE) + .putExtra(KEY_VALUE, path)) + } + } + } + + // //////////////////////////////////////////////////////////////////////// + // Fragment Views + // //////////////////////////////////////////////////////////////////////// + + private fun setupInitialLayout() { + Section().apply { + val carouselAdapter = GroupAdapter() + + carouselAdapter.add(FeedGroupCardItem(-1, getString(R.string.all), FeedGroupIcon.RSS)) + carouselAdapter.add(feedGroupsSection) + carouselAdapter.add(FeedGroupAddItem()) + + carouselAdapter.setOnItemClickListener { item, _ -> + listenerFeedGroups.selected(item) + } + carouselAdapter.setOnItemLongClickListener { item, _ -> + if (item is FeedGroupCardItem) { + if (item.groupId == FeedGroupEntity.GROUP_ALL_ID) { + return@setOnItemLongClickListener false + } + } + listenerFeedGroups.held(item) + return@setOnItemLongClickListener true + } + + feedGroupsCarousel = FeedGroupCarouselItem(requireContext(), carouselAdapter) + feedGroupsSortMenuItem = HeaderWithMenuItem( + getString(R.string.feed_groups_header_title), + ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_sort), + menuItemOnClickListener = ::openReorderDialog + ) + add(Section(feedGroupsSortMenuItem, listOf(feedGroupsCarousel))) + + groupAdapter.add(this) + } + + subscriptionsSection.setPlaceholder(EmptyPlaceholderItem()) + subscriptionsSection.setHideWhenEmpty(true) + + importExportItem = FeedImportExportItem( + { onImportPreviousSelected() }, + { onImportFromServiceSelected(it) }, + { onExportSelected() }, + importExportItemExpandedState ?: false) + groupAdapter.add(Section(importExportItem, listOf(subscriptionsSection))) + } + + override fun initViews(rootView: View, savedInstanceState: Bundle?) { + super.initViews(rootView, savedInstanceState) + + val shouldUseGridLayout = shouldUseGridLayout() + groupAdapter.spanCount = if (shouldUseGridLayout) getGridSpanCount() else 1 + items_list.layoutManager = GridLayoutManager(requireContext(), groupAdapter.spanCount).apply { + spanSizeLookup = groupAdapter.spanSizeLookup + } + items_list.adapter = groupAdapter + + viewModel = ViewModelProviders.of(this).get(SubscriptionViewModel::class.java) + viewModel.stateLiveData.observe(viewLifecycleOwner, androidx.lifecycle.Observer { it?.let(this::handleResult) }) + viewModel.feedGroupsLiveData.observe(viewLifecycleOwner, androidx.lifecycle.Observer { it?.let(this::handleFeedGroups) }) + } + + private fun showLongTapDialog(selectedItem: ChannelInfoItem) { + val commands = arrayOf( + getString(R.string.share), + getString(R.string.unsubscribe) + ) + + val actions = DialogInterface.OnClickListener { _, i -> + when (i) { + 0 -> ShareUtils.shareUrl(requireContext(), selectedItem.name, selectedItem.url) + 1 -> deleteChannel(selectedItem) + } + } + + val bannerView = View.inflate(requireContext(), R.layout.dialog_title, null) + bannerView.isSelected = true + bannerView.itemTitleView.text = selectedItem.name + bannerView.itemAdditionalDetails.visibility = View.GONE + + AlertDialog.Builder(requireContext()) + .setCustomTitle(bannerView) + .setItems(commands, actions) + .create() + .show() + } + + private fun deleteChannel(selectedItem: ChannelInfoItem) { + disposables.add(subscriptionManager.deleteSubscription(selectedItem.serviceId, selectedItem.url).subscribe { + Toast.makeText(requireContext(), getString(R.string.channel_unsubscribed), Toast.LENGTH_SHORT).show() + }) + } + + override fun doInitialLoadLogic() = Unit + override fun startLoading(forceLoad: Boolean) = Unit + + private val listenerFeedGroups = object : OnClickGesture>() { + override fun selected(selectedItem: Item<*>?) { + when (selectedItem) { + is FeedGroupCardItem -> NavigationHelper.openFeedFragment(fm, selectedItem.groupId, selectedItem.name) + is FeedGroupAddItem -> FeedGroupDialog.newInstance().show(fm, null) + } + } + + override fun held(selectedItem: Item<*>?) { + when (selectedItem) { + is FeedGroupCardItem -> FeedGroupDialog.newInstance(selectedItem.groupId).show(fm, null) + } + } + } + + private val listenerChannelItem = object : OnClickGesture() { + override fun selected(selectedItem: ChannelInfoItem) = NavigationHelper.openChannelFragment(fm, + selectedItem.serviceId, selectedItem.url, selectedItem.name) + + override fun held(selectedItem: ChannelInfoItem) = showLongTapDialog(selectedItem) + } + + override fun handleResult(result: SubscriptionState) { + super.handleResult(result) + + val shouldUseGridLayout = shouldUseGridLayout() + when (result) { + is SubscriptionState.LoadedState -> { + result.subscriptions.forEach { + if (it is ChannelItem) { + it.gesturesListener = listenerChannelItem + it.itemVersion = when { + shouldUseGridLayout -> ChannelItem.ItemVersion.GRID + else -> ChannelItem.ItemVersion.MINI + } + } + } + + subscriptionsSection.update(result.subscriptions) + subscriptionsSection.setHideWhenEmpty(false) + + if (result.subscriptions.isEmpty() && importExportItemExpandedState == null) { + items_list.post { + importExportItem.isExpanded = true + importExportItem.notifyChanged(FeedImportExportItem.REFRESH_EXPANDED_STATUS) + } + } + + if (itemsListState != null) { + items_list.layoutManager?.onRestoreInstanceState(itemsListState) + itemsListState = null + } + } + is SubscriptionState.ErrorState -> { + result.error?.let { onError(result.error) } + } + } + } + + private fun handleFeedGroups(groups: List) { + feedGroupsSection.update(groups) + + if (feedGroupsListState != null) { + feedGroupsCarousel?.onRestoreInstanceState(feedGroupsListState) + feedGroupsListState = null + } + + feedGroupsSortMenuItem.showMenuItem = groups.size > 1 + items_list.post { feedGroupsSortMenuItem.notifyChanged(PAYLOAD_UPDATE_VISIBILITY_MENU_ITEM) } + } + + // ///////////////////////////////////////////////////////////////////////// + // Contract + // ///////////////////////////////////////////////////////////////////////// + + override fun showLoading() { + super.showLoading() + animateView(items_list, false, 100) + } + + override fun hideLoading() { + super.hideLoading() + animateView(items_list, true, 200) + } + + // ///////////////////////////////////////////////////////////////////////// + // Fragment Error Handling + // ///////////////////////////////////////////////////////////////////////// + + override fun onError(exception: Throwable): Boolean { + if (super.onError(exception)) return true + + onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, "none", "Subscriptions", R.string.general_error) + return true + } + + // ///////////////////////////////////////////////////////////////////////// + // Grid Mode + // ///////////////////////////////////////////////////////////////////////// + + // TODO: Move these out of this class, as it can be reused + + private fun shouldUseGridLayout(): Boolean { + val listMode = PreferenceManager.getDefaultSharedPreferences(requireContext()) + .getString(getString(R.string.list_view_mode_key), getString(R.string.list_view_mode_value)) + + return when (listMode) { + getString(R.string.list_view_mode_auto_key) -> { + val configuration = resources.configuration + + (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE && + configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE)) + } + getString(R.string.list_view_mode_grid_key) -> true + else -> false + } + } + + private fun getGridSpanCount(): Int { + val minWidth = resources.getDimensionPixelSize(R.dimen.channel_item_grid_min_width) + return max(1, floor(resources.displayMetrics.widthPixels / minWidth.toDouble()).toInt()) + } + + companion object { + private const val REQUEST_EXPORT_CODE = 666 + private const val REQUEST_IMPORT_CODE = 667 + } +} diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt new file mode 100644 index 000000000..2740591e6 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt @@ -0,0 +1,95 @@ +package org.schabi.newpipe.local.subscription + +import android.content.Context +import io.reactivex.Completable +import io.reactivex.Flowable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import org.schabi.newpipe.NewPipeDatabase +import org.schabi.newpipe.database.feed.model.FeedGroupEntity +import org.schabi.newpipe.database.subscription.SubscriptionDAO +import org.schabi.newpipe.database.subscription.SubscriptionEntity +import org.schabi.newpipe.extractor.ListInfo +import org.schabi.newpipe.extractor.channel.ChannelInfo +import org.schabi.newpipe.extractor.feed.FeedInfo +import org.schabi.newpipe.extractor.stream.StreamInfoItem +import org.schabi.newpipe.local.feed.FeedDatabaseManager + +class SubscriptionManager(context: Context) { + private val database = NewPipeDatabase.getInstance(context) + private val subscriptionTable = database.subscriptionDAO() + private val feedDatabaseManager = FeedDatabaseManager(context) + + fun subscriptionTable(): SubscriptionDAO = subscriptionTable + fun subscriptions() = subscriptionTable.all + + fun getSubscriptions( + currentGroupId: Long = FeedGroupEntity.GROUP_ALL_ID, + filterQuery: String = "", + showOnlyUngrouped: Boolean = false + ): Flowable> { + return when { + filterQuery.isNotEmpty() -> { + return if (showOnlyUngrouped) { + subscriptionTable.getSubscriptionsOnlyUngroupedFiltered( + currentGroupId, filterQuery) + } else { + subscriptionTable.getSubscriptionsFiltered(filterQuery) + } + } + showOnlyUngrouped -> subscriptionTable.getSubscriptionsOnlyUngrouped(currentGroupId) + else -> subscriptionTable.all + } + } + + fun upsertAll(infoList: List): List { + val listEntities = subscriptionTable.upsertAll( + infoList.map { SubscriptionEntity.from(it) }) + + database.runInTransaction { + infoList.forEachIndexed { index, info -> + feedDatabaseManager.upsertAll(listEntities[index].uid, info.relatedItems) + } + } + + return listEntities + } + + fun updateChannelInfo(info: ChannelInfo): Completable = subscriptionTable.getSubscription(info.serviceId, info.url) + .flatMapCompletable { + Completable.fromRunnable { + it.setData(info.name, info.avatarUrl, info.description, info.subscriberCount) + subscriptionTable.update(it) + feedDatabaseManager.upsertAll(it.uid, info.relatedItems) + } + } + + fun updateFromInfo(subscriptionId: Long, info: ListInfo) { + val subscriptionEntity = subscriptionTable.getSubscription(subscriptionId) + + if (info is FeedInfo) { + subscriptionEntity.name = info.name + } else if (info is ChannelInfo) { + subscriptionEntity.setData(info.name, info.avatarUrl, info.description, info.subscriberCount) + } + + subscriptionTable.update(subscriptionEntity) + } + + fun deleteSubscription(serviceId: Int, url: String): Completable { + return Completable.fromCallable { subscriptionTable.deleteSubscription(serviceId, url) } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + } + + fun insertSubscription(subscriptionEntity: SubscriptionEntity, info: ChannelInfo) { + database.runInTransaction { + val subscriptionId = subscriptionTable.insert(subscriptionEntity) + feedDatabaseManager.upsertAll(subscriptionId, info.relatedItems) + } + } + + fun deleteSubscription(subscriptionEntity: SubscriptionEntity) { + subscriptionTable.delete(subscriptionEntity) + } +} diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionService.java b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionService.java deleted file mode 100644 index 7d6fa5158..000000000 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionService.java +++ /dev/null @@ -1,162 +0,0 @@ -package org.schabi.newpipe.local.subscription; - -import android.content.Context; -import androidx.annotation.NonNull; -import android.util.Log; - -import org.schabi.newpipe.MainActivity; -import org.schabi.newpipe.NewPipeDatabase; -import org.schabi.newpipe.database.AppDatabase; -import org.schabi.newpipe.database.subscription.SubscriptionDAO; -import org.schabi.newpipe.database.subscription.SubscriptionEntity; -import org.schabi.newpipe.extractor.channel.ChannelInfo; -import org.schabi.newpipe.util.ExtractorHelper; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; - -import io.reactivex.Completable; -import io.reactivex.CompletableSource; -import io.reactivex.Flowable; -import io.reactivex.Maybe; -import io.reactivex.Scheduler; -import io.reactivex.functions.Function; -import io.reactivex.schedulers.Schedulers; - -/** - * Subscription Service singleton: - * Provides a basis for channel Subscriptions. - * Provides access to subscription table in database as well as - * up-to-date observations on the subscribed channels - */ -public class SubscriptionService { - - private static volatile SubscriptionService instance; - - public static SubscriptionService getInstance(@NonNull Context context) { - SubscriptionService result = instance; - if (result == null) { - synchronized (SubscriptionService.class) { - result = instance; - if (result == null) { - instance = (result = new SubscriptionService(context)); - } - } - } - - return result; - } - - protected final String TAG = "SubscriptionService@" + Integer.toHexString(hashCode()); - protected static final boolean DEBUG = MainActivity.DEBUG; - private static final int SUBSCRIPTION_DEBOUNCE_INTERVAL = 500; - private static final int SUBSCRIPTION_THREAD_POOL_SIZE = 4; - - private final AppDatabase db; - private final Flowable> subscription; - - private final Scheduler subscriptionScheduler; - - private SubscriptionService(Context context) { - db = NewPipeDatabase.getInstance(context.getApplicationContext()); - subscription = getSubscriptionInfos(); - - final Executor subscriptionExecutor = Executors.newFixedThreadPool(SUBSCRIPTION_THREAD_POOL_SIZE); - subscriptionScheduler = Schedulers.from(subscriptionExecutor); - } - - /** - * Part of subscription observation pipeline - * - * @see SubscriptionService#getSubscription() - */ - private Flowable> getSubscriptionInfos() { - return subscriptionTable().getAll() - // Wait for a period of infrequent updates and return the latest update - .debounce(SUBSCRIPTION_DEBOUNCE_INTERVAL, TimeUnit.MILLISECONDS) - .share() // Share allows multiple subscribers on the same observable - .replay(1) // Replay synchronizes subscribers to the last emitted result - .autoConnect(); - } - - /** - * Provides an observer to the latest update to the subscription table. - *

- * This observer may be subscribed multiple times, where each subscriber obtains - * the latest synchronized changes available, effectively share the same data - * across all subscribers. - *

- * This observer has a debounce cooldown, meaning if multiple updates are observed - * in the cooldown interval, only the latest changes are emitted to the subscribers. - * This reduces the amount of observations caused by frequent updates to the database. - */ - @androidx.annotation.NonNull - public Flowable> getSubscription() { - return subscription; - } - - public Maybe getChannelInfo(final SubscriptionEntity subscriptionEntity) { - if (DEBUG) Log.d(TAG, "getChannelInfo() called with: subscriptionEntity = [" + subscriptionEntity + "]"); - - return Maybe.fromSingle(ExtractorHelper - .getChannelInfo(subscriptionEntity.getServiceId(), subscriptionEntity.getUrl(), false)) - .subscribeOn(subscriptionScheduler); - } - - /** - * Returns the database access interface for subscription table. - */ - public SubscriptionDAO subscriptionTable() { - return db.subscriptionDAO(); - } - - public Completable updateChannelInfo(final ChannelInfo info) { - final Function, CompletableSource> update = new Function, CompletableSource>() { - @Override - public CompletableSource apply(@NonNull List subscriptionEntities) { - if (DEBUG) Log.d(TAG, "updateChannelInfo() called with: subscriptionEntities = [" + subscriptionEntities + "]"); - if (subscriptionEntities.size() == 1) { - SubscriptionEntity subscription = subscriptionEntities.get(0); - - // Subscriber count changes very often, making this check almost unnecessary. - // Consider removing it later. - if (!isSubscriptionUpToDate(info, subscription)) { - subscription.setData(info.getName(), info.getAvatarUrl(), info.getDescription(), info.getSubscriberCount()); - - return Completable.fromRunnable(() -> subscriptionTable().update(subscription)); - } - } - - return Completable.complete(); - } - }; - - return subscriptionTable().getSubscription(info.getServiceId(), info.getUrl()) - .firstOrError() - .flatMapCompletable(update); - } - - public List upsertAll(final List infoList) { - final List entityList = new ArrayList<>(); - for (ChannelInfo info : infoList) entityList.add(SubscriptionEntity.from(info)); - - return subscriptionTable().upsertAll(entityList); - } - - private boolean isSubscriptionUpToDate(final ChannelInfo info, final SubscriptionEntity entity) { - return equalsAndNotNull(info.getUrl(), entity.getUrl()) && - info.getServiceId() == entity.getServiceId() && - info.getName().equals(entity.getName()) && - equalsAndNotNull(info.getAvatarUrl(), entity.getAvatarUrl()) && - equalsAndNotNull(info.getDescription(), entity.getDescription()) && - info.getSubscriberCount() == entity.getSubscriberCount(); - } - - private boolean equalsAndNotNull(final Object o1, final Object o2) { - return (o1 != null && o2 != null) - && o1.equals(o2); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionViewModel.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionViewModel.kt new file mode 100644 index 000000000..b7f16c319 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionViewModel.kt @@ -0,0 +1,52 @@ +package org.schabi.newpipe.local.subscription + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.xwray.groupie.Group +import io.reactivex.schedulers.Schedulers +import java.util.concurrent.TimeUnit +import org.schabi.newpipe.local.feed.FeedDatabaseManager +import org.schabi.newpipe.local.subscription.item.ChannelItem +import org.schabi.newpipe.local.subscription.item.FeedGroupCardItem +import org.schabi.newpipe.util.DEFAULT_THROTTLE_TIMEOUT + +class SubscriptionViewModel(application: Application) : AndroidViewModel(application) { + private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(application) + private var subscriptionManager = SubscriptionManager(application) + + private val mutableStateLiveData = MutableLiveData() + private val mutableFeedGroupsLiveData = MutableLiveData>() + val stateLiveData: LiveData = mutableStateLiveData + val feedGroupsLiveData: LiveData> = mutableFeedGroupsLiveData + + private var feedGroupItemsDisposable = feedDatabaseManager.groups() + .throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS) + .map { it.map(::FeedGroupCardItem) } + .subscribeOn(Schedulers.io()) + .subscribe( + { mutableFeedGroupsLiveData.postValue(it) }, + { mutableStateLiveData.postValue(SubscriptionState.ErrorState(it)) } + ) + + private var stateItemsDisposable = subscriptionManager.subscriptions() + .throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS) + .map { it.map { entity -> ChannelItem(entity.toChannelInfoItem(), entity.uid, ChannelItem.ItemVersion.MINI) } } + .subscribeOn(Schedulers.io()) + .subscribe( + { mutableStateLiveData.postValue(SubscriptionState.LoadedState(it)) }, + { mutableStateLiveData.postValue(SubscriptionState.ErrorState(it)) } + ) + + override fun onCleared() { + super.onCleared() + stateItemsDisposable.dispose() + feedGroupItemsDisposable.dispose() + } + + sealed class SubscriptionState { + data class LoadedState(val subscriptions: List) : SubscriptionState() + data class ErrorState(val error: Throwable? = null) : SubscriptionState() + } +} diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java index 0a45e680a..d812a2a57 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java @@ -3,11 +3,6 @@ package org.schabi.newpipe.local.subscription; import android.app.Activity; import android.content.Intent; import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; -import androidx.core.text.util.LinkifyCompat; -import androidx.appcompat.app.ActionBar; import android.text.TextUtils; import android.text.util.Linkify; import android.view.LayoutInflater; @@ -17,6 +12,12 @@ import android.widget.Button; import android.widget.EditText; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.appcompat.app.ActionBar; +import androidx.core.text.util.LinkifyCompat; + import com.nononsenseapps.filepicker.Utils; import org.schabi.newpipe.BaseFragment; @@ -24,9 +25,9 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor; +import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; -import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.FilePickerActivityHelper; import org.schabi.newpipe.util.ServiceHelper; @@ -46,51 +47,52 @@ public class SubscriptionsImportFragment extends BaseFragment { private static final int REQUEST_IMPORT_FILE_CODE = 666; @State - protected int currentServiceId = Constants.NO_SERVICE_ID; + int currentServiceId = Constants.NO_SERVICE_ID; private List supportedSources; private String relatedUrl; + @StringRes private int instructionsString; - public static SubscriptionsImportFragment getInstance(int serviceId) { - SubscriptionsImportFragment instance = new SubscriptionsImportFragment(); - instance.setInitialData(serviceId); - return instance; - } - - public void setInitialData(int serviceId) { - this.currentServiceId = serviceId; - } - /*////////////////////////////////////////////////////////////////////////// // Views //////////////////////////////////////////////////////////////////////////*/ private TextView infoTextView; - private EditText inputText; private Button inputButton; + public static SubscriptionsImportFragment getInstance(final int serviceId) { + SubscriptionsImportFragment instance = new SubscriptionsImportFragment(); + instance.setInitialData(serviceId); + return instance; + } + + private void setInitialData(final int serviceId) { + this.currentServiceId = serviceId; + } + /////////////////////////////////////////////////////////////////////////// // Fragment LifeCycle /////////////////////////////////////////////////////////////////////////// - @Override - public void onCreate(Bundle savedInstanceState) { + public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setupServiceVariables(); if (supportedSources.isEmpty() && currentServiceId != Constants.NO_SERVICE_ID) { - ErrorActivity.reportError(activity, Collections.emptyList(), null, null, ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, - NewPipe.getNameOfService(currentServiceId), "Service don't support importing", R.string.general_error)); + ErrorActivity.reportError(activity, Collections.emptyList(), null, null, + ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, + NewPipe.getNameOfService(currentServiceId), + "Service don't support importing", R.string.general_error)); activity.finish(); } } @Override - public void setUserVisibleHint(boolean isVisibleToUser) { + public void setUserVisibleHint(final boolean isVisibleToUser) { super.setUserVisibleHint(isVisibleToUser); if (isVisibleToUser) { setTitle(getString(R.string.import_title)); @@ -99,7 +101,9 @@ public class SubscriptionsImportFragment extends BaseFragment { @Nullable @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { + public View onCreateView(@NonNull final LayoutInflater inflater, + @Nullable final ViewGroup container, + final Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_import, container, false); } @@ -108,7 +112,7 @@ public class SubscriptionsImportFragment extends BaseFragment { /////////////////////////////////////////////////////////////////////////*/ @Override - protected void initViews(View rootView, Bundle savedInstanceState) { + protected void initViews(final View rootView, final Bundle savedInstanceState) { super.initViews(rootView, savedInstanceState); inputButton = rootView.findViewById(R.id.input_button); @@ -116,7 +120,8 @@ public class SubscriptionsImportFragment extends BaseFragment { infoTextView = rootView.findViewById(R.id.info_text_view); - // TODO: Support services that can import from more than one source (show the option to the user) + // TODO: Support services that can import from more than one source + // (show the option to the user) if (supportedSources.contains(CHANNEL_URL)) { inputButton.setText(R.string.import_title); inputText.setVisibility(View.VISIBLE); @@ -151,13 +156,15 @@ public class SubscriptionsImportFragment extends BaseFragment { private void onImportClicked() { if (inputText.getVisibility() == View.VISIBLE) { final String value = inputText.getText().toString(); - if (!value.isEmpty()) onImportUrl(value); + if (!value.isEmpty()) { + onImportUrl(value); + } } else { onImportFile(); } } - public void onImportUrl(String value) { + public void onImportUrl(final String value) { ImportConfirmationDialog.show(this, new Intent(activity, SubscriptionsImportService.class) .putExtra(KEY_MODE, CHANNEL_URL_MODE) .putExtra(KEY_VALUE, value) @@ -165,20 +172,24 @@ public class SubscriptionsImportFragment extends BaseFragment { } public void onImportFile() { - startActivityForResult(FilePickerActivityHelper.chooseSingleFile(activity), REQUEST_IMPORT_FILE_CODE); + startActivityForResult(FilePickerActivityHelper.chooseSingleFile(activity), + REQUEST_IMPORT_FILE_CODE); } @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { + public void onActivityResult(final int requestCode, final int resultCode, final Intent data) { super.onActivityResult(requestCode, resultCode, data); - if (data == null) return; + if (data == null) { + return; + } - if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_IMPORT_FILE_CODE && data.getData() != null) { + if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_IMPORT_FILE_CODE + && data.getData() != null) { final String path = Utils.getFileForUri(data.getData()).getAbsolutePath(); - ImportConfirmationDialog.show(this, new Intent(activity, SubscriptionsImportService.class) - .putExtra(KEY_MODE, INPUT_STREAM_MODE) - .putExtra(KEY_VALUE, path) - .putExtra(Constants.KEY_SERVICE_ID, currentServiceId)); + ImportConfirmationDialog.show(this, + new Intent(activity, SubscriptionsImportService.class) + .putExtra(KEY_MODE, INPUT_STREAM_MODE).putExtra(KEY_VALUE, path) + .putExtra(Constants.KEY_SERVICE_ID, currentServiceId)); } } @@ -189,7 +200,8 @@ public class SubscriptionsImportFragment extends BaseFragment { private void setupServiceVariables() { if (currentServiceId != Constants.NO_SERVICE_ID) { try { - final SubscriptionExtractor extractor = NewPipe.getService(currentServiceId).getSubscriptionExtractor(); + final SubscriptionExtractor extractor = NewPipe.getService(currentServiceId) + .getSubscriptionExtractor(); supportedSources = extractor.getSupportedSources(); relatedUrl = extractor.getRelatedUrl(); instructionsString = ServiceHelper.getImportInstructions(currentServiceId); @@ -203,7 +215,7 @@ public class SubscriptionsImportFragment extends BaseFragment { instructionsString = 0; } - private void setInfoText(String infoString) { + private void setInfoText(final String infoString) { infoTextView.setText(infoString); LinkifyCompat.addLinks(infoTextView, Linkify.WEB_URLS); } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/decoration/FeedGroupCarouselDecoration.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/decoration/FeedGroupCarouselDecoration.kt new file mode 100644 index 000000000..7b7490eaa --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/decoration/FeedGroupCarouselDecoration.kt @@ -0,0 +1,35 @@ +package org.schabi.newpipe.local.subscription.decoration + +import android.content.Context +import android.graphics.Rect +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import org.schabi.newpipe.R + +class FeedGroupCarouselDecoration(context: Context) : RecyclerView.ItemDecoration() { + + private val marginStartEnd: Int + private val marginTopBottom: Int + private val marginBetweenItems: Int + + init { + with(context.resources) { + marginStartEnd = getDimensionPixelOffset(R.dimen.feed_group_carousel_start_end_margin) + marginTopBottom = getDimensionPixelOffset(R.dimen.feed_group_carousel_top_bottom_margin) + marginBetweenItems = getDimensionPixelOffset(R.dimen.feed_group_carousel_between_items_margin) + } + } + + override fun getItemOffsets(outRect: Rect, child: View, parent: RecyclerView, state: RecyclerView.State) { + val childAdapterPosition = parent.getChildAdapterPosition(child) + val childAdapterCount = parent.adapter?.itemCount ?: 0 + + outRect.set(marginBetweenItems, marginTopBottom, 0, marginTopBottom) + + if (childAdapterPosition == 0) { + outRect.left = marginStartEnd + } else if (childAdapterPosition == childAdapterCount - 1) { + outRect.right = marginStartEnd + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt new file mode 100644 index 000000000..66387d298 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt @@ -0,0 +1,512 @@ +package org.schabi.newpipe.local.subscription.dialog + +import android.app.Dialog +import android.content.Context +import android.os.Bundle +import android.os.Parcelable +import android.text.Editable +import android.text.TextUtils +import android.text.TextWatcher +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.InputMethodManager +import android.widget.Toast +import androidx.fragment.app.DialogFragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.xwray.groupie.GroupAdapter +import com.xwray.groupie.OnItemClickListener +import com.xwray.groupie.Section +import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder +import icepick.Icepick +import icepick.State +import java.io.Serializable +import kotlin.collections.contains +import kotlinx.android.synthetic.main.dialog_feed_group_create.* +import kotlinx.android.synthetic.main.toolbar_search_layout.* +import org.schabi.newpipe.R +import org.schabi.newpipe.database.feed.model.FeedGroupEntity +import org.schabi.newpipe.fragments.BackPressable +import org.schabi.newpipe.local.subscription.FeedGroupIcon +import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialog.ScreenState.DeleteScreen +import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialog.ScreenState.IconPickerScreen +import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialog.ScreenState.InitialScreen +import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialog.ScreenState.SubscriptionsPickerScreen +import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialogViewModel.DialogEvent.ProcessingEvent +import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialogViewModel.DialogEvent.SuccessEvent +import org.schabi.newpipe.local.subscription.item.EmptyPlaceholderItem +import org.schabi.newpipe.local.subscription.item.PickerIconItem +import org.schabi.newpipe.local.subscription.item.PickerSubscriptionItem +import org.schabi.newpipe.util.AndroidTvUtils +import org.schabi.newpipe.util.ThemeHelper + +class FeedGroupDialog : DialogFragment(), BackPressable { + private lateinit var viewModel: FeedGroupDialogViewModel + private var groupId: Long = NO_GROUP_SELECTED + private var groupIcon: FeedGroupIcon? = null + private var groupSortOrder: Long = -1 + + sealed class ScreenState : Serializable { + object InitialScreen : ScreenState() + object IconPickerScreen : ScreenState() + object SubscriptionsPickerScreen : ScreenState() + object DeleteScreen : ScreenState() + } + + @State @JvmField var selectedIcon: FeedGroupIcon? = null + @State @JvmField var selectedSubscriptions: HashSet = HashSet() + @State @JvmField var wasSubscriptionSelectionChanged: Boolean = false + @State @JvmField var currentScreen: ScreenState = InitialScreen + + @State @JvmField var subscriptionsListState: Parcelable? = null + @State @JvmField var iconsListState: Parcelable? = null + @State @JvmField var wasSearchSubscriptionsVisible = false + @State @JvmField var subscriptionsCurrentSearchQuery = "" + @State @JvmField var subscriptionsShowOnlyUngrouped = false + + private val subscriptionMainSection = Section() + private val subscriptionEmptyFooter = Section() + private lateinit var subscriptionGroupAdapter: GroupAdapter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + Icepick.restoreInstanceState(this, savedInstanceState) + + setStyle(STYLE_NO_TITLE, ThemeHelper.getMinWidthDialogTheme(requireContext())) + groupId = arguments?.getLong(KEY_GROUP_ID, NO_GROUP_SELECTED) ?: NO_GROUP_SELECTED + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.dialog_feed_group_create, container) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return object : Dialog(requireActivity(), theme) { + override fun onBackPressed() { + if (!this@FeedGroupDialog.onBackPressed()) { + super.onBackPressed() + } + } + } + } + + override fun onPause() { + super.onPause() + + wasSearchSubscriptionsVisible = isSearchVisible() + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + + iconsListState = icon_selector.layoutManager?.onSaveInstanceState() + subscriptionsListState = subscriptions_selector_list.layoutManager?.onSaveInstanceState() + + Icepick.saveInstanceState(this, outState) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewModel = ViewModelProvider(this, + FeedGroupDialogViewModel.Factory(requireContext(), + groupId, subscriptionsCurrentSearchQuery, subscriptionsShowOnlyUngrouped) + ).get(FeedGroupDialogViewModel::class.java) + + viewModel.groupLiveData.observe(viewLifecycleOwner, Observer(::handleGroup)) + viewModel.subscriptionsLiveData.observe(viewLifecycleOwner, Observer { + setupSubscriptionPicker(it.first, it.second) + }) + viewModel.dialogEventLiveData.observe(viewLifecycleOwner, Observer { + when (it) { + ProcessingEvent -> disableInput() + SuccessEvent -> dismiss() + } + }) + + subscriptionGroupAdapter = GroupAdapter().apply { + add(subscriptionMainSection) + add(subscriptionEmptyFooter) + spanCount = 4 + } + subscriptions_selector_list.apply { + // Disable animations, too distracting. + itemAnimator = null + adapter = subscriptionGroupAdapter + layoutManager = GridLayoutManager(requireContext(), subscriptionGroupAdapter.spanCount, + RecyclerView.VERTICAL, false).apply { + spanSizeLookup = subscriptionGroupAdapter.spanSizeLookup + } + } + + setupIconPicker() + setupListeners() + + showScreen(currentScreen) + + if (currentScreen == SubscriptionsPickerScreen && wasSearchSubscriptionsVisible) { + showSearch() + } else if (currentScreen == InitialScreen && groupId == NO_GROUP_SELECTED) { + showKeyboard() + } + } + + override fun onDestroyView() { + super.onDestroyView() + subscriptions_selector_list?.adapter = null + icon_selector?.adapter = null + } + + /*/​////////////////////////////////////////////////////////////////////////// + // Setup + //​//////////////////////////////////////////////////////////////////////// */ + + override fun onBackPressed(): Boolean { + if (currentScreen is SubscriptionsPickerScreen && isSearchVisible()) { + hideSearch() + return true + } else if (currentScreen !is InitialScreen) { + showScreen(InitialScreen) + return true + } + + return false + } + + private fun setupListeners() { + delete_button.setOnClickListener { showScreen(DeleteScreen) } + + cancel_button.setOnClickListener { + when (currentScreen) { + InitialScreen -> dismiss() + else -> showScreen(InitialScreen) + } + } + + group_name_input_container.error = null + group_name_input.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(s: Editable?) {} + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + if (group_name_input_container.isErrorEnabled && !s.isNullOrBlank()) { + group_name_input_container.error = null + } + } + }) + + confirm_button.setOnClickListener { handlePositiveButton() } + + select_channel_button.setOnClickListener { + subscriptions_selector_list.scrollToPosition(0) + showScreen(SubscriptionsPickerScreen) + } + + val headerMenu = subscriptions_header_toolbar.menu + requireActivity().menuInflater.inflate(R.menu.menu_feed_group_dialog, headerMenu) + + headerMenu.findItem(R.id.action_search).setOnMenuItemClickListener { + showSearch() + true + } + + headerMenu.findItem(R.id.feed_group_toggle_show_only_ungrouped_subscriptions).apply { + isChecked = subscriptionsShowOnlyUngrouped + setOnMenuItemClickListener { + subscriptionsShowOnlyUngrouped = !subscriptionsShowOnlyUngrouped + it.isChecked = subscriptionsShowOnlyUngrouped + viewModel.toggleShowOnlyUngrouped(subscriptionsShowOnlyUngrouped) + true + } + } + + toolbar_search_clear.setOnClickListener { + if (TextUtils.isEmpty(toolbar_search_edit_text.text)) { + hideSearch() + return@setOnClickListener + } + resetSearch() + showKeyboardSearch() + } + + toolbar_search_edit_text.setOnClickListener { + if (AndroidTvUtils.isTv(context)) { + showKeyboardSearch() + } + } + + toolbar_search_edit_text.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit + override fun afterTextChanged(s: Editable) = Unit + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { + val newQuery: String = toolbar_search_edit_text.text.toString() + subscriptionsCurrentSearchQuery = newQuery + viewModel.filterSubscriptionsBy(newQuery) + } + }) + + subscriptionGroupAdapter?.setOnItemClickListener(subscriptionPickerItemListener) + } + + private fun handlePositiveButton() = when { + currentScreen is InitialScreen -> handlePositiveButtonInitialScreen() + currentScreen is DeleteScreen -> viewModel.deleteGroup() + currentScreen is SubscriptionsPickerScreen && isSearchVisible() -> hideSearch() + else -> showScreen(InitialScreen) + } + + private fun handlePositiveButtonInitialScreen() { + val name = group_name_input.text.toString().trim() + val icon = selectedIcon ?: groupIcon ?: FeedGroupIcon.ALL + + if (name.isBlank()) { + group_name_input_container.error = getString(R.string.feed_group_dialog_empty_name) + group_name_input.text = null + group_name_input.requestFocus() + return + } else { + group_name_input_container.error = null + } + + if (selectedSubscriptions.isEmpty()) { + Toast.makeText(requireContext(), getString(R.string.feed_group_dialog_empty_selection), Toast.LENGTH_SHORT).show() + return + } + + when (groupId) { + NO_GROUP_SELECTED -> viewModel.createGroup(name, icon, selectedSubscriptions) + else -> viewModel.updateGroup(name, icon, selectedSubscriptions, groupSortOrder) + } + } + + private fun handleGroup(feedGroupEntity: FeedGroupEntity? = null) { + val icon = feedGroupEntity?.icon ?: FeedGroupIcon.ALL + val name = feedGroupEntity?.name ?: "" + groupIcon = feedGroupEntity?.icon + groupSortOrder = feedGroupEntity?.sortOrder ?: -1 + + val feedGroupIcon = if (selectedIcon == null) icon else selectedIcon!! + icon_preview.setImageResource(feedGroupIcon.getDrawableRes(requireContext())) + + if (group_name_input.text.isNullOrBlank()) { + group_name_input.setText(name) + } + } + + private val subscriptionPickerItemListener = OnItemClickListener { item, view -> + if (item is PickerSubscriptionItem) { + val subscriptionId = item.subscriptionEntity.uid + wasSubscriptionSelectionChanged = true + + val isSelected = if (this.selectedSubscriptions.contains(subscriptionId)) { + this.selectedSubscriptions.remove(subscriptionId) + false + } else { + this.selectedSubscriptions.add(subscriptionId) + true + } + + item.updateSelected(view, isSelected) + updateSubscriptionSelectedCount() + } + } + + private fun setupSubscriptionPicker( + subscriptions: List, + selectedSubscriptions: Set + ) { + if (!wasSubscriptionSelectionChanged) { + this.selectedSubscriptions.addAll(selectedSubscriptions) + } + + updateSubscriptionSelectedCount() + + if (subscriptions.isEmpty()) { + subscriptionEmptyFooter.clear() + subscriptionEmptyFooter.add(EmptyPlaceholderItem()) + } else { + subscriptionEmptyFooter.clear() + } + + subscriptions.forEach { + it.isSelected = this@FeedGroupDialog.selectedSubscriptions + .contains(it.subscriptionEntity.uid) + } + + subscriptionMainSection.update(subscriptions, false) + + if (subscriptionsListState != null) { + subscriptions_selector_list.layoutManager?.onRestoreInstanceState(subscriptionsListState) + subscriptionsListState = null + } else { + subscriptions_selector_list.scrollToPosition(0) + } + } + + private fun updateSubscriptionSelectedCount() { + val selectedCount = this.selectedSubscriptions.size + val selectedCountText = resources.getQuantityString( + R.plurals.feed_group_dialog_selection_count, + selectedCount, selectedCount) + selected_subscription_count_view.text = selectedCountText + subscriptions_header_info.text = selectedCountText + } + + private fun setupIconPicker() { + val groupAdapter = GroupAdapter() + groupAdapter.addAll(FeedGroupIcon.values().map { PickerIconItem(requireContext(), it) }) + + icon_selector.apply { + layoutManager = GridLayoutManager(requireContext(), 7, RecyclerView.VERTICAL, false) + adapter = groupAdapter + + if (iconsListState != null) { + layoutManager?.onRestoreInstanceState(iconsListState) + iconsListState = null + } + } + + groupAdapter.setOnItemClickListener { item, _ -> + when (item) { + is PickerIconItem -> { + selectedIcon = item.icon + icon_preview.setImageResource(item.iconRes) + + showScreen(InitialScreen) + } + } + } + icon_preview.setOnClickListener { + icon_selector.scrollToPosition(0) + showScreen(IconPickerScreen) + } + + if (groupId == NO_GROUP_SELECTED) { + val icon = selectedIcon ?: FeedGroupIcon.ALL + icon_preview.setImageResource(icon.getDrawableRes(requireContext())) + } + } + + /*/​////////////////////////////////////////////////////////////////////////// + // Screen Selector + //​//////////////////////////////////////////////////////////////////////// */ + + private fun showScreen(screen: ScreenState) { + currentScreen = screen + + options_root.onlyVisibleIn(InitialScreen) + icon_selector.onlyVisibleIn(IconPickerScreen) + subscriptions_selector.onlyVisibleIn(SubscriptionsPickerScreen) + delete_screen_message.onlyVisibleIn(DeleteScreen) + + separator.onlyVisibleIn(SubscriptionsPickerScreen, IconPickerScreen) + cancel_button.onlyVisibleIn(InitialScreen, DeleteScreen) + + confirm_button.setText(when { + currentScreen == InitialScreen && groupId == NO_GROUP_SELECTED -> R.string.create + else -> android.R.string.ok + }) + + delete_button.visibility = when { + currentScreen != InitialScreen -> View.GONE + groupId == NO_GROUP_SELECTED -> View.GONE + else -> View.VISIBLE + } + + hideKeyboard() + hideSearch() + } + + private fun View.onlyVisibleIn(vararg screens: ScreenState) { + visibility = when (currentScreen) { + in screens -> View.VISIBLE + else -> View.GONE + } + } + + /*/​////////////////////////////////////////////////////////////////////////// + // Utils + //​//////////////////////////////////////////////////////////////////////// */ + + private fun isSearchVisible() = subscriptions_header_search_container?.visibility == View.VISIBLE + + private fun resetSearch() { + toolbar_search_edit_text.setText("") + subscriptionsCurrentSearchQuery = "" + viewModel.clearSubscriptionsFilter() + } + + private fun hideSearch() { + resetSearch() + subscriptions_header_search_container.visibility = View.GONE + subscriptions_header_info_container.visibility = View.VISIBLE + subscriptions_header_toolbar.menu.findItem(R.id.action_search).isVisible = true + hideKeyboardSearch() + } + + private fun showSearch() { + subscriptions_header_search_container.visibility = View.VISIBLE + subscriptions_header_info_container.visibility = View.GONE + subscriptions_header_toolbar.menu.findItem(R.id.action_search).isVisible = false + showKeyboardSearch() + } + + private val inputMethodManager by lazy { + requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + } + + private fun showKeyboardSearch() { + if (toolbar_search_edit_text.requestFocus()) { + inputMethodManager.showSoftInput(toolbar_search_edit_text, InputMethodManager.SHOW_IMPLICIT) + } + } + + private fun hideKeyboardSearch() { + inputMethodManager.hideSoftInputFromWindow(toolbar_search_edit_text.windowToken, + InputMethodManager.RESULT_UNCHANGED_SHOWN) + toolbar_search_edit_text.clearFocus() + } + + private fun showKeyboard() { + if (group_name_input.requestFocus()) { + inputMethodManager.showSoftInput(group_name_input, InputMethodManager.SHOW_IMPLICIT) + } + } + + private fun hideKeyboard() { + inputMethodManager.hideSoftInputFromWindow(group_name_input.windowToken, + InputMethodManager.RESULT_UNCHANGED_SHOWN) + group_name_input.clearFocus() + } + + private fun disableInput() { + delete_button?.isEnabled = false + confirm_button?.isEnabled = false + cancel_button?.isEnabled = false + isCancelable = false + + hideKeyboard() + } + + companion object { + private const val KEY_GROUP_ID = "KEY_GROUP_ID" + private const val NO_GROUP_SELECTED = -1L + + fun newInstance(groupId: Long = NO_GROUP_SELECTED): FeedGroupDialog { + val dialog = FeedGroupDialog() + + dialog.arguments = Bundle().apply { + putLong(KEY_GROUP_ID, groupId) + } + + return dialog + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialogViewModel.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialogViewModel.kt new file mode 100644 index 000000000..e9a7e4eb7 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialogViewModel.kt @@ -0,0 +1,127 @@ +package org.schabi.newpipe.local.subscription.dialog + +import android.content.Context +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import io.reactivex.Completable +import io.reactivex.Flowable +import io.reactivex.disposables.Disposable +import io.reactivex.functions.BiFunction +import io.reactivex.processors.BehaviorProcessor +import io.reactivex.schedulers.Schedulers +import org.schabi.newpipe.database.feed.model.FeedGroupEntity +import org.schabi.newpipe.local.feed.FeedDatabaseManager +import org.schabi.newpipe.local.subscription.FeedGroupIcon +import org.schabi.newpipe.local.subscription.SubscriptionManager +import org.schabi.newpipe.local.subscription.item.PickerSubscriptionItem + +class FeedGroupDialogViewModel( + applicationContext: Context, + private val groupId: Long = FeedGroupEntity.GROUP_ALL_ID, + initialQuery: String = "", + initialShowOnlyUngrouped: Boolean = false +) : ViewModel() { + + private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(applicationContext) + private var subscriptionManager = SubscriptionManager(applicationContext) + + private var filterSubscriptions = BehaviorProcessor.create() + private var toggleShowOnlyUngrouped = BehaviorProcessor.create() + + private var subscriptionsFlowable = Flowable + .combineLatest( + filterSubscriptions.startWith(initialQuery), + toggleShowOnlyUngrouped.startWith(initialShowOnlyUngrouped), + BiFunction { t1: String, t2: Boolean -> Filter(t1, t2) } + ) + .distinctUntilChanged() + .switchMap { filter -> + subscriptionManager.getSubscriptions(groupId, filter.query, filter.showOnlyUngrouped) + }.map { list -> list.map { PickerSubscriptionItem(it) } } + + private val mutableGroupLiveData = MutableLiveData() + private val mutableSubscriptionsLiveData = MutableLiveData, Set>>() + private val mutableDialogEventLiveData = MutableLiveData() + val groupLiveData: LiveData = mutableGroupLiveData + val subscriptionsLiveData: LiveData, Set>> = mutableSubscriptionsLiveData + val dialogEventLiveData: LiveData = mutableDialogEventLiveData + + private var actionProcessingDisposable: Disposable? = null + + private var feedGroupDisposable = feedDatabaseManager.getGroup(groupId) + .subscribeOn(Schedulers.io()) + .subscribe(mutableGroupLiveData::postValue) + + private var subscriptionsDisposable = Flowable + .combineLatest(subscriptionsFlowable, feedDatabaseManager.subscriptionIdsForGroup(groupId), + BiFunction { t1: List, t2: List -> t1 to t2.toSet() }) + .subscribeOn(Schedulers.io()) + .subscribe(mutableSubscriptionsLiveData::postValue) + + override fun onCleared() { + super.onCleared() + actionProcessingDisposable?.dispose() + subscriptionsDisposable.dispose() + feedGroupDisposable.dispose() + } + + fun createGroup(name: String, selectedIcon: FeedGroupIcon, selectedSubscriptions: Set) { + doAction(feedDatabaseManager.createGroup(name, selectedIcon) + .flatMapCompletable { + feedDatabaseManager.updateSubscriptionsForGroup(it, selectedSubscriptions.toList()) + }) + } + + fun updateGroup(name: String, selectedIcon: FeedGroupIcon, selectedSubscriptions: Set, sortOrder: Long) { + doAction(feedDatabaseManager.updateSubscriptionsForGroup(groupId, selectedSubscriptions.toList()) + .andThen(feedDatabaseManager.updateGroup(FeedGroupEntity(groupId, name, selectedIcon, sortOrder)))) + } + + fun deleteGroup() { + doAction(feedDatabaseManager.deleteGroup(groupId)) + } + + private fun doAction(completable: Completable) { + if (actionProcessingDisposable == null) { + mutableDialogEventLiveData.value = DialogEvent.ProcessingEvent + + actionProcessingDisposable = completable + .subscribeOn(Schedulers.io()) + .subscribe { mutableDialogEventLiveData.postValue(DialogEvent.SuccessEvent) } + } + } + + fun filterSubscriptionsBy(query: String) { + filterSubscriptions.onNext(query) + } + + fun clearSubscriptionsFilter() { + filterSubscriptions.onNext("") + } + + fun toggleShowOnlyUngrouped(showOnlyUngrouped: Boolean) { + toggleShowOnlyUngrouped.onNext(showOnlyUngrouped) + } + + sealed class DialogEvent { + object ProcessingEvent : DialogEvent() + object SuccessEvent : DialogEvent() + } + + data class Filter(val query: String, val showOnlyUngrouped: Boolean) + + class Factory( + private val context: Context, + private val groupId: Long = FeedGroupEntity.GROUP_ALL_ID, + private val initialQuery: String = "", + private val initialShowOnlyUngrouped: Boolean = false + ) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return FeedGroupDialogViewModel(context.applicationContext, + groupId, initialQuery, initialShowOnlyUngrouped) as T + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupReorderDialog.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupReorderDialog.kt new file mode 100644 index 000000000..92c063b4b --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupReorderDialog.kt @@ -0,0 +1,115 @@ +package org.schabi.newpipe.local.subscription.dialog + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.ItemTouchHelper.SimpleCallback +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.xwray.groupie.GroupAdapter +import com.xwray.groupie.TouchCallback +import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder +import icepick.Icepick +import icepick.State +import java.util.Collections +import kotlinx.android.synthetic.main.dialog_feed_group_reorder.confirm_button +import kotlinx.android.synthetic.main.dialog_feed_group_reorder.feed_groups_list +import org.schabi.newpipe.R +import org.schabi.newpipe.database.feed.model.FeedGroupEntity +import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialogViewModel.DialogEvent.ProcessingEvent +import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialogViewModel.DialogEvent.SuccessEvent +import org.schabi.newpipe.local.subscription.item.FeedGroupReorderItem +import org.schabi.newpipe.util.ThemeHelper + +class FeedGroupReorderDialog : DialogFragment() { + private lateinit var viewModel: FeedGroupReorderDialogViewModel + + @State + @JvmField + var groupOrderedIdList = ArrayList() + private val groupAdapter = GroupAdapter() + private val itemTouchHelper = ItemTouchHelper(getItemTouchCallback()) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + Icepick.restoreInstanceState(this, savedInstanceState) + + setStyle(STYLE_NO_TITLE, ThemeHelper.getMinWidthDialogTheme(requireContext())) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.dialog_feed_group_reorder, container) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewModel = ViewModelProviders.of(this).get(FeedGroupReorderDialogViewModel::class.java) + viewModel.groupsLiveData.observe(viewLifecycleOwner, Observer(::handleGroups)) + viewModel.dialogEventLiveData.observe(viewLifecycleOwner, Observer { + when (it) { + ProcessingEvent -> disableInput() + SuccessEvent -> dismiss() + } + }) + + feed_groups_list.layoutManager = LinearLayoutManager(requireContext()) + feed_groups_list.adapter = groupAdapter + itemTouchHelper.attachToRecyclerView(feed_groups_list) + + confirm_button.setOnClickListener { + viewModel.updateOrder(groupOrderedIdList) + } + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + Icepick.saveInstanceState(this, outState) + } + + private fun handleGroups(list: List) { + val groupList: List + + if (groupOrderedIdList.isEmpty()) { + groupList = list + groupOrderedIdList.addAll(groupList.map { it.uid }) + } else { + groupList = list.sortedBy { groupOrderedIdList.indexOf(it.uid) } + } + + groupAdapter.update(groupList.map { FeedGroupReorderItem(it, itemTouchHelper) }) + } + + private fun disableInput() { + confirm_button?.isEnabled = false + isCancelable = false + } + + private fun getItemTouchCallback(): SimpleCallback { + return object : TouchCallback() { + + override fun onMove( + recyclerView: RecyclerView, + source: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + val sourceIndex = source.adapterPosition + val targetIndex = target.adapterPosition + + groupAdapter.notifyItemMoved(sourceIndex, targetIndex) + Collections.swap(groupOrderedIdList, sourceIndex, targetIndex) + + return true + } + + override fun isLongPressDragEnabled(): Boolean = false + override fun isItemViewSwipeEnabled(): Boolean = false + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, swipeDir: Int) {} + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupReorderDialogViewModel.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupReorderDialogViewModel.kt new file mode 100644 index 000000000..ea2cbe98f --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupReorderDialogViewModel.kt @@ -0,0 +1,52 @@ +package org.schabi.newpipe.local.subscription.dialog + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import io.reactivex.Completable +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import org.schabi.newpipe.database.feed.model.FeedGroupEntity +import org.schabi.newpipe.local.feed.FeedDatabaseManager + +class FeedGroupReorderDialogViewModel(application: Application) : AndroidViewModel(application) { + private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(application) + + private val mutableGroupsLiveData = MutableLiveData>() + private val mutableDialogEventLiveData = MutableLiveData() + val groupsLiveData: LiveData> = mutableGroupsLiveData + val dialogEventLiveData: LiveData = mutableDialogEventLiveData + + private var actionProcessingDisposable: Disposable? = null + + private var groupsDisposable = feedDatabaseManager.groups() + .limit(1) + .subscribeOn(Schedulers.io()) + .subscribe(mutableGroupsLiveData::postValue) + + override fun onCleared() { + super.onCleared() + actionProcessingDisposable?.dispose() + groupsDisposable.dispose() + } + + fun updateOrder(groupIdList: List) { + doAction(feedDatabaseManager.updateGroupsOrder(groupIdList)) + } + + private fun doAction(completable: Completable) { + if (actionProcessingDisposable == null) { + mutableDialogEventLiveData.value = DialogEvent.ProcessingEvent + + actionProcessingDisposable = completable + .subscribeOn(Schedulers.io()) + .subscribe { mutableDialogEventLiveData.postValue(DialogEvent.SuccessEvent) } + } + } + + sealed class DialogEvent { + object ProcessingEvent : DialogEvent() + object SuccessEvent : DialogEvent() + } +} diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/ChannelItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/ChannelItem.kt new file mode 100644 index 000000000..f33c58f43 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/ChannelItem.kt @@ -0,0 +1,67 @@ +package org.schabi.newpipe.local.subscription.item + +import android.content.Context +import com.nostra13.universalimageloader.core.ImageLoader +import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder +import com.xwray.groupie.kotlinandroidextensions.Item +import kotlinx.android.synthetic.main.list_channel_item.itemAdditionalDetails +import kotlinx.android.synthetic.main.list_channel_item.itemChannelDescriptionView +import kotlinx.android.synthetic.main.list_channel_item.itemThumbnailView +import kotlinx.android.synthetic.main.list_channel_item.itemTitleView +import org.schabi.newpipe.R +import org.schabi.newpipe.extractor.channel.ChannelInfoItem +import org.schabi.newpipe.util.ImageDisplayConstants +import org.schabi.newpipe.util.Localization +import org.schabi.newpipe.util.OnClickGesture + +class ChannelItem( + private val infoItem: ChannelInfoItem, + private val subscriptionId: Long = -1L, + var itemVersion: ItemVersion = ItemVersion.NORMAL, + var gesturesListener: OnClickGesture? = null +) : Item() { + + override fun getId(): Long = if (subscriptionId == -1L) super.getId() else subscriptionId + + enum class ItemVersion { NORMAL, MINI, GRID } + + override fun getLayout(): Int = when (itemVersion) { + ItemVersion.NORMAL -> R.layout.list_channel_item + ItemVersion.MINI -> R.layout.list_channel_mini_item + ItemVersion.GRID -> R.layout.list_channel_grid_item + } + + override fun bind(viewHolder: GroupieViewHolder, position: Int) { + viewHolder.itemTitleView.text = infoItem.name + viewHolder.itemAdditionalDetails.text = getDetailLine(viewHolder.root.context) + if (itemVersion == ItemVersion.NORMAL) viewHolder.itemChannelDescriptionView.text = infoItem.description + + ImageLoader.getInstance().displayImage(infoItem.thumbnailUrl, viewHolder.itemThumbnailView, + ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS) + + gesturesListener?.run { + viewHolder.containerView.setOnClickListener { selected(infoItem) } + viewHolder.containerView.setOnLongClickListener { held(infoItem); true } + } + } + + private fun getDetailLine(context: Context): String { + var details = if (infoItem.subscriberCount >= 0) { + Localization.shortSubscriberCount(context, infoItem.subscriberCount) + } else { + context.getString(R.string.subscribers_count_not_available) + } + + if (itemVersion == ItemVersion.NORMAL) { + if (infoItem.streamCount >= 0) { + val formattedVideoAmount = Localization.localizeStreamCount(context, infoItem.streamCount) + details = Localization.concatenateStrings(details, formattedVideoAmount) + } + } + return details + } + + override fun getSpanSize(spanCount: Int, position: Int): Int { + return if (itemVersion == ItemVersion.GRID) 1 else spanCount + } +} diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/EmptyPlaceholderItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/EmptyPlaceholderItem.kt new file mode 100644 index 000000000..ef7eb93cd --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/EmptyPlaceholderItem.kt @@ -0,0 +1,11 @@ +package org.schabi.newpipe.local.subscription.item + +import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder +import com.xwray.groupie.kotlinandroidextensions.Item +import org.schabi.newpipe.R + +class EmptyPlaceholderItem : Item() { + override fun getLayout(): Int = R.layout.list_empty_view + override fun bind(viewHolder: GroupieViewHolder, position: Int) {} + override fun getSpanSize(spanCount: Int, position: Int): Int = spanCount +} diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupAddItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupAddItem.kt new file mode 100644 index 000000000..1bc4d1673 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupAddItem.kt @@ -0,0 +1,10 @@ +package org.schabi.newpipe.local.subscription.item + +import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder +import com.xwray.groupie.kotlinandroidextensions.Item +import org.schabi.newpipe.R + +class FeedGroupAddItem : Item() { + override fun getLayout(): Int = R.layout.feed_group_add_new_item + override fun bind(viewHolder: GroupieViewHolder, position: Int) {} +} diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupCardItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupCardItem.kt new file mode 100644 index 000000000..12ff47b3f --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupCardItem.kt @@ -0,0 +1,31 @@ +package org.schabi.newpipe.local.subscription.item + +import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder +import com.xwray.groupie.kotlinandroidextensions.Item +import kotlinx.android.synthetic.main.feed_group_card_item.icon +import kotlinx.android.synthetic.main.feed_group_card_item.title +import org.schabi.newpipe.R +import org.schabi.newpipe.database.feed.model.FeedGroupEntity +import org.schabi.newpipe.local.subscription.FeedGroupIcon + +data class FeedGroupCardItem( + val groupId: Long = FeedGroupEntity.GROUP_ALL_ID, + val name: String, + val icon: FeedGroupIcon +) : Item() { + constructor (feedGroupEntity: FeedGroupEntity) : this(feedGroupEntity.uid, feedGroupEntity.name, feedGroupEntity.icon) + + override fun getId(): Long { + return when (groupId) { + FeedGroupEntity.GROUP_ALL_ID -> super.getId() + else -> groupId + } + } + + override fun getLayout(): Int = R.layout.feed_group_card_item + + override fun bind(viewHolder: GroupieViewHolder, position: Int) { + viewHolder.title.text = name + viewHolder.icon.setImageResource(icon.getDrawableRes(viewHolder.containerView.context)) + } +} diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupCarouselItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupCarouselItem.kt new file mode 100644 index 000000000..dfffce59c --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupCarouselItem.kt @@ -0,0 +1,57 @@ +package org.schabi.newpipe.local.subscription.item + +import android.content.Context +import android.os.Parcelable +import android.view.View +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.xwray.groupie.GroupAdapter +import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder +import com.xwray.groupie.kotlinandroidextensions.Item +import kotlinx.android.synthetic.main.feed_item_carousel.recycler_view +import org.schabi.newpipe.R +import org.schabi.newpipe.local.subscription.decoration.FeedGroupCarouselDecoration + +class FeedGroupCarouselItem(context: Context, private val carouselAdapter: GroupAdapter) : Item() { + private val feedGroupCarouselDecoration = FeedGroupCarouselDecoration(context) + + private var linearLayoutManager: LinearLayoutManager? = null + private var listState: Parcelable? = null + + override fun getLayout() = R.layout.feed_item_carousel + + fun onSaveInstanceState(): Parcelable? { + listState = linearLayoutManager?.onSaveInstanceState() + return listState + } + + fun onRestoreInstanceState(state: Parcelable?) { + linearLayoutManager?.onRestoreInstanceState(state) + listState = state + } + + override fun createViewHolder(itemView: View): GroupieViewHolder { + val viewHolder = super.createViewHolder(itemView) + + linearLayoutManager = LinearLayoutManager(itemView.context, RecyclerView.HORIZONTAL, false) + + viewHolder.recycler_view.apply { + layoutManager = linearLayoutManager + adapter = carouselAdapter + addItemDecoration(feedGroupCarouselDecoration) + } + + return viewHolder + } + + override fun bind(viewHolder: GroupieViewHolder, position: Int) { + viewHolder.recycler_view.apply { adapter = carouselAdapter } + linearLayoutManager?.onRestoreInstanceState(listState) + } + + override fun unbind(viewHolder: GroupieViewHolder) { + super.unbind(viewHolder) + + listState = linearLayoutManager?.onSaveInstanceState() + } +} diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupReorderItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupReorderItem.kt new file mode 100644 index 000000000..717e2410a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupReorderItem.kt @@ -0,0 +1,50 @@ +package org.schabi.newpipe.local.subscription.item + +import android.view.MotionEvent +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.ItemTouchHelper.DOWN +import androidx.recyclerview.widget.ItemTouchHelper.UP +import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder +import com.xwray.groupie.kotlinandroidextensions.Item +import kotlinx.android.synthetic.main.feed_group_reorder_item.group_icon +import kotlinx.android.synthetic.main.feed_group_reorder_item.group_name +import kotlinx.android.synthetic.main.feed_group_reorder_item.handle +import org.schabi.newpipe.R +import org.schabi.newpipe.database.feed.model.FeedGroupEntity +import org.schabi.newpipe.local.subscription.FeedGroupIcon + +data class FeedGroupReorderItem( + val groupId: Long = FeedGroupEntity.GROUP_ALL_ID, + val name: String, + val icon: FeedGroupIcon, + val dragCallback: ItemTouchHelper +) : Item() { + constructor (feedGroupEntity: FeedGroupEntity, dragCallback: ItemTouchHelper) : + this(feedGroupEntity.uid, feedGroupEntity.name, feedGroupEntity.icon, dragCallback) + + override fun getId(): Long { + return when (groupId) { + FeedGroupEntity.GROUP_ALL_ID -> super.getId() + else -> groupId + } + } + + override fun getLayout(): Int = R.layout.feed_group_reorder_item + + override fun bind(viewHolder: GroupieViewHolder, position: Int) { + viewHolder.group_name.text = name + viewHolder.group_icon.setImageResource(icon.getDrawableRes(viewHolder.containerView.context)) + viewHolder.handle.setOnTouchListener { _, event -> + if (event.actionMasked == MotionEvent.ACTION_DOWN) { + dragCallback.startDrag(viewHolder) + return@setOnTouchListener true + } + + false + } + } + + override fun getDragDirs(): Int { + return UP or DOWN + } +} diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedImportExportItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedImportExportItem.kt new file mode 100644 index 000000000..5478dcac4 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedImportExportItem.kt @@ -0,0 +1,119 @@ +package org.schabi.newpipe.local.subscription.item + +import android.graphics.Color +import android.graphics.PorterDuff +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.DrawableRes +import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder +import com.xwray.groupie.kotlinandroidextensions.Item +import kotlinx.android.synthetic.main.feed_import_export_group.export_to_options +import kotlinx.android.synthetic.main.feed_import_export_group.import_export +import kotlinx.android.synthetic.main.feed_import_export_group.import_export_expand_icon +import kotlinx.android.synthetic.main.feed_import_export_group.import_export_options +import kotlinx.android.synthetic.main.feed_import_export_group.import_from_options +import org.schabi.newpipe.R +import org.schabi.newpipe.extractor.NewPipe +import org.schabi.newpipe.extractor.exceptions.ExtractionException +import org.schabi.newpipe.util.AnimationUtils +import org.schabi.newpipe.util.ServiceHelper +import org.schabi.newpipe.util.ThemeHelper +import org.schabi.newpipe.views.CollapsibleView + +class FeedImportExportItem( + val onImportPreviousSelected: () -> Unit, + val onImportFromServiceSelected: (Int) -> Unit, + val onExportSelected: () -> Unit, + var isExpanded: Boolean = false +) : Item() { + companion object { + const val REFRESH_EXPANDED_STATUS = 123 + } + + override fun bind(viewHolder: GroupieViewHolder, position: Int, payloads: MutableList) { + if (payloads.contains(REFRESH_EXPANDED_STATUS)) { + viewHolder.import_export_options.apply { if (isExpanded) expand() else collapse() } + return + } + + super.bind(viewHolder, position, payloads) + } + + override fun getLayout(): Int = R.layout.feed_import_export_group + + override fun bind(viewHolder: GroupieViewHolder, position: Int) { + if (viewHolder.import_from_options.childCount == 0) setupImportFromItems(viewHolder.import_from_options) + if (viewHolder.export_to_options.childCount == 0) setupExportToItems(viewHolder.export_to_options) + + expandIconListener?.let { viewHolder.import_export_options.removeListener(it) } + expandIconListener = CollapsibleView.StateListener { newState -> + AnimationUtils.animateRotation(viewHolder.import_export_expand_icon, + 250, if (newState == CollapsibleView.COLLAPSED) 0 else 180) + } + + viewHolder.import_export_options.currentState = if (isExpanded) CollapsibleView.EXPANDED else CollapsibleView.COLLAPSED + viewHolder.import_export_expand_icon.rotation = if (isExpanded) 180F else 0F + viewHolder.import_export_options.ready() + + viewHolder.import_export_options.addListener(expandIconListener) + viewHolder.import_export.setOnClickListener { + viewHolder.import_export_options.switchState() + isExpanded = viewHolder.import_export_options.currentState == CollapsibleView.EXPANDED + } + } + + override fun unbind(viewHolder: GroupieViewHolder) { + super.unbind(viewHolder) + expandIconListener?.let { viewHolder.import_export_options.removeListener(it) } + expandIconListener = null + } + + private var expandIconListener: CollapsibleView.StateListener? = null + + private fun addItemView(title: String, @DrawableRes icon: Int, container: ViewGroup): View { + val itemRoot = View.inflate(container.context, R.layout.subscription_import_export_item, null) + val titleView = itemRoot.findViewById(android.R.id.text1) + val iconView = itemRoot.findViewById(android.R.id.icon1) + + titleView.text = title + iconView.setImageResource(icon) + + container.addView(itemRoot) + return itemRoot + } + + private fun setupImportFromItems(listHolder: ViewGroup) { + val previousBackupItem = addItemView(listHolder.context.getString(R.string.previous_export), + ThemeHelper.resolveResourceIdFromAttr(listHolder.context, R.attr.ic_backup), listHolder) + previousBackupItem.setOnClickListener { onImportPreviousSelected() } + + val iconColor = if (ThemeHelper.isLightThemeSelected(listHolder.context)) Color.BLACK else Color.WHITE + val services = listHolder.context.resources.getStringArray(R.array.service_list) + for (serviceName in services) { + try { + val service = NewPipe.getService(serviceName) + + val subscriptionExtractor = service.subscriptionExtractor ?: continue + + val supportedSources = subscriptionExtractor.supportedSources + if (supportedSources.isEmpty()) continue + + val itemView = addItemView(serviceName, ServiceHelper.getIcon(service.serviceId), listHolder) + val iconView = itemView.findViewById(android.R.id.icon1) + iconView.setColorFilter(iconColor, PorterDuff.Mode.SRC_IN) + + itemView.setOnClickListener { onImportFromServiceSelected(service.serviceId) } + } catch (e: ExtractionException) { + throw RuntimeException("Services array contains an entry that it's not a valid service name ($serviceName)", e) + } + } + } + + private fun setupExportToItems(listHolder: ViewGroup) { + val previousBackupItem = addItemView(listHolder.context.getString(R.string.file), + ThemeHelper.resolveResourceIdFromAttr(listHolder.context, R.attr.ic_save), listHolder) + previousBackupItem.setOnClickListener { onExportSelected() } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/HeaderItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/HeaderItem.kt new file mode 100644 index 000000000..9798dac1b --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/HeaderItem.kt @@ -0,0 +1,19 @@ +package org.schabi.newpipe.local.subscription.item + +import android.view.View.OnClickListener +import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder +import com.xwray.groupie.kotlinandroidextensions.Item +import kotlinx.android.synthetic.main.header_item.header_title +import org.schabi.newpipe.R + +class HeaderItem(val title: String, private val onClickListener: (() -> Unit)? = null) : Item() { + + override fun getLayout(): Int = R.layout.header_item + + override fun bind(viewHolder: GroupieViewHolder, position: Int) { + viewHolder.header_title.text = title + + val listener: OnClickListener? = if (onClickListener != null) OnClickListener { onClickListener.invoke() } else null + viewHolder.root.setOnClickListener(listener) + } +} diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/HeaderWithMenuItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/HeaderWithMenuItem.kt new file mode 100644 index 000000000..324932256 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/HeaderWithMenuItem.kt @@ -0,0 +1,52 @@ +package org.schabi.newpipe.local.subscription.item + +import android.view.View.GONE +import android.view.View.OnClickListener +import android.view.View.VISIBLE +import androidx.annotation.DrawableRes +import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder +import com.xwray.groupie.kotlinandroidextensions.Item +import kotlinx.android.synthetic.main.header_with_menu_item.header_menu_item +import kotlinx.android.synthetic.main.header_with_menu_item.header_title +import org.schabi.newpipe.R + +class HeaderWithMenuItem( + val title: String, + @DrawableRes val itemIcon: Int = 0, + var showMenuItem: Boolean = true, + private val onClickListener: (() -> Unit)? = null, + private val menuItemOnClickListener: (() -> Unit)? = null +) : Item() { + companion object { + const val PAYLOAD_UPDATE_VISIBILITY_MENU_ITEM = 1 + } + + override fun getLayout(): Int = R.layout.header_with_menu_item + + override fun bind(viewHolder: GroupieViewHolder, position: Int, payloads: MutableList) { + if (payloads.contains(PAYLOAD_UPDATE_VISIBILITY_MENU_ITEM)) { + updateMenuItemVisibility(viewHolder) + return + } + + super.bind(viewHolder, position, payloads) + } + + override fun bind(viewHolder: GroupieViewHolder, position: Int) { + viewHolder.header_title.text = title + viewHolder.header_menu_item.setImageResource(itemIcon) + + val listener: OnClickListener? = + onClickListener?.let { OnClickListener { onClickListener.invoke() } } + viewHolder.root.setOnClickListener(listener) + + val menuItemListener: OnClickListener? = + menuItemOnClickListener?.let { OnClickListener { menuItemOnClickListener.invoke() } } + viewHolder.header_menu_item.setOnClickListener(menuItemListener) + updateMenuItemVisibility(viewHolder) + } + + private fun updateMenuItemVisibility(viewHolder: GroupieViewHolder) { + viewHolder.header_menu_item.visibility = if (showMenuItem) VISIBLE else GONE + } +} diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/PickerIconItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/PickerIconItem.kt new file mode 100644 index 000000000..4f3678e95 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/PickerIconItem.kt @@ -0,0 +1,20 @@ +package org.schabi.newpipe.local.subscription.item + +import android.content.Context +import androidx.annotation.DrawableRes +import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder +import com.xwray.groupie.kotlinandroidextensions.Item +import kotlinx.android.synthetic.main.picker_icon_item.icon_view +import org.schabi.newpipe.R +import org.schabi.newpipe.local.subscription.FeedGroupIcon + +class PickerIconItem(context: Context, val icon: FeedGroupIcon) : Item() { + @DrawableRes + val iconRes: Int = icon.getDrawableRes(context) + + override fun getLayout(): Int = R.layout.picker_icon_item + + override fun bind(viewHolder: GroupieViewHolder, position: Int) { + viewHolder.icon_view.setImageResource(iconRes) + } +} diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/PickerSubscriptionItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/PickerSubscriptionItem.kt new file mode 100644 index 000000000..7d33da71f --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/PickerSubscriptionItem.kt @@ -0,0 +1,44 @@ +package org.schabi.newpipe.local.subscription.item + +import android.view.View +import com.nostra13.universalimageloader.core.ImageLoader +import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder +import com.xwray.groupie.kotlinandroidextensions.Item +import kotlinx.android.synthetic.main.picker_subscription_item.* +import kotlinx.android.synthetic.main.picker_subscription_item.view.* +import org.schabi.newpipe.R +import org.schabi.newpipe.database.subscription.SubscriptionEntity +import org.schabi.newpipe.util.AnimationUtils +import org.schabi.newpipe.util.AnimationUtils.animateView +import org.schabi.newpipe.util.ImageDisplayConstants + +data class PickerSubscriptionItem( + val subscriptionEntity: SubscriptionEntity, + var isSelected: Boolean = false +) : Item() { + override fun getId(): Long = subscriptionEntity.uid + override fun getLayout(): Int = R.layout.picker_subscription_item + override fun getSpanSize(spanCount: Int, position: Int): Int = 1 + + override fun bind(viewHolder: GroupieViewHolder, position: Int) { + ImageLoader.getInstance().displayImage(subscriptionEntity.avatarUrl, + viewHolder.thumbnail_view, ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS) + + viewHolder.title_view.text = subscriptionEntity.name + viewHolder.selected_highlight.visibility = if (isSelected) View.VISIBLE else View.GONE + } + + override fun unbind(viewHolder: GroupieViewHolder) { + super.unbind(viewHolder) + + viewHolder.selected_highlight.animate().setListener(null).cancel() + viewHolder.selected_highlight.visibility = View.GONE + viewHolder.selected_highlight.alpha = 1F + } + + fun updateSelected(containerView: View, isSelected: Boolean) { + this.isSelected = isSelected + animateView(containerView.selected_highlight, + AnimationUtils.Type.LIGHT_SCALE_AND_ALPHA, isSelected, 150) + } +} diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/services/BaseImportExportService.java b/app/src/main/java/org/schabi/newpipe/local/subscription/services/BaseImportExportService.java index 6b607cdca..f485844ea 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/services/BaseImportExportService.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/services/BaseImportExportService.java @@ -23,24 +23,24 @@ import android.app.Service; import android.content.Intent; import android.os.Build; import android.os.IBinder; +import android.text.TextUtils; +import android.widget.Toast; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; -import android.text.TextUtils; -import android.widget.Toast; import org.reactivestreams.Publisher; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor; +import org.schabi.newpipe.local.subscription.SubscriptionManager; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; -import org.schabi.newpipe.local.subscription.ImportExportEventListener; -import org.schabi.newpipe.local.subscription.SubscriptionService; +import org.schabi.newpipe.util.ExceptionUtils; import java.io.FileNotFoundException; -import java.io.IOException; import java.util.Collections; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -54,23 +54,43 @@ import io.reactivex.processors.PublishProcessor; public abstract class BaseImportExportService extends Service { protected final String TAG = this.getClass().getSimpleName(); - protected NotificationManagerCompat notificationManager; - protected NotificationCompat.Builder notificationBuilder; - - protected SubscriptionService subscriptionService; protected final CompositeDisposable disposables = new CompositeDisposable(); protected final PublishProcessor notificationUpdater = PublishProcessor.create(); + protected NotificationManagerCompat notificationManager; + protected NotificationCompat.Builder notificationBuilder; + protected SubscriptionManager subscriptionManager; + + private static final int NOTIFICATION_SAMPLING_PERIOD = 2500; + + protected final AtomicInteger currentProgress = new AtomicInteger(-1); + protected final AtomicInteger maxProgress = new AtomicInteger(-1); + protected final ImportExportEventListener eventListener = new ImportExportEventListener() { + @Override + public void onSizeReceived(final int size) { + maxProgress.set(size); + currentProgress.set(0); + } + + @Override + public void onItemCompleted(final String itemName) { + currentProgress.incrementAndGet(); + notificationUpdater.onNext(itemName); + } + }; + + protected Toast toast; + @Nullable @Override - public IBinder onBind(Intent intent) { + public IBinder onBind(final Intent intent) { return null; } @Override public void onCreate() { super.onCreate(); - subscriptionService = SubscriptionService.getInstance(this); + subscriptionManager = new SubscriptionManager(this); setupNotification(); } @@ -88,25 +108,8 @@ public abstract class BaseImportExportService extends Service { // Notification Impl //////////////////////////////////////////////////////////////////////////*/ - private static final int NOTIFICATION_SAMPLING_PERIOD = 2500; - - protected final AtomicInteger currentProgress = new AtomicInteger(-1); - protected final AtomicInteger maxProgress = new AtomicInteger(-1); - protected final ImportExportEventListener eventListener = new ImportExportEventListener() { - @Override - public void onSizeReceived(int size) { - maxProgress.set(size); - currentProgress.set(0); - } - - @Override - public void onItemCompleted(String itemName) { - currentProgress.incrementAndGet(); - notificationUpdater.onNext(itemName); - } - }; - protected abstract int getNotificationId(); + @StringRes public abstract int getTitle(); @@ -115,8 +118,9 @@ public abstract class BaseImportExportService extends Service { notificationBuilder = createNotification(); startForeground(getNotificationId(), notificationBuilder.build()); - final Function, Publisher> throttleAfterFirstEmission = flow -> flow.limit(1) - .concatWith(flow.skip(1).throttleLast(NOTIFICATION_SAMPLING_PERIOD, TimeUnit.MILLISECONDS)); + final Function, Publisher> throttleAfterFirstEmission = flow -> + flow.limit(1).concatWith(flow.skip(1) + .throttleLast(NOTIFICATION_SAMPLING_PERIOD, TimeUnit.MILLISECONDS)); disposables.add(notificationUpdater .filter(s -> !s.isEmpty()) @@ -125,17 +129,20 @@ public abstract class BaseImportExportService extends Service { .subscribe(this::updateNotification)); } - protected void updateNotification(String text) { - notificationBuilder.setProgress(maxProgress.get(), currentProgress.get(), maxProgress.get() == -1); + protected void updateNotification(final String text) { + notificationBuilder + .setProgress(maxProgress.get(), currentProgress.get(), maxProgress.get() == -1); final String progressText = currentProgress + "/" + maxProgress; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - if (!TextUtils.isEmpty(text)) text = text + " (" + progressText + ")"; + if (!TextUtils.isEmpty(text)) { + notificationBuilder.setContentText(text + " (" + progressText + ")"); + } } else { notificationBuilder.setContentInfo(progressText); + notificationBuilder.setContentText(text); } - if (!TextUtils.isEmpty(text)) notificationBuilder.setContentText(text); notificationManager.notify(getNotificationId(), notificationBuilder.build()); } @@ -143,16 +150,16 @@ public abstract class BaseImportExportService extends Service { postErrorResult(null, null); } - protected void stopAndReportError(@Nullable Throwable error, String request) { + protected void stopAndReportError(@Nullable final Throwable error, final String request) { stopService(); - final ErrorActivity.ErrorInfo errorInfo = ErrorActivity.ErrorInfo.make(UserAction.SUBSCRIPTION, "unknown", - request, R.string.general_error); - ErrorActivity.reportError(this, error != null ? Collections.singletonList(error) : Collections.emptyList(), - null, null, errorInfo); + final ErrorActivity.ErrorInfo errorInfo = ErrorActivity.ErrorInfo + .make(UserAction.SUBSCRIPTION, "unknown", request, R.string.general_error); + ErrorActivity.reportError(this, error != null ? Collections.singletonList(error) + : Collections.emptyList(), null, null, errorInfo); } - protected void postErrorResult(String title, String text) { + protected void postErrorResult(final String title, final String text) { disposeAll(); stopForeground(true); stopSelf(); @@ -161,13 +168,14 @@ public abstract class BaseImportExportService extends Service { return; } - text = text == null ? "" : text; - notificationBuilder = new NotificationCompat.Builder(this, getString(R.string.notification_channel_id)) + final String textOrEmpty = text == null ? "" : text; + notificationBuilder = new NotificationCompat + .Builder(this, getString(R.string.notification_channel_id)) .setSmallIcon(R.drawable.ic_newpipe_triangle_white) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setContentTitle(title) - .setStyle(new NotificationCompat.BigTextStyle().bigText(text)) - .setContentText(text); + .setStyle(new NotificationCompat.BigTextStyle().bigText(textOrEmpty)) + .setContentText(textOrEmpty); notificationManager.notify(getNotificationId(), notificationBuilder.build()); } @@ -184,14 +192,14 @@ public abstract class BaseImportExportService extends Service { // Toast //////////////////////////////////////////////////////////////////////////*/ - protected Toast toast; - - protected void showToast(@StringRes int message) { + protected void showToast(@StringRes final int message) { showToast(getString(message)); } - protected void showToast(String message) { - if (toast != null) toast.cancel(); + protected void showToast(final String message) { + if (toast != null) { + toast.cancel(); + } toast = Toast.makeText(this, message, Toast.LENGTH_SHORT); toast.show(); @@ -201,7 +209,7 @@ public abstract class BaseImportExportService extends Service { // Error handling //////////////////////////////////////////////////////////////////////////*/ - protected void handleError(@StringRes int errorTitle, @NonNull Throwable error) { + protected void handleError(@StringRes final int errorTitle, @NonNull final Throwable error) { String message = getErrorMessage(error); if (TextUtils.isEmpty(message)) { @@ -213,13 +221,13 @@ public abstract class BaseImportExportService extends Service { postErrorResult(getString(errorTitle), message); } - protected String getErrorMessage(Throwable error) { + protected String getErrorMessage(final Throwable error) { String message = null; if (error instanceof SubscriptionExtractor.InvalidSourceException) { message = getString(R.string.invalid_source); } else if (error instanceof FileNotFoundException) { message = getString(R.string.invalid_file); - } else if (error instanceof IOException) { + } else if (ExceptionUtils.isNetworkRelated(error)) { message = getString(R.string.network_error); } return message; diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/ImportExportEventListener.java b/app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportEventListener.java similarity index 87% rename from app/src/main/java/org/schabi/newpipe/local/subscription/ImportExportEventListener.java rename to app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportEventListener.java index 01c0427f3..34bd68f5e 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/ImportExportEventListener.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportEventListener.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.local.subscription; +package org.schabi.newpipe.local.subscription.services; public interface ImportExportEventListener { /** @@ -14,4 +14,4 @@ public interface ImportExportEventListener { * @param itemName the name of the subscription item */ void onItemCompleted(String itemName); -} \ No newline at end of file +} diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/ImportExportJsonHelper.java b/app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportJsonHelper.java similarity index 70% rename from app/src/main/java/org/schabi/newpipe/local/subscription/ImportExportJsonHelper.java rename to app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportJsonHelper.java index ebfff9fe2..e6e081689 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/ImportExportJsonHelper.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportJsonHelper.java @@ -17,7 +17,7 @@ * along with this program. If not, see . */ -package org.schabi.newpipe.local.subscription; +package org.schabi.newpipe.local.subscription.services; import androidx.annotation.Nullable; @@ -41,8 +41,7 @@ import java.util.List; * A JSON implementation capable of importing and exporting subscriptions, it has the advantage * of being able to transfer subscriptions to any device. */ -public class ImportExportJsonHelper { - +public final class ImportExportJsonHelper { /*////////////////////////////////////////////////////////////////////////// // Json implementation //////////////////////////////////////////////////////////////////////////*/ @@ -56,26 +55,37 @@ public class ImportExportJsonHelper { private static final String JSON_URL_KEY = "url"; private static final String JSON_NAME_KEY = "name"; + private ImportExportJsonHelper() { } + /** - * Read a JSON source through the input stream and return the parsed subscription items. + * Read a JSON source through the input stream. * * @param in the input stream (e.g. a file) * @param eventListener listener for the events generated + * @return the parsed subscription items */ - public static List readFrom(InputStream in, @Nullable ImportExportEventListener eventListener) throws InvalidSourceException { - if (in == null) throw new InvalidSourceException("input is null"); + public static List readFrom( + final InputStream in, @Nullable final ImportExportEventListener eventListener) + throws InvalidSourceException { + if (in == null) { + throw new InvalidSourceException("input is null"); + } final List channels = new ArrayList<>(); try { - JsonObject parentObject = JsonParser.object().from(in); - JsonArray channelsArray = parentObject.getArray(JSON_SUBSCRIPTIONS_ARRAY_KEY); - if (eventListener != null) eventListener.onSizeReceived(channelsArray.size()); + final JsonObject parentObject = JsonParser.object().from(in); - if (channelsArray == null) { + if (!parentObject.has(JSON_SUBSCRIPTIONS_ARRAY_KEY)) { throw new InvalidSourceException("Channels array is null"); } + final JsonArray channelsArray = parentObject.getArray(JSON_SUBSCRIPTIONS_ARRAY_KEY); + + if (eventListener != null) { + eventListener.onSizeReceived(channelsArray.size()); + } + for (Object o : channelsArray) { if (o instanceof JsonObject) { JsonObject itemObject = (JsonObject) o; @@ -85,7 +95,9 @@ public class ImportExportJsonHelper { if (url != null && name != null && !url.isEmpty() && !name.isEmpty()) { channels.add(new SubscriptionItem(serviceId, url, name)); - if (eventListener != null) eventListener.onItemCompleted(name); + if (eventListener != null) { + eventListener.onItemCompleted(name); + } } } } @@ -103,7 +115,8 @@ public class ImportExportJsonHelper { * @param out the output stream (e.g. a file) * @param eventListener listener for the events generated */ - public static void writeTo(List items, OutputStream out, @Nullable ImportExportEventListener eventListener) { + public static void writeTo(final List items, final OutputStream out, + @Nullable final ImportExportEventListener eventListener) { JsonAppendableWriter writer = JsonWriter.on(out); writeTo(items, writer, eventListener); writer.done(); @@ -111,9 +124,15 @@ public class ImportExportJsonHelper { /** * @see #writeTo(List, OutputStream, ImportExportEventListener) + * @param items the list of subscriptions items + * @param writer the output {@link JsonSink} + * @param eventListener listener for the events generated */ - public static void writeTo(List items, JsonSink writer, @Nullable ImportExportEventListener eventListener) { - if (eventListener != null) eventListener.onSizeReceived(items.size()); + public static void writeTo(final List items, final JsonSink writer, + @Nullable final ImportExportEventListener eventListener) { + if (eventListener != null) { + eventListener.onSizeReceived(items.size()); + } writer.object(); @@ -128,11 +147,12 @@ public class ImportExportJsonHelper { writer.value(JSON_NAME_KEY, item.getName()); writer.end(); - if (eventListener != null) eventListener.onItemCompleted(item.getName()); + if (eventListener != null) { + eventListener.onItemCompleted(item.getName()); + } } writer.end(); writer.end(); } - } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java index 31cd4b603..12b64d89d 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java @@ -20,16 +20,16 @@ package org.schabi.newpipe.local.subscription.services; import android.content.Intent; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; import android.text.TextUtils; import android.util.Log; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.schabi.newpipe.R; import org.schabi.newpipe.database.subscription.SubscriptionEntity; import org.schabi.newpipe.extractor.subscription.SubscriptionItem; -import org.schabi.newpipe.local.subscription.ImportExportJsonHelper; import java.io.File; import java.io.FileNotFoundException; @@ -47,26 +47,33 @@ public class SubscriptionsExportService extends BaseImportExportService { public static final String KEY_FILE_PATH = "key_file_path"; /** - * A {@link LocalBroadcastManager local broadcast} will be made with this action when the export is successfully completed. + * A {@link LocalBroadcastManager local broadcast} will be made with this action + * when the export is successfully completed. */ - public static final String EXPORT_COMPLETE_ACTION = "org.schabi.newpipe.local.subscription.services.SubscriptionsExportService.EXPORT_COMPLETE"; + public static final String EXPORT_COMPLETE_ACTION = "org.schabi.newpipe.local.subscription" + + ".services.SubscriptionsExportService.EXPORT_COMPLETE"; private Subscription subscription; private File outFile; private FileOutputStream outputStream; @Override - public int onStartCommand(Intent intent, int flags, int startId) { - if (intent == null || subscription != null) return START_NOT_STICKY; + public int onStartCommand(final Intent intent, final int flags, final int startId) { + if (intent == null || subscription != null) { + return START_NOT_STICKY; + } final String path = intent.getStringExtra(KEY_FILE_PATH); if (TextUtils.isEmpty(path)) { - stopAndReportError(new IllegalStateException("Exporting to a file, but the path is empty or null"), "Exporting subscriptions"); + stopAndReportError(new IllegalStateException( + "Exporting to a file, but the path is empty or null"), + "Exporting subscriptions"); return START_NOT_STICKY; } try { - outputStream = new FileOutputStream(outFile = new File(path)); + outFile = new File(path); + outputStream = new FileOutputStream(outFile); } catch (FileNotFoundException e) { handleError(e); return START_NOT_STICKY; @@ -90,19 +97,21 @@ public class SubscriptionsExportService extends BaseImportExportService { @Override protected void disposeAll() { super.disposeAll(); - if (subscription != null) subscription.cancel(); + if (subscription != null) { + subscription.cancel(); + } } private void startExport() { showToast(R.string.export_ongoing); - subscriptionService.subscriptionTable() - .getAll() - .take(1) + subscriptionManager.subscriptionTable().getAll().take(1) .map(subscriptionEntities -> { - final List result = new ArrayList<>(subscriptionEntities.size()); + final List result + = new ArrayList<>(subscriptionEntities.size()); for (SubscriptionEntity entity : subscriptionEntities) { - result.add(new SubscriptionItem(entity.getServiceId(), entity.getUrl(), entity.getName())); + result.add(new SubscriptionItem(entity.getServiceId(), entity.getUrl(), + entity.getName())); } return result; }) @@ -115,25 +124,28 @@ public class SubscriptionsExportService extends BaseImportExportService { private Subscriber getSubscriber() { return new Subscriber() { @Override - public void onSubscribe(Subscription s) { + public void onSubscribe(final Subscription s) { subscription = s; s.request(1); } @Override - public void onNext(File file) { - if (DEBUG) Log.d(TAG, "startExport() success: file = " + file); + public void onNext(final File file) { + if (DEBUG) { + Log.d(TAG, "startExport() success: file = " + file); + } } @Override - public void onError(Throwable error) { + public void onError(final Throwable error) { Log.e(TAG, "onError() called with: error = [" + error + "]", error); handleError(error); } @Override public void onComplete() { - LocalBroadcastManager.getInstance(SubscriptionsExportService.this).sendBroadcast(new Intent(EXPORT_COMPLETE_ACTION)); + LocalBroadcastManager.getInstance(SubscriptionsExportService.this) + .sendBroadcast(new Intent(EXPORT_COMPLETE_ACTION)); showToast(R.string.export_complete_toast); stopService(); } @@ -147,7 +159,7 @@ public class SubscriptionsExportService extends BaseImportExportService { }; } - protected void handleError(Throwable error) { + protected void handleError(final Throwable error) { super.handleError(R.string.subscriptions_export_unsuccessful, error); } } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java index 62c1dfeb9..06ba55106 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java @@ -20,11 +20,12 @@ package org.schabi.newpipe.local.subscription.services; import android.content.Intent; +import android.text.TextUtils; +import android.util.Log; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import android.text.TextUtils; -import android.util.Log; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; @@ -33,8 +34,8 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.subscription.SubscriptionItem; -import org.schabi.newpipe.local.subscription.ImportExportJsonHelper; import org.schabi.newpipe.util.Constants; +import org.schabi.newpipe.util.ExceptionUtils; import org.schabi.newpipe.util.ExtractorHelper; import java.io.File; @@ -62,22 +63,36 @@ public class SubscriptionsImportService extends BaseImportExportService { public static final String KEY_VALUE = "key_value"; /** - * A {@link LocalBroadcastManager local broadcast} will be made with this action when the import is successfully completed. + * A {@link LocalBroadcastManager local broadcast} will be made with this action + * when the import is successfully completed. */ - public static final String IMPORT_COMPLETE_ACTION = "org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.IMPORT_COMPLETE"; + public static final String IMPORT_COMPLETE_ACTION = "org.schabi.newpipe.local.subscription" + + ".services.SubscriptionsImportService.IMPORT_COMPLETE"; + + /** + * How many extractions running in parallel. + */ + public static final int PARALLEL_EXTRACTIONS = 8; + + /** + * Number of items to buffer to mass-insert in the subscriptions table, + * this leads to a better performance as we can then use db transactions. + */ + public static final int BUFFER_COUNT_BEFORE_INSERT = 50; private Subscription subscription; private int currentMode; private int currentServiceId; - @Nullable private String channelUrl; @Nullable private InputStream inputStream; @Override - public int onStartCommand(Intent intent, int flags, int startId) { - if (intent == null || subscription != null) return START_NOT_STICKY; + public int onStartCommand(final Intent intent, final int flags, final int startId) { + if (intent == null || subscription != null) { + return START_NOT_STICKY; + } currentMode = intent.getIntExtra(KEY_MODE, -1); currentServiceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, Constants.NO_SERVICE_ID); @@ -87,7 +102,9 @@ public class SubscriptionsImportService extends BaseImportExportService { } else { final String filePath = intent.getStringExtra(KEY_VALUE); if (TextUtils.isEmpty(filePath)) { - stopAndReportError(new IllegalStateException("Importing from input stream, but file path is empty or null"), "Importing subscriptions"); + stopAndReportError(new IllegalStateException( + "Importing from input stream, but file path is empty or null"), + "Importing subscriptions"); return START_NOT_STICKY; } @@ -100,8 +117,12 @@ public class SubscriptionsImportService extends BaseImportExportService { } if (currentMode == -1 || currentMode == CHANNEL_URL_MODE && channelUrl == null) { - final String errorDescription = "Some important field is null or in illegal state: currentMode=[" + currentMode + "], channelUrl=[" + channelUrl + "], inputStream=[" + inputStream + "]"; - stopAndReportError(new IllegalStateException(errorDescription), "Importing subscriptions"); + final String errorDescription = "Some important field is null or in illegal state: " + + "currentMode=[" + currentMode + "], " + + "channelUrl=[" + channelUrl + "], " + + "inputStream=[" + inputStream + "]"; + stopAndReportError(new IllegalStateException(errorDescription), + "Importing subscriptions"); return START_NOT_STICKY; } @@ -122,24 +143,15 @@ public class SubscriptionsImportService extends BaseImportExportService { @Override protected void disposeAll() { super.disposeAll(); - if (subscription != null) subscription.cancel(); + if (subscription != null) { + subscription.cancel(); + } } /*////////////////////////////////////////////////////////////////////////// // Imports //////////////////////////////////////////////////////////////////////////*/ - /** - * How many extractions running in parallel. - */ - public static final int PARALLEL_EXTRACTIONS = 8; - - /** - * Number of items to buffer to mass-insert in the subscriptions table, this leads to - * a better performance as we can then use db transactions. - */ - public static final int BUFFER_COUNT_BEFORE_INSERT = 50; - private void startImport() { showToast(R.string.import_ongoing); @@ -157,12 +169,14 @@ public class SubscriptionsImportService extends BaseImportExportService { } if (flowable == null) { - final String message = "Flowable given by \"importFrom\" is null (current mode: " + currentMode + ")"; + final String message = "Flowable given by \"importFrom\" is null " + + "(current mode: " + currentMode + ")"; stopAndReportError(new IllegalStateException(message), "Importing subscriptions"); return; } - flowable.doOnNext(subscriptionItems -> eventListener.onSizeReceived(subscriptionItems.size())) + flowable.doOnNext(subscriptionItems -> + eventListener.onSizeReceived(subscriptionItems.size())) .flatMap(Flowable::fromIterable) .parallel(PARALLEL_EXTRACTIONS) @@ -170,7 +184,8 @@ public class SubscriptionsImportService extends BaseImportExportService { .map((Function>) subscriptionItem -> { try { return Notification.createOnNext(ExtractorHelper - .getChannelInfo(subscriptionItem.getServiceId(), subscriptionItem.getUrl(), true) + .getChannelInfo(subscriptionItem.getServiceId(), + subscriptionItem.getUrl(), true) .blockingGet()); } catch (Throwable e) { return Notification.createOnError(e); @@ -180,6 +195,7 @@ public class SubscriptionsImportService extends BaseImportExportService { .observeOn(Schedulers.io()) .doOnNext(getNotificationsConsumer()) + .buffer(BUFFER_COUNT_BEFORE_INSERT) .map(upsertBatch()) @@ -190,26 +206,30 @@ public class SubscriptionsImportService extends BaseImportExportService { private Subscriber> getSubscriber() { return new Subscriber>() { - @Override - public void onSubscribe(Subscription s) { + public void onSubscribe(final Subscription s) { subscription = s; s.request(Long.MAX_VALUE); } @Override - public void onNext(List successfulInserted) { - if (DEBUG) Log.d(TAG, "startImport() " + successfulInserted.size() + " items successfully inserted into the database"); + public void onNext(final List successfulInserted) { + if (DEBUG) { + Log.d(TAG, "startImport() " + successfulInserted.size() + + " items successfully inserted into the database"); + } } @Override - public void onError(Throwable error) { + public void onError(final Throwable error) { + Log.e(TAG, "Got an error!", error); handleError(error); } @Override public void onComplete() { - LocalBroadcastManager.getInstance(SubscriptionsImportService.this).sendBroadcast(new Intent(IMPORT_COMPLETE_ACTION)); + LocalBroadcastManager.getInstance(SubscriptionsImportService.this) + .sendBroadcast(new Intent(IMPORT_COMPLETE_ACTION)); showToast(R.string.import_complete_toast); stopService(); } @@ -226,8 +246,10 @@ public class SubscriptionsImportService extends BaseImportExportService { final Throwable cause = error.getCause(); if (error instanceof IOException) { throw (IOException) error; - } else if (cause != null && cause instanceof IOException) { + } else if (cause instanceof IOException) { throw (IOException) cause; + } else if (ExceptionUtils.isNetworkRelated(error)) { + throw new IOException(error); } eventListener.onItemCompleted(""); @@ -239,10 +261,12 @@ public class SubscriptionsImportService extends BaseImportExportService { return notificationList -> { final List infoList = new ArrayList<>(notificationList.size()); for (Notification n : notificationList) { - if (n.isOnNext()) infoList.add(n.getValue()); + if (n.isOnNext()) { + infoList.add(n.getValue()); + } } - return subscriptionService.upsertAll(infoList); + return subscriptionManager.upsertAll(infoList); }; } @@ -262,7 +286,7 @@ public class SubscriptionsImportService extends BaseImportExportService { return Flowable.fromCallable(() -> ImportExportJsonHelper.readFrom(inputStream, null)); } - protected void handleError(@NonNull Throwable error) { + protected void handleError(@NonNull final Throwable error) { super.handleError(R.string.subscriptions_import_unsuccessful, error); } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/schabi/newpipe/player/AudioServiceLeakFix.java b/app/src/main/java/org/schabi/newpipe/player/AudioServiceLeakFix.java index 9f0c849f5..c36a77421 100644 --- a/app/src/main/java/org/schabi/newpipe/player/AudioServiceLeakFix.java +++ b/app/src/main/java/org/schabi/newpipe/player/AudioServiceLeakFix.java @@ -11,20 +11,19 @@ import android.content.ContextWrapper; * https://gist.github.com/jankovd/891d96f476f7a9ce24e2 */ public class AudioServiceLeakFix extends ContextWrapper { + AudioServiceLeakFix(final Context base) { + super(base); + } - AudioServiceLeakFix(Context base) { - super(base); - } + public static ContextWrapper preventLeakOf(final Context base) { + return new AudioServiceLeakFix(base); + } - public static ContextWrapper preventLeakOf(Context base) { - return new AudioServiceLeakFix(base); - } - - @Override - public Object getSystemService(String name) { - if (Context.AUDIO_SERVICE.equals(name)) { - return getApplicationContext().getSystemService(name); - } - return super.getSystemService(name); - } -} \ No newline at end of file + @Override + public Object getSystemService(final String name) { + if (Context.AUDIO_SERVICE.equals(name)) { + return getApplicationContext().getSystemService(name); + } + return super.getSystemService(name); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java index 99b38aae7..a75ea7de8 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java @@ -25,16 +25,21 @@ import android.app.Service; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.content.res.Resources; import android.graphics.Bitmap; import android.os.Build; import android.os.IBinder; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.app.NotificationCompat; +import android.preference.PreferenceManager; import android.util.Log; import android.view.View; import android.widget.RemoteViews; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.core.app.NotificationCompat; + import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.source.MediaSource; @@ -44,56 +49,60 @@ import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.player.event.PlayerEventListener; -import org.schabi.newpipe.player.helper.LockManager; import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.player.resolver.AudioPlaybackResolver; import org.schabi.newpipe.player.resolver.MediaSourceTag; +import org.schabi.newpipe.util.BitmapUtils; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.ThemeHelper; import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; - +import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; /** - * Base players joining the common properties + * Service Background Player implementing {@link VideoPlayer}. * * @author mauriciocolli */ public final class BackgroundPlayer extends Service { - private static final String TAG = "BackgroundPlayer"; - private static final boolean DEBUG = BasePlayer.DEBUG; - - public static final String ACTION_CLOSE = "org.schabi.newpipe.player.BackgroundPlayer.CLOSE"; - public static final String ACTION_PLAY_PAUSE = "org.schabi.newpipe.player.BackgroundPlayer.PLAY_PAUSE"; - public static final String ACTION_REPEAT = "org.schabi.newpipe.player.BackgroundPlayer.REPEAT"; - public static final String ACTION_PLAY_NEXT = "org.schabi.newpipe.player.BackgroundPlayer.ACTION_PLAY_NEXT"; - public static final String ACTION_PLAY_PREVIOUS = "org.schabi.newpipe.player.BackgroundPlayer.ACTION_PLAY_PREVIOUS"; - public static final String ACTION_FAST_REWIND = "org.schabi.newpipe.player.BackgroundPlayer.ACTION_FAST_REWIND"; - public static final String ACTION_FAST_FORWARD = "org.schabi.newpipe.player.BackgroundPlayer.ACTION_FAST_FORWARD"; + public static final String ACTION_CLOSE + = "org.schabi.newpipe.player.BackgroundPlayer.CLOSE"; + public static final String ACTION_PLAY_PAUSE + = "org.schabi.newpipe.player.BackgroundPlayer.PLAY_PAUSE"; + public static final String ACTION_REPEAT + = "org.schabi.newpipe.player.BackgroundPlayer.REPEAT"; + public static final String ACTION_PLAY_NEXT + = "org.schabi.newpipe.player.BackgroundPlayer.ACTION_PLAY_NEXT"; + public static final String ACTION_PLAY_PREVIOUS + = "org.schabi.newpipe.player.BackgroundPlayer.ACTION_PLAY_PREVIOUS"; + public static final String ACTION_FAST_REWIND + = "org.schabi.newpipe.player.BackgroundPlayer.ACTION_FAST_REWIND"; + public static final String ACTION_FAST_FORWARD + = "org.schabi.newpipe.player.BackgroundPlayer.ACTION_FAST_FORWARD"; public static final String SET_IMAGE_RESOURCE_METHOD = "setImageResource"; - + private static final String TAG = "BackgroundPlayer"; + private static final boolean DEBUG = BasePlayer.DEBUG; + private static final int NOTIFICATION_ID = 123789; + private static final int NOTIFICATION_UPDATES_BEFORE_RESET = 60; private BasePlayerImpl basePlayerImpl; - private LockManager lockManager; /*////////////////////////////////////////////////////////////////////////// // Service-Activity Binder //////////////////////////////////////////////////////////////////////////*/ - - private PlayerEventListener activityListener; - private IBinder mBinder; + private SharedPreferences sharedPreferences; /*////////////////////////////////////////////////////////////////////////// // Notification //////////////////////////////////////////////////////////////////////////*/ - - private static final int NOTIFICATION_ID = 123789; + private PlayerEventListener activityListener; + private IBinder mBinder; private NotificationManager notificationManager; private NotificationCompat.Builder notBuilder; private RemoteViews notRemoteView; private RemoteViews bigNotRemoteView; - private boolean shouldUpdateOnProgress; + private int timesNotificationUpdated; /*////////////////////////////////////////////////////////////////////////// // Service's LifeCycle @@ -101,10 +110,12 @@ public final class BackgroundPlayer extends Service { @Override public void onCreate() { - if (DEBUG) Log.d(TAG, "onCreate() called"); + if (DEBUG) { + Log.d(TAG, "onCreate() called"); + } notificationManager = ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)); - lockManager = new LockManager(this); - + sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); + assureCorrectAppLanguage(this); ThemeHelper.setTheme(this); basePlayerImpl = new BasePlayerImpl(this); basePlayerImpl.setup(); @@ -114,9 +125,11 @@ public final class BackgroundPlayer extends Service { } @Override - public int onStartCommand(Intent intent, int flags, int startId) { - if (DEBUG) Log.d(TAG, "onStartCommand() called with: intent = [" + intent + - "], flags = [" + flags + "], startId = [" + startId + "]"); + public int onStartCommand(final Intent intent, final int flags, final int startId) { + if (DEBUG) { + Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "], " + + "flags = [" + flags + "], startId = [" + startId + "]"); + } basePlayerImpl.handleIntent(intent); if (basePlayerImpl.mediaSessionManager != null) { basePlayerImpl.mediaSessionManager.handleMediaButtonIntent(intent); @@ -126,17 +139,19 @@ public final class BackgroundPlayer extends Service { @Override public void onDestroy() { - if (DEBUG) Log.d(TAG, "destroy() called"); + if (DEBUG) { + Log.d(TAG, "destroy() called"); + } onClose(); } @Override - protected void attachBaseContext(Context base) { + protected void attachBaseContext(final Context base) { super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base)); } @Override - public IBinder onBind(Intent intent) { + public IBinder onBind(final Intent intent) { return mBinder; } @@ -144,27 +159,29 @@ public final class BackgroundPlayer extends Service { // Actions //////////////////////////////////////////////////////////////////////////*/ private void onClose() { - if (DEBUG) Log.d(TAG, "onClose() called"); - - if (lockManager != null) { - lockManager.releaseWifiAndCpu(); + if (DEBUG) { + Log.d(TAG, "onClose() called"); } + if (basePlayerImpl != null) { basePlayerImpl.savePlaybackState(); basePlayerImpl.stopActivityBinding(); basePlayerImpl.destroy(); } - if (notificationManager != null) notificationManager.cancel(NOTIFICATION_ID); + if (notificationManager != null) { + notificationManager.cancel(NOTIFICATION_ID); + } mBinder = null; basePlayerImpl = null; - lockManager = null; stopForeground(true); stopSelf(); } - private void onScreenOnOff(boolean on) { - if (DEBUG) Log.d(TAG, "onScreenOnOff() called with: on = [" + on + "]"); + private void onScreenOnOff(final boolean on) { + if (DEBUG) { + Log.d(TAG, "onScreenOnOff() called with: on = [" + on + "]"); + } shouldUpdateOnProgress = on; basePlayerImpl.triggerProgressUpdate(); if (on) { @@ -180,59 +197,105 @@ public final class BackgroundPlayer extends Service { private void resetNotification() { notBuilder = createNotification(); + timesNotificationUpdated = 0; } private NotificationCompat.Builder createNotification() { - notRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification); - bigNotRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification_expanded); + notRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, + R.layout.player_background_notification); + bigNotRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, + R.layout.player_background_notification_expanded); setupNotification(notRemoteView); setupNotification(bigNotRemoteView); - NotificationCompat.Builder builder = new NotificationCompat.Builder(this, getString(R.string.notification_channel_id)) + NotificationCompat.Builder builder = new NotificationCompat + .Builder(this, getString(R.string.notification_channel_id)) .setOngoing(true) .setSmallIcon(R.drawable.ic_newpipe_triangle_white) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setCustomContentView(notRemoteView) .setCustomBigContentView(bigNotRemoteView); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + setLockScreenThumbnail(builder); + } + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { builder.setPriority(NotificationCompat.PRIORITY_MAX); } return builder; } - private void setupNotification(RemoteViews remoteViews) { - if (basePlayerImpl == null) return; + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + private void setLockScreenThumbnail(final NotificationCompat.Builder builder) { + boolean isLockScreenThumbnailEnabled = sharedPreferences.getBoolean( + getString(R.string.enable_lock_screen_video_thumbnail_key), true); + + if (isLockScreenThumbnailEnabled) { + basePlayerImpl.mediaSessionManager.setLockScreenArt( + builder, + getCenteredThumbnailBitmap() + ); + } else { + basePlayerImpl.mediaSessionManager.clearLockScreenArt(builder); + } + } + + @Nullable + private Bitmap getCenteredThumbnailBitmap() { + final int screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels; + final int screenHeight = Resources.getSystem().getDisplayMetrics().heightPixels; + + return BitmapUtils.centerCrop(basePlayerImpl.getThumbnail(), screenWidth, screenHeight); + } + + private void setupNotification(final RemoteViews remoteViews) { + if (basePlayerImpl == null) { + return; + } remoteViews.setTextViewText(R.id.notificationSongName, basePlayerImpl.getVideoTitle()); remoteViews.setTextViewText(R.id.notificationArtist, basePlayerImpl.getUploaderName()); remoteViews.setOnClickPendingIntent(R.id.notificationPlayPause, - PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT)); + PendingIntent.getBroadcast(this, NOTIFICATION_ID, + new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT)); remoteViews.setOnClickPendingIntent(R.id.notificationStop, - PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_CLOSE), PendingIntent.FLAG_UPDATE_CURRENT)); + PendingIntent.getBroadcast(this, NOTIFICATION_ID, + new Intent(ACTION_CLOSE), PendingIntent.FLAG_UPDATE_CURRENT)); remoteViews.setOnClickPendingIntent(R.id.notificationRepeat, - PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_REPEAT), PendingIntent.FLAG_UPDATE_CURRENT)); + PendingIntent.getBroadcast(this, NOTIFICATION_ID, + new Intent(ACTION_REPEAT), PendingIntent.FLAG_UPDATE_CURRENT)); // Starts background player activity -- attempts to unlock lockscreen final Intent intent = NavigationHelper.getBackgroundPlayerActivityIntent(this); remoteViews.setOnClickPendingIntent(R.id.notificationContent, - PendingIntent.getActivity(this, NOTIFICATION_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT)); + PendingIntent.getActivity(this, NOTIFICATION_ID, intent, + PendingIntent.FLAG_UPDATE_CURRENT)); if (basePlayerImpl.playQueue != null && basePlayerImpl.playQueue.size() > 1) { - remoteViews.setInt(R.id.notificationFRewind, SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_previous); - remoteViews.setInt(R.id.notificationFForward, SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_next); + remoteViews.setInt(R.id.notificationFRewind, SET_IMAGE_RESOURCE_METHOD, + R.drawable.exo_controls_previous); + remoteViews.setInt(R.id.notificationFForward, SET_IMAGE_RESOURCE_METHOD, + R.drawable.exo_controls_next); remoteViews.setOnClickPendingIntent(R.id.notificationFRewind, - PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_PREVIOUS), PendingIntent.FLAG_UPDATE_CURRENT)); + PendingIntent.getBroadcast(this, NOTIFICATION_ID, + new Intent(ACTION_PLAY_PREVIOUS), PendingIntent.FLAG_UPDATE_CURRENT)); remoteViews.setOnClickPendingIntent(R.id.notificationFForward, - PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_NEXT), PendingIntent.FLAG_UPDATE_CURRENT)); + PendingIntent.getBroadcast(this, NOTIFICATION_ID, + new Intent(ACTION_PLAY_NEXT), PendingIntent.FLAG_UPDATE_CURRENT)); } else { - remoteViews.setInt(R.id.notificationFRewind, SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_rewind); - remoteViews.setInt(R.id.notificationFForward, SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_fastforward); + remoteViews.setInt(R.id.notificationFRewind, SET_IMAGE_RESOURCE_METHOD, + R.drawable.exo_controls_rewind); + remoteViews.setInt(R.id.notificationFForward, SET_IMAGE_RESOURCE_METHOD, + R.drawable.exo_controls_fastforward); remoteViews.setOnClickPendingIntent(R.id.notificationFRewind, - PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_FAST_REWIND), PendingIntent.FLAG_UPDATE_CURRENT)); + PendingIntent.getBroadcast(this, NOTIFICATION_ID, + new Intent(ACTION_FAST_REWIND), PendingIntent.FLAG_UPDATE_CURRENT)); remoteViews.setOnClickPendingIntent(R.id.notificationFForward, - PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_FAST_FORWARD), PendingIntent.FLAG_UPDATE_CURRENT)); + PendingIntent.getBroadcast(this, NOTIFICATION_ID, + new Intent(ACTION_FAST_FORWARD), PendingIntent.FLAG_UPDATE_CURRENT)); } setRepeatModeIcon(remoteViews, basePlayerImpl.getRepeatMode()); @@ -244,14 +307,23 @@ public final class BackgroundPlayer extends Service { * * @param drawableId if != -1, sets the drawable with that id on the play/pause button */ - private synchronized void updateNotification(int drawableId) { - //if (DEBUG) Log.d(TAG, "updateNotification() called with: drawableId = [" + drawableId + "]"); - if (notBuilder == null) return; + private synchronized void updateNotification(final int drawableId) { +// if (DEBUG) { +// Log.d(TAG, "updateNotification() called with: drawableId = [" + drawableId + "]"); +// } + if (notBuilder == null) { + return; + } if (drawableId != -1) { - if (notRemoteView != null) notRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId); - if (bigNotRemoteView != null) bigNotRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId); + if (notRemoteView != null) { + notRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId); + } + if (bigNotRemoteView != null) { + bigNotRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId); + } } notificationManager.notify(NOTIFICATION_ID, notBuilder.build()); + timesNotificationUpdated++; } /*////////////////////////////////////////////////////////////////////////// @@ -261,31 +333,34 @@ public final class BackgroundPlayer extends Service { private void setRepeatModeIcon(final RemoteViews remoteViews, final int repeatMode) { switch (repeatMode) { case Player.REPEAT_MODE_OFF: - remoteViews.setInt(R.id.notificationRepeat, SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_repeat_off); + remoteViews.setInt(R.id.notificationRepeat, SET_IMAGE_RESOURCE_METHOD, + R.drawable.exo_controls_repeat_off); break; case Player.REPEAT_MODE_ONE: - remoteViews.setInt(R.id.notificationRepeat, SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_repeat_one); + remoteViews.setInt(R.id.notificationRepeat, SET_IMAGE_RESOURCE_METHOD, + R.drawable.exo_controls_repeat_one); break; case Player.REPEAT_MODE_ALL: - remoteViews.setInt(R.id.notificationRepeat, SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_repeat_all); + remoteViews.setInt(R.id.notificationRepeat, SET_IMAGE_RESOURCE_METHOD, + R.drawable.exo_controls_repeat_all); break; } } ////////////////////////////////////////////////////////////////////////// protected class BasePlayerImpl extends BasePlayer { - - @NonNull final private AudioPlaybackResolver resolver; + @NonNull + private final AudioPlaybackResolver resolver; private int cachedDuration; private String cachedDurationString; - BasePlayerImpl(Context context) { + BasePlayerImpl(final Context context) { super(context); this.resolver = new AudioPlaybackResolver(context, dataSource); } @Override - public void initPlayer(boolean playOnReady) { + public void initPlayer(final boolean playOnReady) { super.initPlayer(playOnReady); } @@ -294,8 +369,12 @@ public final class BackgroundPlayer extends Service { super.handleIntent(intent); resetNotification(); - if (bigNotRemoteView != null) bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 0, false); - if (notRemoteView != null) notRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 0, false); + if (bigNotRemoteView != null) { + bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 0, false); + } + if (notRemoteView != null) { + notRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 0, false); + } startForeground(NOTIFICATION_ID, notBuilder.build()); } @@ -304,7 +383,9 @@ public final class BackgroundPlayer extends Service { //////////////////////////////////////////////////////////////////////////*/ private void updateNotificationThumbnail() { - if (basePlayerImpl == null) return; + if (basePlayerImpl == null) { + return; + } if (notRemoteView != null) { notRemoteView.setImageViewBitmap(R.id.notificationCover, basePlayerImpl.getThumbnail()); @@ -316,7 +397,8 @@ public final class BackgroundPlayer extends Service { } @Override - public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { + public void onLoadingComplete(final String imageUri, final View view, + final Bitmap loadedImage) { super.onLoadingComplete(imageUri, view, loadedImage); resetNotification(); updateNotificationThumbnail(); @@ -324,20 +406,21 @@ public final class BackgroundPlayer extends Service { } @Override - public void onLoadingFailed(String imageUri, View view, FailReason failReason) { + public void onLoadingFailed(final String imageUri, final View view, + final FailReason failReason) { super.onLoadingFailed(imageUri, view, failReason); resetNotification(); updateNotificationThumbnail(); updateNotification(-1); } + /*////////////////////////////////////////////////////////////////////////// // States Implementation //////////////////////////////////////////////////////////////////////////*/ @Override - public void onPrepared(boolean playWhenReady) { + public void onPrepared(final boolean playWhenReady) { super.onPrepared(playWhenReady); - simpleExoPlayer.setVolume(1.0f); } @Override @@ -347,22 +430,39 @@ public final class BackgroundPlayer extends Service { } @Override - public void onUpdateProgress(int currentProgress, int duration, int bufferPercent) { + public void onMuteUnmuteButtonClicked() { + super.onMuteUnmuteButtonClicked(); + updatePlayback(); + } + + @Override + public void onUpdateProgress(final int currentProgress, final int duration, + final int bufferPercent) { updateProgress(currentProgress, duration, bufferPercent); - if (!shouldUpdateOnProgress) return; - resetNotification(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O /*Oreo*/) updateNotificationThumbnail(); + if (!shouldUpdateOnProgress) { + return; + } + if (timesNotificationUpdated > NOTIFICATION_UPDATES_BEFORE_RESET) { + resetNotification(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O /*Oreo*/) { + updateNotificationThumbnail(); + } + } if (bigNotRemoteView != null) { - if(cachedDuration != duration) { + if (cachedDuration != duration) { cachedDuration = duration; cachedDurationString = getTimeString(duration); } - bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, duration, currentProgress, false); - bigNotRemoteView.setTextViewText(R.id.notificationTime, getTimeString(currentProgress) + " / " + cachedDurationString); + bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, duration, + currentProgress, false); + bigNotRemoteView.setTextViewText(R.id.notificationTime, + getTimeString(currentProgress) + " / " + cachedDurationString); } if (notRemoteView != null) { - notRemoteView.setProgressBar(R.id.notificationProgressBar, duration, currentProgress, false); + notRemoteView.setProgressBar(R.id.notificationProgressBar, duration, + currentProgress, false); } updateNotification(-1); } @@ -382,8 +482,12 @@ public final class BackgroundPlayer extends Service { @Override public void destroy() { super.destroy(); - if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, null); - if (bigNotRemoteView != null) bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, null); + if (notRemoteView != null) { + notRemoteView.setImageViewBitmap(R.id.notificationCover, null); + } + if (bigNotRemoteView != null) { + bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, null); + } } /*////////////////////////////////////////////////////////////////////////// @@ -391,18 +495,18 @@ public final class BackgroundPlayer extends Service { //////////////////////////////////////////////////////////////////////////*/ @Override - public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + public void onPlaybackParametersChanged(final PlaybackParameters playbackParameters) { super.onPlaybackParametersChanged(playbackParameters); updatePlayback(); } @Override - public void onLoadingChanged(boolean isLoading) { + public void onLoadingChanged(final boolean isLoading) { // Disable default behavior } @Override - public void onRepeatModeChanged(int i) { + public void onRepeatModeChanged(final int i) { resetNotification(); updateNotification(-1); updatePlayback(); @@ -436,14 +540,14 @@ public final class BackgroundPlayer extends Service { // Activity Event Listener //////////////////////////////////////////////////////////////////////////*/ - /*package-private*/ void setActivityListener(PlayerEventListener listener) { + /*package-private*/ void setActivityListener(final PlayerEventListener listener) { activityListener = listener; updateMetadata(); updatePlayback(); triggerProgressUpdate(); } - /*package-private*/ void removeActivityListener(PlayerEventListener listener) { + /*package-private*/ void removeActivityListener(final PlayerEventListener listener) { if (activityListener == listener) { activityListener = null; } @@ -462,7 +566,8 @@ public final class BackgroundPlayer extends Service { } } - private void updateProgress(int currentProgress, int duration, int bufferPercent) { + private void updateProgress(final int currentProgress, final int duration, + final int bufferPercent) { if (activityListener != null) { activityListener.onProgressUpdate(currentProgress, duration, bufferPercent); } @@ -480,27 +585,31 @@ public final class BackgroundPlayer extends Service { //////////////////////////////////////////////////////////////////////////*/ @Override - protected void setupBroadcastReceiver(IntentFilter intentFilter) { - super.setupBroadcastReceiver(intentFilter); - intentFilter.addAction(ACTION_CLOSE); - intentFilter.addAction(ACTION_PLAY_PAUSE); - intentFilter.addAction(ACTION_REPEAT); - intentFilter.addAction(ACTION_PLAY_PREVIOUS); - intentFilter.addAction(ACTION_PLAY_NEXT); - intentFilter.addAction(ACTION_FAST_REWIND); - intentFilter.addAction(ACTION_FAST_FORWARD); + protected void setupBroadcastReceiver(final IntentFilter intentFltr) { + super.setupBroadcastReceiver(intentFltr); + intentFltr.addAction(ACTION_CLOSE); + intentFltr.addAction(ACTION_PLAY_PAUSE); + intentFltr.addAction(ACTION_REPEAT); + intentFltr.addAction(ACTION_PLAY_PREVIOUS); + intentFltr.addAction(ACTION_PLAY_NEXT); + intentFltr.addAction(ACTION_FAST_REWIND); + intentFltr.addAction(ACTION_FAST_FORWARD); - intentFilter.addAction(Intent.ACTION_SCREEN_ON); - intentFilter.addAction(Intent.ACTION_SCREEN_OFF); + intentFltr.addAction(Intent.ACTION_SCREEN_ON); + intentFltr.addAction(Intent.ACTION_SCREEN_OFF); - intentFilter.addAction(Intent.ACTION_HEADSET_PLUG); + intentFltr.addAction(Intent.ACTION_HEADSET_PLUG); } @Override - public void onBroadcastReceived(Intent intent) { + public void onBroadcastReceived(final Intent intent) { super.onBroadcastReceived(intent); - if (intent == null || intent.getAction() == null) return; - if (DEBUG) Log.d(TAG, "onBroadcastReceived() called with: intent = [" + intent + "]"); + if (intent == null || intent.getAction() == null) { + return; + } + if (DEBUG) { + Log.d(TAG, "onBroadcastReceived() called with: intent = [" + intent + "]"); + } switch (intent.getAction()) { case ACTION_CLOSE: onClose(); @@ -537,7 +646,7 @@ public final class BackgroundPlayer extends Service { //////////////////////////////////////////////////////////////////////////*/ @Override - public void changeState(int state) { + public void changeState(final int state) { super.changeState(state); updatePlayback(); } @@ -547,8 +656,7 @@ public final class BackgroundPlayer extends Service { super.onPlaying(); resetNotification(); updateNotificationThumbnail(); - updateNotification(R.drawable.ic_pause_white); - lockManager.acquireWifiAndCpu(); + updateNotification(R.drawable.exo_controls_pause); } @Override @@ -556,8 +664,7 @@ public final class BackgroundPlayer extends Service { super.onPaused(); resetNotification(); updateNotificationThumbnail(); - updateNotification(R.drawable.ic_play_arrow_white); - lockManager.releaseWifiAndCpu(); + updateNotification(R.drawable.exo_controls_play); } @Override @@ -571,8 +678,7 @@ public final class BackgroundPlayer extends Service { notRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 100, false); } updateNotificationThumbnail(); - updateNotification(R.drawable.ic_replay_white); - lockManager.releaseWifiAndCpu(); + updateNotification(R.drawable.ic_replay_white_24dp); } } } diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java index 5078a01b8..2577665ff 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java @@ -49,7 +49,7 @@ public final class BackgroundPlayerActivity extends ServicePlayerActivity { } @Override - public boolean onPlayerOptionSelected(MenuItem item) { + public boolean onPlayerOptionSelected(final MenuItem item) { if (item.getItemId() == R.id.action_switch_popup) { if (!PermissionHelper.isPopupEnabled(this)) { @@ -58,13 +58,13 @@ public final class BackgroundPlayerActivity extends ServicePlayerActivity { } this.player.setRecovery(); - NavigationHelper.playOnPopupPlayer(getApplicationContext(), player.playQueue, true); + NavigationHelper.playOnPopupPlayer(getApplicationContext(), player.playQueue, this.player.isPlaying()); return true; } if (item.getItemId() == R.id.action_switch_background) { this.player.setRecovery(); - NavigationHelper.playOnBackgroundPlayer(getApplicationContext(), player.playQueue, true); + NavigationHelper.playOnBackgroundPlayer(getApplicationContext(), player.playQueue, this.player.isPlaying()); return true; } @@ -78,9 +78,4 @@ public final class BackgroundPlayerActivity extends ServicePlayerActivity { menu.findItem(R.id.action_switch_popup).setVisible(!((VideoPlayerImpl)player).popupPlayerSelected()); menu.findItem(R.id.action_switch_background).setVisible(!((VideoPlayerImpl)player).audioPlayerSelected()); } - - //@Override - public Intent getPlayerShutdownIntent() { - return new Intent(ACTION_CLOSE); - } } diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index 634ce7779..f232c018a 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -35,9 +35,9 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.LoadControl; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; @@ -54,6 +54,7 @@ import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.assist.FailReason; import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; +import io.reactivex.android.schedulers.AndroidSchedulers; import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.DownloaderImpl; import org.schabi.newpipe.R; @@ -64,7 +65,6 @@ import org.schabi.newpipe.player.helper.LoadController; import org.schabi.newpipe.player.helper.MediaSessionManager; import org.schabi.newpipe.player.helper.PlayerDataSource; import org.schabi.newpipe.player.helper.PlayerHelper; -import org.schabi.newpipe.player.mediasource.FailedMediaSource; import org.schabi.newpipe.player.playback.BasePlayerMediaSession; import org.schabi.newpipe.player.playback.CustomTrackSelector; import org.schabi.newpipe.player.playback.MediaSourceManager; @@ -77,11 +77,8 @@ import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.SerializedCache; import java.io.IOException; -import java.net.UnknownHostException; -import java.util.concurrent.TimeUnit; import io.reactivex.Observable; -import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; import io.reactivex.disposables.SerialDisposable; @@ -90,45 +87,29 @@ import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_INTERNAL import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_PERIOD_TRANSITION; import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK; import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT; +import static io.reactivex.android.schedulers.AndroidSchedulers.mainThread; +import static java.util.concurrent.TimeUnit.MILLISECONDS; /** - * Base for the players, joining the common properties + * Base for the players, joining the common properties. * * @author mauriciocolli */ @SuppressWarnings({"WeakerAccess"}) 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 - final protected Context context; + public static final int STATE_PREFLIGHT = -1; + public static final int STATE_BLOCKED = 123; + public static final int STATE_PLAYING = 124; + public static final int STATE_BUFFERING = 125; + public static final int STATE_PAUSED = 126; + public static final int STATE_PAUSED_SEEK = 127; + public static final int STATE_COMPLETED = 128; - @NonNull - final protected BroadcastReceiver broadcastReceiver; - @NonNull - final protected IntentFilter intentFilter; - - @NonNull - final protected HistoryRecordManager recordManager; - - @NonNull - final protected CustomTrackSelector trackSelector; - @NonNull - final protected PlayerDataSource dataSource; - - @NonNull - final private LoadControl loadControl; - @NonNull - final private RenderersFactory renderFactory; - - @NonNull - final private SerialDisposable progressUpdateReactor; - @NonNull - final private CompositeDisposable databaseUpdateReactor; /*////////////////////////////////////////////////////////////////////////// // Intent //////////////////////////////////////////////////////////////////////////*/ @@ -136,12 +117,6 @@ public abstract class BasePlayer implements @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"; @@ -150,9 +125,13 @@ public abstract class BasePlayer implements @NonNull public static final String RESUME_PLAYBACK = "resume_playback"; @NonNull + public static final String START_PAUSED = "start_paused"; + @NonNull public static final String SELECT_ON_APPEND = "select_on_append"; @NonNull public static final String PLAYER_TYPE = "player_type"; + @NonNull + public static final String IS_MUTED = "is_muted"; /*////////////////////////////////////////////////////////////////////////// // Playback @@ -180,9 +159,8 @@ public abstract class BasePlayer implements // Player //////////////////////////////////////////////////////////////////////////*/ - protected final static int PLAY_PREV_ACTIVATION_LIMIT_MILLIS = 5000; // 5 seconds - protected final static int PROGRESS_LOOP_INTERVAL_MILLIS = 500; - protected final static int RECOVERY_SKIP_THRESHOLD_MILLIS = 3000; // 3 seconds + protected static final int PLAY_PREV_ACTIVATION_LIMIT_MILLIS = 5000; // 5 seconds + protected static final int PROGRESS_LOOP_INTERVAL_MILLIS = 500; public final static int PLAYER_TYPE_VIDEO = 0; public final static int PLAYER_TYPE_AUDIO = 1; @@ -192,17 +170,40 @@ public abstract class BasePlayer implements protected AudioReactor audioReactor; protected MediaSessionManager mediaSessionManager; + + @NonNull + protected final Context context; + @NonNull + protected final BroadcastReceiver broadcastReceiver; + @NonNull + protected final IntentFilter intentFilter; + @NonNull + protected final HistoryRecordManager recordManager; + @NonNull + protected final CustomTrackSelector trackSelector; + @NonNull + protected final PlayerDataSource dataSource; + @NonNull + private final LoadControl loadControl; + + @NonNull + private final RenderersFactory renderFactory; + @NonNull + private final SerialDisposable progressUpdateReactor; + @NonNull + private final CompositeDisposable databaseUpdateReactor; + private boolean isPrepared = false; private Disposable stateLoader; - //////////////////////////////////////////////////////////////////////////*/ + protected int currentState = STATE_PREFLIGHT; public BasePlayer(@NonNull final Context context) { this.context = context; this.broadcastReceiver = new BroadcastReceiver() { @Override - public void onReceive(Context context, Intent intent) { + public void onReceive(final Context ctx, final Intent intent) { onBroadcastReceived(intent); } }; @@ -215,13 +216,15 @@ public abstract class BasePlayer implements this.databaseUpdateReactor = new CompositeDisposable(); final String userAgent = DownloaderImpl.USER_AGENT; - final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter.Builder(context).build(); + final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter.Builder(context) + .build(); this.dataSource = new PlayerDataSource(context, userAgent, bandwidthMeter); - final TrackSelection.Factory trackSelectionFactory = PlayerHelper.getQualitySelector(context); - this.trackSelector = new CustomTrackSelector(trackSelectionFactory); + final TrackSelection.Factory trackSelectionFactory = PlayerHelper + .getQualitySelector(context); + this.trackSelector = new CustomTrackSelector(context, trackSelectionFactory); - this.loadControl = new LoadController(context); + this.loadControl = new LoadController(); this.renderFactory = new DefaultRenderersFactory(context); } @@ -233,12 +236,19 @@ public abstract class BasePlayer implements } public void initPlayer(final boolean playOnReady) { - if (DEBUG) Log.d(TAG, "initPlayer() called with: context = [" + context + "]"); + if (DEBUG) { + Log.d(TAG, "initPlayer() called with: playOnReady = [" + playOnReady + "]"); + } - simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(context, renderFactory, trackSelector, loadControl); + simpleExoPlayer = new SimpleExoPlayer.Builder(context, renderFactory) + .setTrackSelector(trackSelector) + .setLoadControl(loadControl) + .build(); simpleExoPlayer.addListener(this); simpleExoPlayer.setPlayWhenReady(playOnReady); simpleExoPlayer.setSeekParameters(PlayerHelper.getSeekParameters(context)); + simpleExoPlayer.setWakeMode(C.WAKE_MODE_NETWORK); + simpleExoPlayer.setHandleAudioBecomingNoisy(true); audioReactor = new AudioReactor(context, simpleExoPlayer); mediaSessionManager = new MediaSessionManager(context, simpleExoPlayer, @@ -247,40 +257,49 @@ 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 + "]"); - if (intent == null) return; + public void handleIntent(final Intent intent) { + if (DEBUG) { + Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]"); + } + if (intent == null) { + return; + } // Resolve play queue - if (!intent.hasExtra(PLAY_QUEUE_KEY)) return; + if (!intent.hasExtra(PLAY_QUEUE_KEY)) { + return; + } final String intentCacheKey = intent.getStringExtra(PLAY_QUEUE_KEY); final PlayQueue queue = SerializedCache.getInstance().take(intentCacheKey, PlayQueue.class); - if (queue == null) return; + if (queue == null) { + return; + } // Resolve append intents if (intent.getBooleanExtra(APPEND_ONLY, false) && playQueue != null) { int sizeBeforeAppend = playQueue.size(); playQueue.append(queue.getStreams()); - if ((intent.getBooleanExtra(SELECT_ON_APPEND, false) || - getCurrentState() == STATE_COMPLETED) && - queue.getStreams().size() > 0) { + if ((intent.getBooleanExtra(SELECT_ON_APPEND, false) + || getCurrentState() == STATE_COMPLETED) && queue.getStreams().size() > 0) { playQueue.setIndex(sizeBeforeAppend); } return; } + final PlaybackParameters savedParameters = retrievePlaybackParametersFromPreferences(); + final float playbackSpeed = savedParameters.speed; + final float playbackPitch = savedParameters.pitch; + final boolean playbackSkipSilence = savedParameters.skipSilence; + final boolean samePlayQueue = playQueue != null && playQueue.equals(queue); final int repeatMode = intent.getIntExtra(REPEAT_MODE, getRepeatMode()); - final float playbackSpeed = intent.getFloatExtra(PLAYBACK_SPEED, getPlaybackSpeed()); - final float playbackPitch = intent.getFloatExtra(PLAYBACK_PITCH, getPlaybackPitch()); - final boolean playbackSkipSilence = intent.getBooleanExtra(PLAYBACK_SKIP_SILENCE, - getPlaybackSkipSilence()); + final boolean isMuted = intent + .getBooleanExtra(IS_MUTED, simpleExoPlayer != null && isMuted()); /* * There are 3 situations when playback shouldn't be started from scratch (zero timestamp): @@ -320,16 +339,16 @@ public abstract class BasePlayer implements .subscribe( state -> { queue.setRecovery(queue.getIndex(), state.getProgressTime()); - initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence, true); + initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence, true, isMuted); }, error -> { if (DEBUG) error.printStackTrace(); // In case any error we can start playback without history - initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence, true); + initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence, true, isMuted); }, () -> { // Completed but not found in history - initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence, true); + initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence, true, isMuted); } ); databaseUpdateReactor.add(stateLoader); @@ -338,7 +357,21 @@ public abstract class BasePlayer implements } // Good to go... // In a case of equal PlayQueues we can re-init old one but only when it is disposed - initPlayback(samePlayQueue ? playQueue : queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence, true); + initPlayback(samePlayQueue ? playQueue : queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence, + !intent.getBooleanExtra(START_PAUSED, false), isMuted); + } + + private PlaybackParameters retrievePlaybackParametersFromPreferences() { + final SharedPreferences preferences = + PreferenceManager.getDefaultSharedPreferences(context); + + final float speed = preferences.getFloat( + context.getString(R.string.playback_speed_key), getPlaybackSpeed()); + final float pitch = preferences.getFloat( + context.getString(R.string.playback_pitch_key), getPlaybackPitch()); + final boolean skipSilence = preferences.getBoolean( + context.getString(R.string.playback_skip_silence_key), getPlaybackSkipSilence()); + return new PlaybackParameters(speed, pitch, skipSilence); } protected void initPlayback(@NonNull final PlayQueue queue, @@ -346,7 +379,8 @@ public abstract class BasePlayer implements final float playbackSpeed, final float playbackPitch, final boolean playbackSkipSilence, - final boolean playOnReady) { + final boolean playOnReady, + final boolean isMuted) { destroyPlayer(); initPlayer(playOnReady); setRepeatMode(repeatMode); @@ -354,26 +388,46 @@ public abstract class BasePlayer implements playQueue = queue; playQueue.init(); - if (playbackManager != null) playbackManager.dispose(); + if (playbackManager != null) { + playbackManager.dispose(); + } playbackManager = new MediaSourceManager(this, playQueue); - if (playQueueAdapter != null) playQueueAdapter.dispose(); + if (playQueueAdapter != null) { + playQueueAdapter.dispose(); + } playQueueAdapter = new PlayQueueAdapter(context, playQueue); + + simpleExoPlayer.setVolume(isMuted ? 0 : 1); } public void destroyPlayer() { - if (DEBUG) Log.d(TAG, "destroyPlayer() called"); + if (DEBUG) { + Log.d(TAG, "destroyPlayer() called"); + } if (simpleExoPlayer != null) { simpleExoPlayer.removeListener(this); simpleExoPlayer.stop(); simpleExoPlayer.release(); } - if (isProgressLoopRunning()) stopProgressLoop(); - if (playQueue != null) playQueue.dispose(); - if (audioReactor != null) audioReactor.dispose(); - if (playbackManager != null) playbackManager.dispose(); - if (mediaSessionManager != null) mediaSessionManager.dispose(); - if (stateLoader != null) stateLoader.dispose(); + if (isProgressLoopRunning()) { + stopProgressLoop(); + } + if (playQueue != null) { + playQueue.dispose(); + } + if (audioReactor != null) { + audioReactor.dispose(); + } + if (playbackManager != null) { + playbackManager.dispose(); + } + if (mediaSessionManager != null) { + mediaSessionManager.dispose(); + } + if (stateLoader != null) { + stateLoader.dispose(); + } if (playQueueAdapter != null) { playQueueAdapter.unsetSelectedListener(); @@ -382,7 +436,9 @@ public abstract class BasePlayer implements } public void destroy() { - if (DEBUG) Log.d(TAG, "destroy() called"); + if (DEBUG) { + Log.d(TAG, "destroy() called"); + } destroyPlayer(); unregisterBroadcastReceiver(); @@ -396,38 +452,50 @@ public abstract class BasePlayer implements //////////////////////////////////////////////////////////////////////////*/ private void initThumbnail(final String url) { - if (DEBUG) Log.d(TAG, "Thumbnail - initThumbnail() called"); - if (url == null || url.isEmpty()) return; + if (DEBUG) { + Log.d(TAG, "Thumbnail - initThumbnail() called"); + } + if (url == null || url.isEmpty()) { + return; + } ImageLoader.getInstance().resume(); - ImageLoader.getInstance().loadImage(url, ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, - this); + ImageLoader.getInstance() + .loadImage(url, ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, this); } @Override - public void onLoadingStarted(String imageUri, View view) { - if (DEBUG) Log.d(TAG, "Thumbnail - onLoadingStarted() called on: " + - "imageUri = [" + imageUri + "], view = [" + view + "]"); + public void onLoadingStarted(final String imageUri, final View view) { + if (DEBUG) { + Log.d(TAG, "Thumbnail - onLoadingStarted() called on: " + + "imageUri = [" + imageUri + "], view = [" + view + "]"); + } } @Override - public void onLoadingFailed(String imageUri, View view, FailReason failReason) { + public void onLoadingFailed(final String imageUri, final View view, + final FailReason failReason) { Log.e(TAG, "Thumbnail - onLoadingFailed() called on imageUri = [" + imageUri + "]", failReason.getCause()); currentThumbnail = null; } @Override - public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { - if (DEBUG) Log.d(TAG, "Thumbnail - onLoadingComplete() called with: " + - "imageUri = [" + imageUri + "], view = [" + view + "], " + - "loadedImage = [" + loadedImage + "]"); + public void onLoadingComplete(final String imageUri, final View view, + final Bitmap loadedImage) { + if (DEBUG) { + Log.d(TAG, "Thumbnail - onLoadingComplete() called with: " + + "imageUri = [" + imageUri + "], view = [" + view + "], " + + "loadedImage = [" + loadedImage + "]"); + } currentThumbnail = loadedImage; } @Override - public void onLoadingCancelled(String imageUri, View view) { - if (DEBUG) Log.d(TAG, "Thumbnail - onLoadingCancelled() called with: " + - "imageUri = [" + imageUri + "], view = [" + view + "]"); + public void onLoadingCancelled(final String imageUri, final View view) { + if (DEBUG) { + Log.d(TAG, "Thumbnail - onLoadingCancelled() called with: " + + "imageUri = [" + imageUri + "], view = [" + view + "]"); + } currentThumbnail = null; } @@ -436,16 +504,18 @@ public abstract class BasePlayer implements //////////////////////////////////////////////////////////////////////////*/ /** - * Add your action in the intentFilter + * Add your action in the intentFilter. * - * @param intentFilter intent filter that will be used for register the receiver + * @param intentFltr intent filter that will be used for register the receiver */ - protected void setupBroadcastReceiver(IntentFilter intentFilter) { - intentFilter.addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY); + protected void setupBroadcastReceiver(final IntentFilter intentFltr) { + intentFltr.addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY); } - public void onBroadcastReceived(Intent intent) { - if (intent == null || intent.getAction() == null) return; + public void onBroadcastReceived(final Intent intent) { + if (intent == null || intent.getAction() == null) { + return; + } switch (intent.getAction()) { case AudioManager.ACTION_AUDIO_BECOMING_NOISY: onPause(); @@ -463,7 +533,8 @@ public abstract class BasePlayer implements try { context.unregisterReceiver(broadcastReceiver); } catch (final IllegalArgumentException unregisteredException) { - Log.w(TAG, "Broadcast receiver already unregistered (" + unregisteredException.getMessage() + ")"); + Log.w(TAG, "Broadcast receiver already unregistered " + + "(" + unregisteredException.getMessage() + ")"); } } @@ -471,18 +542,10 @@ public abstract class BasePlayer implements // States Implementation //////////////////////////////////////////////////////////////////////////*/ - public static final int STATE_PREFLIGHT = -1; - public static final int STATE_BLOCKED = 123; - public static final int STATE_PLAYING = 124; - public static final int STATE_BUFFERING = 125; - public static final int STATE_PAUSED = 126; - public static final int STATE_PAUSED_SEEK = 127; - public static final int STATE_COMPLETED = 128; - - protected int currentState = STATE_PREFLIGHT; - - public void changeState(int state) { - if (DEBUG) Log.d(TAG, "changeState() called with: state = [" + state + "]"); + public void changeState(final int state) { + if (DEBUG) { + Log.d(TAG, "changeState() called with: state = [" + state + "]"); + } currentState = state; switch (state) { case STATE_BLOCKED: @@ -507,29 +570,44 @@ public abstract class BasePlayer implements } public void onBlocked() { - if (DEBUG) Log.d(TAG, "onBlocked() called"); - if (!isProgressLoopRunning()) startProgressLoop(); + if (DEBUG) { + Log.d(TAG, "onBlocked() called"); + } + if (!isProgressLoopRunning()) { + startProgressLoop(); + } } public void onPlaying() { - if (DEBUG) Log.d(TAG, "onPlaying() called"); - if (!isProgressLoopRunning()) startProgressLoop(); + if (DEBUG) { + Log.d(TAG, "onPlaying() called"); + } + if (!isProgressLoopRunning()) { + startProgressLoop(); + } } public void onBuffering() { } public void onPaused() { - if (isProgressLoopRunning()) stopProgressLoop(); + if (isProgressLoopRunning()) { + stopProgressLoop(); + } } - public void onPausedSeek() { - } + public void onPausedSeek() { } public void onCompleted() { - if (DEBUG) Log.d(TAG, "onCompleted() called"); - if (playQueue.getIndex() < playQueue.size() - 1) playQueue.offsetIndex(+1); - if (isProgressLoopRunning()) stopProgressLoop(); + if (DEBUG) { + Log.d(TAG, "onCompleted() called"); + } + if (playQueue.getIndex() < playQueue.size() - 1) { + playQueue.offsetIndex(+1); + } + if (isProgressLoopRunning()) { + stopProgressLoop(); + } } /*////////////////////////////////////////////////////////////////////////// @@ -537,7 +615,9 @@ public abstract class BasePlayer implements //////////////////////////////////////////////////////////////////////////*/ public void onRepeatClicked() { - if (DEBUG) Log.d(TAG, "onRepeatClicked() called"); + if (DEBUG) { + Log.d(TAG, "onRepeatClicked() called"); + } final int mode; @@ -555,15 +635,35 @@ public abstract class BasePlayer implements } setRepeatMode(mode); - if (DEBUG) Log.d(TAG, "onRepeatClicked() currentRepeatMode = " + getRepeatMode()); + if (DEBUG) { + Log.d(TAG, "onRepeatClicked() currentRepeatMode = " + getRepeatMode()); + } } public void onShuffleClicked() { - if (DEBUG) Log.d(TAG, "onShuffleClicked() called"); + if (DEBUG) { + Log.d(TAG, "onShuffleClicked() called"); + } - if (simpleExoPlayer == null) return; + if (simpleExoPlayer == null) { + return; + } simpleExoPlayer.setShuffleModeEnabled(!simpleExoPlayer.getShuffleModeEnabled()); } + /*////////////////////////////////////////////////////////////////////////// + // Mute / Unmute + //////////////////////////////////////////////////////////////////////////*/ + + public void onMuteUnmuteButtonClicked() { + if (DEBUG) { + Log.d(TAG, "onMuteUnmuteButtonClicled() called"); + } + simpleExoPlayer.setVolume(isMuted() ? 1 : 0); + } + + public boolean isMuted() { + return simpleExoPlayer.getVolume() == 0; + } /*////////////////////////////////////////////////////////////////////////// // Progress Updates @@ -580,7 +680,9 @@ public abstract class BasePlayer implements } public void triggerProgressUpdate() { - if (simpleExoPlayer == null) return; + if (simpleExoPlayer == null) { + return; + } onUpdateProgress( Math.max((int) simpleExoPlayer.getCurrentPosition(), 0), (int) simpleExoPlayer.getDuration(), @@ -589,8 +691,8 @@ public abstract class BasePlayer implements } private Disposable getProgressReactor() { - return Observable.interval(PROGRESS_LOOP_INTERVAL_MILLIS, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) + return Observable.interval(PROGRESS_LOOP_INTERVAL_MILLIS, MILLISECONDS, mainThread()) + .observeOn(mainThread()) .subscribe(ignored -> triggerProgressUpdate(), error -> Log.e(TAG, "Progress update failure: ", error)); } @@ -600,35 +702,42 @@ public abstract class BasePlayer implements //////////////////////////////////////////////////////////////////////////*/ @Override - public void onTimelineChanged(Timeline timeline, Object manifest, - @Player.TimelineChangeReason final int reason) { - if (DEBUG) Log.d(TAG, "ExoPlayer - onTimelineChanged() called with " + - (manifest == null ? "no manifest" : "available manifest") + ", " + - "timeline size = [" + timeline.getWindowCount() + "], " + - "reason = [" + reason + "]"); + public void onTimelineChanged(final Timeline timeline, final int reason) { + if (DEBUG) { + Log.d(TAG, "ExoPlayer - onTimelineChanged() called with " + + "timeline size = [" + timeline.getWindowCount() + "], " + + "reason = [" + reason + "]"); + } maybeUpdateCurrentMetadata(); } @Override - public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - if (DEBUG) Log.d(TAG, "ExoPlayer - onTracksChanged(), " + - "track group size = " + trackGroups.length); + public void onTracksChanged(final TrackGroupArray trackGroups, + final TrackSelectionArray trackSelections) { + if (DEBUG) { + Log.d(TAG, "ExoPlayer - onTracksChanged(), " + + "track group size = " + trackGroups.length); + } maybeUpdateCurrentMetadata(); } @Override - public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - if (DEBUG) Log.d(TAG, "ExoPlayer - playbackParameters(), " + - "speed: " + playbackParameters.speed + ", " + - "pitch: " + playbackParameters.pitch); + public void onPlaybackParametersChanged(final PlaybackParameters playbackParameters) { + if (DEBUG) { + Log.d(TAG, "ExoPlayer - playbackParameters(), " + + "speed: " + playbackParameters.speed + ", " + + "pitch: " + playbackParameters.pitch); + } } @Override public void onLoadingChanged(final boolean isLoading) { - if (DEBUG) Log.d(TAG, "ExoPlayer - onLoadingChanged() called with: " + - "isLoading = [" + isLoading + "]"); + if (DEBUG) { + Log.d(TAG, "ExoPlayer - onLoadingChanged() called with: " + + "isLoading = [" + isLoading + "]"); + } if (!isLoading && getCurrentState() == STATE_PAUSED && isProgressLoopRunning()) { stopProgressLoop(); @@ -640,13 +749,17 @@ public abstract class BasePlayer implements } @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (DEBUG) Log.d(TAG, "ExoPlayer - onPlayerStateChanged() called with: " + - "playWhenReady = [" + playWhenReady + "], " + - "playbackState = [" + playbackState + "]"); + public void onPlayerStateChanged(final boolean playWhenReady, final int playbackState) { + if (DEBUG) { + Log.d(TAG, "ExoPlayer - onPlayerStateChanged() called with: " + + "playWhenReady = [" + playWhenReady + "], " + + "playbackState = [" + playbackState + "]"); + } if (getCurrentState() == STATE_PAUSED_SEEK) { - if (DEBUG) Log.d(TAG, "ExoPlayer - onPlayerStateChanged() is currently blocked"); + if (DEBUG) { + Log.d(TAG, "ExoPlayer - onPlayerStateChanged() is currently blocked"); + } return; } @@ -680,41 +793,47 @@ public abstract class BasePlayer implements } private void maybeCorrectSeekPosition() { - if (playQueue == null || simpleExoPlayer == null || currentMetadata == null) return; + if (playQueue == null || simpleExoPlayer == null || currentMetadata == null) { + return; + } final PlayQueueItem currentSourceItem = playQueue.getItem(); - if (currentSourceItem == null) return; + if (currentSourceItem == null) { + return; + } final StreamInfo currentInfo = currentMetadata.getMetadata(); final long presetStartPositionMillis = currentInfo.getStartPosition() * 1000; if (presetStartPositionMillis > 0L) { // Has another start position? - if (DEBUG) Log.d(TAG, "Playback - Seeking to preset start " + - "position=[" + presetStartPositionMillis + "]"); + if (DEBUG) { + Log.d(TAG, "Playback - Seeking to preset start " + + "position=[" + presetStartPositionMillis + "]"); + } seekTo(presetStartPositionMillis); } } /** - * Processes the exceptions produced by {@link com.google.android.exoplayer2.ExoPlayer ExoPlayer}. - * There are multiple types of errors:

- *

- * {@link ExoPlaybackException#TYPE_SOURCE TYPE_SOURCE}:

- *

- * {@link ExoPlaybackException#TYPE_UNEXPECTED TYPE_UNEXPECTED}:

+ * Process exceptions produced by {@link com.google.android.exoplayer2.ExoPlayer ExoPlayer}. + *

There are multiple types of errors:

+ *
    + *
  • {@link ExoPlaybackException#TYPE_SOURCE TYPE_SOURCE}
  • + *
  • {@link ExoPlaybackException#TYPE_UNEXPECTED TYPE_UNEXPECTED}: * If a runtime error occurred, then we can try to recover it by restarting the playback - * after setting the timestamp recovery.

    - *

    - * {@link ExoPlaybackException#TYPE_RENDERER TYPE_RENDERER}:

    - * If the renderer failed, treat the error as unrecoverable. + * after setting the timestamp recovery.

  • + *
  • {@link ExoPlaybackException#TYPE_RENDERER TYPE_RENDERER}: + * 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: " + - "error = [" + error + "]"); + public void onPlayerError(final ExoPlaybackException error) { + if (DEBUG) { + Log.d(TAG, "ExoPlayer - onPlayerError() called with: " + "error = [" + error + "]"); + } if (errorToast != null) { errorToast.cancel(); errorToast = null; @@ -740,30 +859,28 @@ public abstract class BasePlayer implements } private void processSourceError(final IOException error) { - if (simpleExoPlayer == null || playQueue == null) return; + if (simpleExoPlayer == null || playQueue == null) { + return; + } setRecovery(); final Throwable cause = error.getCause(); if (error instanceof BehindLiveWindowException) { reload(); - } else if (cause instanceof UnknownHostException) { - playQueue.error(/*isNetworkProblem=*/true); - } else if (isCurrentWindowValid()) { - playQueue.error(/*isTransitioningToBadStream=*/true); - } else if (cause instanceof FailedMediaSource.MediaSourceResolutionException) { - playQueue.error(/*recoverableWithNoAvailableStream=*/false); - } else if (cause instanceof FailedMediaSource.StreamInfoLoadException) { - playQueue.error(/*recoverableIfLoadFailsWhenNetworkIsFine=*/false); } else { - playQueue.error(/*noIdeaWhatHappenedAndLetUserChooseWhatToDo=*/true); + playQueue.error(); } } @Override public void onPositionDiscontinuity(@Player.DiscontinuityReason final int reason) { - if (DEBUG) Log.d(TAG, "ExoPlayer - onPositionDiscontinuity() called with " + - "reason = [" + reason + "]"); - if (playQueue == null) return; + if (DEBUG) { + Log.d(TAG, "ExoPlayer - onPositionDiscontinuity() called with " + + "reason = [" + reason + "]"); + } + if (playQueue == null) { + return; + } // Refresh the playback if there is a transition to the next video final int newWindowIndex = simpleExoPlayer.getCurrentWindowIndex(); @@ -771,8 +888,8 @@ public abstract class BasePlayer implements case DISCONTINUITY_REASON_PERIOD_TRANSITION: // When player is in single repeat mode and a period transition occurs, // we need to register a view count here since no metadata has changed - if (getRepeatMode() == Player.REPEAT_MODE_ONE && - newWindowIndex == playQueue.getIndex()) { + if (getRepeatMode() == Player.REPEAT_MODE_ONE + && newWindowIndex == playQueue.getIndex()) { registerView(); break; } @@ -791,15 +908,21 @@ public abstract class BasePlayer implements @Override public void onRepeatModeChanged(@Player.RepeatMode final int reason) { - if (DEBUG) Log.d(TAG, "ExoPlayer - onRepeatModeChanged() called with: " + - "mode = [" + reason + "]"); + if (DEBUG) { + Log.d(TAG, "ExoPlayer - onRepeatModeChanged() called with: " + + "mode = [" + reason + "]"); + } } @Override public void onShuffleModeEnabledChanged(final boolean shuffleModeEnabled) { - if (DEBUG) Log.d(TAG, "ExoPlayer - onShuffleModeEnabledChanged() called with: " + - "mode = [" + shuffleModeEnabled + "]"); - if (playQueue == null) return; + if (DEBUG) { + Log.d(TAG, "ExoPlayer - onShuffleModeEnabledChanged() called with: " + + "mode = [" + shuffleModeEnabled + "]"); + } + if (playQueue == null) { + return; + } if (shuffleModeEnabled) { playQueue.shuffle(); } else { @@ -809,7 +932,9 @@ public abstract class BasePlayer implements @Override public void onSeekProcessed() { - if (DEBUG) Log.d(TAG, "ExoPlayer - onSeekProcessed() called"); + if (DEBUG) { + Log.d(TAG, "ExoPlayer - onSeekProcessed() called"); + } if (isPrepared) { savePlaybackState(); } @@ -822,7 +947,9 @@ public abstract class BasePlayer implements public boolean isApproachingPlaybackEdge(final long timeToEndMillis) { // If live, then not near playback edge // If not playing, then not approaching playback edge - if (simpleExoPlayer == null || isLive() || !isPlaying()) return false; + if (simpleExoPlayer == null || isLive() || !isPlaying()) { + return false; + } final long currentPositionMillis = simpleExoPlayer.getCurrentPosition(); final long currentDurationMillis = simpleExoPlayer.getDuration(); @@ -831,8 +958,12 @@ public abstract class BasePlayer implements @Override public void onPlaybackBlock() { - if (simpleExoPlayer == null) return; - if (DEBUG) Log.d(TAG, "Playback - onPlaybackBlock() called"); + if (simpleExoPlayer == null) { + return; + } + if (DEBUG) { + Log.d(TAG, "Playback - onPlaybackBlock() called"); + } currentItem = null; currentMetadata = null; @@ -844,18 +975,28 @@ public abstract class BasePlayer implements @Override public void onPlaybackUnblock(final MediaSource mediaSource) { - if (simpleExoPlayer == null) return; - if (DEBUG) Log.d(TAG, "Playback - onPlaybackUnblock() called"); + if (simpleExoPlayer == null) { + return; + } + if (DEBUG) { + Log.d(TAG, "Playback - onPlaybackUnblock() called"); + } - if (getCurrentState() == STATE_BLOCKED) changeState(STATE_BUFFERING); + if (getCurrentState() == STATE_BLOCKED) { + changeState(STATE_BUFFERING); + } simpleExoPlayer.prepare(mediaSource); } public void onPlaybackSynchronize(@NonNull final PlayQueueItem item) { - if (DEBUG) Log.d(TAG, "Playback - onPlaybackSynchronize() called with " + - "item=[" + item.getTitle() + "], url=[" + item.getUrl() + "]"); - if (simpleExoPlayer == null || playQueue == null) return; + if (DEBUG) { + Log.d(TAG, "Playback - onPlaybackSynchronize() called with " + + "item=[" + item.getTitle() + "], url=[" + item.getUrl() + "]"); + } + if (simpleExoPlayer == null || playQueue == null) { + return; + } final boolean onPlaybackInitial = currentItem == null; final boolean hasPlayQueueItemChanged = currentItem != item; @@ -865,27 +1006,32 @@ public abstract class BasePlayer implements final int currentPlaylistSize = simpleExoPlayer.getCurrentTimeline().getWindowCount(); // If nothing to synchronize - if (!hasPlayQueueItemChanged) return; + if (!hasPlayQueueItemChanged) { + return; + } currentItem = item; // Check if on wrong window if (currentPlayQueueIndex != playQueue.getIndex()) { - Log.e(TAG, "Playback - Play Queue may be desynchronized: item " + - "index=[" + currentPlayQueueIndex + "], " + - "queue index=[" + playQueue.getIndex() + "]"); + Log.e(TAG, "Playback - Play Queue may be desynchronized: item " + + "index=[" + currentPlayQueueIndex + "], " + + "queue index=[" + playQueue.getIndex() + "]"); // Check if bad seek position - } else if ((currentPlaylistSize > 0 && currentPlayQueueIndex >= currentPlaylistSize) || - currentPlayQueueIndex < 0) { - Log.e(TAG, "Playback - Trying to seek to invalid " + - "index=[" + currentPlayQueueIndex + "] with " + - "playlist length=[" + currentPlaylistSize + "]"); + } else if ((currentPlaylistSize > 0 && currentPlayQueueIndex >= currentPlaylistSize) + || currentPlayQueueIndex < 0) { + Log.e(TAG, "Playback - Trying to seek to invalid " + + "index=[" + currentPlayQueueIndex + "] with " + + "playlist length=[" + currentPlaylistSize + "]"); - } else if (currentPlaylistIndex != currentPlayQueueIndex || onPlaybackInitial || - !isPlaying()) { - if (DEBUG) Log.d(TAG, "Playback - Rewinding to correct" + - " index=[" + currentPlayQueueIndex + "]," + - " from=[" + currentPlaylistIndex + "], size=[" + currentPlaylistSize + "]."); + } else if (currentPlaylistIndex != currentPlayQueueIndex || onPlaybackInitial + || !isPlaying()) { + if (DEBUG) { + Log.d(TAG, "Playback - Rewinding to correct " + + "index=[" + currentPlayQueueIndex + "], " + + "from=[" + currentPlaylistIndex + "], " + + "size=[" + currentPlaylistSize + "]."); + } if (item.getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) { simpleExoPlayer.seekTo(currentPlayQueueIndex, item.getRecoveryPosition()); @@ -908,7 +1054,9 @@ public abstract class BasePlayer implements @Override public void onPlaybackShutdown() { - if (DEBUG) Log.d(TAG, "Shutting down..."); + if (DEBUG) { + Log.d(TAG, "Shutting down..."); + } destroy(); } @@ -916,43 +1064,54 @@ public abstract class BasePlayer implements // General Player //////////////////////////////////////////////////////////////////////////*/ - public void showStreamError(Exception exception) { + public void showStreamError(final Exception exception) { exception.printStackTrace(); if (errorToast == null) { - errorToast = Toast.makeText(context, R.string.player_stream_failure, Toast.LENGTH_SHORT); + errorToast = Toast + .makeText(context, R.string.player_stream_failure, Toast.LENGTH_SHORT); errorToast.show(); } } - public void showRecoverableError(Exception exception) { + public void showRecoverableError(final Exception exception) { exception.printStackTrace(); if (errorToast == null) { - errorToast = Toast.makeText(context, R.string.player_recoverable_failure, Toast.LENGTH_SHORT); + errorToast = Toast + .makeText(context, R.string.player_recoverable_failure, Toast.LENGTH_SHORT); errorToast.show(); } } - public void showUnrecoverableError(Exception exception) { + public void showUnrecoverableError(final Exception exception) { exception.printStackTrace(); if (errorToast != null) { errorToast.cancel(); } - errorToast = Toast.makeText(context, R.string.player_unrecoverable_failure, Toast.LENGTH_SHORT); + errorToast = Toast + .makeText(context, R.string.player_unrecoverable_failure, Toast.LENGTH_SHORT); errorToast.show(); } - public void onPrepared(boolean playWhenReady) { - if (DEBUG) Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]"); - if (playWhenReady) audioReactor.requestAudioFocus(); + public void onPrepared(final boolean playWhenReady) { + if (DEBUG) { + Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]"); + } + if (playWhenReady) { + audioReactor.requestAudioFocus(); + } changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); } public void onPlay() { - if (DEBUG) Log.d(TAG, "onPlay() called"); - if (audioReactor == null || playQueue == null || simpleExoPlayer == null) return; + if (DEBUG) { + Log.d(TAG, "onPlay() called"); + } + if (audioReactor == null || playQueue == null || simpleExoPlayer == null) { + return; + } audioReactor.requestAudioFocus(); @@ -969,8 +1128,12 @@ public abstract class BasePlayer implements } public void onPause() { - if (DEBUG) Log.d(TAG, "onPause() called"); - if (audioReactor == null || simpleExoPlayer == null) return; + if (DEBUG) { + Log.d(TAG, "onPause() called"); + } + if (audioReactor == null || simpleExoPlayer == null) { + return; + } audioReactor.abandonAudioFocus(); simpleExoPlayer.setPlayWhenReady(false); @@ -978,41 +1141,54 @@ public abstract class BasePlayer implements } public void onPlayPause() { - if (DEBUG) Log.d(TAG, "onPlayPause() called"); + if (DEBUG) { + Log.d(TAG, "onPlayPause() called"); + } - if (!isPlaying()) { - onPlay(); - } else { + if (isPlaying()) { onPause(); + } else { + onPlay(); } } public void onFastRewind() { - if (DEBUG) Log.d(TAG, "onFastRewind() called"); + if (DEBUG) { + Log.d(TAG, "onFastRewind() called"); + } seekBy(-getSeekDuration()); + triggerProgressUpdate(); } public void onFastForward() { - if (DEBUG) Log.d(TAG, "onFastForward() called"); + if (DEBUG) { + Log.d(TAG, "onFastForward() called"); + } seekBy(getSeekDuration()); + triggerProgressUpdate(); } private int getSeekDuration() { final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); final String key = context.getString(R.string.seek_duration_key); - final String value = prefs.getString(key, context.getString(R.string.seek_duration_default_value)); + final String value = prefs + .getString(key, context.getString(R.string.seek_duration_default_value)); return Integer.parseInt(value); } public void onPlayPrevious() { - if (simpleExoPlayer == null || playQueue == null) return; - if (DEBUG) Log.d(TAG, "onPlayPrevious() called"); + if (simpleExoPlayer == null || playQueue == null) { + return; + } + 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.*/ - if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT_MILLIS || - playQueue.getIndex() == 0) { + if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT_MILLIS + || playQueue.getIndex() == 0) { seekToDefault(); playQueue.offsetIndex(0); } else { @@ -1022,18 +1198,26 @@ public abstract class BasePlayer implements } public void onPlayNext() { - if (playQueue == null) return; - if (DEBUG) Log.d(TAG, "onPlayNext() called"); + if (playQueue == null) { + return; + } + if (DEBUG) { + Log.d(TAG, "onPlayNext() called"); + } savePlaybackState(); playQueue.offsetIndex(+1); } public void onSelected(final PlayQueueItem item) { - if (playQueue == null || simpleExoPlayer == null) return; + if (playQueue == null || simpleExoPlayer == null) { + return; + } final int index = playQueue.indexOf(item); - if (index == -1) return; + if (index == -1) { + return; + } if (playQueue.getIndex() == index && simpleExoPlayer.getCurrentWindowIndex() == index) { seekToDefault(); @@ -1043,13 +1227,19 @@ public abstract class BasePlayer implements playQueue.setIndex(index); } - public void seekTo(long positionMillis) { - if (DEBUG) Log.d(TAG, "seekBy() called with: position = [" + positionMillis + "]"); - if (simpleExoPlayer != null) simpleExoPlayer.seekTo(positionMillis); + public void seekTo(final long positionMillis) { + if (DEBUG) { + Log.d(TAG, "seekBy() called with: position = [" + positionMillis + "]"); + } + if (simpleExoPlayer != null) { + simpleExoPlayer.seekTo(positionMillis); + } } - public void seekBy(long offsetMillis) { - if (DEBUG) Log.d(TAG, "seekBy() called with: offsetMillis = [" + offsetMillis + "]"); + public void seekBy(final long offsetMillis) { + if (DEBUG) { + Log.d(TAG, "seekBy() called with: offsetMillis = [" + offsetMillis + "]"); + } seekTo(simpleExoPlayer.getCurrentPosition() + offsetMillis); } @@ -1069,11 +1259,13 @@ public abstract class BasePlayer implements //////////////////////////////////////////////////////////////////////////*/ private void registerView() { - if (currentMetadata == null) return; + if (currentMetadata == null) { + return; + } final StreamInfo currentInfo = currentMetadata.getMetadata(); final Disposable viewRegister = recordManager.onViewed(currentInfo).onErrorComplete() .subscribe( - ignored -> {/* successful */}, + ignored -> { /* successful */ }, error -> Log.e(TAG, "Player onViewed() failure: ", error) ); databaseUpdateReactor.add(viewRegister); @@ -1090,14 +1282,20 @@ public abstract class BasePlayer implements } private void savePlaybackState(final StreamInfo info, final long progress) { - if (info == null) return; - if (DEBUG) Log.d(TAG, "savePlaybackState() called"); + if (info == null) { + return; + } + if (DEBUG) { + Log.d(TAG, "savePlaybackState() called"); + } final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); if (prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) { final Disposable stateSaver = recordManager.saveStreamState(info, progress) - .observeOn(AndroidSchedulers.mainThread()) + .observeOn(mainThread()) .doOnError((e) -> { - if (DEBUG) e.printStackTrace(); + if (DEBUG) { + e.printStackTrace(); + } }) .onErrorComplete() .subscribe(); @@ -1106,14 +1304,18 @@ public abstract class BasePlayer implements } private void resetPlaybackState(final PlayQueueItem queueItem) { - if (queueItem == null) return; + if (queueItem == null) { + return; + } final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); if (prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) { final Disposable stateSaver = queueItem.getStream() .flatMapCompletable(info -> recordManager.saveStreamState(info, 0)) - .observeOn(AndroidSchedulers.mainThread()) + .observeOn(mainThread()) .doOnError((e) -> { - if (DEBUG) e.printStackTrace(); + if (DEBUG) { + e.printStackTrace(); + } }) .onErrorComplete() .subscribe(); @@ -1126,40 +1328,55 @@ public abstract class BasePlayer implements } public void savePlaybackState() { - if (simpleExoPlayer == null || currentMetadata == null) return; + if (simpleExoPlayer == null || currentMetadata == null) { + return; + } final StreamInfo currentInfo = currentMetadata.getMetadata(); savePlaybackState(currentInfo, simpleExoPlayer.getCurrentPosition()); } private void maybeUpdateCurrentMetadata() { - if (simpleExoPlayer == null) return; + if (simpleExoPlayer == null) { + return; + } final MediaSourceTag metadata; 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()); + error.printStackTrace(); + } return; } - if (metadata == null) return; + if (metadata == null) { + return; + } maybeAutoQueueNextStream(metadata); - if (currentMetadata == metadata) return; + if (currentMetadata == metadata) { + return; + } currentMetadata = metadata; onMetadataChanged(metadata); } - private void maybeAutoQueueNextStream(@NonNull final MediaSourceTag currentMetadata) { - if (playQueue == null || playQueue.getIndex() != playQueue.size() - 1 || - getRepeatMode() != Player.REPEAT_MODE_OFF || - !PlayerHelper.isAutoQueueEnabled(context)) return; + private void maybeAutoQueueNextStream(@NonNull final MediaSourceTag metadata) { + if (playQueue == null || playQueue.getIndex() != playQueue.size() - 1 + || getRepeatMode() != Player.REPEAT_MODE_OFF + || !PlayerHelper.isAutoQueueEnabled(context)) { + return; + } // auto queue when starting playback on the last item when not repeating - final PlayQueue autoQueue = PlayerHelper.autoQueueOf(currentMetadata.getMetadata(), + final PlayQueue autoQueue = PlayerHelper.autoQueueOf(metadata.getMetadata(), playQueue.getStreams()); - if (autoQueue != null) playQueue.append(autoQueue.getStreams()); + if (autoQueue != null) { + playQueue.append(autoQueue.getStreams()); + } } + /*////////////////////////////////////////////////////////////////////////// // Getters and Setters //////////////////////////////////////////////////////////////////////////*/ @@ -1183,37 +1400,47 @@ public abstract class BasePlayer implements @NonNull public String getVideoUrl() { - return currentMetadata == null ? context.getString(R.string.unknown_content) : currentMetadata.getMetadata().getUrl(); + return currentMetadata == null + ? context.getString(R.string.unknown_content) + : currentMetadata.getMetadata().getUrl(); } @NonNull public String getVideoTitle() { - return currentMetadata == null ? context.getString(R.string.unknown_content) : currentMetadata.getMetadata().getName(); + return currentMetadata == null + ? context.getString(R.string.unknown_content) + : currentMetadata.getMetadata().getName(); } @NonNull public String getUploaderName() { - return currentMetadata == null ? context.getString(R.string.unknown_content) : currentMetadata.getMetadata().getUploaderName(); + return currentMetadata == null + ? context.getString(R.string.unknown_content) + : currentMetadata.getMetadata().getUploaderName(); } @Nullable public Bitmap getThumbnail() { - return currentThumbnail == null ? - BitmapFactory.decodeResource(context.getResources(), R.drawable.dummy_thumbnail) : - currentThumbnail; + return currentThumbnail == null + ? BitmapFactory.decodeResource(context.getResources(), R.drawable.dummy_thumbnail) + : 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. + * + * @return whether the livestream is playing at or beyond the edge */ @SuppressWarnings("BooleanMethodIsAlwaysInverted") public boolean isLiveEdge() { - if (simpleExoPlayer == null || !isLive()) return false; + if (simpleExoPlayer == null || !isLive()) { + return false; + } final Timeline currentTimeline = simpleExoPlayer.getCurrentTimeline(); final int currentWindowIndex = simpleExoPlayer.getCurrentWindowIndex(); - if (currentTimeline.isEmpty() || currentWindowIndex < 0 || - currentWindowIndex >= currentTimeline.getWindowCount()) { + if (currentTimeline.isEmpty() || currentWindowIndex < 0 + || currentWindowIndex >= currentTimeline.getWindowCount()) { return false; } @@ -1223,14 +1450,18 @@ public abstract class BasePlayer implements } public boolean isLive() { - if (simpleExoPlayer == null) return false; + if (simpleExoPlayer == null) { + return false; + } try { return simpleExoPlayer.isCurrentWindowDynamic(); - } catch (@NonNull IndexOutOfBoundsException ignored) { + } catch (@NonNull IndexOutOfBoundsException e) { // 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: " + e.getMessage()); + e.printStackTrace(); + } return false; } } @@ -1251,13 +1482,19 @@ public abstract class BasePlayer implements } public void setRepeatMode(@Player.RepeatMode final int repeatMode) { - if (simpleExoPlayer != null) simpleExoPlayer.setRepeatMode(repeatMode); + if (simpleExoPlayer != null) { + simpleExoPlayer.setRepeatMode(repeatMode); + } } public float getPlaybackSpeed() { return getPlaybackParameters().speed; } + public void setPlaybackSpeed(final float speed) { + setPlaybackParameters(speed, getPlaybackPitch(), getPlaybackSkipSilence()); + } + public float getPlaybackPitch() { return getPlaybackParameters().pitch; } @@ -1266,18 +1503,39 @@ public abstract class BasePlayer implements return getPlaybackParameters().skipSilence; } - public void setPlaybackSpeed(float speed) { - setPlaybackParameters(speed, getPlaybackPitch(), getPlaybackSkipSilence()); - } - public PlaybackParameters getPlaybackParameters() { - if (simpleExoPlayer == null) return PlaybackParameters.DEFAULT; + if (simpleExoPlayer == null) { + return PlaybackParameters.DEFAULT; + } final PlaybackParameters parameters = simpleExoPlayer.getPlaybackParameters(); return parameters == null ? PlaybackParameters.DEFAULT : parameters; } - public void setPlaybackParameters(float speed, float pitch, boolean skipSilence) { - simpleExoPlayer.setPlaybackParameters(new PlaybackParameters(speed, pitch, skipSilence)); + /** + * Sets the playback parameters of the player, and also saves them to shared preferences. + * Speed and pitch are rounded up to 2 decimal places before being used or saved. + * @param speed the playback speed, will be rounded to up to 2 decimal places + * @param pitch the playback pitch, will be rounded to up to 2 decimal places + * @param skipSilence skip silence during playback + */ + public void setPlaybackParameters(final float speed, final float pitch, + final boolean skipSilence) { + final float roundedSpeed = Math.round(speed * 100.0f) / 100.0f; + final float roundedPitch = Math.round(pitch * 100.0f) / 100.0f; + + savePlaybackParametersToPreferences(roundedSpeed, roundedPitch, skipSilence); + simpleExoPlayer.setPlaybackParameters( + new PlaybackParameters(roundedSpeed, roundedPitch, skipSilence)); + } + + private void savePlaybackParametersToPreferences(final float speed, final float pitch, + final boolean skipSilence) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putFloat(context.getString(R.string.playback_speed_key), speed) + .putFloat(context.getString(R.string.playback_pitch_key), pitch) + .putBoolean(context.getString(R.string.playback_skip_silence_key), skipSilence) + .apply(); } public PlayQueue getPlayQueue() { @@ -1297,7 +1555,9 @@ public abstract class BasePlayer implements } public void setRecovery() { - if (playQueue == null || simpleExoPlayer == null) return; + if (playQueue == null || simpleExoPlayer == null) { + return; + } final int queuePos = playQueue.getIndex(); final long windowPos = simpleExoPlayer.getCurrentPosition(); @@ -1308,9 +1568,13 @@ public abstract class BasePlayer implements } public void setRecovery(final int queuePos, final long windowPos) { - if (playQueue.size() <= queuePos) return; + if (playQueue.size() <= queuePos) { + return; + } - if (DEBUG) Log.d(TAG, "Setting recovery, queue: " + queuePos + ", pos: " + windowPos); + if (DEBUG) { + Log.d(TAG, "Setting recovery, queue: " + queuePos + ", pos: " + windowPos); + } playQueue.setRecovery(queuePos, windowPos); } diff --git a/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java index 5bfa4732e..f6c0c9761 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java @@ -24,11 +24,18 @@ import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.graphics.Bitmap; import android.os.Binder; +import android.os.Build; import android.os.IBinder; +import android.preference.PreferenceManager; import android.util.DisplayMetrics; import android.view.ViewGroup; import android.view.WindowManager; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.core.app.NotificationCompat; import android.util.Log; import android.view.View; @@ -39,10 +46,12 @@ import com.google.android.exoplayer2.Player; import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; -import org.schabi.newpipe.player.helper.LockManager; +import org.schabi.newpipe.util.BitmapUtils; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.ThemeHelper; +import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; + /** * One service for all players @@ -55,7 +64,7 @@ public final class MainPlayer extends Service { private VideoPlayerImpl playerImpl; private WindowManager windowManager; - private LockManager lockManager; + private SharedPreferences sharedPreferences; private final IBinder mBinder = new MainPlayer.LocalBinder(); @@ -93,9 +102,10 @@ public final class MainPlayer extends Service { @Override public void onCreate() { if (DEBUG) Log.d(TAG, "onCreate() called"); + assureCorrectAppLanguage(this); notificationManager = ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)); - lockManager = new LockManager(this); windowManager = (WindowManager) getSystemService(WINDOW_SERVICE); + sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); ThemeHelper.setTheme(this); createView(); @@ -171,9 +181,6 @@ public final class MainPlayer extends Service { private void onClose() { if (DEBUG) Log.d(TAG, "onClose() called"); - if (lockManager != null) { - lockManager.releaseWifiAndCpu(); - } if (playerImpl != null) { removeViewFromParent(); @@ -235,25 +242,56 @@ public final class MainPlayer extends Service { void resetNotification() { notBuilder = createNotification(); + playerImpl.timesNotificationUpdated = 0; } private NotificationCompat.Builder createNotification() { - notRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification); - bigNotRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification_expanded); + notRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_background_notification); + bigNotRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_background_notification_expanded); setupNotification(notRemoteView); setupNotification(bigNotRemoteView); - final NotificationCompat.Builder builder = new NotificationCompat.Builder(this, getString(R.string.notification_channel_id)) + final NotificationCompat.Builder builder = new NotificationCompat + .Builder(this, getString(R.string.notification_channel_id)) .setOngoing(true) .setSmallIcon(R.drawable.ic_newpipe_triangle_white) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setCustomContentView(notRemoteView) .setCustomBigContentView(bigNotRemoteView); - builder.setPriority(NotificationCompat.PRIORITY_MAX); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + setLockScreenThumbnail(builder); + } + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { + builder.setPriority(NotificationCompat.PRIORITY_MAX); + } return builder; } + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + private void setLockScreenThumbnail(final NotificationCompat.Builder builder) { + final boolean isLockScreenThumbnailEnabled = sharedPreferences.getBoolean( + getString(R.string.enable_lock_screen_video_thumbnail_key), true); + + if (isLockScreenThumbnailEnabled) { + playerImpl.mediaSessionManager.setLockScreenArt( + builder, + getCenteredThumbnailBitmap() + ); + } else { + playerImpl.mediaSessionManager.clearLockScreenArt(builder); + } + } + + @Nullable + private Bitmap getCenteredThumbnailBitmap() { + final int screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels; + final int screenHeight = Resources.getSystem().getDisplayMetrics().heightPixels; + + return BitmapUtils.centerCrop(playerImpl.getThumbnail(), screenWidth, screenHeight); + } + private void setupNotification(final RemoteViews remoteViews) { // Don't show anything until player is playing if (playerImpl == null) return; @@ -299,13 +337,16 @@ public final class MainPlayer extends Service { * @param drawableId if != -1, sets the drawable with that id on the play/pause button */ synchronized void updateNotification(final int drawableId) { - //if (DEBUG) Log.d(TAG, "updateNotification() called with: drawableId = [" + drawableId + "]"); + /*if (DEBUG) { + Log.d(TAG, "updateNotification() called with: drawableId = [" + drawableId + "]"); + }*/ if (notBuilder == null) return; if (drawableId != -1) { if (notRemoteView != null) notRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId); if (bigNotRemoteView != null) bigNotRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId); } notificationManager.notify(NOTIFICATION_ID, notBuilder.build()); + playerImpl.timesNotificationUpdated++; } /*////////////////////////////////////////////////////////////////////////// @@ -347,10 +388,6 @@ public final class MainPlayer extends Service { // Getters //////////////////////////////////////////////////////////////////////////*/ - LockManager getLockManager() { - return lockManager; - } - NotificationCompat.Builder getNotBuilder() { return notBuilder; } diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index a958c8b31..a3cdd42e2 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -1,5 +1,6 @@ /* * Copyright 2017 Mauricio Colli + * Copyright 2019 Eltex ltd * MainVideoPlayer.java is part of NewPipe * * License: GPL-3.0+ @@ -28,25 +29,21 @@ import android.database.ContentObserver; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.media.AudioManager; +import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.preference.PreferenceManager; import android.provider.Settings; -import androidx.annotation.ColorInt; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.app.ActivityCompat; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.content.res.AppCompatResources; -import androidx.recyclerview.widget.RecyclerView; -import androidx.recyclerview.widget.ItemTouchHelper; import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; +import android.view.DisplayCutout; import android.view.GestureDetector; +import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; +import android.view.WindowInsets; import android.view.WindowManager; import android.widget.Button; import android.widget.ImageButton; @@ -58,6 +55,15 @@ import android.widget.SeekBar; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.core.app.ActivityCompat; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.RecyclerView; + import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.text.CaptionStyleCompat; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; @@ -74,13 +80,16 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItemHolder; import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback; import org.schabi.newpipe.player.resolver.MediaSourceTag; import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; +import org.schabi.newpipe.util.AndroidTvUtils; import org.schabi.newpipe.util.AnimationUtils; +import org.schabi.newpipe.util.KoreUtil; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.ShareUtils; import org.schabi.newpipe.util.StateSaver; import org.schabi.newpipe.util.ThemeHelper; +import org.schabi.newpipe.views.FocusOverlayView; import java.util.List; import java.util.Queue; @@ -89,14 +98,16 @@ import java.util.UUID; 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.VideoPlayer.DPAD_CONTROLS_HIDE_TIME; import static org.schabi.newpipe.util.AnimationUtils.Type.SCALE_AND_ALPHA; import static org.schabi.newpipe.util.AnimationUtils.Type.SLIDE_AND_ALPHA; import static org.schabi.newpipe.util.AnimationUtils.animateRotation; import static org.schabi.newpipe.util.AnimationUtils.animateView; +import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; import static org.schabi.newpipe.util.StateSaver.KEY_SAVED_STATE; /** - * Activity Player implementing VideoPlayer + * Activity Player implementing {@link VideoPlayer}. * * @author mauriciocolli */ @@ -111,7 +122,8 @@ public final class MainVideoPlayer extends AppCompatActivity private SharedPreferences defaultPreferences; - @Nullable private PlayerState playerState; + @Nullable + private PlayerState playerState; private boolean isInMultiWindow; private boolean isBackPressed; @@ -122,13 +134,19 @@ public final class MainVideoPlayer extends AppCompatActivity //////////////////////////////////////////////////////////////////////////*/ @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { + protected void onCreate(@Nullable final Bundle savedInstanceState) { + assureCorrectAppLanguage(this); super.onCreate(savedInstanceState); - if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]"); + if (DEBUG) { + Log.d(TAG, "onCreate() called with: " + + "savedInstanceState = [" + savedInstanceState + "]"); + } defaultPreferences = PreferenceManager.getDefaultSharedPreferences(this); ThemeHelper.setTheme(this); getWindow().setBackgroundDrawable(new ColorDrawable(Color.BLACK)); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) getWindow().setStatusBarColor(Color.BLACK); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + getWindow().setStatusBarColor(Color.BLACK); + } setVolumeControlStream(AudioManager.STREAM_MUSIC); WindowManager.LayoutParams lp = getWindow().getAttributes(); @@ -137,7 +155,8 @@ public final class MainVideoPlayer extends AppCompatActivity hideSystemUi(); setContentView(R.layout.activity_main_player); - playerImpl = new VideoPlayerImpl(this); + + playerImpl = new VideoPlayerImpl(this); playerImpl.setup(findViewById(android.R.id.content)); if (savedInstanceState != null && savedInstanceState.get(KEY_SAVED_STATE) != null) { @@ -154,32 +173,43 @@ public final class MainVideoPlayer extends AppCompatActivity rotationObserver = new ContentObserver(new Handler()) { @Override - public void onChange(boolean selfChange) { + public void onChange(final boolean selfChange) { super.onChange(selfChange); if (globalScreenOrientationLocked()) { - final boolean lastOrientationWasLandscape = defaultPreferences.getBoolean( - getString(R.string.last_orientation_landscape_key), false); + final String orientKey = getString(R.string.last_orientation_landscape_key); + + final boolean lastOrientationWasLandscape = defaultPreferences + .getBoolean(orientKey, AndroidTvUtils.isTv(getApplicationContext())); setLandscape(lastOrientationWasLandscape); } else { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); } } }; + getContentResolver().registerContentObserver( Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), false, rotationObserver); + + if (AndroidTvUtils.isTv(this)) { + FocusOverlayView.setupFocusObserver(this); + } } @Override - protected void onRestoreInstanceState(@NonNull Bundle bundle) { - if (DEBUG) Log.d(TAG, "onRestoreInstanceState() called"); + protected void onRestoreInstanceState(@NonNull final Bundle bundle) { + if (DEBUG) { + Log.d(TAG, "onRestoreInstanceState() called"); + } super.onRestoreInstanceState(bundle); StateSaver.tryToRestore(bundle, this); } @Override - protected void onNewIntent(Intent intent) { - if (DEBUG) Log.d(TAG, "onNewIntent() called with: intent = [" + intent + "]"); + protected void onNewIntent(final Intent intent) { + if (DEBUG) { + Log.d(TAG, "onNewIntent() called with: intent = [" + intent + "]"); + } super.onNewIntent(intent); if (intent != null) { playerState = null; @@ -187,14 +217,62 @@ public final class MainVideoPlayer extends AppCompatActivity } } + @Override + public boolean onKeyDown(final int keyCode, final KeyEvent event) { + switch (event.getKeyCode()) { + default: + break; + case KeyEvent.KEYCODE_BACK: + if (AndroidTvUtils.isTv(getApplicationContext()) + && playerImpl.isControlsVisible()) { + playerImpl.hideControls(0, 0); + hideSystemUi(); + return true; + } + break; + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_DPAD_LEFT: + case KeyEvent.KEYCODE_DPAD_DOWN: + case KeyEvent.KEYCODE_DPAD_RIGHT: + case KeyEvent.KEYCODE_DPAD_CENTER: + View playerRoot = playerImpl.getRootView(); + View controls = playerImpl.getControlsRoot(); + if (playerRoot.hasFocus() && !controls.hasFocus()) { + // do not interfere with focus in playlist etc. + return super.onKeyDown(keyCode, event); + } + + if (playerImpl.getCurrentState() == BasePlayer.STATE_BLOCKED) { + return true; + } + + if (!playerImpl.isControlsVisible()) { + playerImpl.playPauseButton.requestFocus(); + playerImpl.showControlsThenHide(); + showSystemUi(); + return true; + } else { + playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DPAD_CONTROLS_HIDE_TIME); + } + break; + } + + return super.onKeyDown(keyCode, event); + } + @Override protected void onResume() { - if (DEBUG) Log.d(TAG, "onResume() called"); + if (DEBUG) { + Log.d(TAG, "onResume() called"); + } + assureCorrectAppLanguage(this); super.onResume(); if (globalScreenOrientationLocked()) { - boolean lastOrientationWasLandscape = defaultPreferences.getBoolean( - getString(R.string.last_orientation_landscape_key), false); + final String orientKey = getString(R.string.last_orientation_landscape_key); + + boolean lastOrientationWasLandscape = defaultPreferences + .getBoolean(orientKey, AndroidTvUtils.isTv(getApplicationContext())); setLandscape(lastOrientationWasLandscape); } @@ -206,20 +284,24 @@ public final class MainVideoPlayer extends AppCompatActivity // since the first onResume needs to restore the player. // Subsequent onResume calls while multiwindow mode remains the same and the player is // prepared should be ignored. - if (isInMultiWindow) return; + if (isInMultiWindow) { + return; + } isInMultiWindow = isInMultiWindow(); if (playerState != null) { playerImpl.setPlaybackQuality(playerState.getPlaybackQuality()); playerImpl.initPlayback(playerState.getPlayQueue(), playerState.getRepeatMode(), playerState.getPlaybackSpeed(), playerState.getPlaybackPitch(), - playerState.isPlaybackSkipSilence(), playerState.wasPlaying()); + playerState.isPlaybackSkipSilence(), playerState.wasPlaying(), + playerImpl.isMuted()); } } @Override - public void onConfigurationChanged(Configuration newConfig) { + public void onConfigurationChanged(final Configuration newConfig) { super.onConfigurationChanged(newConfig); + assureCorrectAppLanguage(this); if (playerImpl.isSomePopupMenuVisible()) { playerImpl.getQualityPopupMenu().dismiss(); @@ -234,13 +316,17 @@ public final class MainVideoPlayer extends AppCompatActivity } @Override - protected void onSaveInstanceState(Bundle outState) { - if (DEBUG) Log.d(TAG, "onSaveInstanceState() called"); + protected void onSaveInstanceState(final Bundle outState) { + if (DEBUG) { + Log.d(TAG, "onSaveInstanceState() called"); + } super.onSaveInstanceState(outState); - if (playerImpl == null) return; + if (playerImpl == null) { + return; + } playerImpl.setRecovery(); - if(!playerImpl.gotDestroyed()) { + if (!playerImpl.gotDestroyed()) { playerState = createPlayerState(); } StateSaver.tryToSave(isChangingConfigurations(), null, outState, this); @@ -248,27 +334,32 @@ public final class MainVideoPlayer extends AppCompatActivity @Override protected void onStop() { - if (DEBUG) Log.d(TAG, "onStop() called"); + if (DEBUG) { + Log.d(TAG, "onStop() called"); + } super.onStop(); PlayerHelper.setScreenBrightness(getApplicationContext(), getWindow().getAttributes().screenBrightness); - if (playerImpl == null) return; + if (playerImpl == null) { + return; + } if (!isBackPressed) { playerImpl.minimize(); } playerState = createPlayerState(); playerImpl.destroy(); - if (rotationObserver != null) + if (rotationObserver != null) { getContentResolver().unregisterContentObserver(rotationObserver); + } isInMultiWindow = false; isBackPressed = false; } @Override - protected void attachBaseContext(Context newBase) { + protected void attachBaseContext(final Context newBase) { super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(newBase)); } @@ -295,14 +386,16 @@ public final class MainVideoPlayer extends AppCompatActivity } @Override - public void writeTo(Queue objectsToSave) { - if (objectsToSave == null) return; + public void writeTo(final Queue objectsToSave) { + if (objectsToSave == null) { + return; + } objectsToSave.add(playerState); } @Override @SuppressWarnings("unchecked") - public void readFrom(@NonNull Queue savedObjects) { + public void readFrom(@NonNull final Queue savedObjects) { playerState = (PlayerState) savedObjects.poll(); } @@ -311,8 +404,12 @@ public final class MainVideoPlayer extends AppCompatActivity //////////////////////////////////////////////////////////////////////////*/ private void showSystemUi() { - if (DEBUG) Log.d(TAG, "showSystemUi() called"); - if (playerImpl != null && playerImpl.queueVisible) return; + if (DEBUG) { + Log.d(TAG, "showSystemUi() called"); + } + if (playerImpl != null && playerImpl.queueVisible) { + return; + } final int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN @@ -330,7 +427,9 @@ public final class MainVideoPlayer extends AppCompatActivity } private void hideSystemUi() { - if (DEBUG) Log.d(TAG, "hideSystemUi() called"); + if (DEBUG) { + Log.d(TAG, "hideSystemUi() called"); + } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN @@ -354,19 +453,21 @@ public final class MainVideoPlayer extends AppCompatActivity } private boolean isLandscape() { - return getResources().getDisplayMetrics().heightPixels < getResources().getDisplayMetrics().widthPixels; + return getResources().getDisplayMetrics().heightPixels + < getResources().getDisplayMetrics().widthPixels; } - private void setLandscape(boolean v) { + private void setLandscape(final boolean v) { setRequestedOrientation(v ? ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE : ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT); } private boolean globalScreenOrientationLocked() { - // 1: Screen orientation changes using acelerometer - // 0: Screen orientatino is locked - return !(android.provider.Settings.System.getInt(getContentResolver(), Settings.System.ACCELEROMETER_ROTATION, 0) == 1); + // 1: Screen orientation changes using accelerometer + // 0: Screen orientation is locked + return !(android.provider.Settings.System + .getInt(getContentResolver(), Settings.System.ACCELEROMETER_ROTATION, 0) == 1); } protected void setRepeatModeButton(final ImageButton imageButton, final int repeatMode) { @@ -388,6 +489,12 @@ public final class MainVideoPlayer extends AppCompatActivity shuffleButton.setImageAlpha(shuffleAlpha); } + protected void setMuteButton(final ImageButton muteButton, final boolean isMuted) { + muteButton.setImageDrawable(AppCompatResources.getDrawable(getApplicationContext(), isMuted + ? R.drawable.ic_volume_off_white_24dp : R.drawable.ic_volume_up_white_24dp)); + } + + private boolean isInMultiWindow() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInMultiWindowMode(); } @@ -397,8 +504,8 @@ public final class MainVideoPlayer extends AppCompatActivity //////////////////////////////////////////////////////////////////////////// @Override - public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch, - boolean playbackSkipSilence) { + public void onPlaybackParameterChanged(final float playbackTempo, final float playbackPitch, + final boolean playbackSkipSilence) { if (playerImpl != null) { playerImpl.setPlaybackParameters(playbackTempo, playbackPitch, playbackSkipSilence); } @@ -408,7 +515,7 @@ public final class MainVideoPlayer extends AppCompatActivity @SuppressWarnings({"unused", "WeakerAccess"}) private class VideoPlayerImpl extends VideoPlayer { - private final float MAX_GESTURE_LENGTH = 0.75f; + private static final float MAX_GESTURE_LENGTH = 0.75f; private TextView titleTextView; private TextView channelTextView; @@ -435,10 +542,12 @@ public final class MainVideoPlayer extends AppCompatActivity private boolean queueVisible; private ImageButton moreOptionsButton; + private ImageButton kodiButton; private ImageButton shareButton; private ImageButton toggleOrientationButton; private ImageButton switchPopupButton; private ImageButton switchBackgroundButton; + private ImageButton muteButton; private RelativeLayout windowRootLayout; private View secondaryControls; @@ -450,31 +559,33 @@ public final class MainVideoPlayer extends AppCompatActivity } @Override - public void initViews(View rootView) { - super.initViews(rootView); - this.titleTextView = rootView.findViewById(R.id.titleTextView); - this.channelTextView = rootView.findViewById(R.id.channelTextView); - this.volumeRelativeLayout = rootView.findViewById(R.id.volumeRelativeLayout); - this.volumeProgressBar = rootView.findViewById(R.id.volumeProgressBar); - this.volumeImageView = rootView.findViewById(R.id.volumeImageView); - this.brightnessRelativeLayout = rootView.findViewById(R.id.brightnessRelativeLayout); - this.brightnessProgressBar = rootView.findViewById(R.id.brightnessProgressBar); - this.brightnessImageView = rootView.findViewById(R.id.brightnessImageView); - this.queueButton = rootView.findViewById(R.id.queueButton); - this.repeatButton = rootView.findViewById(R.id.repeatButton); - this.shuffleButton = rootView.findViewById(R.id.shuffleButton); + public void initViews(final View view) { + super.initViews(view); + this.titleTextView = view.findViewById(R.id.titleTextView); + this.channelTextView = view.findViewById(R.id.channelTextView); + this.volumeRelativeLayout = view.findViewById(R.id.volumeRelativeLayout); + this.volumeProgressBar = view.findViewById(R.id.volumeProgressBar); + this.volumeImageView = view.findViewById(R.id.volumeImageView); + this.brightnessRelativeLayout = view.findViewById(R.id.brightnessRelativeLayout); + this.brightnessProgressBar = view.findViewById(R.id.brightnessProgressBar); + this.brightnessImageView = view.findViewById(R.id.brightnessImageView); + this.queueButton = view.findViewById(R.id.queueButton); + this.repeatButton = view.findViewById(R.id.repeatButton); + this.shuffleButton = view.findViewById(R.id.shuffleButton); - this.playPauseButton = rootView.findViewById(R.id.playPauseButton); - this.playPreviousButton = rootView.findViewById(R.id.playPreviousButton); - this.playNextButton = rootView.findViewById(R.id.playNextButton); - this.closeButton = rootView.findViewById(R.id.closeButton); + this.playPauseButton = view.findViewById(R.id.playPauseButton); + this.playPreviousButton = view.findViewById(R.id.playPreviousButton); + this.playNextButton = view.findViewById(R.id.playNextButton); + this.closeButton = view.findViewById(R.id.closeButton); - this.moreOptionsButton = rootView.findViewById(R.id.moreOptionsButton); - this.secondaryControls = rootView.findViewById(R.id.secondaryControls); - this.shareButton = rootView.findViewById(R.id.share); - this.toggleOrientationButton = rootView.findViewById(R.id.toggleOrientation); - this.switchBackgroundButton = rootView.findViewById(R.id.switchBackground); - this.switchPopupButton = rootView.findViewById(R.id.switchPopup); + this.moreOptionsButton = view.findViewById(R.id.moreOptionsButton); + this.secondaryControls = view.findViewById(R.id.secondaryControls); + this.kodiButton = view.findViewById(R.id.playWithKodi); + this.shareButton = view.findViewById(R.id.share); + this.toggleOrientationButton = view.findViewById(R.id.toggleOrientation); + this.switchBackgroundButton = view.findViewById(R.id.switchBackground); + this.muteButton = view.findViewById(R.id.switchMute); + this.switchPopupButton = view.findViewById(R.id.switchPopup); this.queueLayout = findViewById(R.id.playQueuePanel); this.itemsListCloseButton = findViewById(R.id.playQueueClose); @@ -487,7 +598,7 @@ public final class MainVideoPlayer extends AppCompatActivity } @Override - protected void setupSubtitleView(@NonNull SubtitleView view, + protected void setupSubtitleView(@NonNull final SubtitleView view, final float captionScale, @NonNull final CaptionStyleCompat captionStyle) { final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); @@ -518,19 +629,24 @@ public final class MainVideoPlayer extends AppCompatActivity closeButton.setOnClickListener(this); moreOptionsButton.setOnClickListener(this); + kodiButton.setOnClickListener(this); shareButton.setOnClickListener(this); toggleOrientationButton.setOnClickListener(this); switchBackgroundButton.setOnClickListener(this); + muteButton.setOnClickListener(this); switchPopupButton.setOnClickListener(this); getRootView().addOnLayoutChangeListener((view, l, t, r, b, ol, ot, or, ob) -> { if (l != ol || t != ot || r != or || b != ob) { // Use smaller value to be consistent between screen orientations // (and to make usage easier) - int width = r - l, height = b - t; + int width = r - l; + int height = b - t; maxGestureLength = (int) (Math.min(width, height) * MAX_GESTURE_LENGTH); - if (DEBUG) Log.d(TAG, "maxGestureLength = " + maxGestureLength); + if (DEBUG) { + Log.d(TAG, "maxGestureLength = " + maxGestureLength); + } volumeProgressBar.setMax(maxGestureLength); brightnessProgressBar.setMax(maxGestureLength); @@ -538,6 +654,21 @@ public final class MainVideoPlayer extends AppCompatActivity setInitialGestureValues(); } }); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + queueLayout.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() { + @Override + public WindowInsets onApplyWindowInsets(final View view, + final WindowInsets windowInsets) { + final DisplayCutout cutout = windowInsets.getDisplayCutout(); + if (cutout != null) { + view.setPadding(cutout.getSafeInsetLeft(), cutout.getSafeInsetTop(), + cutout.getSafeInsetRight(), cutout.getSafeInsetBottom()); + } + return windowInsets; + } + }); + } } public void minimize() { @@ -560,7 +691,7 @@ public final class MainVideoPlayer extends AppCompatActivity //////////////////////////////////////////////////////////////////////////*/ @Override - public void onRepeatModeChanged(int i) { + public void onRepeatModeChanged(final int i) { super.onRepeatModeChanged(i); updatePlaybackButtons(); } @@ -578,6 +709,13 @@ public final class MainVideoPlayer extends AppCompatActivity protected void onMetadataChanged(@NonNull final MediaSourceTag tag) { super.onMetadataChanged(tag); + // show kodi button if it supports the current service and it is enabled in settings + final boolean showKodiButton = + KoreUtil.isServiceSupportedByKore(tag.getMetadata().getServiceId()) + && PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean(context.getString(R.string.show_play_with_kodi_key), false); + kodiButton.setVisibility(showKodiButton ? View.VISIBLE : View.GONE); + titleTextView.setText(tag.getMetadata().getName()); channelTextView.setText(tag.getMetadata().getUploaderName()); } @@ -588,6 +726,18 @@ public final class MainVideoPlayer extends AppCompatActivity finish(); } + public void onKodiShare() { + onPause(); + try { + NavigationHelper.playWithKore(context, Uri.parse(playerImpl.getVideoUrl())); + } catch (Exception e) { + if (DEBUG) { + Log.i(TAG, "Failed to start kore", e); + } + KoreUtil.showInstallKoreDialog(context); + } + } + /*////////////////////////////////////////////////////////////////////////// // Player Overrides //////////////////////////////////////////////////////////////////////////*/ @@ -596,8 +746,12 @@ public final class MainVideoPlayer extends AppCompatActivity public void toggleFullscreen() { super.toggleFullscreen(); - if (DEBUG) Log.d(TAG, "onFullScreenButtonClicked() called"); - if (simpleExoPlayer == null) return; + if (DEBUG) { + Log.d(TAG, "onFullScreenButtonClicked() called"); + } + if (simpleExoPlayer == null) { + return; + } if (!PermissionHelper.isPopupEnabled(context)) { PermissionHelper.showPopupEnablementToast(context); @@ -614,7 +768,9 @@ public final class MainVideoPlayer extends AppCompatActivity this.getPlaybackPitch(), this.getPlaybackSkipSilence(), this.getPlaybackQuality(), - false + false, + !isPlaying(), + isMuted() ); context.startService(intent); @@ -624,8 +780,12 @@ public final class MainVideoPlayer extends AppCompatActivity } public void onPlayBackgroundButtonClicked() { - if (DEBUG) Log.d(TAG, "onPlayBackgroundButtonClicked() called"); - if (playerImpl.getPlayer() == null) return; + if (DEBUG) { + Log.d(TAG, "onPlayBackgroundButtonClicked() called"); + } + if (playerImpl.getPlayer() == null) { + return; + } setRecovery(); final Intent intent = NavigationHelper.getPlayerIntent( @@ -637,7 +797,9 @@ public final class MainVideoPlayer extends AppCompatActivity this.getPlaybackPitch(), this.getPlaybackSkipSilence(), this.getPlaybackQuality(), - false + false, + !isPlaying(), + isMuted() ); context.startService(intent); @@ -646,19 +808,22 @@ public final class MainVideoPlayer extends AppCompatActivity finish(); } + @Override + public void onMuteUnmuteButtonClicked() { + super.onMuteUnmuteButtonClicked(); + setMuteButton(muteButton, playerImpl.isMuted()); + } + @Override - public void onClick(View v) { + public void onClick(final View v) { super.onClick(v); if (v.getId() == playPauseButton.getId()) { onPlayPause(); - } else if (v.getId() == playPreviousButton.getId()) { onPlayPrevious(); - } else if (v.getId() == playNextButton.getId()) { onPlayNext(); - } else if (v.getId() == queueButton.getId()) { onQueueClicked(); return; @@ -670,29 +835,28 @@ public final class MainVideoPlayer extends AppCompatActivity return; } else if (v.getId() == moreOptionsButton.getId()) { onMoreOptionsClicked(); - } else if (v.getId() == shareButton.getId()) { onShareClicked(); - } else if (v.getId() == toggleOrientationButton.getId()) { onScreenRotationClicked(); - } else if (v.getId() == switchPopupButton.getId()) { toggleFullscreen(); - } else if (v.getId() == switchBackgroundButton.getId()) { onPlayBackgroundButtonClicked(); - + } else if (v.getId() == muteButton.getId()) { + onMuteUnmuteButtonClicked(); } else if (v.getId() == closeButton.getId()) { onPlaybackShutdown(); return; + } else if (v.getId() == kodiButton.getId()) { + onKodiShare(); } if (getCurrentState() != STATE_COMPLETED) { getControlsVisibilityHandler().removeCallbacksAndMessages(null); animateView(getControlsRoot(), true, DEFAULT_CONTROLS_DURATION, 0, () -> { if (getCurrentState() == STATE_PLAYING && !isSomePopupMenuVisible()) { - hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); + safeHideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); } }); } @@ -706,39 +870,43 @@ public final class MainVideoPlayer extends AppCompatActivity updatePlaybackButtons(); getControlsRoot().setVisibility(View.INVISIBLE); - animateView(queueLayout, SLIDE_AND_ALPHA, /*visible=*/true, - DEFAULT_CONTROLS_DURATION); + animateView(queueLayout, SLIDE_AND_ALPHA, true, DEFAULT_CONTROLS_DURATION); itemsList.scrollToPosition(playQueue.getIndex()); } private void onQueueClosed() { - animateView(queueLayout, SLIDE_AND_ALPHA, /*visible=*/false, - DEFAULT_CONTROLS_DURATION); + animateView(queueLayout, SLIDE_AND_ALPHA, false, DEFAULT_CONTROLS_DURATION); queueVisible = false; } private void onMoreOptionsClicked() { - if (DEBUG) Log.d(TAG, "onMoreOptionsClicked() called"); + if (DEBUG) { + Log.d(TAG, "onMoreOptionsClicked() called"); + } - final boolean isMoreControlsVisible = secondaryControls.getVisibility() == View.VISIBLE; + final boolean isMoreControlsVisible + = secondaryControls.getVisibility() == View.VISIBLE; animateRotation(moreOptionsButton, DEFAULT_CONTROLS_DURATION, isMoreControlsVisible ? 0 : 180); animateView(secondaryControls, SLIDE_AND_ALPHA, !isMoreControlsVisible, DEFAULT_CONTROLS_DURATION); showControls(DEFAULT_CONTROLS_DURATION); + setMuteButton(muteButton, playerImpl.isMuted()); } private void onShareClicked() { // share video at the current time (youtube.com/watch?v=ID&t=SECONDS) - ShareUtils.shareUrl(MainVideoPlayer.this, - playerImpl.getVideoTitle(), - playerImpl.getVideoUrl() + "&t=" + String.valueOf(playerImpl.getPlaybackSeekBar().getProgress()/1000)); + ShareUtils.shareUrl(MainVideoPlayer.this, playerImpl.getVideoTitle(), + playerImpl.getVideoUrl() + + "&t=" + playerImpl.getPlaybackSeekBar().getProgress() / 1000); } private void onScreenRotationClicked() { - if (DEBUG) Log.d(TAG, "onScreenRotationClicked() called"); + if (DEBUG) { + Log.d(TAG, "onScreenRotationClicked() called"); + } toggleOrientation(); showControlsThenHide(); } @@ -751,20 +919,24 @@ public final class MainVideoPlayer extends AppCompatActivity } @Override - public void onStopTrackingTouch(SeekBar seekBar) { + public void onStopTrackingTouch(final SeekBar seekBar) { super.onStopTrackingTouch(seekBar); - if (wasPlaying()) showControlsThenHide(); + if (wasPlaying()) { + showControlsThenHide(); + } } @Override - public void onDismiss(PopupMenu menu) { + public void onDismiss(final PopupMenu menu) { super.onDismiss(menu); - if (isPlaying()) hideControls(DEFAULT_CONTROLS_DURATION, 0); + if (isPlaying()) { + hideControls(DEFAULT_CONTROLS_DURATION, 0); + } hideSystemUi(); } @Override - protected int nextResizeMode(int currentResizeMode) { + protected int nextResizeMode(final int currentResizeMode) { final int newResizeMode; switch (currentResizeMode) { case AspectRatioFrameLayout.RESIZE_MODE_FIT: @@ -782,7 +954,7 @@ public final class MainVideoPlayer extends AppCompatActivity return newResizeMode; } - private void storeResizeMode(@AspectRatioFrameLayout.ResizeMode int resizeMode) { + private void storeResizeMode(@AspectRatioFrameLayout.ResizeMode final int resizeMode) { defaultPreferences.edit() .putInt(getString(R.string.last_resize_mode), resizeMode) .apply(); @@ -792,13 +964,13 @@ public final class MainVideoPlayer extends AppCompatActivity protected VideoPlaybackResolver.QualityResolver getQualityResolver() { return new VideoPlaybackResolver.QualityResolver() { @Override - public int getDefaultResolutionIndex(List sortedVideos) { + public int getDefaultResolutionIndex(final List sortedVideos) { return ListHelper.getDefaultResolutionIndex(context, sortedVideos); } @Override - public int getOverrideResolutionIndex(List sortedVideos, - String playbackQuality) { + public int getOverrideResolutionIndex(final List sortedVideos, + final String playbackQuality) { return ListHelper.getResolutionIndex(context, sortedVideos, playbackQuality); } }; @@ -817,7 +989,7 @@ public final class MainVideoPlayer extends AppCompatActivity @Override public void onBlocked() { super.onBlocked(); - playPauseButton.setImageResource(R.drawable.ic_pause_white); + playPauseButton.setImageResource(R.drawable.ic_pause_white_24dp); animatePlayButtons(false, 100); animateView(closeButton, false, DEFAULT_CONTROLS_DURATION); getRootView().setKeepScreenOn(true); @@ -833,8 +1005,9 @@ public final class MainVideoPlayer extends AppCompatActivity public void onPlaying() { super.onPlaying(); animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 80, 0, () -> { - playPauseButton.setImageResource(R.drawable.ic_pause_white); + playPauseButton.setImageResource(R.drawable.ic_pause_white_24dp); animatePlayButtons(true, 200); + playPauseButton.requestFocus(); animateView(closeButton, false, DEFAULT_CONTROLS_DURATION); }); @@ -845,8 +1018,9 @@ public final class MainVideoPlayer extends AppCompatActivity public void onPaused() { super.onPaused(); animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 80, 0, () -> { - playPauseButton.setImageResource(R.drawable.ic_play_arrow_white); + playPauseButton.setImageResource(R.drawable.ic_play_arrow_white_24dp); animatePlayButtons(true, 200); + playPauseButton.requestFocus(); animateView(closeButton, false, DEFAULT_CONTROLS_DURATION); }); @@ -865,7 +1039,7 @@ public final class MainVideoPlayer extends AppCompatActivity @Override public void onCompleted() { animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 0, 0, () -> { - playPauseButton.setImageResource(R.drawable.ic_replay_white); + playPauseButton.setImageResource(R.drawable.ic_replay_white_24dp); animatePlayButtons(true, DEFAULT_CONTROLS_DURATION); animateView(closeButton, true, DEFAULT_CONTROLS_DURATION); }); @@ -879,28 +1053,67 @@ public final class MainVideoPlayer extends AppCompatActivity private void setInitialGestureValues() { if (getAudioReactor() != null) { - final float currentVolumeNormalized = (float) getAudioReactor().getVolume() / getAudioReactor().getMaxVolume(); - volumeProgressBar.setProgress((int) (volumeProgressBar.getMax() * currentVolumeNormalized)); + final float currentVolumeNormalized + = (float) getAudioReactor().getVolume() / getAudioReactor().getMaxVolume(); + volumeProgressBar.setProgress( + (int) (volumeProgressBar.getMax() * currentVolumeNormalized)); + } + + float screenBrightness = getWindow().getAttributes().screenBrightness; + if (screenBrightness < 0) { + screenBrightness = Settings.System.getInt(getContentResolver(), + Settings.System.SCREEN_BRIGHTNESS, 0) / 255.0f; + } + + brightnessProgressBar.setProgress( + (int) (brightnessProgressBar.getMax() * screenBrightness)); + + if (DEBUG) { + Log.d(TAG, "setInitialGestureValues: volumeProgressBar.getProgress() [" + + volumeProgressBar.getProgress() + "] " + + "brightnessProgressBar.getProgress() [" + + brightnessProgressBar.getProgress() + "]"); } } @Override public void showControlsThenHide() { - if (queueVisible) return; + if (queueVisible) { + return; + } super.showControlsThenHide(); } @Override - public void showControls(long duration) { - if (queueVisible) return; + public void showControls(final long duration) { + if (queueVisible) { + return; + } super.showControls(duration); } @Override - public void hideControls(final long duration, long delay) { - if (DEBUG) Log.d(TAG, "hideControls() called with: delay = [" + delay + "]"); + public void safeHideControls(final long duration, final long delay) { + if (DEBUG) { + Log.d(TAG, "safeHideControls() called with: delay = [" + delay + "]"); + } + + View controlsRoot = getControlsRoot(); + if (controlsRoot.isInTouchMode()) { + getControlsVisibilityHandler().removeCallbacksAndMessages(null); + getControlsVisibilityHandler().postDelayed(() -> + animateView(controlsRoot, false, duration, 0, + MainVideoPlayer.this::hideSystemUi), delay); + } + } + + @Override + public void hideControls(final long duration, final long delay) { + if (DEBUG) { + Log.d(TAG, "hideControls() called with: delay = [" + delay + "]"); + } getControlsVisibilityHandler().removeCallbacksAndMessages(null); getControlsVisibilityHandler().postDelayed(() -> animateView(getControlsRoot(), false, duration, 0, @@ -913,8 +1126,10 @@ public final class MainVideoPlayer extends AppCompatActivity public void hideSystemUIIfNeeded() { } private void updatePlaybackButtons() { - if (repeatButton == null || shuffleButton == null || - simpleExoPlayer == null || playQueue == null) return; + if (repeatButton == null || shuffleButton == null + || simpleExoPlayer == null || playQueue == null) { + return; + } setRepeatModeButton(repeatButton, getRepeatMode()); setShuffleButton(shuffleButton, playQueue.isShuffled()); @@ -939,7 +1154,7 @@ public final class MainVideoPlayer extends AppCompatActivity private OnScrollBelowItemsListener getQueueScrollListener() { return new OnScrollBelowItemsListener() { @Override - public void onScrolledDown(RecyclerView recyclerView) { + public void onScrolledDown(final RecyclerView recyclerView) { if (playQueue != null && !playQueue.isComplete()) { playQueue.fetch(); } else if (itemsList != null) { @@ -952,13 +1167,17 @@ public final class MainVideoPlayer extends AppCompatActivity private ItemTouchHelper.SimpleCallback getItemTouchCallback() { return new PlayQueueItemTouchCallback() { @Override - public void onMove(int sourceIndex, int targetIndex) { - if (playQueue != null) playQueue.move(sourceIndex, targetIndex); + public void onMove(final int sourceIndex, final int targetIndex) { + if (playQueue != null) { + playQueue.move(sourceIndex, targetIndex); + } } @Override - public void onSwiped(int index) { - if(index != -1) playQueue.remove(index); + public void onSwiped(final int index) { + if (index != -1) { + playQueue.remove(index); + } } }; } @@ -966,19 +1185,23 @@ public final class MainVideoPlayer extends AppCompatActivity private PlayQueueItemBuilder.OnSelectedListener getOnSelectedListener() { return new PlayQueueItemBuilder.OnSelectedListener() { @Override - public void selected(PlayQueueItem item, View view) { + public void selected(final PlayQueueItem item, final View view) { onSelected(item); } @Override - public void held(PlayQueueItem item, View view) { + public void held(final PlayQueueItem item, final View view) { final int index = playQueue.indexOf(item); - if (index != -1) playQueue.remove(index); + if (index != -1) { + playQueue.remove(index); + } } @Override - public void onStartDrag(PlayQueueItemHolder viewHolder) { - if (itemTouchHelper != null) itemTouchHelper.startDrag(viewHolder); + public void onStartDrag(final PlayQueueItemHolder viewHolder) { + if (itemTouchHelper != null) { + itemTouchHelper.startDrag(viewHolder); + } } }; } @@ -1023,6 +1246,10 @@ public final class MainVideoPlayer extends AppCompatActivity return repeatButton; } + public ImageButton getMuteButton() { + return muteButton; + } + public ImageButton getPlayPauseButton() { return playPauseButton; } @@ -1032,12 +1259,27 @@ public final class MainVideoPlayer extends AppCompatActivity } } - private class PlayerGestureListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener { + private class PlayerGestureListener extends GestureDetector.SimpleOnGestureListener + implements View.OnTouchListener { + private static final int MOVEMENT_THRESHOLD = 40; + + private final boolean isVolumeGestureEnabled = PlayerHelper + .isVolumeGestureEnabled(getApplicationContext()); + private final boolean isBrightnessGestureEnabled = PlayerHelper + .isBrightnessGestureEnabled(getApplicationContext()); + + private final int maxVolume = playerImpl.getAudioReactor().getMaxVolume(); + private boolean isMoving; @Override - public boolean onDoubleTap(MotionEvent e) { - if (DEBUG) Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY()); + public boolean onDoubleTap(final MotionEvent e) { + if (DEBUG) { + Log.d(TAG, "onDoubleTap() called with: " + + "e = [" + e + "], " + + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", " + + "xy = " + e.getX() + ", " + e.getY()); + } if (e.getX() > playerImpl.getRootView().getWidth() * 2 / 3) { playerImpl.onFastForward(); @@ -1051,44 +1293,59 @@ public final class MainVideoPlayer extends AppCompatActivity } @Override - public boolean onSingleTapConfirmed(MotionEvent e) { - if (DEBUG) Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]"); - if (playerImpl.getCurrentState() == BasePlayer.STATE_BLOCKED) return true; + public boolean onSingleTapConfirmed(final MotionEvent e) { + if (DEBUG) { + Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]"); + } + if (playerImpl.getCurrentState() == BasePlayer.STATE_BLOCKED) { + return true; + } if (playerImpl.isControlsVisible()) { playerImpl.hideControls(150, 0); } else { + playerImpl.playPauseButton.requestFocus(); playerImpl.showControlsThenHide(); showSystemUi(); } + return true; } @Override - public boolean onDown(MotionEvent e) { - if (DEBUG) Log.d(TAG, "onDown() called with: e = [" + e + "]"); + public boolean onDown(final MotionEvent e) { + if (DEBUG) { + Log.d(TAG, "onDown() called with: e = [" + e + "]"); + } return super.onDown(e); } - private static final int MOVEMENT_THRESHOLD = 40; - - 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 (!isVolumeGestureEnabled && !isBrightnessGestureEnabled) return false; + public boolean onScroll(final MotionEvent initialEvent, final MotionEvent movingEvent, + final float distanceX, final float distanceY) { + if (!isVolumeGestureEnabled && !isBrightnessGestureEnabled) { + return false; + } - //noinspection PointlessBooleanExpression - if (DEBUG && false) Log.d(TAG, "MainVideoPlayer.onScroll = " + - ", e1.getRaw = [" + initialEvent.getRawX() + ", " + initialEvent.getRawY() + "]" + - ", e2.getRaw = [" + movingEvent.getRawX() + ", " + movingEvent.getRawY() + "]" + - ", distanceXy = [" + distanceX + ", " + distanceY + "]"); + final boolean isTouchingStatusBar = initialEvent.getY() < getStatusBarHeight(); + final boolean isTouchingNavigationBar = initialEvent.getY() + > playerImpl.getRootView().getHeight() - getNavigationBarHeight(); + if (isTouchingStatusBar || isTouchingNavigationBar) { + return false; + } - final boolean insideThreshold = Math.abs(movingEvent.getY() - initialEvent.getY()) <= MOVEMENT_THRESHOLD; +// if (DEBUG) { +// Log.d(TAG, "MainVideoPlayer.onScroll = " + +// "e1.getRaw = [" + initialEvent.getRawX() + ", " +// + initialEvent.getRawY() + "], " + +// "e2.getRaw = [" + movingEvent.getRawX() + ", " +// + movingEvent.getRawY() + "], " + +// "distanceXy = [" + distanceX + ", " + distanceY + "]"); +// } + + final boolean insideThreshold + = Math.abs(movingEvent.getY() - initialEvent.getY()) <= MOVEMENT_THRESHOLD; if (!isMoving && (insideThreshold || Math.abs(distanceX) > Math.abs(distanceY)) || playerImpl.getCurrentState() == BasePlayer.STATE_COMPLETED) { return false; @@ -1097,23 +1354,29 @@ public final class MainVideoPlayer extends AppCompatActivity isMoving = true; boolean acceptAnyArea = isVolumeGestureEnabled != isBrightnessGestureEnabled; - boolean acceptVolumeArea = acceptAnyArea || initialEvent.getX() > playerImpl.getRootView().getWidth() / 2; + 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(); + (float) playerImpl.getVolumeProgressBar().getProgress() + / playerImpl.getMaxGestureLength(); int currentVolume = (int) (maxVolume * currentProgressPercent); playerImpl.getAudioReactor().setVolume(currentVolume); - if (DEBUG) Log.d(TAG, "onScroll().volumeControl, currentVolume = " + currentVolume); + if (DEBUG) { + Log.d(TAG, "onScroll().volumeControl, currentVolume = " + currentVolume); + } - final int resId = - currentProgressPercent <= 0 ? R.drawable.ic_volume_off_white_72dp - : currentProgressPercent < 0.25 ? R.drawable.ic_volume_mute_white_72dp - : currentProgressPercent < 0.75 ? R.drawable.ic_volume_down_white_72dp - : R.drawable.ic_volume_up_white_72dp; + final int resId = currentProgressPercent <= 0 + ? R.drawable.ic_volume_off_white_24dp + : currentProgressPercent < 0.25 + ? R.drawable.ic_volume_mute_white_24dp + : currentProgressPercent < 0.75 + ? R.drawable.ic_volume_down_white_24dp + : R.drawable.ic_volume_up_white_24dp; playerImpl.getVolumeImageView().setImageDrawable( AppCompatResources.getDrawable(getApplicationContext(), resId) @@ -1127,25 +1390,31 @@ public final class MainVideoPlayer extends AppCompatActivity } } else if (isBrightnessGestureEnabled && acceptBrightnessArea) { playerImpl.getBrightnessProgressBar().incrementProgressBy((int) distanceY); - float currentProgressPercent = - (float) playerImpl.getBrightnessProgressBar().getProgress() / playerImpl.getMaxGestureLength(); + float currentProgressPercent + = (float) playerImpl.getBrightnessProgressBar().getProgress() + / playerImpl.getMaxGestureLength(); WindowManager.LayoutParams layoutParams = getWindow().getAttributes(); layoutParams.screenBrightness = currentProgressPercent; getWindow().setAttributes(layoutParams); - if (DEBUG) Log.d(TAG, "onScroll().brightnessControl, currentBrightness = " + currentProgressPercent); + if (DEBUG) { + Log.d(TAG, "onScroll().brightnessControl, currentBrightness = " + + currentProgressPercent); + } - final int resId = - currentProgressPercent < 0.25 ? R.drawable.ic_brightness_low_white_72dp - : currentProgressPercent < 0.75 ? R.drawable.ic_brightness_medium_white_72dp - : R.drawable.ic_brightness_high_white_72dp; + final int resId = currentProgressPercent < 0.25 + ? R.drawable.ic_brightness_low_white_24dp + : currentProgressPercent < 0.75 + ? R.drawable.ic_brightness_medium_white_24dp + : R.drawable.ic_brightness_high_white_24dp; playerImpl.getBrightnessImageView().setImageDrawable( AppCompatResources.getDrawable(getApplicationContext(), resId) ); if (playerImpl.getBrightnessRelativeLayout().getVisibility() != View.VISIBLE) { - animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, true, 200); + animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, true, + 200); } if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) { playerImpl.getVolumeRelativeLayout().setVisibility(View.GONE); @@ -1154,14 +1423,34 @@ public final class MainVideoPlayer extends AppCompatActivity return true; } + private int getNavigationBarHeight() { + int resId = getResources().getIdentifier("navigation_bar_height", "dimen", "android"); + if (resId > 0) { + return getResources().getDimensionPixelSize(resId); + } + return 0; + } + + private int getStatusBarHeight() { + int resId = getResources().getIdentifier("status_bar_height", "dimen", "android"); + if (resId > 0) { + return getResources().getDimensionPixelSize(resId); + } + return 0; + } + private void onScrollEnd() { - if (DEBUG) Log.d(TAG, "onScrollEnd() called"); + if (DEBUG) { + Log.d(TAG, "onScrollEnd() called"); + } if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) { - animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA, false, 200, 200); + animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA, false, + 200, 200); } if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) { - animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, false, 200, 200); + animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, false, + 200, 200); } if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) { @@ -1170,9 +1459,10 @@ public final class MainVideoPlayer extends AppCompatActivity } @Override - public boolean onTouch(View v, MotionEvent event) { - //noinspection PointlessBooleanExpression - if (DEBUG && false) Log.d(TAG, "onTouch() called with: v = [" + v + "], event = [" + event + "]"); + public boolean onTouch(final View v, final MotionEvent event) { +// if (DEBUG) { +// Log.d(TAG, "onTouch() called with: v = [" + v + "], event = [" + event + "]"); +// } gestureDetector.onTouchEvent(event); if (event.getAction() == MotionEvent.ACTION_UP && isMoving) { isMoving = false; diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerServiceBinder.java b/app/src/main/java/org/schabi/newpipe/player/PlayerServiceBinder.java index ef9d92aa0..e8bd7dc85 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PlayerServiceBinder.java +++ b/app/src/main/java/org/schabi/newpipe/player/PlayerServiceBinder.java @@ -1,6 +1,7 @@ package org.schabi.newpipe.player; import android.os.Binder; + import androidx.annotation.NonNull; class PlayerServiceBinder extends Binder { diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerState.java b/app/src/main/java/org/schabi/newpipe/player/PlayerState.java index 308e8100e..af875a32b 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PlayerState.java +++ b/app/src/main/java/org/schabi/newpipe/player/PlayerState.java @@ -9,11 +9,13 @@ import java.io.Serializable; public class PlayerState implements Serializable { - @NonNull private final PlayQueue playQueue; + @NonNull + private final PlayQueue playQueue; private final int repeatMode; private final float playbackSpeed; private final float playbackPitch; - @Nullable private final String playbackQuality; + @Nullable + private final String playbackQuality; private final boolean playbackSkipSilence; private final boolean wasPlaying; diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index 22681d9ce..c0de59275 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -35,15 +35,13 @@ import android.graphics.PixelFormat; import android.os.Build; import android.os.IBinder; import android.preference.PreferenceManager; -import androidx.annotation.NonNull; -import com.google.android.material.floatingactionbutton.FloatingActionButton; -import androidx.core.app.NotificationCompat; import android.util.DisplayMetrics; import android.util.Log; import android.view.GestureDetector; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; +import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.WindowManager; import android.view.animation.AnticipateInterpolator; @@ -54,19 +52,22 @@ import android.widget.RemoteViews; import android.widget.SeekBar; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; + import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.text.CaptionStyleCompat; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.ui.SubtitleView; +import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.nostra13.universalimageloader.core.assist.FailReason; import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.R; 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.resolver.MediaSourceTag; import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; @@ -80,31 +81,31 @@ 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.util.AnimationUtils.animateView; +import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; /** - * Service Popup Player implementing VideoPlayer + * Service Popup Player implementing {@link VideoPlayer}. * * @author mauriciocolli */ public final class PopupVideoPlayer extends Service { + public static final String ACTION_CLOSE = "org.schabi.newpipe.player.PopupVideoPlayer.CLOSE"; + public static final String ACTION_PLAY_PAUSE + = "org.schabi.newpipe.player.PopupVideoPlayer.PLAY_PAUSE"; + public static final String ACTION_REPEAT = "org.schabi.newpipe.player.PopupVideoPlayer.REPEAT"; private static final String TAG = ".PopupVideoPlayer"; private static final boolean DEBUG = BasePlayer.DEBUG; - private static final int NOTIFICATION_ID = 40028922; - public static final String ACTION_CLOSE = "org.schabi.newpipe.player.PopupVideoPlayer.CLOSE"; - public static final String ACTION_PLAY_PAUSE = "org.schabi.newpipe.player.PopupVideoPlayer.PLAY_PAUSE"; - public static final String ACTION_REPEAT = "org.schabi.newpipe.player.PopupVideoPlayer.REPEAT"; - private static final String POPUP_SAVED_WIDTH = "popup_saved_width"; private static final String POPUP_SAVED_X = "popup_saved_x"; private static final String POPUP_SAVED_Y = "popup_saved_y"; private static final int MINIMUM_SHOW_EXTRA_WIDTH_DP = 300; - private static final int IDLE_WINDOW_FLAGS = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | - WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; - private static final int ONGOING_PLAYBACK_WINDOW_FLAGS = IDLE_WINDOW_FLAGS | - WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; + private static final int IDLE_WINDOW_FLAGS = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; + private static final int ONGOING_PLAYBACK_WINDOW_FLAGS = IDLE_WINDOW_FLAGS + | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; private WindowManager windowManager; private WindowManager.LayoutParams popupLayoutParams; @@ -115,18 +116,21 @@ public final class PopupVideoPlayer extends Service { private int tossFlingVelocity; - private float screenWidth, screenHeight; - private float popupWidth, popupHeight; + private float screenWidth; + private float screenHeight; + private float popupWidth; + private float popupHeight; - private float minimumWidth, minimumHeight; - private float maximumWidth, maximumHeight; + private float minimumWidth; + private float minimumHeight; + private float maximumWidth; + private float maximumHeight; private NotificationManager notificationManager; private NotificationCompat.Builder notBuilder; private RemoteViews notRemoteView; private VideoPlayerImpl playerImpl; - private LockManager lockManager; private boolean isPopupClosing = false; /*////////////////////////////////////////////////////////////////////////// @@ -142,10 +146,10 @@ public final class PopupVideoPlayer extends Service { @Override public void onCreate() { + assureCorrectAppLanguage(this); windowManager = (WindowManager) getSystemService(WINDOW_SERVICE); notificationManager = ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)); - lockManager = new LockManager(this); playerImpl = new VideoPlayerImpl(this); ThemeHelper.setTheme(this); @@ -153,14 +157,15 @@ public final class PopupVideoPlayer extends Service { } @Override - public int onStartCommand(final Intent intent, int flags, int startId) { - if (DEBUG) - Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "], flags = [" + flags + "], startId = [" + startId + "]"); + public int onStartCommand(final Intent intent, final int flags, final int startId) { + if (DEBUG) { + Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "], " + + "flags = [" + flags + "], startId = [" + startId + "]"); + } if (playerImpl.getPlayer() == null) { initPopup(); initPopupCloseOverlay(); } - if (!playerImpl.isPlaying()) playerImpl.getPlayer().setPlayWhenReady(true); playerImpl.handleIntent(intent); @@ -168,8 +173,12 @@ public final class PopupVideoPlayer extends Service { } @Override - public void onConfigurationChanged(Configuration newConfig) { - if (DEBUG) Log.d(TAG, "onConfigurationChanged() called with: newConfig = [" + newConfig + "]"); + public void onConfigurationChanged(final Configuration newConfig) { + assureCorrectAppLanguage(this); + if (DEBUG) { + Log.d(TAG, "onConfigurationChanged() called with: " + + "newConfig = [" + newConfig + "]"); + } updateScreenSize(); updatePopupSize(popupLayoutParams.width, -1); checkPopupPositionBounds(); @@ -177,17 +186,19 @@ public final class PopupVideoPlayer extends Service { @Override public void onDestroy() { - if (DEBUG) Log.d(TAG, "onDestroy() called"); + if (DEBUG) { + Log.d(TAG, "onDestroy() called"); + } closePopup(); } @Override - protected void attachBaseContext(Context base) { + protected void attachBaseContext(final Context base) { super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base)); } @Override - public IBinder onBind(Intent intent) { + public IBinder onBind(final Intent intent) { return mBinder; } @@ -197,7 +208,9 @@ public final class PopupVideoPlayer extends Service { @SuppressLint("RtlHardcoded") private void initPopup() { - if (DEBUG) Log.d(TAG, "initPopup() called"); + if (DEBUG) { + Log.d(TAG, "initPopup() called"); + } View rootView = View.inflate(this, R.layout.player_popup, null); playerImpl.setup(rootView); @@ -208,11 +221,12 @@ public final class PopupVideoPlayer extends Service { final boolean popupRememberSizeAndPos = PlayerHelper.isRememberingPopupDimensions(this); final float defaultSize = getResources().getDimension(R.dimen.popup_default_width); SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - popupWidth = popupRememberSizeAndPos ? sharedPreferences.getFloat(POPUP_SAVED_WIDTH, defaultSize) : defaultSize; + popupWidth = popupRememberSizeAndPos + ? sharedPreferences.getFloat(POPUP_SAVED_WIDTH, defaultSize) : defaultSize; - final int layoutParamType = Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O ? - WindowManager.LayoutParams.TYPE_PHONE : - WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + final int layoutParamType = Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O + ? WindowManager.LayoutParams.TYPE_PHONE + : WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; popupLayoutParams = new WindowManager.LayoutParams( (int) popupWidth, (int) getMinimumVideoHeight(popupWidth), @@ -224,8 +238,10 @@ public final class PopupVideoPlayer extends Service { int centerX = (int) (screenWidth / 2f - popupWidth / 2f); int centerY = (int) (screenHeight / 2f - popupHeight / 2f); - popupLayoutParams.x = popupRememberSizeAndPos ? sharedPreferences.getInt(POPUP_SAVED_X, centerX) : centerX; - popupLayoutParams.y = popupRememberSizeAndPos ? sharedPreferences.getInt(POPUP_SAVED_Y, centerY) : centerY; + popupLayoutParams.x = popupRememberSizeAndPos + ? sharedPreferences.getInt(POPUP_SAVED_X, centerX) : centerX; + popupLayoutParams.y = popupRememberSizeAndPos + ? sharedPreferences.getInt(POPUP_SAVED_Y, centerY) : centerY; checkPopupPositionBounds(); @@ -240,14 +256,17 @@ public final class PopupVideoPlayer extends Service { @SuppressLint("RtlHardcoded") private void initPopupCloseOverlay() { - if (DEBUG) Log.d(TAG, "initPopupCloseOverlay() called"); + if (DEBUG) { + Log.d(TAG, "initPopupCloseOverlay() called"); + } closeOverlayView = View.inflate(this, R.layout.player_popup_close_overlay, null); closeOverlayButton = closeOverlayView.findViewById(R.id.closeButton); - final int layoutParamType = Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O ? - WindowManager.LayoutParams.TYPE_PHONE : - WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; - final int flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE + final int layoutParamType = Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O + ? WindowManager.LayoutParams.TYPE_PHONE + : WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + final int flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; WindowManager.LayoutParams closeOverlayLayoutParams = new WindowManager.LayoutParams( @@ -256,7 +275,8 @@ public final class PopupVideoPlayer extends Service { flags, PixelFormat.TRANSLUCENT); closeOverlayLayoutParams.gravity = Gravity.LEFT | Gravity.TOP; - closeOverlayLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; + closeOverlayLayoutParams.softInputMode = WindowManager + .LayoutParams.SOFT_INPUT_ADJUST_RESIZE; closeOverlayButton.setVisibility(View.GONE); windowManager.addView(closeOverlayView, closeOverlayLayoutParams); @@ -271,27 +291,33 @@ public final class PopupVideoPlayer extends Service { } private NotificationCompat.Builder createNotification() { - notRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_popup_notification); + notRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, + R.layout.player_popup_notification); notRemoteView.setTextViewText(R.id.notificationSongName, playerImpl.getVideoTitle()); notRemoteView.setTextViewText(R.id.notificationArtist, playerImpl.getUploaderName()); notRemoteView.setImageViewBitmap(R.id.notificationCover, playerImpl.getThumbnail()); notRemoteView.setOnClickPendingIntent(R.id.notificationPlayPause, - PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT)); + PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_PAUSE), + PendingIntent.FLAG_UPDATE_CURRENT)); notRemoteView.setOnClickPendingIntent(R.id.notificationStop, - PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_CLOSE), PendingIntent.FLAG_UPDATE_CURRENT)); + PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_CLOSE), + PendingIntent.FLAG_UPDATE_CURRENT)); notRemoteView.setOnClickPendingIntent(R.id.notificationRepeat, - PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_REPEAT), PendingIntent.FLAG_UPDATE_CURRENT)); + PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_REPEAT), + PendingIntent.FLAG_UPDATE_CURRENT)); // Starts popup player activity -- attempts to unlock lockscreen final Intent intent = NavigationHelper.getBackgroundPlayerActivityIntent(this); notRemoteView.setOnClickPendingIntent(R.id.notificationContent, - PendingIntent.getActivity(this, NOTIFICATION_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT)); + PendingIntent.getActivity(this, NOTIFICATION_ID, intent, + PendingIntent.FLAG_UPDATE_CURRENT)); setRepeatModeRemote(notRemoteView, playerImpl.getRepeatMode()); - NotificationCompat.Builder builder = new NotificationCompat.Builder(this, getString(R.string.notification_channel_id)) + NotificationCompat.Builder builder = new NotificationCompat + .Builder(this, getString(R.string.notification_channel_id)) .setOngoing(true) .setSmallIcon(R.drawable.ic_newpipe_triangle_white) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) @@ -308,10 +334,16 @@ public final class PopupVideoPlayer extends Service { * * @param drawableId if != -1, sets the drawable with that id on the play/pause button */ - private void updateNotification(int drawableId) { - if (DEBUG) Log.d(TAG, "updateNotification() called with: drawableId = [" + drawableId + "]"); - if (notBuilder == null || notRemoteView == null) return; - if (drawableId != -1) notRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId); + private void updateNotification(final int drawableId) { + if (DEBUG) { + Log.d(TAG, "updateNotification() called with: drawableId = [" + drawableId + "]"); + } + if (notBuilder == null || notRemoteView == null) { + return; + } + if (drawableId != -1) { + notRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId); + } notificationManager.notify(NOTIFICATION_ID, notBuilder.build()); } @@ -320,8 +352,12 @@ public final class PopupVideoPlayer extends Service { //////////////////////////////////////////////////////////////////////////*/ public void closePopup() { - if (DEBUG) Log.d(TAG, "closePopup() called, isPopupClosing = " + isPopupClosing); - if (isPopupClosing) return; + if (DEBUG) { + Log.d(TAG, "closePopup() called, isPopupClosing = " + isPopupClosing); + } + if (isPopupClosing) { + return; + } isPopupClosing = true; if (playerImpl != null) { @@ -336,14 +372,16 @@ public final class PopupVideoPlayer extends Service { } mBinder = null; - if (lockManager != null) lockManager.releaseWifiAndCpu(); - if (notificationManager != null) notificationManager.cancel(NOTIFICATION_ID); + if (notificationManager != null) { + notificationManager.cancel(NOTIFICATION_ID); + } animateOverlayAndFinishService(); } private void animateOverlayAndFinishService() { - final int targetTranslationY = (int) (closeOverlayButton.getRootView().getHeight() - closeOverlayButton.getY()); + final int targetTranslationY = (int) (closeOverlayButton.getRootView().getHeight() + - closeOverlayButton.getY()); closeOverlayButton.animate().setListener(null).cancel(); closeOverlayButton.animate() @@ -352,12 +390,12 @@ public final class PopupVideoPlayer extends Service { .setDuration(400) .setListener(new AnimatorListenerAdapter() { @Override - public void onAnimationCancel(Animator animation) { + public void onAnimationCancel(final Animator animation) { end(); } @Override - public void onAnimationEnd(Animator animation) { + public void onAnimationEnd(final Animator animation) { end(); } @@ -376,6 +414,7 @@ public final class PopupVideoPlayer extends Service { /** * @see #checkPopupPositionBounds(float, float) + * @return if the popup was out of bounds and have been moved back to it */ @SuppressWarnings("UnusedReturnValue") private boolean checkPopupPositionBounds() { @@ -383,16 +422,23 @@ public final class PopupVideoPlayer extends Service { } /** - * Check if {@link #popupLayoutParams}' position is within a arbitrary boundary that goes from (0,0) to (boundaryWidth,boundaryHeight). + * Check if {@link #popupLayoutParams}' position is within a arbitrary boundary + * that goes from (0, 0) to (boundaryWidth, boundaryHeight). *

- * If it's out of these boundaries, {@link #popupLayoutParams}' position is changed and {@code true} is returned - * to represent this change. + * If it's out of these boundaries, {@link #popupLayoutParams}' position is changed + * and {@code true} is returned to represent this change. + *

* + * @param boundaryWidth width of the boundary + * @param boundaryHeight height of the boundary * @return if the popup was out of bounds and have been moved back to it */ - private boolean checkPopupPositionBounds(final float boundaryWidth, final float boundaryHeight) { + private boolean checkPopupPositionBounds(final float boundaryWidth, + final float boundaryHeight) { if (DEBUG) { - Log.d(TAG, "checkPopupPositionBounds() called with: boundaryWidth = [" + boundaryWidth + "], boundaryHeight = [" + boundaryHeight + "]"); + Log.d(TAG, "checkPopupPositionBounds() called with: " + + "boundaryWidth = [" + boundaryWidth + "], " + + "boundaryHeight = [" + boundaryHeight + "]"); } if (popupLayoutParams.x < 0) { @@ -415,15 +461,20 @@ public final class PopupVideoPlayer extends Service { } private void savePositionAndSize() { - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(PopupVideoPlayer.this); + SharedPreferences sharedPreferences = PreferenceManager + .getDefaultSharedPreferences(PopupVideoPlayer.this); sharedPreferences.edit().putInt(POPUP_SAVED_X, popupLayoutParams.x).apply(); sharedPreferences.edit().putInt(POPUP_SAVED_Y, popupLayoutParams.y).apply(); sharedPreferences.edit().putFloat(POPUP_SAVED_WIDTH, popupLayoutParams.width).apply(); } - private float getMinimumVideoHeight(float width) { - //if (DEBUG) Log.d(TAG, "getMinimumVideoHeight() called with: width = [" + width + "], returned: " + height); - return width / (16.0f / 9.0f); // Respect the 16:9 ratio that most videos have + private float getMinimumVideoHeight(final float width) { + final float height = width / (16.0f / 9.0f); // Respect the 16:9 ratio that most videos have +// if (DEBUG) { +// Log.d(TAG, "getMinimumVideoHeight() called with: width = [" + width + "], " +// + "returned: " + height); +// } + return height; } private void updateScreenSize() { @@ -432,7 +483,10 @@ public final class PopupVideoPlayer extends Service { screenWidth = metrics.widthPixels; screenHeight = metrics.heightPixels; - if (DEBUG) Log.d(TAG, "updateScreenSize() called > screenWidth = " + screenWidth + ", screenHeight = " + screenHeight); + if (DEBUG) { + Log.d(TAG, "updateScreenSize() called > screenWidth = " + screenWidth + ", " + + "screenHeight = " + screenHeight); + } popupWidth = getResources().getDimension(R.dimen.popup_default_width); popupHeight = getMinimumVideoHeight(popupWidth); @@ -444,44 +498,65 @@ public final class PopupVideoPlayer extends Service { maximumHeight = screenHeight; } - private void updatePopupSize(int width, int height) { - if (playerImpl == null) return; - if (DEBUG) Log.d(TAG, "updatePopupSize() called with: width = [" + width + "], height = [" + height + "]"); + private void updatePopupSize(final int width, final int height) { + if (playerImpl == null) { + return; + } + if (DEBUG) { + Log.d(TAG, "updatePopupSize() called with: " + + "width = [" + width + "], height = [" + height + "]"); + } - width = (int) (width > maximumWidth ? maximumWidth : width < minimumWidth ? minimumWidth : width); + final int actualWidth = (int) (width > maximumWidth ? maximumWidth + : width < minimumWidth ? minimumWidth : width); - if (height == -1) height = (int) getMinimumVideoHeight(width); - else height = (int) (height > maximumHeight ? maximumHeight : height < minimumHeight ? minimumHeight : height); + final int actualHeight; + if (height == -1) { + actualHeight = (int) getMinimumVideoHeight(width); + } else { + actualHeight = (int) (height > maximumHeight ? maximumHeight + : height < minimumHeight ? minimumHeight : height); + } - popupLayoutParams.width = width; - popupLayoutParams.height = height; - popupWidth = width; - popupHeight = height; + popupLayoutParams.width = actualWidth; + popupLayoutParams.height = actualHeight; + popupWidth = actualWidth; + popupHeight = actualHeight; - if (DEBUG) Log.d(TAG, "updatePopupSize() updated values: width = [" + width + "], height = [" + height + "]"); + if (DEBUG) { + Log.d(TAG, "updatePopupSize() updated values: " + + "width = [" + actualWidth + "], height = [" + actualHeight + "]"); + } windowManager.updateViewLayout(playerImpl.getRootView(), popupLayoutParams); } protected void setRepeatModeRemote(final RemoteViews remoteViews, final int repeatMode) { final String methodName = "setImageResource"; - if (remoteViews == null) return; + if (remoteViews == null) { + return; + } switch (repeatMode) { case Player.REPEAT_MODE_OFF: - remoteViews.setInt(R.id.notificationRepeat, methodName, R.drawable.exo_controls_repeat_off); + remoteViews.setInt(R.id.notificationRepeat, methodName, + R.drawable.exo_controls_repeat_off); break; case Player.REPEAT_MODE_ONE: - remoteViews.setInt(R.id.notificationRepeat, methodName, R.drawable.exo_controls_repeat_one); + remoteViews.setInt(R.id.notificationRepeat, methodName, + R.drawable.exo_controls_repeat_one); break; case Player.REPEAT_MODE_ALL: - remoteViews.setInt(R.id.notificationRepeat, methodName, R.drawable.exo_controls_repeat_all); + remoteViews.setInt(R.id.notificationRepeat, methodName, + R.drawable.exo_controls_repeat_all); break; } } private void updateWindowFlags(final int flags) { - if (popupLayoutParams == null || windowManager == null || playerImpl == null) return; + if (popupLayoutParams == null || windowManager == null || playerImpl == null) { + return; + } popupLayoutParams.flags = flags; windowManager.updateViewLayout(playerImpl.getRootView(), popupLayoutParams); @@ -496,18 +571,18 @@ public final class PopupVideoPlayer extends Service { private View extraOptionsView; private View closingOverlayView; + VideoPlayerImpl(final Context context) { + super("VideoPlayerImpl" + PopupVideoPlayer.TAG, context); + } + @Override - public void handleIntent(Intent intent) { + public void handleIntent(final Intent intent) { super.handleIntent(intent); resetNotification(); startForeground(NOTIFICATION_ID, notBuilder.build()); } - VideoPlayerImpl(final Context context) { - super("VideoPlayerImpl" + PopupVideoPlayer.TAG, context); - } - @Override public void initViews(View rootView) { super.initViews(rootView); @@ -528,8 +603,7 @@ public final class PopupVideoPlayer extends Service { } @Override - protected void setupSubtitleView(@NonNull SubtitleView view, - final float captionScale, + protected void setupSubtitleView(@NonNull final SubtitleView view, final float captionScale, @NonNull final CaptionStyleCompat captionStyle) { float captionRatio = (captionScale - 1f) / 5f + 1f; view.setFractionalTextSize(SubtitleView.DEFAULT_TEXT_SIZE_FRACTION * captionRatio); @@ -538,8 +612,9 @@ public final class PopupVideoPlayer extends Service { } @Override - public void onLayoutChange(final View view, int left, int top, int right, int bottom, - int oldLeft, int oldTop, int oldRight, int oldBottom) { + public void onLayoutChange(final View view, final int left, final int top, final int right, + final int bottom, final int oldLeft, final int oldTop, + final int oldRight, final int oldBottom) { float widthDp = Math.abs(right - left) / getResources().getDisplayMetrics().density; final int visibility = widthDp > MINIMUM_SHOW_EXTRA_WIDTH_DP ? View.VISIBLE : View.GONE; extraOptionsView.setVisibility(visibility); @@ -547,7 +622,9 @@ public final class PopupVideoPlayer extends Service { @Override public void destroy() { - if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, null); + if (notRemoteView != null) { + notRemoteView.setImageViewBitmap(R.id.notificationCover, null); + } super.destroy(); } @@ -555,7 +632,9 @@ public final class PopupVideoPlayer extends Service { public void toggleFullscreen() { super.toggleFullscreen(); - if (DEBUG) Log.d(TAG, "onFullScreenButtonClicked() called"); + if (DEBUG) { + Log.d(TAG, "onFullScreenButtonClicked() called"); + } setRecovery(); final Intent intent = NavigationHelper.getPlayerIntent( @@ -567,7 +646,9 @@ public final class PopupVideoPlayer extends Service { this.getPlaybackPitch(), this.getPlaybackSkipSilence(), this.getPlaybackQuality(), - false + false, + !isPlaying(), + isMuted() ); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); @@ -575,13 +656,15 @@ public final class PopupVideoPlayer extends Service { } @Override - public void onDismiss(PopupMenu menu) { + public void onDismiss(final PopupMenu menu) { super.onDismiss(menu); - if (isPlaying()) hideControls(500, 0); + if (isPlaying()) { + hideControls(500, 0); + } } @Override - protected int nextResizeMode(int resizeMode) { + protected int nextResizeMode(final int resizeMode) { if (resizeMode == AspectRatioFrameLayout.RESIZE_MODE_FILL) { return AspectRatioFrameLayout.RESIZE_MODE_FIT; } else { @@ -590,7 +673,7 @@ public final class PopupVideoPlayer extends Service { } @Override - public void onStopTrackingTouch(SeekBar seekBar) { + public void onStopTrackingTouch(final SeekBar seekBar) { super.onStopTrackingTouch(seekBar); if (wasPlaying()) { hideControls(100, 0); @@ -604,7 +687,14 @@ public final class PopupVideoPlayer extends Service { } @Override - public void onUpdateProgress(int currentProgress, int duration, int bufferPercent) { + public void onMuteUnmuteButtonClicked() { + super.onMuteUnmuteButtonClicked(); + updatePlayback(); + } + + @Override + public void onUpdateProgress(final int currentProgress, final int duration, + final int bufferPercent) { updateProgress(currentProgress, duration, bufferPercent); super.onUpdateProgress(currentProgress, duration, bufferPercent); } @@ -613,13 +703,13 @@ public final class PopupVideoPlayer extends Service { protected VideoPlaybackResolver.QualityResolver getQualityResolver() { return new VideoPlaybackResolver.QualityResolver() { @Override - public int getDefaultResolutionIndex(List sortedVideos) { + public int getDefaultResolutionIndex(final List sortedVideos) { return ListHelper.getPopupDefaultResolutionIndex(context, sortedVideos); } @Override - public int getOverrideResolutionIndex(List sortedVideos, - String playbackQuality) { + public int getOverrideResolutionIndex(final List sortedVideos, + final String playbackQuality) { return ListHelper.getPopupResolutionIndex(context, sortedVideos, playbackQuality); } @@ -631,9 +721,12 @@ public final class PopupVideoPlayer extends Service { //////////////////////////////////////////////////////////////////////////*/ @Override - public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { + public void onLoadingComplete(final String imageUri, final View view, + final Bitmap loadedImage) { super.onLoadingComplete(imageUri, view, loadedImage); - if (playerImpl == null) return; + if (playerImpl == null) { + return; + } // rebuild notification here since remote view does not release bitmaps, // causing memory leaks resetNotification(); @@ -641,14 +734,15 @@ public final class PopupVideoPlayer extends Service { } @Override - public void onLoadingFailed(String imageUri, View view, FailReason failReason) { + public void onLoadingFailed(final String imageUri, final View view, + final FailReason failReason) { super.onLoadingFailed(imageUri, view, failReason); resetNotification(); updateNotification(-1); } @Override - public void onLoadingCancelled(String imageUri, View view) { + public void onLoadingCancelled(final String imageUri, final View view) { super.onLoadingCancelled(imageUri, view); resetNotification(); updateNotification(-1); @@ -658,14 +752,14 @@ public final class PopupVideoPlayer extends Service { // Activity Event Listener //////////////////////////////////////////////////////////////////////////*/ - /*package-private*/ void setActivityListener(PlayerEventListener listener) { + /*package-private*/ void setActivityListener(final PlayerEventListener listener) { activityListener = listener; updateMetadata(); updatePlayback(); triggerProgressUpdate(); } - /*package-private*/ void removeActivityListener(PlayerEventListener listener) { + /*package-private*/ void removeActivityListener(final PlayerEventListener listener) { if (activityListener == listener) { activityListener = null; } @@ -684,7 +778,8 @@ public final class PopupVideoPlayer extends Service { } } - private void updateProgress(int currentProgress, int duration, int bufferPercent) { + private void updateProgress(final int currentProgress, final int duration, + final int bufferPercent) { if (activityListener != null) { activityListener.onProgressUpdate(currentProgress, duration, bufferPercent); } @@ -702,7 +797,7 @@ public final class PopupVideoPlayer extends Service { //////////////////////////////////////////////////////////////////////////*/ @Override - public void onRepeatModeChanged(int i) { + public void onRepeatModeChanged(final int i) { super.onRepeatModeChanged(i); setRepeatModeRemote(notRemoteView, i); updatePlayback(); @@ -711,7 +806,7 @@ public final class PopupVideoPlayer extends Service { } @Override - public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + public void onPlaybackParametersChanged(final PlaybackParameters playbackParameters) { super.onPlaybackParametersChanged(playbackParameters); updatePlayback(); } @@ -738,22 +833,29 @@ public final class PopupVideoPlayer extends Service { //////////////////////////////////////////////////////////////////////////*/ @Override - protected void setupBroadcastReceiver(IntentFilter intentFilter) { - super.setupBroadcastReceiver(intentFilter); - if (DEBUG) Log.d(TAG, "setupBroadcastReceiver() called with: intentFilter = [" + intentFilter + "]"); - intentFilter.addAction(ACTION_CLOSE); - intentFilter.addAction(ACTION_PLAY_PAUSE); - intentFilter.addAction(ACTION_REPEAT); + protected void setupBroadcastReceiver(final IntentFilter intentFltr) { + super.setupBroadcastReceiver(intentFltr); + if (DEBUG) { + Log.d(TAG, "setupBroadcastReceiver() called with: " + + "intentFilter = [" + intentFltr + "]"); + } + intentFltr.addAction(ACTION_CLOSE); + intentFltr.addAction(ACTION_PLAY_PAUSE); + intentFltr.addAction(ACTION_REPEAT); - intentFilter.addAction(Intent.ACTION_SCREEN_ON); - intentFilter.addAction(Intent.ACTION_SCREEN_OFF); + intentFltr.addAction(Intent.ACTION_SCREEN_ON); + intentFltr.addAction(Intent.ACTION_SCREEN_OFF); } @Override - public void onBroadcastReceived(Intent intent) { + public void onBroadcastReceived(final Intent intent) { super.onBroadcastReceived(intent); - if (intent == null || intent.getAction() == null) return; - if (DEBUG) Log.d(TAG, "onBroadcastReceived() called with: intent = [" + intent + "]"); + if (intent == null || intent.getAction() == null) { + return; + } + if (DEBUG) { + Log.d(TAG, "onBroadcastReceived() called with: intent = [" + intent + "]"); + } switch (intent.getAction()) { case ACTION_CLOSE: closePopup(); @@ -778,7 +880,7 @@ public final class PopupVideoPlayer extends Service { //////////////////////////////////////////////////////////////////////////*/ @Override - public void changeState(int state) { + public void changeState(final int state) { super.changeState(state); updatePlayback(); } @@ -787,7 +889,7 @@ public final class PopupVideoPlayer extends Service { public void onBlocked() { super.onBlocked(); resetNotification(); - updateNotification(R.drawable.ic_play_arrow_white); + updateNotification(R.drawable.exo_controls_play); } @Override @@ -797,20 +899,19 @@ public final class PopupVideoPlayer extends Service { updateWindowFlags(ONGOING_PLAYBACK_WINDOW_FLAGS); resetNotification(); - updateNotification(R.drawable.ic_pause_white); + updateNotification(R.drawable.exo_controls_pause); - videoPlayPause.setBackgroundResource(R.drawable.ic_pause_white); + videoPlayPause.setBackgroundResource(R.drawable.exo_controls_pause); hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); startForeground(NOTIFICATION_ID, notBuilder.build()); - lockManager.acquireWifiAndCpu(); } @Override public void onBuffering() { super.onBuffering(); resetNotification(); - updateNotification(R.drawable.ic_play_arrow_white); + updateNotification(R.drawable.exo_controls_play); } @Override @@ -820,10 +921,8 @@ public final class PopupVideoPlayer extends Service { updateWindowFlags(IDLE_WINDOW_FLAGS); resetNotification(); - updateNotification(R.drawable.ic_play_arrow_white); - - videoPlayPause.setBackgroundResource(R.drawable.ic_play_arrow_white); - lockManager.releaseWifiAndCpu(); + updateNotification(R.drawable.exo_controls_play); + videoPlayPause.setBackgroundResource(R.drawable.exo_controls_play); stopForeground(false); } @@ -832,9 +931,9 @@ public final class PopupVideoPlayer extends Service { public void onPausedSeek() { super.onPausedSeek(); resetNotification(); - updateNotification(R.drawable.ic_play_arrow_white); + updateNotification(R.drawable.exo_controls_play); - videoPlayPause.setBackgroundResource(R.drawable.ic_pause_white); + videoPlayPause.setBackgroundResource(R.drawable.exo_controls_play); } @Override @@ -844,10 +943,8 @@ public final class PopupVideoPlayer extends Service { updateWindowFlags(IDLE_WINDOW_FLAGS); resetNotification(); - updateNotification(R.drawable.ic_replay_white); - - videoPlayPause.setBackgroundResource(R.drawable.ic_replay_white); - lockManager.releaseWifiAndCpu(); + updateNotification(R.drawable.ic_replay_white_24dp); + videoPlayPause.setBackgroundResource(R.drawable.ic_replay_white_24dp); stopForeground(false); } @@ -858,12 +955,12 @@ public final class PopupVideoPlayer extends Service { super.showControlsThenHide(); } - public void showControls(long duration) { + public void showControls(final long duration) { videoPlayPause.setVisibility(View.VISIBLE); super.showControls(duration); } - public void hideControls(final long duration, long delay) { + public void hideControls(final long duration, final long delay) { super.hideControlsAndButton(duration, delay, videoPlayPause); } @@ -896,16 +993,31 @@ public final class PopupVideoPlayer extends Service { } } - private class PopupWindowGestureListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener { - private int initialPopupX, initialPopupY; + private class PopupWindowGestureListener extends GestureDetector.SimpleOnGestureListener + implements View.OnTouchListener { + private int initialPopupX; + private int initialPopupY; private boolean isMoving; private boolean isResizing; + //initial co-ordinates and distance between fingers + private double initPointerDistance = -1; + private float initFirstPointerX = -1; + private float initFirstPointerY = -1; + private float initSecPointerX = -1; + private float initSecPointerY = -1; + + @Override - public boolean onDoubleTap(MotionEvent e) { - if (DEBUG) - Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY()); - if (playerImpl == null || !playerImpl.isPlaying()) return false; + public boolean onDoubleTap(final MotionEvent e) { + if (DEBUG) { + Log.d(TAG, "onDoubleTap() called with: e = [" + e + "], " + + "rawXy = " + e.getRawX() + ", " + e.getRawY() + + ", xy = " + e.getX() + ", " + e.getY()); + } + if (playerImpl == null || !playerImpl.isPlaying()) { + return false; + } playerImpl.hideControls(0, 0); @@ -919,9 +1031,13 @@ public final class PopupVideoPlayer extends Service { } @Override - public boolean onSingleTapConfirmed(MotionEvent e) { - if (DEBUG) Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]"); - if (playerImpl == null || playerImpl.getPlayer() == null) return false; + public boolean onSingleTapConfirmed(final MotionEvent e) { + if (DEBUG) { + Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]"); + } + if (playerImpl == null || playerImpl.getPlayer() == null) { + return false; + } if (playerImpl.isControlsVisible()) { playerImpl.hideControls(100, 100); } else { @@ -932,8 +1048,10 @@ public final class PopupVideoPlayer extends Service { } @Override - public boolean onDown(MotionEvent e) { - if (DEBUG) Log.d(TAG, "onDown() called with: e = [" + e + "]"); + public boolean onDown(final MotionEvent e) { + if (DEBUG) { + Log.d(TAG, "onDown() called with: e = [" + e + "]"); + } // Fix popup position when the user touch it, it may have the wrong one // because the soft input is visible (the draggable area is currently resized). @@ -947,16 +1065,21 @@ public final class PopupVideoPlayer extends Service { } @Override - public void onLongPress(MotionEvent e) { - if (DEBUG) Log.d(TAG, "onLongPress() called with: e = [" + e + "]"); + public void onLongPress(final MotionEvent e) { + if (DEBUG) { + Log.d(TAG, "onLongPress() called with: e = [" + e + "]"); + } updateScreenSize(); checkPopupPositionBounds(); updatePopupSize((int) screenWidth, -1); } @Override - public boolean onScroll(MotionEvent initialEvent, MotionEvent movingEvent, float distanceX, float distanceY) { - if (isResizing || playerImpl == null) return super.onScroll(initialEvent, movingEvent, distanceX, distanceY); + public boolean onScroll(final MotionEvent initialEvent, final MotionEvent movingEvent, + final float distanceX, final float distanceY) { + if (isResizing || playerImpl == null) { + return super.onScroll(initialEvent, movingEvent, distanceX, distanceY); + } if (!isMoving) { animateView(closeOverlayButton, true, 200); @@ -964,14 +1087,22 @@ public final class PopupVideoPlayer extends Service { isMoving = true; - float diffX = (int) (movingEvent.getRawX() - initialEvent.getRawX()), posX = (int) (initialPopupX + diffX); - float diffY = (int) (movingEvent.getRawY() - initialEvent.getRawY()), posY = (int) (initialPopupY + diffY); + float diffX = (int) (movingEvent.getRawX() - initialEvent.getRawX()); + float posX = (int) (initialPopupX + diffX); + float diffY = (int) (movingEvent.getRawY() - initialEvent.getRawY()); + float posY = (int) (initialPopupY + diffY); - if (posX > (screenWidth - popupWidth)) posX = (int) (screenWidth - popupWidth); - else if (posX < 0) posX = 0; + if (posX > (screenWidth - popupWidth)) { + posX = (int) (screenWidth - popupWidth); + } else if (posX < 0) { + posX = 0; + } - if (posY > (screenHeight - popupHeight)) posY = (int) (screenHeight - popupHeight); - else if (posY < 0) posY = 0; + if (posY > (screenHeight - popupHeight)) { + posY = (int) (screenHeight - popupHeight); + } else if (posY < 0) { + posY = 0; + } popupLayoutParams.x = (int) posX; popupLayoutParams.y = (int) posY; @@ -987,22 +1118,30 @@ public final class PopupVideoPlayer extends Service { } } - //noinspection PointlessBooleanExpression - if (DEBUG && false) { - Log.d(TAG, "PopupVideoPlayer.onScroll = " + - ", e1.getRaw = [" + initialEvent.getRawX() + ", " + initialEvent.getRawY() + "]" + ", e1.getX,Y = [" + initialEvent.getX() + ", " + initialEvent.getY() + "]" + - ", e2.getRaw = [" + movingEvent.getRawX() + ", " + movingEvent.getRawY() + "]" + ", e2.getX,Y = [" + movingEvent.getX() + ", " + movingEvent.getY() + "]" + - ", distanceX,Y = [" + distanceX + ", " + distanceY + "]" + - ", posX,Y = [" + posX + ", " + posY + "]" + - ", popupW,H = [" + popupWidth + " x " + popupHeight + "]"); - } +// if (DEBUG) { +// Log.d(TAG, "PopupVideoPlayer.onScroll = " +// + "e1.getRaw = [" + initialEvent.getRawX() + ", " +// + initialEvent.getRawY() + "], " +// + "e1.getX,Y = [" + initialEvent.getX() + ", " +// + initialEvent.getY() + "], " +// + "e2.getRaw = [" + movingEvent.getRawX() + ", " +// + movingEvent.getRawY() + "], " +// + "e2.getX,Y = [" + movingEvent.getX() + ", " + movingEvent.getY() + "], " +// + "distanceX,Y = [" + distanceX + ", " + distanceY + "], " +// + "posX,Y = [" + posX + ", " + posY + "], " +// + "popupW,H = [" + popupWidth + " x " + popupHeight + "]"); +// } windowManager.updateViewLayout(playerImpl.getRootView(), popupLayoutParams); return true; } - private void onScrollEnd(MotionEvent event) { - if (DEBUG) Log.d(TAG, "onScrollEnd() called"); - if (playerImpl == null) return; + private void onScrollEnd(final MotionEvent event) { + if (DEBUG) { + Log.d(TAG, "onScrollEnd() called"); + } + if (playerImpl == null) { + return; + } if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) { playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); } @@ -1019,15 +1158,24 @@ public final class PopupVideoPlayer extends Service { } @Override - public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { - if (DEBUG) Log.d(TAG, "Fling velocity: dX=[" + velocityX + "], dY=[" + velocityY + "]"); - if (playerImpl == null) return false; + public boolean onFling(final MotionEvent e1, final MotionEvent e2, + final float velocityX, final float velocityY) { + if (DEBUG) { + Log.d(TAG, "Fling velocity: dX=[" + velocityX + "], dY=[" + velocityY + "]"); + } + if (playerImpl == null) { + return false; + } final float absVelocityX = Math.abs(velocityX); final float absVelocityY = Math.abs(velocityY); if (Math.max(absVelocityX, absVelocityY) > tossFlingVelocity) { - if (absVelocityX > tossFlingVelocity) popupLayoutParams.x = (int) velocityX; - if (absVelocityY > tossFlingVelocity) popupLayoutParams.y = (int) velocityY; + if (absVelocityX > tossFlingVelocity) { + popupLayoutParams.x = (int) velocityX; + } + if (absVelocityY > tossFlingVelocity) { + popupLayoutParams.y = (int) velocityY; + } checkPopupPositionBounds(); windowManager.updateViewLayout(playerImpl.getRootView(), popupLayoutParams); return true; @@ -1036,28 +1184,47 @@ public final class PopupVideoPlayer extends Service { } @Override - public boolean onTouch(View v, MotionEvent event) { + public boolean onTouch(final View v, final MotionEvent event) { popupGestureDetector.onTouchEvent(event); - if (playerImpl == null) return false; + if (playerImpl == null) { + return false; + } if (event.getPointerCount() == 2 && !isMoving && !isResizing) { - if (DEBUG) Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing."); + if (DEBUG) { + Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing."); + } playerImpl.showAndAnimateControl(-1, true); playerImpl.getLoadingPanel().setVisibility(View.GONE); playerImpl.hideControls(0, 0); animateView(playerImpl.getCurrentDisplaySeek(), false, 0, 0); animateView(playerImpl.getResizingIndicator(), true, 200, 0); + + //record co-ordinates of fingers + initFirstPointerX = event.getX(0); + initFirstPointerY = event.getY(0); + initSecPointerX = event.getX(1); + initSecPointerY = event.getY(1); + //record distance between fingers + initPointerDistance = Math.hypot(initFirstPointerX - initSecPointerX, + initFirstPointerY - initSecPointerY); + isResizing = true; } if (event.getAction() == MotionEvent.ACTION_MOVE && !isMoving && isResizing) { - if (DEBUG) Log.d(TAG, "onTouch() ACTION_MOVE > v = [" + v + "], e1.getRaw = [" + event.getRawX() + ", " + event.getRawY() + "]"); + if (DEBUG) { + Log.d(TAG, "onTouch() ACTION_MOVE > v = [" + v + "], " + + "e1.getRaw = [" + event.getRawX() + ", " + event.getRawY() + "]"); + } return handleMultiDrag(event); } if (event.getAction() == MotionEvent.ACTION_UP) { - if (DEBUG) - Log.d(TAG, "onTouch() ACTION_UP > v = [" + v + "], e1.getRaw = [" + event.getRawX() + ", " + event.getRawY() + "]"); + if (DEBUG) { + Log.d(TAG, "onTouch() ACTION_UP > v = [" + v + "], " + + "e1.getRaw = [" + event.getRawX() + ", " + event.getRawY() + "]"); + } if (isMoving) { isMoving = false; onScrollEnd(event); @@ -1065,6 +1232,13 @@ public final class PopupVideoPlayer extends Service { if (isResizing) { isResizing = false; + + initPointerDistance = -1; + initFirstPointerX = -1; + initFirstPointerY = -1; + initSecPointerX = -1; + initSecPointerY = -1; + animateView(playerImpl.getResizingIndicator(), false, 100, 0); playerImpl.changeState(playerImpl.getCurrentState()); } @@ -1079,41 +1253,52 @@ public final class PopupVideoPlayer extends Service { } private boolean handleMultiDrag(final MotionEvent event) { - if (event.getPointerCount() != 2) return false; + if (initPointerDistance != -1 && event.getPointerCount() == 2) { + // get the movements of the fingers + double firstPointerMove = Math.hypot(event.getX(0) - initFirstPointerX, + event.getY(0) - initFirstPointerY); + double secPointerMove = Math.hypot(event.getX(1) - initSecPointerX, + event.getY(1) - initSecPointerY); - final float firstPointerX = event.getX(0); - final float secondPointerX = event.getX(1); + // minimum threshold beyond which pinch gesture will work + int minimumMove = ViewConfiguration.get(PopupVideoPlayer.this).getScaledTouchSlop(); - final float diff = Math.abs(firstPointerX - secondPointerX); - if (firstPointerX > secondPointerX) { - // second pointer is the anchor (the leftmost pointer) - popupLayoutParams.x = (int) (event.getRawX() - diff); - } else { - // first pointer is the anchor - popupLayoutParams.x = (int) event.getRawX(); + if (Math.max(firstPointerMove, secPointerMove) > minimumMove) { + // calculate current distance between the pointers + double currentPointerDistance = + Math.hypot(event.getX(0) - event.getX(1), + event.getY(0) - event.getY(1)); + + // change co-ordinates of popup so the center stays at the same position + double newWidth = (popupWidth * currentPointerDistance / initPointerDistance); + initPointerDistance = currentPointerDistance; + popupLayoutParams.x += (popupWidth - newWidth) / 2; + + checkPopupPositionBounds(); + updateScreenSize(); + + updatePopupSize((int) Math.min(screenWidth, newWidth), -1); + return true; + } } - - checkPopupPositionBounds(); - updateScreenSize(); - - final int width = (int) Math.min(screenWidth, diff); - updatePopupSize(width, -1); - - return true; + return false; } /*////////////////////////////////////////////////////////////////////////// // Utils //////////////////////////////////////////////////////////////////////////*/ - private int distanceFromCloseButton(MotionEvent popupMotionEvent) { - final int closeOverlayButtonX = closeOverlayButton.getLeft() + closeOverlayButton.getWidth() / 2; - final int closeOverlayButtonY = closeOverlayButton.getTop() + closeOverlayButton.getHeight() / 2; + private int distanceFromCloseButton(final MotionEvent popupMotionEvent) { + final int closeOverlayButtonX = closeOverlayButton.getLeft() + + closeOverlayButton.getWidth() / 2; + final int closeOverlayButtonY = closeOverlayButton.getTop() + + closeOverlayButton.getHeight() / 2; float fingerX = popupLayoutParams.x + popupMotionEvent.getX(); float fingerY = popupLayoutParams.y + popupMotionEvent.getY(); - return (int) Math.sqrt(Math.pow(closeOverlayButtonX - fingerX, 2) + Math.pow(closeOverlayButtonY - fingerY, 2)); + return (int) Math.sqrt(Math.pow(closeOverlayButtonX - fingerX, 2) + + Math.pow(closeOverlayButtonY - fingerY, 2)); } private float getClosingRadius() { @@ -1122,8 +1307,8 @@ public final class PopupVideoPlayer extends Service { return buttonRadius * 1.2f; } - private boolean isInsideClosingRadius(MotionEvent popupMotionEvent) { + private boolean isInsideClosingRadius(final MotionEvent popupMotionEvent) { return distanceFromCloseButton(popupMotionEvent) <= getClosingRadius(); } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayerActivity.java index f29c5a14c..7b41220ee 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayerActivity.java @@ -47,11 +47,14 @@ public final class PopupVideoPlayerActivity extends ServicePlayerActivity { } @Override - public boolean onPlayerOptionSelected(MenuItem item) { + public boolean onPlayerOptionSelected(final MenuItem item) { if (item.getItemId() == R.id.action_switch_background) { this.player.setRecovery(); getApplicationContext().sendBroadcast(getPlayerShutdownIntent()); - getApplicationContext().startService(getSwitchIntent(MainPlayer.class, MainPlayer.PlayerType.AUDIO)); + getApplicationContext().startService( + getSwitchIntent(MainPlayer.class, MainPlayer.PlayerType.AUDIO) + .putExtra(BasePlayer.START_PAUSED, !this.player.isPlaying()) + ); return true; } return false; diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java index 9b5838a40..f62f815be 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -6,11 +6,6 @@ import android.content.ServiceConnection; import android.os.Bundle; import android.os.IBinder; import android.provider.Settings; -import androidx.appcompat.app.AppCompatActivity; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import androidx.appcompat.widget.Toolbar; -import androidx.recyclerview.widget.ItemTouchHelper; import android.util.Log; import android.view.Menu; import android.view.MenuItem; @@ -23,6 +18,12 @@ import android.widget.ProgressBar; import android.widget.SeekBar; import android.widget.TextView; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; @@ -31,7 +32,6 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; -import org.schabi.newpipe.fragments.detail.VideoDetailFragment; import org.schabi.newpipe.local.dialog.PlaylistAppendDialog; import org.schabi.newpipe.player.event.PlayerEventListener; import org.schabi.newpipe.player.helper.PlaybackParameterDialog; @@ -44,28 +44,27 @@ import org.schabi.newpipe.util.ThemeHelper; import java.util.Collections; import java.util.List; -import static org.schabi.newpipe.player.helper.PlayerHelper.formatPitch; import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed; +import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; public abstract class ServicePlayerActivity extends AppCompatActivity implements PlayerEventListener, SeekBar.OnSeekBarChangeListener, View.OnClickListener, PlaybackParameterDialog.Callback { + private static final int RECYCLER_ITEM_POPUP_MENU_GROUP_ID = 47; + private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80; + + protected BasePlayer player; private boolean serviceBound; private ServiceConnection serviceConnection; - protected BasePlayer player; - private boolean seeking; private boolean redraw; + //////////////////////////////////////////////////////////////////////////// // Views //////////////////////////////////////////////////////////////////////////// - private static final int RECYCLER_ITEM_POPUP_MENU_GROUP_ID = 47; - - private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80; - private View rootView; private RecyclerView itemsList; @@ -83,13 +82,14 @@ public abstract class ServicePlayerActivity extends AppCompatActivity private ImageButton repeatButton; private ImageButton backwardButton; + private ImageButton fastRewindButton; private ImageButton playPauseButton; + private ImageButton fastForwardButton; private ImageButton forwardButton; private ImageButton shuffleButton; private ProgressBar progressBar; - private TextView playbackSpeedButton; - private TextView playbackPitchButton; + private Menu menu; //////////////////////////////////////////////////////////////////////////// // Abstracts @@ -115,7 +115,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity //////////////////////////////////////////////////////////////////////////// @Override - protected void onCreate(Bundle savedInstanceState) { + protected void onCreate(final Bundle savedInstanceState) { + assureCorrectAppLanguage(this); super.onCreate(savedInstanceState); ThemeHelper.setTheme(this); setContentView(R.layout.activity_player_queue_control); @@ -142,9 +143,11 @@ public abstract class ServicePlayerActivity extends AppCompatActivity } @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.menu_play_queue, menu); - getMenuInflater().inflate(getPlayerOptionMenuResource(), menu); + public boolean onCreateOptionsMenu(final Menu m) { + this.menu = m; + getMenuInflater().inflate(R.menu.menu_play_queue, m); + getMenuInflater().inflate(getPlayerOptionMenuResource(), m); + onMaybeMuteChanged(); return true; } @@ -156,24 +159,31 @@ public abstract class ServicePlayerActivity extends AppCompatActivity } @Override - public boolean onOptionsItemSelected(MenuItem item) { + public boolean onOptionsItemSelected(final MenuItem item) { switch (item.getItemId()) { case android.R.id.home: finish(); return true; + case R.id.action_settings: + NavigationHelper.openSettings(this); + return true; case R.id.action_append_playlist: appendAllToPlaylist(); return true; - case R.id.action_settings: - NavigationHelper.openSettings(this); - redraw = true; + case R.id.action_playback_speed: + openPlaybackParameterDialog(); + return true; + case R.id.action_mute: + player.onMuteUnmuteButtonClicked(); return true; case R.id.action_system_audio: startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS)); return true; case R.id.action_switch_main: this.player.setRecovery(); - getApplicationContext().startActivity(getSwitchIntent(MainActivity.class, MainPlayer.PlayerType.VIDEO)); + getApplicationContext().startActivity( + getSwitchIntent(MainActivity.class, MainPlayer.PlayerType.VIDEO) + .putExtra(BasePlayer.START_PAUSED, !this.player.isPlaying())); return true; } return onPlayerOptionSelected(item) || super.onOptionsItemSelected(item); @@ -185,25 +195,21 @@ public abstract class ServicePlayerActivity extends AppCompatActivity unbind(); } - Intent getSwitchIntent(final Class clazz, final MainPlayer.PlayerType playerType) { - final Intent intent = NavigationHelper.getPlayerIntent( - getApplicationContext(), - clazz, - this.player.getPlayQueue(), - this.player.getRepeatMode(), - this.player.getPlaybackSpeed(), - this.player.getPlaybackPitch(), + protected Intent getSwitchIntent(final Class clazz, final MainPlayer.PlayerType playerType) { + return NavigationHelper.getPlayerIntent(getApplicationContext(), clazz, + this.player.getPlayQueue(), this.player.getRepeatMode(), + this.player.getPlaybackSpeed(), this.player.getPlaybackPitch(), this.player.getPlaybackSkipSilence(), null, - true); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.putExtra(Constants.KEY_LINK_TYPE, StreamingService.LinkType.STREAM); - intent.putExtra(Constants.KEY_URL, this.player.getVideoUrl()); - intent.putExtra(Constants.KEY_TITLE, this.player.getVideoTitle()); - intent.putExtra(VideoDetailFragment.AUTO_PLAY, true); - intent.putExtra(Constants.KEY_SERVICE_ID, this.player.getCurrentMetadata().getMetadata().getServiceId()); - intent.putExtra(VideoPlayer.PLAYER_TYPE, playerType); - return intent; + true, + !this.player.isPlaying(), + this.player.isMuted()) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .putExtra(Constants.KEY_LINK_TYPE, StreamingService.LinkType.STREAM) + .putExtra(Constants.KEY_URL, this.player.getVideoUrl()) + .putExtra(Constants.KEY_TITLE, this.player.getVideoTitle()) + .putExtra(Constants.KEY_SERVICE_ID, this.player.getCurrentMetadata().getMetadata().getServiceId()) + .putExtra(VideoPlayer.PLAYER_TYPE, playerType); } //////////////////////////////////////////////////////////////////////////// @@ -219,7 +225,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity } private void unbind() { - if(serviceBound) { + if (serviceBound) { unbindService(serviceConnection); serviceBound = false; stopPlayerListener(); @@ -227,8 +233,12 @@ public abstract class ServicePlayerActivity extends AppCompatActivity if (player != null && player.getPlayQueueAdapter() != null) { player.getPlayQueueAdapter().unsetSelectedListener(); } - if (itemsList != null) itemsList.setAdapter(null); - if (itemTouchHelper != null) itemTouchHelper.attachToRecyclerView(null); + if (itemsList != null) { + itemsList.setAdapter(null); + } + if (itemTouchHelper != null) { + itemTouchHelper.attachToRecyclerView(null); + } itemsList = null; itemTouchHelper = null; @@ -239,12 +249,12 @@ public abstract class ServicePlayerActivity extends AppCompatActivity private ServiceConnection getServiceConnection() { return new ServiceConnection() { @Override - public void onServiceDisconnected(ComponentName name) { + public void onServiceDisconnected(final ComponentName name) { Log.d(getTag(), "Player service is disconnected"); } @Override - public void onServiceConnected(ComponentName name, IBinder service) { + public void onServiceConnected(final ComponentName name, final IBinder service) { Log.d(getTag(), "Player service is connected"); if (service instanceof PlayerServiceBinder) { @@ -253,8 +263,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity player = ((MainPlayer.LocalBinder) service).getPlayer(); } - if (player == null || player.getPlayQueue() == null || - player.getPlayQueueAdapter() == null || player.getPlayer() == null) { + if (player == null || player.getPlayQueue() == null + || player.getPlayQueueAdapter() == null || player.getPlayer() == null) { unbind(); finish(); } else { @@ -315,56 +325,60 @@ public abstract class ServicePlayerActivity extends AppCompatActivity private void buildControls() { repeatButton = rootView.findViewById(R.id.control_repeat); backwardButton = rootView.findViewById(R.id.control_backward); + fastRewindButton = rootView.findViewById(R.id.control_fast_rewind); playPauseButton = rootView.findViewById(R.id.control_play_pause); + fastForwardButton = rootView.findViewById(R.id.control_fast_forward); forwardButton = rootView.findViewById(R.id.control_forward); shuffleButton = rootView.findViewById(R.id.control_shuffle); - playbackSpeedButton = rootView.findViewById(R.id.control_playback_speed); - playbackPitchButton = rootView.findViewById(R.id.control_playback_pitch); progressBar = rootView.findViewById(R.id.control_progress_bar); repeatButton.setOnClickListener(this); backwardButton.setOnClickListener(this); + fastRewindButton.setOnClickListener(this); playPauseButton.setOnClickListener(this); + fastForwardButton.setOnClickListener(this); forwardButton.setOnClickListener(this); shuffleButton.setOnClickListener(this); - playbackSpeedButton.setOnClickListener(this); - playbackPitchButton.setOnClickListener(this); } private void buildItemPopupMenu(final PlayQueueItem item, final View view) { - final PopupMenu menu = new PopupMenu(this, view); - final MenuItem remove = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, /*pos=*/0, + final PopupMenu popupMenu = new PopupMenu(this, view); + final MenuItem remove = popupMenu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 0, Menu.NONE, R.string.play_queue_remove); remove.setOnMenuItemClickListener(menuItem -> { - if (player == null) return false; + if (player == null) { + return false; + } final int index = player.getPlayQueue().indexOf(item); - if (index != -1) player.getPlayQueue().remove(index); + if (index != -1) { + player.getPlayQueue().remove(index); + } return true; }); - final MenuItem detail = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, /*pos=*/1, + final MenuItem detail = popupMenu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 1, Menu.NONE, R.string.play_queue_stream_detail); detail.setOnMenuItemClickListener(menuItem -> { onOpenDetail(item.getServiceId(), item.getUrl(), item.getTitle()); return true; }); - final MenuItem append = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, /*pos=*/2, + final MenuItem append = popupMenu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 2, Menu.NONE, R.string.append_playlist); append.setOnMenuItemClickListener(menuItem -> { openPlaylistAppendDialog(Collections.singletonList(item)); return true; }); - final MenuItem share = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, /*pos=*/3, + final MenuItem share = popupMenu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 3, Menu.NONE, R.string.share); share.setOnMenuItemClickListener(menuItem -> { shareUrl(item.getTitle(), item.getUrl()); return true; }); - menu.show(); + popupMenu.show(); } //////////////////////////////////////////////////////////////////////////// @@ -374,8 +388,9 @@ public abstract class ServicePlayerActivity extends AppCompatActivity private OnScrollBelowItemsListener getQueueScrollListener() { return new OnScrollBelowItemsListener() { @Override - public void onScrolledDown(RecyclerView recyclerView) { - if (player != null && player.getPlayQueue() != null && !player.getPlayQueue().isComplete()) { + public void onScrolledDown(final RecyclerView recyclerView) { + if (player != null && player.getPlayQueue() != null + && !player.getPlayQueue().isComplete()) { player.getPlayQueue().fetch(); } else if (itemsList != null) { itemsList.clearOnScrollListeners(); @@ -387,13 +402,17 @@ public abstract class ServicePlayerActivity extends AppCompatActivity private ItemTouchHelper.SimpleCallback getItemTouchCallback() { return new PlayQueueItemTouchCallback() { @Override - public void onMove(int sourceIndex, int targetIndex) { - if (player != null) player.getPlayQueue().move(sourceIndex, targetIndex); + public void onMove(final int sourceIndex, final int targetIndex) { + if (player != null) { + player.getPlayQueue().move(sourceIndex, targetIndex); + } } @Override - public void onSwiped(int index) { - if (index != -1) player.getPlayQueue().remove(index); + public void onSwiped(final int index) { + if (index != -1) { + player.getPlayQueue().remove(index); + } } }; } @@ -401,31 +420,42 @@ public abstract class ServicePlayerActivity extends AppCompatActivity private PlayQueueItemBuilder.OnSelectedListener getOnSelectedListener() { return new PlayQueueItemBuilder.OnSelectedListener() { @Override - public void selected(PlayQueueItem item, View view) { - if (player != null) player.onSelected(item); + public void selected(final PlayQueueItem item, final View view) { + if (player != null) { + player.onSelected(item); + } } @Override - public void held(PlayQueueItem item, View view) { - if (player == null) return; + public void held(final PlayQueueItem item, final View view) { + if (player == null) { + return; + } final int index = player.getPlayQueue().indexOf(item); - if (index != -1) buildItemPopupMenu(item, view); + if (index != -1) { + buildItemPopupMenu(item, view); + } } @Override - public void onStartDrag(PlayQueueItemHolder viewHolder) { - if (itemTouchHelper != null) itemTouchHelper.startDrag(viewHolder); + public void onStartDrag(final PlayQueueItemHolder viewHolder) { + if (itemTouchHelper != null) { + itemTouchHelper.startDrag(viewHolder); + } } }; } - private void onOpenDetail(int serviceId, String videoUrl, String videoTitle) { + private void onOpenDetail(final int serviceId, final String videoUrl, + final String videoTitle) { NavigationHelper.openVideoDetail(this, serviceId, videoUrl, videoTitle); } private void scrollToSelected() { - if (player == null) return; + if (player == null) { + return; + } final int currentPlayingIndex = player.getPlayQueue().getIndex(); final int currentVisibleIndex; @@ -449,36 +479,29 @@ public abstract class ServicePlayerActivity extends AppCompatActivity //////////////////////////////////////////////////////////////////////////// @Override - public void onClick(View view) { - if (player == null) return; + public void onClick(final View view) { + if (player == null) { + return; + } if (view.getId() == repeatButton.getId()) { player.onRepeatClicked(); - } else if (view.getId() == backwardButton.getId()) { player.onPlayPrevious(); - + } else if (view.getId() == fastRewindButton.getId()) { + player.onFastRewind(); } else if (view.getId() == playPauseButton.getId()) { player.onPlayPause(); - + } else if (view.getId() == fastForwardButton.getId()) { + player.onFastForward(); } else if (view.getId() == forwardButton.getId()) { player.onPlayNext(); - } else if (view.getId() == shuffleButton.getId()) { player.onShuffleClicked(); - - } else if (view.getId() == playbackSpeedButton.getId()) { - openPlaybackParameterDialog(); - - } else if (view.getId() == playbackPitchButton.getId()) { - openPlaybackParameterDialog(); - } else if (view.getId() == metadata.getId()) { scrollToSelected(); - } else if (view.getId() == progressLiveSync.getId()) { player.seekToDefault(); - } } @@ -487,14 +510,16 @@ public abstract class ServicePlayerActivity extends AppCompatActivity //////////////////////////////////////////////////////////////////////////// private void openPlaybackParameterDialog() { - if (player == null) return; + if (player == null) { + return; + } PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), player.getPlaybackPitch(), player.getPlaybackSkipSilence(), this).show(getSupportFragmentManager(), getTag()); } @Override - public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch, - boolean playbackSkipSilence) { + public void onPlaybackParameterChanged(final float playbackTempo, final float playbackPitch, + final boolean playbackSkipSilence) { if (player != null) { player.setPlaybackParameters(playbackTempo, playbackPitch, playbackSkipSilence); } @@ -505,7 +530,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity //////////////////////////////////////////////////////////////////////////// @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + public void onProgressChanged(final SeekBar seekBar, final int progress, + final boolean fromUser) { if (fromUser) { final String seekTime = Localization.getDurationString(progress / 1000); progressCurrentTime.setText(seekTime); @@ -514,14 +540,16 @@ public abstract class ServicePlayerActivity extends AppCompatActivity } @Override - public void onStartTrackingTouch(SeekBar seekBar) { + public void onStartTrackingTouch(final SeekBar seekBar) { seeking = true; seekDisplay.setVisibility(View.VISIBLE); } @Override - public void onStopTrackingTouch(SeekBar seekBar) { - if (player != null) player.seekTo(seekBar.getProgress()); + public void onStopTrackingTouch(final SeekBar seekBar) { + if (player != null) { + player.seekTo(seekBar.getProgress()); + } seekDisplay.setVisibility(View.GONE); seeking = false; } @@ -545,7 +573,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity // Share //////////////////////////////////////////////////////////////////////////// - private void shareUrl(String subject, String url) { + private void shareUrl(final String subject, final String url) { Intent intent = new Intent(Intent.ACTION_SEND); intent.setType("text/plain"); intent.putExtra(Intent.EXTRA_SUBJECT, subject); @@ -562,17 +590,21 @@ public abstract class ServicePlayerActivity extends AppCompatActivity } @Override - public void onPlaybackUpdate(int state, int repeatMode, boolean shuffled, PlaybackParameters parameters) { + public void onPlaybackUpdate(final int state, final int repeatMode, final boolean shuffled, + final PlaybackParameters parameters) { onStateChanged(state); onPlayModeChanged(repeatMode, shuffled); onPlaybackParameterChanged(parameters); onMaybePlaybackAdapterChanged(); + onMaybeMuteChanged(); } @Override - public void onProgressUpdate(int currentProgress, int duration, int bufferPercent) { + public void onProgressUpdate(final int currentProgress, final int duration, + final int bufferPercent) { // Set buffer progress - progressSeekBar.setSecondaryProgress((int) (progressSeekBar.getMax() * ((float) bufferPercent / 100))); + progressSeekBar.setSecondaryProgress((int) (progressSeekBar.getMax() + * ((float) bufferPercent / 100))); // Set Duration progressSeekBar.setMax(duration); @@ -596,7 +628,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity } @Override - public void onMetadataUpdate(StreamInfo info, PlayQueue queue) { + public void onMetadataUpdate(final StreamInfo info, final PlayQueue queue) { if (info != null) { metadataTitle.setText(info.getName()); metadataArtist.setText(info.getUploaderName()); @@ -630,13 +662,13 @@ public abstract class ServicePlayerActivity extends AppCompatActivity private void onStateChanged(final int state) { switch (state) { case BasePlayer.STATE_PAUSED: - playPauseButton.setImageResource(R.drawable.ic_play_arrow_white); + playPauseButton.setImageResource(R.drawable.ic_play_arrow_white_24dp); break; case BasePlayer.STATE_PLAYING: - playPauseButton.setImageResource(R.drawable.ic_pause_white); + playPauseButton.setImageResource(R.drawable.ic_pause_white_24dp); break; case BasePlayer.STATE_COMPLETED: - playPauseButton.setImageResource(R.drawable.ic_replay_white); + playPauseButton.setImageResource(R.drawable.ic_replay_white_24dp); break; default: break; @@ -677,16 +709,38 @@ public abstract class ServicePlayerActivity extends AppCompatActivity private void onPlaybackParameterChanged(final PlaybackParameters parameters) { if (parameters != null) { - playbackSpeedButton.setText(formatSpeed(parameters.speed)); - playbackPitchButton.setText(formatPitch(parameters.pitch)); + if (menu != null && player != null) { + final MenuItem item = menu.findItem(R.id.action_playback_speed); + item.setTitle(formatSpeed(parameters.speed)); + } } } private void onMaybePlaybackAdapterChanged() { - if (itemsList == null || player == null) return; + if (itemsList == null || player == null) { + return; + } final PlayQueueAdapter maybeNewAdapter = player.getPlayQueueAdapter(); if (maybeNewAdapter != null && itemsList.getAdapter() != maybeNewAdapter) { itemsList.setAdapter(maybeNewAdapter); } } + + private void onMaybeMuteChanged() { + if (menu != null && player != null) { + MenuItem item = menu.findItem(R.id.action_mute); + + //Change the mute-button item in ActionBar + //1) Text change: + item.setTitle(player.isMuted() ? R.string.unmute : R.string.mute); + + //2) Icon change accordingly to current App Theme + // using rootView.getContext() because getApplicationContext() didn't work + item.setIcon(player.isMuted() + ? ThemeHelper.resolveResourceIdFromAttr(rootView.getContext(), + R.attr.ic_volume_off) + : ThemeHelper.resolveResourceIdFromAttr(rootView.getContext(), + R.attr.ic_volume_up)); + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java index 632044b06..1b00d872d 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -39,7 +39,7 @@ import android.widget.*; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; +import androidx.appcompat.content.res.AppCompatResources; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.PlaybackParameters; @@ -63,6 +63,7 @@ import org.schabi.newpipe.player.resolver.MediaSourceTag; import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.views.ExpandableSurfaceView; +import org.schabi.newpipe.views.ExpandableSurfaceView; import java.util.ArrayList; import java.util.List; @@ -72,7 +73,7 @@ import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; import static org.schabi.newpipe.util.AnimationUtils.animateView; /** - * Base for video players + * Base for video players. * * @author mauriciocolli */ @@ -84,23 +85,27 @@ public abstract class VideoPlayer extends BasePlayer Player.EventListener, PopupMenu.OnMenuItemClickListener, PopupMenu.OnDismissListener { - public static final boolean DEBUG = BasePlayer.DEBUG; public final String TAG; + public static final boolean DEBUG = BasePlayer.DEBUG; /*////////////////////////////////////////////////////////////////////////// // Player //////////////////////////////////////////////////////////////////////////*/ - protected static final int RENDERER_UNAVAILABLE = -1; public static final int DEFAULT_CONTROLS_DURATION = 300; // 300 millis public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds + public static final int DPAD_CONTROLS_HIDE_TIME = 7000; // 7 Seconds + + protected static final int RENDERER_UNAVAILABLE = -1; + + @NonNull + private final VideoPlaybackResolver resolver; private List availableStreams; private int selectedStreamIndex; protected boolean wasPlaying = false; - @NonNull final private VideoPlaybackResolver resolver; /*////////////////////////////////////////////////////////////////////////// // Views //////////////////////////////////////////////////////////////////////////*/ @@ -136,6 +141,7 @@ public abstract class VideoPlayer extends BasePlayer private final Handler controlsVisibilityHandler = new Handler(); boolean isSomePopupMenuVisible = false; + private final int qualityPopupMenuGroupId = 69; private PopupMenu qualityPopupMenu; @@ -147,49 +153,62 @@ public abstract class VideoPlayer extends BasePlayer /////////////////////////////////////////////////////////////////////////// - public VideoPlayer(String debugTag, Context context) { + public VideoPlayer(final String debugTag, final Context context) { super(context); this.TAG = debugTag; this.resolver = new VideoPlaybackResolver(context, dataSource, getQualityResolver()); } - public void setup(View rootView) { - initViews(rootView); + // workaround to match normalized captions like english to English or deutsch to Deutsch + private static boolean containsCaseInsensitive(final List list, final String toFind) { + for (String i : list) { + if (i.equalsIgnoreCase(toFind)) { + return true; + } + } + return false; + } + + public void setup(final View view) { + initViews(view); setup(); } - public void initViews(View rootView) { - this.rootView = rootView; - this.surfaceView = rootView.findViewById(R.id.surfaceView); - this.surfaceForeground = rootView.findViewById(R.id.surfaceForeground); - this.loadingPanel = rootView.findViewById(R.id.loading_panel); - this.endScreen = rootView.findViewById(R.id.endScreen); - this.controlAnimationView = rootView.findViewById(R.id.controlAnimationView); - this.controlsRoot = rootView.findViewById(R.id.playbackControlRoot); - this.currentDisplaySeek = rootView.findViewById(R.id.currentDisplaySeek); - this.playbackSeekBar = rootView.findViewById(R.id.playbackSeekBar); - this.playbackCurrentTime = rootView.findViewById(R.id.playbackCurrentTime); - this.playbackEndTime = rootView.findViewById(R.id.playbackEndTime); - this.playbackLiveSync = rootView.findViewById(R.id.playbackLiveSync); - this.playbackSpeedTextView = rootView.findViewById(R.id.playbackSpeed); - this.bottomControlsRoot = rootView.findViewById(R.id.bottomControls); - this.topControlsRoot = rootView.findViewById(R.id.topControls); - this.qualityTextView = rootView.findViewById(R.id.qualityTextView); + public void initViews(final View view) { + this.rootView = view; + this.surfaceView = view.findViewById(R.id.surfaceView); + this.surfaceForeground = view.findViewById(R.id.surfaceForeground); + this.loadingPanel = view.findViewById(R.id.loading_panel); + this.endScreen = view.findViewById(R.id.endScreen); + this.controlAnimationView = view.findViewById(R.id.controlAnimationView); + this.controlsRoot = view.findViewById(R.id.playbackControlRoot); + this.currentDisplaySeek = view.findViewById(R.id.currentDisplaySeek); + this.playbackSeekBar = view.findViewById(R.id.playbackSeekBar); + this.playbackCurrentTime = view.findViewById(R.id.playbackCurrentTime); + this.playbackEndTime = view.findViewById(R.id.playbackEndTime); + this.playbackLiveSync = view.findViewById(R.id.playbackLiveSync); + this.playbackSpeedTextView = view.findViewById(R.id.playbackSpeed); + this.bottomControlsRoot = view.findViewById(R.id.bottomControls); + this.topControlsRoot = view.findViewById(R.id.topControls); + this.qualityTextView = view.findViewById(R.id.qualityTextView); - this.subtitleView = rootView.findViewById(R.id.subtitleView); + this.subtitleView = view.findViewById(R.id.subtitleView); final float captionScale = PlayerHelper.getCaptionScale(context); final CaptionStyleCompat captionStyle = PlayerHelper.getCaptionStyle(context); setupSubtitleView(subtitleView, captionScale, captionStyle); - this.resizeView = rootView.findViewById(R.id.resizeTextView); - resizeView.setText(PlayerHelper.resizeTypeOf(context, getSurfaceView().getResizeMode())); + this.resizeView = view.findViewById(R.id.resizeTextView); + resizeView.setText(PlayerHelper + .resizeTypeOf(context, getSurfaceView().getResizeMode())); - this.captionTextView = rootView.findViewById(R.id.captionTextView); + this.captionTextView = view.findViewById(R.id.captionTextView); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN); - this.playbackSeekBar.getProgressDrawable().setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY); + } + this.playbackSeekBar.getProgressDrawable(). + setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY); this.qualityPopupMenu = new PopupMenu(context, qualityTextView); this.playbackSpeedPopupMenu = new PopupMenu(context, playbackSpeedTextView); @@ -199,9 +218,8 @@ public abstract class VideoPlayer extends BasePlayer .getIndeterminateDrawable().setColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY); } - protected abstract void setupSubtitleView(@NonNull SubtitleView view, - final float captionScale, - @NonNull final CaptionStyleCompat captionStyle); + protected abstract void setupSubtitleView(@NonNull SubtitleView view, float captionScale, + @NonNull CaptionStyleCompat captionStyle); @Override public void initListeners() { @@ -233,7 +251,9 @@ public abstract class VideoPlayer extends BasePlayer @Override public void handleIntent(final Intent intent) { - if (intent == null) return; + if (intent == null) { + return; + } if (intent.hasExtra(PLAYBACK_QUALITY)) { setPlaybackQuality(intent.getStringExtra(PLAYBACK_QUALITY)); @@ -247,13 +267,15 @@ public abstract class VideoPlayer extends BasePlayer //////////////////////////////////////////////////////////////////////////*/ public void buildQualityMenu() { - if (qualityPopupMenu == null) return; + if (qualityPopupMenu == null) { + return; + } qualityPopupMenu.getMenu().removeGroup(qualityPopupMenuGroupId); for (int i = 0; i < availableStreams.size(); i++) { VideoStream videoStream = availableStreams.get(i); - qualityPopupMenu.getMenu().add(qualityPopupMenuGroupId, i, Menu.NONE, - MediaFormat.getNameById(videoStream.getFormatId()) + " " + videoStream.resolution); + qualityPopupMenu.getMenu().add(qualityPopupMenuGroupId, i, Menu.NONE, MediaFormat + .getNameById(videoStream.getFormatId()) + " " + videoStream.resolution); } if (getSelectedVideoStream() != null) { qualityTextView.setText(getSelectedVideoStream().resolution); @@ -263,11 +285,14 @@ public abstract class VideoPlayer extends BasePlayer } private void buildPlaybackSpeedMenu() { - if (playbackSpeedPopupMenu == null) return; + if (playbackSpeedPopupMenu == null) { + return; + } playbackSpeedPopupMenu.getMenu().removeGroup(playbackSpeedPopupMenuGroupId); for (int i = 0; i < PLAYBACK_SPEEDS.length; i++) { - playbackSpeedPopupMenu.getMenu().add(playbackSpeedPopupMenuGroupId, i, Menu.NONE, formatSpeed(PLAYBACK_SPEEDS[i])); + playbackSpeedPopupMenu.getMenu().add(playbackSpeedPopupMenuGroupId, i, Menu.NONE, + formatSpeed(PLAYBACK_SPEEDS[i])); } playbackSpeedTextView.setText(formatSpeed(getPlaybackSpeed())); playbackSpeedPopupMenu.setOnMenuItemClickListener(this); @@ -275,7 +300,9 @@ public abstract class VideoPlayer extends BasePlayer } private void buildCaptionMenu(final List availableLanguages) { - if (captionPopupMenu == null) return; + if (captionPopupMenu == null) { + return; + } captionPopupMenu.getMenu().removeGroup(captionPopupMenuGroupId); String userPreferredLanguage = PreferenceManager.getDefaultSharedPreferences(context) @@ -286,8 +313,8 @@ public abstract class VideoPlayer extends BasePlayer * we are only looking for "(" instead of "(auto-generated)" to hopefully get all * internationalized variants such as "(automatisch-erzeugt)" and so on */ - boolean searchForAutogenerated = userPreferredLanguage != null && - !userPreferredLanguage.contains("("); + boolean searchForAutogenerated = userPreferredLanguage != null + && !userPreferredLanguage.contains("("); // Add option for turning off caption MenuItem captionOffItem = captionPopupMenu.getMenu().add(captionPopupMenuGroupId, @@ -314,18 +341,19 @@ public abstract class VideoPlayer extends BasePlayer trackSelector.setPreferredTextLanguage(captionLanguage); trackSelector.setParameters(trackSelector.buildUponParameters() .setRendererDisabled(textRendererIndex, false)); - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + final SharedPreferences prefs = PreferenceManager + .getDefaultSharedPreferences(context); prefs.edit().putString(context.getString(R.string.caption_user_set_key), captionLanguage).commit(); } return true; }); // apply caption language from previous user preference - if (userPreferredLanguage != null && (captionLanguage.equals(userPreferredLanguage) || - searchForAutogenerated && captionLanguage.startsWith(userPreferredLanguage) || - userPreferredLanguage.contains("(") && - captionLanguage.startsWith(userPreferredLanguage.substring(0, - userPreferredLanguage.indexOf('('))))) { + if (userPreferredLanguage != null && (captionLanguage.equals(userPreferredLanguage) + || searchForAutogenerated && captionLanguage.startsWith(userPreferredLanguage) + || userPreferredLanguage.contains("(") && captionLanguage.startsWith( + userPreferredLanguage + .substring(0, userPreferredLanguage.indexOf('('))))) { final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT); if (textRendererIndex != RENDERER_UNAVAILABLE) { trackSelector.setPreferredTextLanguage(captionLanguage); @@ -339,7 +367,9 @@ public abstract class VideoPlayer extends BasePlayer } private void updateStreamRelatedViews() { - if (getCurrentMetadata() == null) return; + if (getCurrentMetadata() == null) { + return; + } final MediaSourceTag tag = getCurrentMetadata(); final StreamInfo metadata = tag.getMetadata(); @@ -370,8 +400,10 @@ public abstract class VideoPlayer extends BasePlayer break; case VIDEO_STREAM: - if (metadata.getVideoStreams().size() + metadata.getVideoOnlyStreams().size() == 0) + if (metadata.getVideoStreams().size() + metadata.getVideoOnlyStreams().size() + == 0) { break; + } availableStreams = tag.getSortedAvailableVideoStreams(); selectedStreamIndex = tag.getSelectedVideoStreamIndex(); @@ -388,6 +420,7 @@ public abstract class VideoPlayer extends BasePlayer buildPlaybackSpeedMenu(); playbackSpeedTextView.setVisibility(View.VISIBLE); } + /*////////////////////////////////////////////////////////////////////////// // Playback Listener //////////////////////////////////////////////////////////////////////////*/ @@ -417,9 +450,11 @@ public abstract class VideoPlayer extends BasePlayer animateView(controlsRoot, false, DEFAULT_CONTROLS_DURATION); playbackSeekBar.setEnabled(false); - // Bug on lower api, disabling and enabling the seekBar resets the thumb color -.-, so sets the color again - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) + // Bug on lower api, disabling and enabling the seekBar resets the thumb color -.-, + // so sets the color again + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN); + } loadingPanel.setBackgroundColor(Color.BLACK); animateView(loadingPanel, true, 0); @@ -435,9 +470,11 @@ public abstract class VideoPlayer extends BasePlayer showAndAnimateControl(-1, true); playbackSeekBar.setEnabled(true); - // Bug on lower api, disabling and enabling the seekBar resets the thumb color -.-, so sets the color again - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) + // Bug on lower api, disabling and enabling the seekBar resets the thumb color -.-, + // so sets the color again + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN); + } loadingPanel.setVisibility(View.GONE); @@ -446,20 +483,26 @@ public abstract class VideoPlayer extends BasePlayer @Override public void onBuffering() { - if (DEBUG) Log.d(TAG, "onBuffering() called"); + if (DEBUG) { + Log.d(TAG, "onBuffering() called"); + } loadingPanel.setBackgroundColor(Color.TRANSPARENT); } @Override public void onPaused() { - if (DEBUG) Log.d(TAG, "onPaused() called"); + if (DEBUG) { + Log.d(TAG, "onPaused() called"); + } showControls(400); loadingPanel.setVisibility(View.GONE); } @Override public void onPausedSeek() { - if (DEBUG) Log.d(TAG, "onPausedSeek() called"); + if (DEBUG) { + Log.d(TAG, "onPausedSeek() called"); + } showAndAnimateControl(-1, true); } @@ -479,21 +522,28 @@ public abstract class VideoPlayer extends BasePlayer //////////////////////////////////////////////////////////////////////////*/ @Override - public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + public void onTracksChanged(final TrackGroupArray trackGroups, + final TrackSelectionArray trackSelections) { super.onTracksChanged(trackGroups, trackSelections); onTextTrackUpdate(); } @Override - public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + public void onPlaybackParametersChanged(final PlaybackParameters playbackParameters) { super.onPlaybackParametersChanged(playbackParameters); playbackSpeedTextView.setText(formatSpeed(playbackParameters.speed)); } @Override - public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { + public void onVideoSizeChanged(final int width, final int height, + final int unappliedRotationDegrees, + final float pixelWidthHeightRatio) { if (DEBUG) { - Log.d(TAG, "onVideoSizeChanged() called with: width / height = [" + width + " / " + height + " = " + (((float) width) / height) + "], unappliedRotationDegrees = [" + unappliedRotationDegrees + "], pixelWidthHeightRatio = [" + pixelWidthHeightRatio + "]"); + Log.d(TAG, "onVideoSizeChanged() called with: " + + "width / height = [" + width + " / " + height + + " = " + (((float) width) / height) + "], " + + "unappliedRotationDegrees = [" + unappliedRotationDegrees + "], " + + "pixelWidthHeightRatio = [" + pixelWidthHeightRatio + "]"); } getSurfaceView().setAspectRatio(((float) width) / height); } @@ -510,8 +560,11 @@ public abstract class VideoPlayer extends BasePlayer private void onTextTrackUpdate() { final int textRenderer = getRendererIndex(C.TRACK_TYPE_TEXT); - if (captionTextView == null) return; - if (trackSelector.getCurrentMappedTrackInfo() == null || textRenderer == RENDERER_UNAVAILABLE) { + if (captionTextView == null) { + return; + } + if (trackSelector.getCurrentMappedTrackInfo() == null + || textRenderer == RENDERER_UNAVAILABLE) { captionTextView.setVisibility(View.GONE); return; } @@ -532,8 +585,8 @@ public abstract class VideoPlayer extends BasePlayer final String preferredLanguage = trackSelector.getPreferredTextLanguage(); // Build UI buildCaptionMenu(availableLanguages); - if (trackSelector.getParameters().getRendererDisabled(textRenderer) || - preferredLanguage == null || (!availableLanguages.contains(preferredLanguage) + if (trackSelector.getParameters().getRendererDisabled(textRenderer) + || preferredLanguage == null || (!availableLanguages.contains(preferredLanguage) && !containsCaseInsensitive(availableLanguages, preferredLanguage))) { captionTextView.setText(R.string.caption_none); } else { @@ -542,22 +595,15 @@ public abstract class VideoPlayer extends BasePlayer captionTextView.setVisibility(availableLanguages.isEmpty() ? View.GONE : View.VISIBLE); } - // workaround to match normalized captions like english to English or deutsch to Deutsch - private static boolean containsCaseInsensitive(List list, String toFind) { - for(String i : list){ - if(i.equalsIgnoreCase(toFind)) - return true; - } - return false; - } - /*////////////////////////////////////////////////////////////////////////// // General Player //////////////////////////////////////////////////////////////////////////*/ @Override - public void onPrepared(boolean playWhenReady) { - if (DEBUG) Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]"); + public void onPrepared(final boolean playWhenReady) { + if (DEBUG) { + Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]"); + } playbackSeekBar.setMax((int) simpleExoPlayer.getDuration()); playbackEndTime.setText(getTimeString((int) simpleExoPlayer.getDuration())); @@ -569,34 +615,48 @@ public abstract class VideoPlayer extends BasePlayer @Override public void destroy() { super.destroy(); - if (endScreen != null) endScreen.setImageBitmap(null); + if (endScreen != null) { + endScreen.setImageBitmap(null); + } } @Override - public void onUpdateProgress(int currentProgress, int duration, int bufferPercent) { - if (!isPrepared()) return; + public void onUpdateProgress(final int currentProgress, final int duration, + final int bufferPercent) { + if (!isPrepared()) { + return; + } if (duration != playbackSeekBar.getMax()) { playbackEndTime.setText(getTimeString(duration)); playbackSeekBar.setMax(duration); } if (currentState != STATE_PAUSED) { - if (currentState != STATE_PAUSED_SEEK) playbackSeekBar.setProgress(currentProgress); + if (currentState != STATE_PAUSED_SEEK) { + playbackSeekBar.setProgress(currentProgress); + } playbackCurrentTime.setText(getTimeString(currentProgress)); } if (simpleExoPlayer.isLoading() || bufferPercent > 90) { - playbackSeekBar.setSecondaryProgress((int) (playbackSeekBar.getMax() * ((float) bufferPercent / 100))); + playbackSeekBar.setSecondaryProgress( + (int) (playbackSeekBar.getMax() * ((float) bufferPercent / 100))); } if (DEBUG && bufferPercent % 20 == 0) { //Limit log - Log.d(TAG, "updateProgress() called with: isVisible = " + isControlsVisible() + ", currentProgress = [" + currentProgress + "], duration = [" + duration + "], bufferPercent = [" + bufferPercent + "]"); + Log.d(TAG, "updateProgress() called with: " + + "isVisible = " + isControlsVisible() + ", " + + "currentProgress = [" + currentProgress + "], " + + "duration = [" + duration + "], bufferPercent = [" + bufferPercent + "]"); } playbackLiveSync.setClickable(!isLiveEdge()); } @Override - public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { + public void onLoadingComplete(final String imageUri, final View view, + final Bitmap loadedImage) { super.onLoadingComplete(imageUri, view, loadedImage); - if (loadedImage != null) endScreen.setImageBitmap(loadedImage); + if (loadedImage != null) { + endScreen.setImageBitmap(loadedImage); + } } protected void toggleFullscreen() { @@ -606,13 +666,13 @@ public abstract class VideoPlayer extends BasePlayer @Override public void onFastRewind() { super.onFastRewind(); - showAndAnimateControl(R.drawable.ic_action_av_fast_rewind, true); + showAndAnimateControl(R.drawable.ic_fast_rewind_white_24dp, true); } @Override public void onFastForward() { super.onFastForward(); - showAndAnimateControl(R.drawable.ic_action_av_fast_forward, true); + showAndAnimateControl(R.drawable.ic_fast_forward_white_24dp, true); } /*////////////////////////////////////////////////////////////////////////// @@ -620,8 +680,10 @@ public abstract class VideoPlayer extends BasePlayer //////////////////////////////////////////////////////////////////////////*/ @Override - public void onClick(View v) { - if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]"); + public void onClick(final View v) { + if (DEBUG) { + Log.d(TAG, "onClick() called with: v = [" + v + "]"); + } if (v.getId() == qualityTextView.getId()) { onQualitySelectorClicked(); } else if (v.getId() == playbackSpeedTextView.getId()) { @@ -636,17 +698,22 @@ public abstract class VideoPlayer extends BasePlayer } /** - * Called when an item of the quality selector or the playback speed selector is selected + * Called when an item of the quality selector or the playback speed selector is selected. */ @Override - public boolean onMenuItemClick(MenuItem menuItem) { - if (DEBUG) - Log.d(TAG, "onMenuItemClick() called with: menuItem = [" + menuItem + "], menuItem.getItemId = [" + menuItem.getItemId() + "]"); + public boolean onMenuItemClick(final MenuItem menuItem) { + if (DEBUG) { + Log.d(TAG, "onMenuItemClick() called with: " + + "menuItem = [" + menuItem + "], " + + "menuItem.getItemId = [" + menuItem.getItemId() + "]"); + } if (qualityPopupMenuGroupId == menuItem.getGroupId()) { final int menuItemIndex = menuItem.getItemId(); - if (selectedStreamIndex == menuItemIndex || - availableStreams == null || availableStreams.size() <= menuItemIndex) return true; + if (selectedStreamIndex == menuItemIndex || availableStreams == null + || availableStreams.size() <= menuItemIndex) { + return true; + } final String newResolution = availableStreams.get(menuItemIndex).resolution; setRecovery(); @@ -667,11 +734,13 @@ public abstract class VideoPlayer extends BasePlayer } /** - * Called when some popup menu is dismissed + * Called when some popup menu is dismissed. */ @Override - public void onDismiss(PopupMenu menu) { - if (DEBUG) Log.d(TAG, "onDismiss() called with: menu = [" + menu + "]"); + public void onDismiss(final PopupMenu menu) { + if (DEBUG) { + Log.d(TAG, "onDismiss() called with: menu = [" + menu + "]"); + } isSomePopupMenuVisible = false; if (getSelectedVideoStream() != null) { qualityTextView.setText(getSelectedVideoStream().resolution); @@ -679,7 +748,9 @@ public abstract class VideoPlayer extends BasePlayer } public void onQualitySelectorClicked() { - if (DEBUG) Log.d(TAG, "onQualitySelectorClicked() called"); + if (DEBUG) { + Log.d(TAG, "onQualitySelectorClicked() called"); + } qualityPopupMenu.show(); isSomePopupMenuVisible = true; showControls(DEFAULT_CONTROLS_DURATION); @@ -695,14 +766,18 @@ public abstract class VideoPlayer extends BasePlayer } public void onPlaybackSpeedClicked() { - if (DEBUG) Log.d(TAG, "onPlaybackSpeedClicked() called"); + if (DEBUG) { + Log.d(TAG, "onPlaybackSpeedClicked() called"); + } playbackSpeedPopupMenu.show(); isSomePopupMenuVisible = true; showControls(DEFAULT_CONTROLS_DURATION); } private void onCaptionClicked() { - if (DEBUG) Log.d(TAG, "onCaptionClicked() called"); + if (DEBUG) { + Log.d(TAG, "onCaptionClicked() called"); + } captionPopupMenu.show(); isSomePopupMenuVisible = true; showControls(DEFAULT_CONTROLS_DURATION); @@ -721,26 +796,38 @@ public abstract class VideoPlayer extends BasePlayer getResizeView().setText(PlayerHelper.resizeTypeOf(context, resizeMode)); } - protected abstract int nextResizeMode(@AspectRatioFrameLayout.ResizeMode final int resizeMode); + protected abstract int nextResizeMode(@AspectRatioFrameLayout.ResizeMode int resizeMode); /*////////////////////////////////////////////////////////////////////////// // SeekBar Listener //////////////////////////////////////////////////////////////////////////*/ @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - if (DEBUG && fromUser) Log.d(TAG, "onProgressChanged() called with: seekBar = [" + seekBar + "], progress = [" + progress + "]"); + public void onProgressChanged(final SeekBar seekBar, final int progress, + final boolean fromUser) { + if (DEBUG && fromUser) { + Log.d(TAG, "onProgressChanged() called with: " + + "seekBar = [" + seekBar + "], progress = [" + progress + "]"); + } //if (fromUser) playbackCurrentTime.setText(getTimeString(progress)); - if (fromUser) currentDisplaySeek.setText(getTimeString(progress)); + if (fromUser) { + currentDisplaySeek.setText(getTimeString(progress)); + } } @Override - public void onStartTrackingTouch(SeekBar seekBar) { - if (DEBUG) Log.d(TAG, "onStartTrackingTouch() called with: seekBar = [" + seekBar + "]"); - if (getCurrentState() != STATE_PAUSED_SEEK) changeState(STATE_PAUSED_SEEK); + public void onStartTrackingTouch(final SeekBar seekBar) { + if (DEBUG) { + Log.d(TAG, "onStartTrackingTouch() called with: seekBar = [" + seekBar + "]"); + } + if (getCurrentState() != STATE_PAUSED_SEEK) { + changeState(STATE_PAUSED_SEEK); + } wasPlaying = simpleExoPlayer.getPlayWhenReady(); - if (isPlaying()) simpleExoPlayer.setPlayWhenReady(false); + if (isPlaying()) { + simpleExoPlayer.setPlayWhenReady(false); + } showControls(0); animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, true, @@ -748,17 +835,25 @@ public abstract class VideoPlayer extends BasePlayer } @Override - public void onStopTrackingTouch(SeekBar seekBar) { - if (DEBUG) Log.d(TAG, "onStopTrackingTouch() called with: seekBar = [" + seekBar + "]"); + public void onStopTrackingTouch(final SeekBar seekBar) { + if (DEBUG) { + Log.d(TAG, "onStopTrackingTouch() called with: seekBar = [" + seekBar + "]"); + } seekTo(seekBar.getProgress()); - if (wasPlaying || simpleExoPlayer.getDuration() == seekBar.getProgress()) simpleExoPlayer.setPlayWhenReady(true); + if (wasPlaying || simpleExoPlayer.getDuration() == seekBar.getProgress()) { + simpleExoPlayer.setPlayWhenReady(true); + } playbackCurrentTime.setText(getTimeString(seekBar.getProgress())); animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, 200); - if (getCurrentState() == STATE_PAUSED_SEEK) changeState(STATE_BUFFERING); - if (!isProgressLoopRunning()) startProgressLoop(); + if (getCurrentState() == STATE_PAUSED_SEEK) { + changeState(STATE_BUFFERING); + } + if (!isProgressLoopRunning()) { + startProgressLoop(); + } } /*////////////////////////////////////////////////////////////////////////// @@ -766,7 +861,9 @@ public abstract class VideoPlayer extends BasePlayer //////////////////////////////////////////////////////////////////////////*/ public int getRendererIndex(final int trackIndex) { - if (simpleExoPlayer == null) return RENDERER_UNAVAILABLE; + if (simpleExoPlayer == null) { + return RENDERER_UNAVAILABLE; + } for (int t = 0; t < simpleExoPlayer.getRendererCount(); t++) { if (simpleExoPlayer.getRendererType(t) == trackIndex) { @@ -782,15 +879,21 @@ public abstract class VideoPlayer extends BasePlayer } /** - * Show a animation, and depending on goneOnEnd, will stay on the screen or be gone + * Show a animation, and depending on goneOnEnd, will stay on the screen or be gone. * - * @param drawableId the drawable that will be used to animate, pass -1 to clear any animation that is visible + * @param drawableId the drawable that will be used to animate, + * pass -1 to clear any animation that is visible * @param goneOnEnd will set the animation view to GONE on the end of the animation */ public void showAndAnimateControl(final int drawableId, final boolean goneOnEnd) { - if (DEBUG) Log.d(TAG, "showAndAnimateControl() called with: drawableId = [" + drawableId + "], goneOnEnd = [" + goneOnEnd + "]"); + if (DEBUG) { + Log.d(TAG, "showAndAnimateControl() called with: " + + "drawableId = [" + drawableId + "], goneOnEnd = [" + goneOnEnd + "]"); + } if (controlViewAnimator != null && controlViewAnimator.isRunning()) { - if (DEBUG) Log.d(TAG, "showAndAnimateControl: controlViewAnimator.isRunning"); + if (DEBUG) { + Log.d(TAG, "showAndAnimateControl: controlViewAnimator.isRunning"); + } controlViewAnimator.end(); } @@ -803,7 +906,7 @@ public abstract class VideoPlayer extends BasePlayer ).setDuration(DEFAULT_CONTROLS_DURATION); controlViewAnimator.addListener(new AnimatorListenerAdapter() { @Override - public void onAnimationEnd(Animator animation) { + public void onAnimationEnd(final Animator animation) { controlAnimationView.setVisibility(View.GONE); } }); @@ -812,8 +915,10 @@ public abstract class VideoPlayer extends BasePlayer return; } - float scaleFrom = goneOnEnd ? 1.0f : 1.0f, scaleTo = goneOnEnd ? 1.8f : 1.4f; - float alphaFrom = goneOnEnd ? 1.0f : 0.0f, alphaTo = goneOnEnd ? 0.0f : 1.0f; + float scaleFrom = goneOnEnd ? 1f : 1f; + float scaleTo = goneOnEnd ? 1.8f : 1.4f; + float alphaFrom = goneOnEnd ? 1f : 0f; + float alphaTo = goneOnEnd ? 0f : 1f; controlViewAnimator = ObjectAnimator.ofPropertyValuesHolder(controlAnimationView, @@ -824,15 +929,18 @@ public abstract class VideoPlayer extends BasePlayer controlViewAnimator.setDuration(goneOnEnd ? 1000 : 500); controlViewAnimator.addListener(new AnimatorListenerAdapter() { @Override - public void onAnimationEnd(Animator animation) { - if (goneOnEnd) controlAnimationView.setVisibility(View.GONE); - else controlAnimationView.setVisibility(View.VISIBLE); + public void onAnimationEnd(final Animator animation) { + if (goneOnEnd) { + controlAnimationView.setVisibility(View.GONE); + } else { + controlAnimationView.setVisibility(View.VISIBLE); + } } }); controlAnimationView.setVisibility(View.VISIBLE); - controlAnimationView.setImageDrawable(ContextCompat.getDrawable(context, drawableId)); + controlAnimationView.setImageDrawable(AppCompatResources.getDrawable(context, drawableId)); controlViewAnimator.start(); } @@ -841,35 +949,59 @@ public abstract class VideoPlayer extends BasePlayer } public void showControlsThenHide() { - if (DEBUG) Log.d(TAG, "showControlsThenHide() called"); + if (DEBUG) { + Log.d(TAG, "showControlsThenHide() called"); + } + + final int hideTime = controlsRoot.isInTouchMode() + ? DEFAULT_CONTROLS_HIDE_TIME + : DPAD_CONTROLS_HIDE_TIME; + animateView(controlsRoot, true, DEFAULT_CONTROLS_DURATION, 0, - () -> hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME)); + () -> hideControls(DEFAULT_CONTROLS_DURATION, hideTime)); } - public void showControls(long duration) { - if (DEBUG) Log.d(TAG, "showControls() called"); + public void showControls(final long duration) { + if (DEBUG) { + Log.d(TAG, "showControls() called"); + } controlsVisibilityHandler.removeCallbacksAndMessages(null); animateView(controlsRoot, true, duration); } - public void hideControls(final long duration, long delay) { - if (DEBUG) Log.d(TAG, "hideControls() called with: delay = [" + delay + "]"); - controlsVisibilityHandler.removeCallbacksAndMessages(null); - controlsVisibilityHandler.postDelayed( - () -> animateView(controlsRoot, false, duration), delay); + public void safeHideControls(final long duration, final long delay) { + if (DEBUG) { + Log.d(TAG, "safeHideControls() called with: delay = [" + delay + "]"); + } + if (rootView.isInTouchMode()) { + controlsVisibilityHandler.removeCallbacksAndMessages(null); + controlsVisibilityHandler.postDelayed( + () -> animateView(controlsRoot, false, duration), delay); + } } - public void hideControlsAndButton(final long duration, long delay, View button) { - if (DEBUG) Log.d(TAG, "hideControls() called with: delay = [" + delay + "]"); + public void hideControls(final long duration, final long delay) { + if (DEBUG) { + Log.d(TAG, "hideControls() called with: delay = [" + delay + "]"); + } controlsVisibilityHandler.removeCallbacksAndMessages(null); - controlsVisibilityHandler.postDelayed(hideControlsAndButtonHandler(duration, button), delay); + controlsVisibilityHandler.postDelayed(() -> + animateView(controlsRoot, false, duration), delay); } - private Runnable hideControlsAndButtonHandler(long duration, View videoPlayPause) - { + public void hideControlsAndButton(final long duration, final long delay, final View button) { + if (DEBUG) { + Log.d(TAG, "hideControls() called with: delay = [" + delay + "]"); + } + controlsVisibilityHandler.removeCallbacksAndMessages(null); + controlsVisibilityHandler + .postDelayed(hideControlsAndButtonHandler(duration, button), delay); + } + + private Runnable hideControlsAndButtonHandler(final long duration, final View videoPlayPause) { return () -> { videoPlayPause.setVisibility(View.INVISIBLE); - animateView(controlsRoot, false,duration); + animateView(controlsRoot, false, duration); }; } @@ -879,15 +1011,15 @@ public abstract class VideoPlayer extends BasePlayer // Getters and Setters //////////////////////////////////////////////////////////////////////////*/ - public void setPlaybackQuality(final String quality) { - this.resolver.setPlaybackQuality(quality); - } - @Nullable public String getPlaybackQuality() { return resolver.getPlaybackQuality(); } + public void setPlaybackQuality(final String quality) { + this.resolver.setPlaybackQuality(quality); + } + public ExpandableSurfaceView getSurfaceView() { return surfaceView; } @@ -898,9 +1030,9 @@ public abstract class VideoPlayer extends BasePlayer @Nullable public VideoStream getSelectedVideoStream() { - return (selectedStreamIndex >= 0 && availableStreams != null && - availableStreams.size() > selectedStreamIndex) ? - availableStreams.get(selectedStreamIndex) : null; + return (selectedStreamIndex >= 0 && availableStreams != null + && availableStreams.size() > selectedStreamIndex) + ? availableStreams.get(selectedStreamIndex) : null; } public Handler getControlsVisibilityHandler() { @@ -911,7 +1043,7 @@ public abstract class VideoPlayer extends BasePlayer return rootView; } - public void setRootView(View rootView) { + public void setRootView(final View rootView) { this.rootView = rootView; } diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java index 6c1843763..1b72d7a96 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java @@ -42,6 +42,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.content.res.AppCompatResources; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.RecyclerView; import com.google.android.exoplayer2.ExoPlaybackException; @@ -84,6 +85,7 @@ import static org.schabi.newpipe.util.AnimationUtils.animateRotation; import static org.schabi.newpipe.util.AnimationUtils.animateView; import static org.schabi.newpipe.util.ListHelper.getPopupResolutionIndex; import static org.schabi.newpipe.util.ListHelper.getResolutionIndex; +import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; /** * Unified UI for all players @@ -97,7 +99,17 @@ public class VideoPlayerImpl extends VideoPlayer View.OnLongClickListener { private static final String TAG = ".VideoPlayerImpl"; + static final String POPUP_SAVED_WIDTH = "popup_saved_width"; + static final String POPUP_SAVED_X = "popup_saved_x"; + static final String POPUP_SAVED_Y = "popup_saved_y"; + private static final int MINIMUM_SHOW_EXTRA_WIDTH_DP = 300; + private static final int IDLE_WINDOW_FLAGS = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | + WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; + private static final int ONGOING_PLAYBACK_WINDOW_FLAGS = IDLE_WINDOW_FLAGS | + WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; + private static final float MAX_GESTURE_LENGTH = 0.75f; + private static final int NOTIFICATION_UPDATES_BEFORE_RESET = 60; private TextView titleTextView; private TextView channelTextView; @@ -116,6 +128,7 @@ public class VideoPlayerImpl extends VideoPlayer private ImageButton fullscreenButton; private ImageButton playerCloseButton; private ImageButton screenRotationButton; + private ImageButton muteButton; private ImageButton playPauseButton; private ImageButton playPreviousButton; @@ -141,6 +154,7 @@ public class VideoPlayerImpl extends VideoPlayer private boolean isFullscreen = false; private boolean isVerticalVideo = false; boolean shouldUpdateOnProgress; + int timesNotificationUpdated; private final MainPlayer service; private PlayerServiceEventListener fragmentListener; @@ -164,15 +178,6 @@ public class VideoPlayerImpl extends VideoPlayer public boolean isPopupClosing = false; - static final String POPUP_SAVED_WIDTH = "popup_saved_width"; - static final String POPUP_SAVED_X = "popup_saved_x"; - static final String POPUP_SAVED_Y = "popup_saved_y"; - private static final int MINIMUM_SHOW_EXTRA_WIDTH_DP = 300; - private static final int IDLE_WINDOW_FLAGS = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | - WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; - private static final int ONGOING_PLAYBACK_WINDOW_FLAGS = IDLE_WINDOW_FLAGS | - WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; - private float screenWidth, screenHeight; private float popupWidth, popupHeight; private float minimumWidth, minimumHeight; @@ -226,40 +231,41 @@ public class VideoPlayerImpl extends VideoPlayer @SuppressLint("ClickableViewAccessibility") @Override - public void initViews(View rootView) { - super.initViews(rootView); - this.titleTextView = rootView.findViewById(R.id.titleTextView); - this.channelTextView = rootView.findViewById(R.id.channelTextView); - this.volumeRelativeLayout = rootView.findViewById(R.id.volumeRelativeLayout); - this.volumeProgressBar = rootView.findViewById(R.id.volumeProgressBar); - this.volumeImageView = rootView.findViewById(R.id.volumeImageView); - this.brightnessRelativeLayout = rootView.findViewById(R.id.brightnessRelativeLayout); - this.brightnessProgressBar = rootView.findViewById(R.id.brightnessProgressBar); - this.brightnessImageView = rootView.findViewById(R.id.brightnessImageView); - this.resizingIndicator = rootView.findViewById(R.id.resizing_indicator); - this.queueButton = rootView.findViewById(R.id.queueButton); - this.repeatButton = rootView.findViewById(R.id.repeatButton); - this.shuffleButton = rootView.findViewById(R.id.shuffleButton); - this.playWithKodi = rootView.findViewById(R.id.playWithKodi); - this.openInBrowser = rootView.findViewById(R.id.openInBrowser); - this.fullscreenButton = rootView.findViewById(R.id.fullScreenButton); - this.screenRotationButton = rootView.findViewById(R.id.screenRotationButton); - this.playerCloseButton = rootView.findViewById(R.id.playerCloseButton); + public void initViews(View view) { + super.initViews(view); + this.titleTextView = view.findViewById(R.id.titleTextView); + this.channelTextView = view.findViewById(R.id.channelTextView); + this.volumeRelativeLayout = view.findViewById(R.id.volumeRelativeLayout); + this.volumeProgressBar = view.findViewById(R.id.volumeProgressBar); + this.volumeImageView = view.findViewById(R.id.volumeImageView); + this.brightnessRelativeLayout = view.findViewById(R.id.brightnessRelativeLayout); + this.brightnessProgressBar = view.findViewById(R.id.brightnessProgressBar); + this.brightnessImageView = view.findViewById(R.id.brightnessImageView); + this.resizingIndicator = view.findViewById(R.id.resizing_indicator); + this.queueButton = view.findViewById(R.id.queueButton); + this.repeatButton = view.findViewById(R.id.repeatButton); + this.shuffleButton = view.findViewById(R.id.shuffleButton); + this.playWithKodi = view.findViewById(R.id.playWithKodi); + this.openInBrowser = view.findViewById(R.id.openInBrowser); + this.fullscreenButton = view.findViewById(R.id.fullScreenButton); + this.screenRotationButton = view.findViewById(R.id.screenRotationButton); + this.playerCloseButton = view.findViewById(R.id.playerCloseButton); + this.muteButton = view.findViewById(R.id.switchMute); - this.playPauseButton = rootView.findViewById(R.id.playPauseButton); - this.playPreviousButton = rootView.findViewById(R.id.playPreviousButton); - this.playNextButton = rootView.findViewById(R.id.playNextButton); + this.playPauseButton = view.findViewById(R.id.playPauseButton); + this.playPreviousButton = view.findViewById(R.id.playPreviousButton); + this.playNextButton = view.findViewById(R.id.playNextButton); - this.moreOptionsButton = rootView.findViewById(R.id.moreOptionsButton); - this.primaryControls = rootView.findViewById(R.id.primaryControls); - this.secondaryControls = rootView.findViewById(R.id.secondaryControls); - this.shareButton = rootView.findViewById(R.id.share); + this.moreOptionsButton = view.findViewById(R.id.moreOptionsButton); + this.primaryControls = view.findViewById(R.id.primaryControls); + this.secondaryControls = view.findViewById(R.id.secondaryControls); + this.shareButton = view.findViewById(R.id.share); - this.queueLayout = rootView.findViewById(R.id.playQueuePanel); - this.itemsListCloseButton = rootView.findViewById(R.id.playQueueClose); - this.itemsList = rootView.findViewById(R.id.playQueue); + this.queueLayout = view.findViewById(R.id.playQueuePanel); + this.itemsListCloseButton = view.findViewById(R.id.playQueueClose); + this.itemsList = view.findViewById(R.id.playQueue); - closingOverlayView = rootView.findViewById(R.id.closingOverlay); + closingOverlayView = view.findViewById(R.id.closingOverlay); titleTextView.setSelected(true); channelTextView.setSelected(true); @@ -306,6 +312,7 @@ public class VideoPlayerImpl extends VideoPlayer shareButton.setVisibility(View.GONE); playWithKodi.setVisibility(View.GONE); openInBrowser.setVisibility(View.GONE); + muteButton.setVisibility(View.GONE); playerCloseButton.setVisibility(View.GONE); getTopControlsRoot().bringToFront(); getTopControlsRoot().setClickable(false); @@ -324,9 +331,13 @@ public class VideoPlayerImpl extends VideoPlayer moreOptionsButton.setImageDrawable(service.getResources().getDrawable( R.drawable.ic_expand_more_white_24dp)); shareButton.setVisibility(View.VISIBLE); - playWithKodi.setVisibility( - defaultPreferences.getBoolean(service.getString(R.string.show_play_with_kodi_key), false) ? View.VISIBLE : View.GONE); + final boolean supportedByKore = playQueue != null + && playQueue.getItem() != null + && KoreUtil.isServiceSupportedByKore(playQueue.getItem().getServiceId()); + final boolean kodiEnabled = defaultPreferences.getBoolean(service.getString(R.string.show_play_with_kodi_key), false); + playWithKodi.setVisibility(kodiEnabled && supportedByKore ? View.VISIBLE : View.GONE); openInBrowser.setVisibility(View.VISIBLE); + muteButton.setVisibility(View.VISIBLE); playerCloseButton.setVisibility(isFullscreen ? View.GONE : View.VISIBLE); // Top controls have a large minHeight which is allows to drag the player down in fullscreen mode (just larger area // to make easy to locate by finger) @@ -399,6 +410,7 @@ public class VideoPlayerImpl extends VideoPlayer playWithKodi.setOnClickListener(this); openInBrowser.setOnClickListener(this); playerCloseButton.setOnClickListener(this); + muteButton.setOnClickListener(this); settingsContentObserver = new ContentObserver(new Handler()) { @Override @@ -410,6 +422,45 @@ public class VideoPlayerImpl extends VideoPlayer getRootView().addOnLayoutChangeListener(this); } + public boolean onKeyDown(final int keyCode) { + switch (keyCode) { + default: + break; + case KeyEvent.KEYCODE_BACK: + if (AndroidTvUtils.isTv(service) && isControlsVisible()) { + hideControls(0, 0); + hideSystemUIIfNeeded(); + return true; + } + break; + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_DPAD_LEFT: + case KeyEvent.KEYCODE_DPAD_DOWN: + case KeyEvent.KEYCODE_DPAD_RIGHT: + case KeyEvent.KEYCODE_DPAD_CENTER: + if (getRootView().hasFocus() && !getControlsRoot().hasFocus()) { + // do not interfere with focus in playlist etc. + return false; + } + + if (getCurrentState() == BasePlayer.STATE_BLOCKED) { + return true; + } + + if (!isControlsVisible()) { + playPauseButton.requestFocus(); + showControlsThenHide(); + showSystemUIPartially(); + return true; + } else { + hideControls(DEFAULT_CONTROLS_DURATION, DPAD_CONTROLS_HIDE_TIME); + } + break; + } + + return false; + } + public AppCompatActivity getParentActivity() { // ! instanceof ViewGroup means that view was added via windowManager for Popup if (getRootView() == null || getRootView().getParent() == null || !(getRootView().getParent() instanceof ViewGroup)) @@ -494,6 +545,13 @@ public class VideoPlayerImpl extends VideoPlayer protected void onMetadataChanged(@NonNull final MediaSourceTag tag) { super.onMetadataChanged(tag); + // show kodi button if it supports the current service and it is enabled in settings + final boolean showKodiButton = + KoreUtil.isServiceSupportedByKore(tag.getMetadata().getServiceId()) + && PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean(context.getString(R.string.show_play_with_kodi_key), false); + playWithKodi.setVisibility(showKodiButton ? View.VISIBLE : View.GONE); + titleTextView.setText(tag.getMetadata().getName()); channelTextView.setText(tag.getMetadata().getUploaderName()); @@ -508,6 +566,13 @@ public class VideoPlayerImpl extends VideoPlayer // Override it because we don't want playerImpl destroyed } + @Override + public void onMuteUnmuteButtonClicked() { + super.onMuteUnmuteButtonClicked(); + updatePlayback(); + setMuteButton(muteButton, isMuted()); + } + @Override public void onUpdateProgress(final int currentProgress, final int duration, final int bufferPercent) { super.onUpdateProgress(currentProgress, duration, bufferPercent); @@ -518,6 +583,10 @@ public class VideoPlayerImpl extends VideoPlayer || getCurrentState() == BasePlayer.STATE_PAUSED || getPlayQueue() == null) return; + if (timesNotificationUpdated > NOTIFICATION_UPDATES_BEFORE_RESET) { + service.resetNotification(); + } + if (service.getBigNotRemoteView() != null) { if (cachedDuration != duration) { cachedDuration = duration; @@ -560,9 +629,10 @@ public class VideoPlayerImpl extends VideoPlayer } @Override - protected void initPlayback(@NonNull PlayQueue queue, int repeatMode, float playbackSpeed, - float playbackPitch, boolean playbackSkipSilence, boolean playOnReady) { - super.initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence, playOnReady); + protected void initPlayback(@NonNull final PlayQueue queue, final int repeatMode, final float playbackSpeed, + final float playbackPitch, final boolean playbackSkipSilence, + final boolean playOnReady, final boolean isMuted) { + super.initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence, playOnReady, isMuted); updateQueue(); } @@ -587,7 +657,9 @@ public class VideoPlayerImpl extends VideoPlayer this.getPlaybackPitch(), this.getPlaybackSkipSilence(), null, - true + true, + !isPlaying(), + isMuted() ); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra(Constants.KEY_SERVICE_ID, getCurrentMetadata().getMetadata().getServiceId()); @@ -623,13 +695,10 @@ public class VideoPlayerImpl extends VideoPlayer super.onClick(v); if (v.getId() == playPauseButton.getId()) { onPlayPause(); - } else if (v.getId() == playPreviousButton.getId()) { onPlayPrevious(); - } else if (v.getId() == playNextButton.getId()) { onPlayNext(); - } else if (v.getId() == queueButton.getId()) { onQueueClicked(); return; @@ -641,23 +710,19 @@ public class VideoPlayerImpl extends VideoPlayer return; } else if (v.getId() == moreOptionsButton.getId()) { onMoreOptionsClicked(); - } else if (v.getId() == shareButton.getId()) { onShareClicked(); - } else if (v.getId() == playWithKodi.getId()) { onPlayWithKodiClicked(); - } else if (v.getId() == openInBrowser.getId()) { onOpenInBrowserClicked(); - } else if (v.getId() == fullscreenButton.getId()) { toggleFullscreen(); - } else if (v.getId() == screenRotationButton.getId()) { if (!isVerticalVideo) fragmentListener.onScreenRotationButtonClicked(); else toggleFullscreen(); - + } else if (v.getId() == muteButton.getId()) { + onMuteUnmuteButtonClicked(); } else if (v.getId() == playerCloseButton.getId()) { service.sendBroadcast(new Intent(VideoDetailFragment.ACTION_HIDE_MAIN_PLAYER)); } @@ -667,7 +732,7 @@ public class VideoPlayerImpl extends VideoPlayer animateView(getControlsRoot(), true, DEFAULT_CONTROLS_DURATION, 0, () -> { if (getCurrentState() == STATE_PLAYING && !isSomePopupMenuVisible()) { if (v.getId() == playPauseButton.getId()) hideControls(0, 0); - else hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); + else safeHideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); } }); } @@ -734,10 +799,9 @@ public class VideoPlayerImpl extends VideoPlayer private void onPlayWithKodiClicked() { if (getCurrentMetadata() == null) return; - + onPause(); try { - NavigationHelper.playWithKore(getParentActivity(), Uri.parse( - getCurrentMetadata().getMetadata().getUrl().replace("https", "http"))); + NavigationHelper.playWithKore(getParentActivity(), Uri.parse(getVideoUrl())); } catch (Exception e) { if (DEBUG) Log.i(TAG, "Failed to start kore", e); showInstallKoreDialog(getParentActivity()); @@ -766,7 +830,7 @@ public class VideoPlayerImpl extends VideoPlayer final boolean showButton = videoPlayerSelected() && (orientationLocked || isVerticalVideo || tabletInLandscape); screenRotationButton.setVisibility(showButton ? View.VISIBLE : View.GONE); screenRotationButton.setImageDrawable(service.getResources().getDrawable( - isFullscreen() ? R.drawable.ic_fullscreen_exit_white : R.drawable.ic_fullscreen_white)); + isFullscreen() ? R.drawable.ic_fullscreen_exit_white_24dp : R.drawable.ic_fullscreen_white_24dp)); } private void prepareOrientation() { @@ -795,7 +859,9 @@ public class VideoPlayerImpl extends VideoPlayer @Override public void onDismiss(PopupMenu menu) { super.onDismiss(menu); - if (isPlaying()) hideControls(DEFAULT_CONTROLS_DURATION, 0); + if (isPlaying()) { + hideControls(DEFAULT_CONTROLS_DURATION, 0); + } } @Override @@ -894,12 +960,12 @@ public class VideoPlayerImpl extends VideoPlayer @Override public void onBlocked() { super.onBlocked(); - playPauseButton.setImageResource(R.drawable.ic_play_arrow_white); + playPauseButton.setImageResource(R.drawable.exo_controls_play); animatePlayButtons(false, 100); getRootView().setKeepScreenOn(false); service.resetNotification(); - service.updateNotification(R.drawable.ic_play_arrow_white); + service.updateNotification(R.drawable.exo_controls_play); } @Override @@ -908,24 +974,24 @@ public class VideoPlayerImpl extends VideoPlayer getRootView().setKeepScreenOn(true); service.resetNotification(); - service.updateNotification(R.drawable.ic_play_arrow_white); + service.updateNotification(R.drawable.exo_controls_play); } @Override public void onPlaying() { super.onPlaying(); animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 80, 0, () -> { - playPauseButton.setImageResource(R.drawable.ic_pause_white); + playPauseButton.setImageResource(R.drawable.exo_controls_pause); animatePlayButtons(true, 200); + playPauseButton.requestFocus(); }); updateWindowFlags(ONGOING_PLAYBACK_WINDOW_FLAGS); checkLandscape(); getRootView().setKeepScreenOn(true); - service.getLockManager().acquireWifiAndCpu(); service.resetNotification(); - service.updateNotification(R.drawable.ic_pause_white); + service.updateNotification(R.drawable.exo_controls_pause); service.startForeground(NOTIFICATION_ID, service.getNotBuilder().build()); } @@ -934,22 +1000,21 @@ public class VideoPlayerImpl extends VideoPlayer public void onPaused() { super.onPaused(); animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 80, 0, () -> { - playPauseButton.setImageResource(R.drawable.ic_play_arrow_white); + playPauseButton.setImageResource(R.drawable.exo_controls_play); animatePlayButtons(true, 200); + playPauseButton.requestFocus(); }); updateWindowFlags(IDLE_WINDOW_FLAGS); service.resetNotification(); - service.updateNotification(R.drawable.ic_play_arrow_white); + service.updateNotification(R.drawable.exo_controls_play); // Remove running notification when user don't want music (or video in popup) to be played in background if (!minimizeOnPopupEnabled() && !backgroundPlaybackEnabled() && videoPlayerSelected()) service.stopForeground(true); getRootView().setKeepScreenOn(false); - - service.getLockManager().releaseWifiAndCpu(); } @Override @@ -959,14 +1024,14 @@ public class VideoPlayerImpl extends VideoPlayer getRootView().setKeepScreenOn(true); service.resetNotification(); - service.updateNotification(R.drawable.ic_play_arrow_white); + service.updateNotification(R.drawable.exo_controls_play); } @Override public void onCompleted() { animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 0, 0, () -> { - playPauseButton.setImageResource(R.drawable.ic_replay_white); + playPauseButton.setImageResource(R.drawable.ic_replay_white_24dp); animatePlayButtons(true, DEFAULT_CONTROLS_DURATION); }); getRootView().setKeepScreenOn(false); @@ -974,9 +1039,7 @@ public class VideoPlayerImpl extends VideoPlayer updateWindowFlags(IDLE_WINDOW_FLAGS); service.resetNotification(); - service.updateNotification(R.drawable.ic_replay_white); - - service.getLockManager().releaseWifiAndCpu(); + service.updateNotification(R.drawable.ic_replay_white_24dp); super.onCompleted(); } @@ -1065,6 +1128,16 @@ public class VideoPlayerImpl extends VideoPlayer } break; case Intent.ACTION_CONFIGURATION_CHANGED: + assureCorrectAppLanguage(service); + if (DEBUG) { + Log.d(TAG, "onConfigurationChanged() called"); + } + if (popupPlayerSelected()) { + updateScreenSize(); + updatePopupSize(getPopupLayoutParams().width, -1); + checkPopupPositionBounds(); + } + // The only situation I need to re-calculate elements sizes is when a user rotates a device from landscape to landscape // because in that case the controls should be aligned to another side of a screen. The problem is when user leaves // the app and returns back (while the app in landscape) Android reports via DisplayMetrics that orientation is @@ -1098,7 +1171,8 @@ public class VideoPlayerImpl extends VideoPlayer //////////////////////////////////////////////////////////////////////////*/ @Override - public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { + public void onLoadingComplete(String imageUri, View view, + Bitmap loadedImage) { super.onLoadingComplete(imageUri, view, loadedImage); // rebuild notification here since remote view does not release bitmaps, // causing memory leaks @@ -1167,13 +1241,16 @@ public class VideoPlayerImpl extends VideoPlayer } private int distanceFromCloseButton(final MotionEvent popupMotionEvent) { - final int closeOverlayButtonX = closeOverlayButton.getLeft() + closeOverlayButton.getWidth() / 2; - final int closeOverlayButtonY = closeOverlayButton.getTop() + closeOverlayButton.getHeight() / 2; + final int closeOverlayButtonX = closeOverlayButton.getLeft() + + closeOverlayButton.getWidth() / 2; + final int closeOverlayButtonY = closeOverlayButton.getTop() + + closeOverlayButton.getHeight() / 2; final float fingerX = popupLayoutParams.x + popupMotionEvent.getX(); final float fingerY = popupLayoutParams.y + popupMotionEvent.getY(); - return (int) Math.sqrt(Math.pow(closeOverlayButtonX - fingerX, 2) + Math.pow(closeOverlayButtonY - fingerY, 2)); + return (int) Math.sqrt(Math.pow(closeOverlayButtonX - fingerX, 2) + + Math.pow(closeOverlayButtonY - fingerY, 2)); } private float getClosingRadius() { @@ -1221,6 +1298,13 @@ public class VideoPlayerImpl extends VideoPlayer ); } + @Override + public void safeHideControls(long duration, long delay) { + if (getControlsRoot().isInTouchMode()) { + hideControls(duration, delay); + } + } + private void showOrHideButtons() { if (playQueue == null) return; @@ -1301,6 +1385,11 @@ public class VideoPlayerImpl extends VideoPlayer return statusBarHeight; } + protected void setMuteButton(final ImageButton muteButton, final boolean isMuted) { + muteButton.setImageDrawable(AppCompatResources.getDrawable(service, isMuted + ? R.drawable.ic_volume_off_white_24dp : R.drawable.ic_volume_up_white_24dp)); + } + /** * @return true if main player is attached to activity and activity inside multiWindow mode */ @@ -1483,6 +1572,7 @@ public class VideoPlayerImpl extends VideoPlayer /** * @see #checkPopupPositionBounds(float, float) + * @return if the popup was out of bounds and have been moved back to it */ @SuppressWarnings("UnusedReturnValue") public boolean checkPopupPositionBounds() { @@ -1490,18 +1580,22 @@ public class VideoPlayerImpl extends VideoPlayer } /** - * Check if {@link #popupLayoutParams}' position is within a arbitrary boundary that goes from (0,0) to (boundaryWidth, - * boundaryHeight). + * Check if {@link #popupLayoutParams}' position is within a arbitrary boundary + * that goes from (0, 0) to (boundaryWidth, boundaryHeight). *

- * If it's out of these boundaries, {@link #popupLayoutParams}' position is changed and {@code true} is returned - * to represent this change. + * If it's out of these boundaries, {@link #popupLayoutParams}' position is changed + * and {@code true} is returned to represent this change. + *

* + * @param boundaryWidth width of the boundary + * @param boundaryHeight height of the boundary * @return if the popup was out of bounds and have been moved back to it */ public boolean checkPopupPositionBounds(final float boundaryWidth, final float boundaryHeight) { if (DEBUG) { - Log.d(TAG, "checkPopupPositionBounds() called with: boundaryWidth = [" - + boundaryWidth + "], boundaryHeight = [" + boundaryHeight + "]"); + Log.d(TAG, "checkPopupPositionBounds() called with: " + + "boundaryWidth = [" + boundaryWidth + "], " + + "boundaryHeight = [" + boundaryHeight + "]"); } if (popupLayoutParams.x < 0) { @@ -1524,15 +1618,19 @@ public class VideoPlayerImpl extends VideoPlayer } public void savePositionAndSize() { - final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(service); + final SharedPreferences sharedPreferences = PreferenceManager + .getDefaultSharedPreferences(service); sharedPreferences.edit().putInt(POPUP_SAVED_X, popupLayoutParams.x).apply(); sharedPreferences.edit().putInt(POPUP_SAVED_Y, popupLayoutParams.y).apply(); sharedPreferences.edit().putFloat(POPUP_SAVED_WIDTH, popupLayoutParams.width).apply(); } private float getMinimumVideoHeight(final float width) { - //if (DEBUG) Log.d(TAG, "getMinimumVideoHeight() called with: width = [" + width + "], returned: " + height); - return width / (16.0f / 9.0f); // Respect the 16:9 ratio that most videos have + final float height = width / (16.0f / 9.0f); // Respect the 16:9 ratio that most videos have + /*if (DEBUG) { + Log.d(TAG, "getMinimumVideoHeight() called with: width = [" + width + "], returned: " + height); + }*/ + return height; } public void updateScreenSize() { @@ -1554,24 +1652,25 @@ public class VideoPlayerImpl extends VideoPlayer maximumHeight = screenHeight; } - public void updatePopupSize(int width, int height) { + public void updatePopupSize(final int width, final int height) { if (DEBUG) Log.d(TAG, "updatePopupSize() called with: width = [" + width + "], height = [" + height + "]"); if (popupLayoutParams == null || windowManager == null || getParentActivity() != null || getRootView().getParent() == null) return; - width = (int) (width > maximumWidth ? maximumWidth : width < minimumWidth ? minimumWidth : width); + final int actualWidth = (int) (width > maximumWidth ? maximumWidth : width < minimumWidth ? minimumWidth : width); + final int actualHeight; + if (height == -1) actualHeight = (int) getMinimumVideoHeight(width); + else actualHeight = (int) (height > maximumHeight ? maximumHeight : height < minimumHeight ? minimumHeight : height); - if (height == -1) height = (int) getMinimumVideoHeight(width); - else height = (int) (height > maximumHeight ? maximumHeight : height < minimumHeight ? minimumHeight : height); - - popupLayoutParams.width = width; - popupLayoutParams.height = height; - popupWidth = width; - popupHeight = height; + popupLayoutParams.width = actualWidth; + popupLayoutParams.height = actualHeight; + popupWidth = actualWidth; + popupHeight = actualHeight; getSurfaceView().setHeights((int) popupHeight, (int) popupHeight); - if (DEBUG) Log.d(TAG, "updatePopupSize() updated values: width = [" + width + "], height = [" + height + "]"); + if (DEBUG) Log.d(TAG, "updatePopupSize() updated values:" + + " width = [" + actualWidth + "], height = [" + actualHeight + "]"); windowManager.updateViewLayout(getRootView(), popupLayoutParams); } @@ -1613,7 +1712,8 @@ public class VideoPlayerImpl extends VideoPlayer } private void animateOverlayAndFinishService() { - final int targetTranslationY = (int) (closeOverlayButton.getRootView().getHeight() - closeOverlayButton.getY()); + final int targetTranslationY = (int) (closeOverlayButton.getRootView().getHeight() + - closeOverlayButton.getY()); closeOverlayButton.animate().setListener(null).cancel(); closeOverlayButton.animate() @@ -1622,12 +1722,12 @@ public class VideoPlayerImpl extends VideoPlayer .setDuration(400) .setListener(new AnimatorListenerAdapter() { @Override - public void onAnimationCancel(Animator animation) { + public void onAnimationCancel(final Animator animation) { end(); } @Override - public void onAnimationEnd(Animator animation) { + public void onAnimationEnd(final Animator animation) { end(); } @@ -1704,7 +1804,8 @@ public class VideoPlayerImpl extends VideoPlayer } } - private void updateProgress(final int currentProgress, final int duration, final int bufferPercent) { + private void updateProgress(final int currentProgress, final int duration, + final int bufferPercent) { if (fragmentListener != null) { fragmentListener.onProgressUpdate(currentProgress, duration, bufferPercent); } diff --git a/app/src/main/java/org/schabi/newpipe/player/event/OnKeyDownListener.java b/app/src/main/java/org/schabi/newpipe/player/event/OnKeyDownListener.java new file mode 100644 index 000000000..ada8fef2a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/event/OnKeyDownListener.java @@ -0,0 +1,5 @@ +package org.schabi.newpipe.player.event; + +public interface OnKeyDownListener { + boolean onKeyDown(final int keyCode); +} diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerEventListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerEventListener.java index 8741f539f..b5520e8be 100644 --- a/app/src/main/java/org/schabi/newpipe/player/event/PlayerEventListener.java +++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerEventListener.java @@ -8,7 +8,8 @@ import org.schabi.newpipe.player.playqueue.PlayQueue; public interface PlayerEventListener { void onQueueUpdate(PlayQueue queue); - void onPlaybackUpdate(int state, int repeatMode, boolean shuffled, PlaybackParameters parameters); + void onPlaybackUpdate(int state, int repeatMode, boolean shuffled, + PlaybackParameters parameters); void onProgressUpdate(int currentProgress, int duration, int bufferPercent); void onMetadataUpdate(StreamInfo info, PlayQueue queue); void onServiceStopped(); diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java index 8e4a5c2ac..d7bc3a6a0 100644 --- a/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java +++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java @@ -1,6 +1,7 @@ package org.schabi.newpipe.player.event; import android.app.Activity; +import android.content.Context; import android.util.Log; import android.view.*; import androidx.appcompat.content.res.AppCompatResources; @@ -36,6 +37,13 @@ public class PlayerGestureListener extends GestureDetector.SimpleOnGestureListen private final int maxVolume; private static final int MOVEMENT_THRESHOLD = 40; + // [popup] initial coordinates and distance between fingers + private double initPointerDistance = -1; + private float initFirstPointerX = -1; + private float initFirstPointerY = -1; + private float initSecPointerX = -1; + private float initSecPointerY = -1; + public PlayerGestureListener(final VideoPlayerImpl playerImpl, final MainPlayer service) { this.playerImpl = playerImpl; @@ -147,11 +155,17 @@ public class PlayerGestureListener extends GestureDetector.SimpleOnGestureListen final float distanceX, final float distanceY) { if (!isVolumeGestureEnabled && !isBrightnessGestureEnabled) return false; - //noinspection PointlessBooleanExpression - if (DEBUG && false) Log.d(TAG, "MainVideoPlayer.onScroll = " + + final boolean isTouchingStatusBar = initialEvent.getY() < getStatusBarHeight(service); + final boolean isTouchingNavigationBar = initialEvent.getY() + > playerImpl.getRootView().getHeight() - getNavigationBarHeight(service); + if (isTouchingStatusBar || isTouchingNavigationBar) { + return false; + } + + /*if (DEBUG && false) Log.d(TAG, "MainVideoPlayer.onScroll = " + ", e1.getRaw = [" + initialEvent.getRawX() + ", " + initialEvent.getRawY() + "]" + ", e2.getRaw = [" + movingEvent.getRawX() + ", " + movingEvent.getRawY() + "]" + - ", distanceXy = [" + distanceX + ", " + distanceY + "]"); + ", distanceXy = [" + distanceX + ", " + distanceY + "]");*/ final boolean insideThreshold = Math.abs(movingEvent.getY() - initialEvent.getY()) <= MOVEMENT_THRESHOLD; if (!isMovingInMain && (insideThreshold || Math.abs(distanceX) > Math.abs(distanceY)) @@ -171,10 +185,10 @@ public class PlayerGestureListener extends GestureDetector.SimpleOnGestureListen if (DEBUG) Log.d(TAG, "onScroll().volumeControl, currentVolume = " + currentVolume); playerImpl.getVolumeImageView().setImageDrawable( - AppCompatResources.getDrawable(service, currentProgressPercent <= 0 ? R.drawable.ic_volume_off_white_72dp - : currentProgressPercent < 0.25 ? R.drawable.ic_volume_mute_white_72dp - : currentProgressPercent < 0.75 ? R.drawable.ic_volume_down_white_72dp - : R.drawable.ic_volume_up_white_72dp) + AppCompatResources.getDrawable(service, currentProgressPercent <= 0 ? R.drawable.ic_volume_off_white_24dp + : currentProgressPercent < 0.25 ? R.drawable.ic_volume_mute_white_24dp + : currentProgressPercent < 0.75 ? R.drawable.ic_volume_down_white_24dp + : R.drawable.ic_volume_up_white_24dp) ); if (playerImpl.getVolumeRelativeLayout().getVisibility() != View.VISIBLE) { @@ -200,9 +214,9 @@ public class PlayerGestureListener extends GestureDetector.SimpleOnGestureListen playerImpl.getBrightnessImageView().setImageDrawable( AppCompatResources.getDrawable(service, - currentProgressPercent < 0.25 ? R.drawable.ic_brightness_low_white_72dp - : currentProgressPercent < 0.75 ? R.drawable.ic_brightness_medium_white_72dp - : R.drawable.ic_brightness_high_white_72dp) + currentProgressPercent < 0.25 ? R.drawable.ic_brightness_low_white_24dp + : currentProgressPercent < 0.75 ? R.drawable.ic_brightness_medium_white_24dp + : R.drawable.ic_brightness_high_white_24dp) ); if (playerImpl.getBrightnessRelativeLayout().getVisibility() != View.VISIBLE) { @@ -273,8 +287,8 @@ public class PlayerGestureListener extends GestureDetector.SimpleOnGestureListen if (playerImpl.isControlsVisible()) { playerImpl.hideControls(100, 100); } else { + playerImpl.getPlayPauseButton().requestFocus(); playerImpl.showControlsThenHide(); - } return true; } @@ -312,11 +326,17 @@ public class PlayerGestureListener extends GestureDetector.SimpleOnGestureListen final float diffY = (int) (movingEvent.getRawY() - initialEvent.getRawY()); float posY = (int) (initialPopupY + diffY); - if (posX > (playerImpl.getScreenWidth() - playerImpl.getPopupWidth())) posX = (int) (playerImpl.getScreenWidth() - playerImpl.getPopupWidth()); - else if (posX < 0) posX = 0; + if (posX > (playerImpl.getScreenWidth() - playerImpl.getPopupWidth())) { + posX = (int) (playerImpl.getScreenWidth() - playerImpl.getPopupWidth()); + } else if (posX < 0) { + posX = 0; + } - if (posY > (playerImpl.getScreenHeight() - playerImpl.getPopupHeight())) posY = (int) (playerImpl.getScreenHeight() - playerImpl.getPopupHeight()); - else if (posY < 0) posY = 0; + if (posY > (playerImpl.getScreenHeight() - playerImpl.getPopupHeight())) { + posY = (int) (playerImpl.getScreenHeight() - playerImpl.getPopupHeight()); + } else if (posY < 0) { + posY = 0; + } playerImpl.getPopupLayoutParams().x = (int) posX; playerImpl.getPopupLayoutParams().y = (int) posY; @@ -332,15 +352,19 @@ public class PlayerGestureListener extends GestureDetector.SimpleOnGestureListen } } - //noinspection PointlessBooleanExpression - if (DEBUG && false) { - Log.d(TAG, "PopupVideoPlayer.onScroll = " + - ", e1.getRaw = [" + initialEvent.getRawX() + ", " + initialEvent.getRawY() + "]" + ", e1.getX,Y = [" + initialEvent.getX() + ", " + initialEvent.getY() + "]" + - ", e2.getRaw = [" + movingEvent.getRawX() + ", " + movingEvent.getRawY() + "]" + ", e2.getX,Y = [" + movingEvent.getX() + ", " + movingEvent.getY() + "]" + - ", distanceX,Y = [" + distanceX + ", " + distanceY + "]" + - ", posX,Y = [" + posX + ", " + posY + "]" + - ", popupW,H = [" + playerImpl.getPopupWidth() + " x " + playerImpl.getPopupHeight() + "]"); - } +// if (DEBUG) { +// Log.d(TAG, "PopupVideoPlayer.onScroll = " +// + "e1.getRaw = [" + initialEvent.getRawX() + ", " +// + initialEvent.getRawY() + "], " +// + "e1.getX,Y = [" + initialEvent.getX() + ", " +// + initialEvent.getY() + "], " +// + "e2.getRaw = [" + movingEvent.getRawX() + ", " +// + movingEvent.getRawY() + "], " +// + "e2.getX,Y = [" + movingEvent.getX() + ", " + movingEvent.getY() + "], " +// + "distanceX,Y = [" + distanceX + ", " + distanceY + "], " +// + "posX,Y = [" + posX + ", " + posY + "], " +// + "popupW,H = [" + popupWidth + " x " + popupHeight + "]"); +// } playerImpl.windowManager.updateViewLayout(playerImpl.getRootView(), playerImpl.getPopupLayoutParams()); return true; } @@ -378,8 +402,11 @@ public class PlayerGestureListener extends GestureDetector.SimpleOnGestureListen } private boolean onTouchInPopup(View v, MotionEvent event) { + if (playerImpl == null) { + return false; + } playerImpl.getGestureDetector().onTouchEvent(event); - if (playerImpl == null) return false; + if (event.getPointerCount() == 2 && !isMovingInPopup && !isResizing) { if (DEBUG) Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing."); playerImpl.showAndAnimateControl(-1, true); @@ -388,6 +415,15 @@ public class PlayerGestureListener extends GestureDetector.SimpleOnGestureListen playerImpl.hideControls(0, 0); animateView(playerImpl.getCurrentDisplaySeek(), false, 0, 0); animateView(playerImpl.getResizingIndicator(), true, 200, 0); + //record coordinates of fingers + initFirstPointerX = event.getX(0); + initFirstPointerY = event.getY(0); + initSecPointerX = event.getX(1); + initSecPointerY = event.getY(1); + //record distance between fingers + initPointerDistance = Math.hypot(initFirstPointerX - initSecPointerX, + initFirstPointerY - initSecPointerY); + isResizing = true; } @@ -406,6 +442,13 @@ public class PlayerGestureListener extends GestureDetector.SimpleOnGestureListen if (isResizing) { isResizing = false; + + initPointerDistance = -1; + initFirstPointerX = -1; + initFirstPointerY = -1; + initSecPointerX = -1; + initSecPointerY = -1; + animateView(playerImpl.getResizingIndicator(), false, 100, 0); playerImpl.changeState(playerImpl.getCurrentState()); } @@ -420,29 +463,58 @@ public class PlayerGestureListener extends GestureDetector.SimpleOnGestureListen } private boolean handleMultiDrag(final MotionEvent event) { - if (event.getPointerCount() != 2) return false; + if (initPointerDistance != -1 && event.getPointerCount() == 2) { + // get the movements of the fingers + double firstPointerMove = Math.hypot(event.getX(0) - initFirstPointerX, + event.getY(0) - initFirstPointerY); + double secPointerMove = Math.hypot(event.getX(1) - initSecPointerX, + event.getY(1) - initSecPointerY); - final float firstPointerX = event.getX(0); - final float secondPointerX = event.getX(1); + // minimum threshold beyond which pinch gesture will work + int minimumMove = ViewConfiguration.get(service).getScaledTouchSlop(); - final float diff = Math.abs(firstPointerX - secondPointerX); - if (firstPointerX > secondPointerX) { - // second pointer is the anchor (the leftmost pointer) - playerImpl.getPopupLayoutParams().x = (int) (event.getRawX() - diff); - } else { - // first pointer is the anchor - playerImpl.getPopupLayoutParams().x = (int) event.getRawX(); + if (Math.max(firstPointerMove, secPointerMove) > minimumMove) { + // calculate current distance between the pointers + final double currentPointerDistance = + Math.hypot(event.getX(0) - event.getX(1), + event.getY(0) - event.getY(1)); + + double popupWidth = playerImpl.getPopupWidth(); + // change co-ordinates of popup so the center stays at the same position + double newWidth = (popupWidth * currentPointerDistance / initPointerDistance); + initPointerDistance = currentPointerDistance; + playerImpl.getPopupLayoutParams().x += (popupWidth - newWidth) / 2; + + playerImpl.checkPopupPositionBounds(); + playerImpl.updateScreenSize(); + + playerImpl.updatePopupSize((int) Math.min(playerImpl.getScreenWidth(), newWidth), -1); + return true; + } } - - playerImpl.checkPopupPositionBounds(); - playerImpl.updateScreenSize(); - - final int width = (int) Math.min(playerImpl.getScreenWidth(), diff); - playerImpl.updatePopupSize(width, -1); - - return true; + return false; } + + /* + * Utils + * */ + + private int getNavigationBarHeight(Context context) { + int resId = context.getResources().getIdentifier("navigation_bar_height", "dimen", "android"); + if (resId > 0) { + return context.getResources().getDimensionPixelSize(resId); + } + return 0; + } + + private int getStatusBarHeight(Context context) { + int resId = context.getResources().getIdentifier("status_bar_height", "dimen", "android"); + if (resId > 0) { + return context.getResources().getDimensionPixelSize(resId); + } + return 0; + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java index ff9d0c477..4b326ca7d 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java @@ -9,14 +9,14 @@ import android.media.AudioFocusRequest; import android.media.AudioManager; import android.media.audiofx.AudioEffect; import android.os.Build; -import androidx.annotation.NonNull; import android.util.Log; +import androidx.annotation.NonNull; + import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.analytics.AnalyticsListener; -public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, - AnalyticsListener { +public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, AnalyticsListener { private static final String TAG = "AudioFocusReactor"; @@ -82,20 +82,20 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, return audioManager.getStreamVolume(STREAM_TYPE); } - public int getMaxVolume() { - return audioManager.getStreamMaxVolume(STREAM_TYPE); - } - public void setVolume(final int volume) { audioManager.setStreamVolume(STREAM_TYPE, volume, 0); } + public int getMaxVolume() { + return audioManager.getStreamMaxVolume(STREAM_TYPE); + } + /*////////////////////////////////////////////////////////////////////////// // AudioFocus //////////////////////////////////////////////////////////////////////////*/ @Override - public void onAudioFocusChange(int focusChange) { + public void onAudioFocusChange(final int focusChange) { Log.d(TAG, "onAudioFocusChange() called with: focusChange = [" + focusChange + "]"); switch (focusChange) { case AudioManager.AUDIOFOCUS_GAIN: @@ -138,17 +138,17 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, valueAnimator.setDuration(AudioReactor.DUCK_DURATION); valueAnimator.addListener(new AnimatorListenerAdapter() { @Override - public void onAnimationStart(Animator animation) { + public void onAnimationStart(final Animator animation) { player.setVolume(from); } @Override - public void onAnimationCancel(Animator animation) { + public void onAnimationCancel(final Animator animation) { player.setVolume(to); } @Override - public void onAnimationEnd(Animator animation) { + public void onAnimationEnd(final Animator animation) { player.setVolume(to); } }); @@ -162,8 +162,10 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, //////////////////////////////////////////////////////////////////////////*/ @Override - public void onAudioSessionId(EventTime eventTime, int audioSessionId) { - if (!PlayerHelper.isUsingDSP(context)) return; + public void onAudioSessionId(final EventTime eventTime, final int audioSessionId) { + if (!PlayerHelper.isUsingDSP(context)) { + return; + } final Intent intent = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION); intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId); diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java b/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java index 8160640cb..2ef22f2eb 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java @@ -20,8 +20,10 @@ import java.io.File; /* package-private */ class CacheFactory implements DataSource.Factory { private static final String TAG = "CacheFactory"; + private static final String CACHE_FOLDER_NAME = "exoplayer"; - private static final int CACHE_FLAGS = CacheDataSource.FLAG_BLOCK_ON_CACHE | CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR; + private static final int CACHE_FLAGS = CacheDataSource.FLAG_BLOCK_ON_CACHE + | CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR; private final DefaultDataSourceFactory dataSourceFactory; private final File cacheDir; @@ -33,11 +35,11 @@ import java.io.File; // todo: make this a singleton? private static SimpleCache cache; - public CacheFactory(@NonNull final Context context, - @NonNull final String userAgent, - @NonNull final TransferListener transferListener) { - this(context, userAgent, transferListener, PlayerHelper.getPreferredCacheSize(context), - PlayerHelper.getPreferredFileSize(context)); + CacheFactory(@NonNull final Context context, + @NonNull final String userAgent, + @NonNull final TransferListener transferListener) { + this(context, userAgent, transferListener, PlayerHelper.getPreferredCacheSize(), + PlayerHelper.getPreferredFileSize()); } private CacheFactory(@NonNull final Context context, @@ -55,7 +57,8 @@ import java.io.File; } if (cache == null) { - final LeastRecentlyUsedCacheEvictor evictor = new LeastRecentlyUsedCacheEvictor(maxCacheSize); + final LeastRecentlyUsedCacheEvictor evictor + = new LeastRecentlyUsedCacheEvictor(maxCacheSize); cache = new SimpleCache(cacheDir, evictor, new ExoDatabaseProvider(context)); } } @@ -72,7 +75,9 @@ import java.io.File; } public void tryDeleteCacheFiles() { - if (!cacheDir.exists() || !cacheDir.isDirectory()) return; + if (!cacheDir.exists() || !cacheDir.isDirectory()) { + return; + } try { for (File file : cacheDir.listFiles()) { @@ -85,4 +90,4 @@ import java.io.File; Log.e(TAG, "Failed to delete file.", ignored); } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java b/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java index 4239dd62f..92ae009f6 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java @@ -1,7 +1,5 @@ package org.schabi.newpipe.player.helper; -import android.content.Context; - import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.LoadControl; import com.google.android.exoplayer2.Renderer; @@ -20,10 +18,10 @@ public class LoadController implements LoadControl { // Default Load Control //////////////////////////////////////////////////////////////////////////*/ - public LoadController(final Context context) { - this(PlayerHelper.getPlaybackStartBufferMs(context), - PlayerHelper.getPlaybackMinimumBufferMs(context), - PlayerHelper.getPlaybackOptimalBufferMs(context)); + public LoadController() { + this(PlayerHelper.getPlaybackStartBufferMs(), + PlayerHelper.getPlaybackMinimumBufferMs(), + PlayerHelper.getPlaybackOptimalBufferMs()); } private LoadController(final int initialPlaybackBufferMs, @@ -47,8 +45,8 @@ public class LoadController implements LoadControl { } @Override - public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroupArray, - TrackSelectionArray trackSelectionArray) { + public void onTracksSelected(final Renderer[] renderers, final TrackGroupArray trackGroupArray, + final TrackSelectionArray trackSelectionArray) { internalLoadControl.onTracksSelected(renderers, trackGroupArray, trackSelectionArray); } @@ -78,17 +76,18 @@ public class LoadController implements LoadControl { } @Override - public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) { + public boolean shouldContinueLoading(final long bufferedDurationUs, + final float playbackSpeed) { return internalLoadControl.shouldContinueLoading(bufferedDurationUs, playbackSpeed); } @Override - public boolean shouldStartPlayback(long bufferedDurationUs, float playbackSpeed, - boolean rebuffering) { - final boolean isInitialPlaybackBufferFilled = bufferedDurationUs >= - this.initialPlaybackBufferUs * playbackSpeed; - final boolean isInternalStartingPlayback = internalLoadControl.shouldStartPlayback( - bufferedDurationUs, playbackSpeed, rebuffering); + public boolean shouldStartPlayback(final long bufferedDurationUs, final float playbackSpeed, + final boolean rebuffering) { + final boolean isInitialPlaybackBufferFilled + = bufferedDurationUs >= this.initialPlaybackBufferUs * playbackSpeed; + final boolean isInternalStartingPlayback = internalLoadControl + .shouldStartPlayback(bufferedDurationUs, playbackSpeed, rebuffering); return isInitialPlaybackBufferFilled || isInternalStartingPlayback; } } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/LockManager.java b/app/src/main/java/org/schabi/newpipe/player/helper/LockManager.java index 1f352159c..6d0cf8e85 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/LockManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/LockManager.java @@ -18,25 +18,37 @@ public class LockManager { private WifiManager.WifiLock wifiLock; public LockManager(final Context context) { - powerManager = ((PowerManager) context.getApplicationContext().getSystemService(POWER_SERVICE)); - wifiManager = ((WifiManager) context.getApplicationContext().getSystemService(WIFI_SERVICE)); + powerManager = ((PowerManager) context.getApplicationContext() + .getSystemService(POWER_SERVICE)); + wifiManager = ((WifiManager) context.getApplicationContext() + .getSystemService(WIFI_SERVICE)); } public void acquireWifiAndCpu() { Log.d(TAG, "acquireWifiAndCpu() called"); - if (wakeLock != null && wakeLock.isHeld() && wifiLock != null && wifiLock.isHeld()) return; + if (wakeLock != null && wakeLock.isHeld() && wifiLock != null && wifiLock.isHeld()) { + return; + } wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); wifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL, TAG); - if (wakeLock != null) wakeLock.acquire(); - if (wifiLock != null) wifiLock.acquire(); + if (wakeLock != null) { + wakeLock.acquire(); + } + if (wifiLock != null) { + wifiLock.acquire(); + } } public void releaseWifiAndCpu() { Log.d(TAG, "releaseWifiAndCpu() called"); - if (wakeLock != null && wakeLock.isHeld()) wakeLock.release(); - if (wifiLock != null && wifiLock.isHeld()) wifiLock.release(); + if (wakeLock != null && wakeLock.isHeld()) { + wakeLock.release(); + } + if (wifiLock != null && wifiLock.isHeld()) { + wifiLock.release(); + } wakeLock = null; wifiLock = null; diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java b/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java index a5c703837..e101e2185 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java @@ -2,11 +2,18 @@ package org.schabi.newpipe.player.helper; import android.content.Context; import android.content.Intent; +import android.graphics.Bitmap; +import android.media.MediaMetadata; +import android.os.Build; +import android.support.v4.media.MediaMetadataCompat; import android.support.v4.media.session.MediaSessionCompat; import android.view.KeyEvent; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.core.app.NotificationCompat; +import androidx.media.app.NotificationCompat.MediaStyle; import androidx.media.session.MediaButtonReceiver; import com.google.android.exoplayer2.Player; @@ -19,8 +26,10 @@ import org.schabi.newpipe.player.mediasession.PlayQueuePlaybackController; public class MediaSessionManager { private static final String TAG = "MediaSessionManager"; - @NonNull private final MediaSessionCompat mediaSession; - @NonNull private final MediaSessionConnector sessionConnector; + @NonNull + private final MediaSessionCompat mediaSession; + @NonNull + private final MediaSessionConnector sessionConnector; public MediaSessionManager(@NonNull final Context context, @NonNull final Player player, @@ -40,13 +49,46 @@ public class MediaSessionManager { return MediaButtonReceiver.handleIntent(mediaSession, intent); } + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public void setLockScreenArt(final NotificationCompat.Builder builder, + @Nullable final Bitmap thumbnailBitmap) { + if (thumbnailBitmap == null || !mediaSession.isActive()) { + return; + } + + mediaSession.setMetadata( + new MediaMetadataCompat.Builder() + .putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, thumbnailBitmap) + .build() + ); + + MediaStyle mediaStyle = new MediaStyle() + .setMediaSession(mediaSession.getSessionToken()); + + builder.setStyle(mediaStyle); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public void clearLockScreenArt(final NotificationCompat.Builder builder) { + mediaSession.setMetadata( + new MediaMetadataCompat.Builder() + .putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, null) + .build() + ); + + MediaStyle mediaStyle = new MediaStyle() + .setMediaSession(mediaSession.getSessionToken()); + + builder.setStyle(mediaStyle); + } + /** * Should be called on player destruction to prevent leakage. - * */ + */ public void dispose() { this.sessionConnector.setPlayer(null); this.sessionConnector.setQueueNavigator(null); this.mediaSession.setActive(false); this.mediaSession.release(); - } + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java index 457b72120..4693797c3 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java @@ -3,80 +3,92 @@ package org.schabi.newpipe.player.helper; import android.app.Dialog; import android.content.Context; import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.DialogFragment; -import androidx.appcompat.app.AlertDialog; +import android.preference.PreferenceManager; import android.util.Log; import android.view.View; import android.widget.CheckBox; import android.widget.SeekBar; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; + import org.schabi.newpipe.R; import org.schabi.newpipe.util.SliderStrategy; import static org.schabi.newpipe.player.BasePlayer.DEBUG; +import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; public class PlaybackParameterDialog extends DialogFragment { - @NonNull private static final String TAG = "PlaybackParameterDialog"; - // Minimum allowable range in ExoPlayer - public static final double MINIMUM_PLAYBACK_VALUE = 0.10f; - public static final double MAXIMUM_PLAYBACK_VALUE = 3.00f; + private static final double MINIMUM_PLAYBACK_VALUE = 0.10f; + private static final double MAXIMUM_PLAYBACK_VALUE = 3.00f; - public static final char STEP_UP_SIGN = '+'; - public static final char STEP_DOWN_SIGN = '-'; + private static final char STEP_UP_SIGN = '+'; + private static final char STEP_DOWN_SIGN = '-'; - public static final double STEP_ONE_PERCENT_VALUE = 0.01f; - public static final double STEP_FIVE_PERCENT_VALUE = 0.05f; - public static final double STEP_TEN_PERCENT_VALUE = 0.10f; - public static final double STEP_TWENTY_FIVE_PERCENT_VALUE = 0.25f; - public static final double STEP_ONE_HUNDRED_PERCENT_VALUE = 1.00f; + private static final double STEP_ONE_PERCENT_VALUE = 0.01f; + private static final double STEP_FIVE_PERCENT_VALUE = 0.05f; + private static final double STEP_TEN_PERCENT_VALUE = 0.10f; + private static final double STEP_TWENTY_FIVE_PERCENT_VALUE = 0.25f; + private static final double STEP_ONE_HUNDRED_PERCENT_VALUE = 1.00f; - public static final double DEFAULT_TEMPO = 1.00f; - public static final double DEFAULT_PITCH = 1.00f; - public static final double DEFAULT_STEP = STEP_TWENTY_FIVE_PERCENT_VALUE; - public static final boolean DEFAULT_SKIP_SILENCE = false; + private static final double DEFAULT_TEMPO = 1.00f; + private static final double DEFAULT_PITCH = 1.00f; + private static final double DEFAULT_STEP = STEP_TWENTY_FIVE_PERCENT_VALUE; + private static final boolean DEFAULT_SKIP_SILENCE = false; - @NonNull private static final String INITIAL_TEMPO_KEY = "initial_tempo_key"; - @NonNull private static final String INITIAL_PITCH_KEY = "initial_pitch_key"; + @NonNull + private static final String TAG = "PlaybackParameterDialog"; + @NonNull + private static final String INITIAL_TEMPO_KEY = "initial_tempo_key"; + @NonNull + private static final String INITIAL_PITCH_KEY = "initial_pitch_key"; - @NonNull private static final String TEMPO_KEY = "tempo_key"; - @NonNull private static final String PITCH_KEY = "pitch_key"; - @NonNull private static final String STEP_SIZE_KEY = "step_size_key"; + @NonNull + private static final String TEMPO_KEY = "tempo_key"; + @NonNull + private static final String PITCH_KEY = "pitch_key"; + @NonNull + private static final String STEP_SIZE_KEY = "step_size_key"; - public interface Callback { - void onPlaybackParameterChanged(final float playbackTempo, final float playbackPitch, - final boolean playbackSkipSilence); - } - - @Nullable private Callback callback; - - @NonNull private final SliderStrategy strategy = new SliderStrategy.Quadratic( + @NonNull + private final SliderStrategy strategy = new SliderStrategy.Quadratic( MINIMUM_PLAYBACK_VALUE, MAXIMUM_PLAYBACK_VALUE, /*centerAt=*/1.00f, /*sliderGranularity=*/10000); + @Nullable + private Callback callback; + private double initialTempo = DEFAULT_TEMPO; private double initialPitch = DEFAULT_PITCH; private boolean initialSkipSilence = DEFAULT_SKIP_SILENCE; - private double tempo = DEFAULT_TEMPO; private double pitch = DEFAULT_PITCH; private double stepSize = DEFAULT_STEP; - @Nullable private SeekBar tempoSlider; - @Nullable private TextView tempoCurrentText; - @Nullable private TextView tempoStepDownText; - @Nullable private TextView tempoStepUpText; - - @Nullable private SeekBar pitchSlider; - @Nullable private TextView pitchCurrentText; - @Nullable private TextView pitchStepDownText; - @Nullable private TextView pitchStepUpText; - - @Nullable private CheckBox unhookingCheckbox; - @Nullable private CheckBox skipSilenceCheckbox; + @Nullable + private SeekBar tempoSlider; + @Nullable + private TextView tempoCurrentText; + @Nullable + private TextView tempoStepDownText; + @Nullable + private TextView tempoStepUpText; + @Nullable + private SeekBar pitchSlider; + @Nullable + private TextView pitchCurrentText; + @Nullable + private TextView pitchStepDownText; + @Nullable + private TextView pitchStepUpText; + @Nullable + private CheckBox unhookingCheckbox; + @Nullable + private CheckBox skipSilenceCheckbox; public static PlaybackParameterDialog newInstance(final double playbackTempo, final double playbackPitch, @@ -99,7 +111,7 @@ public class PlaybackParameterDialog extends DialogFragment { //////////////////////////////////////////////////////////////////////////*/ @Override - public void onAttach(Context context) { + public void onAttach(final Context context) { super.onAttach(context); if (context instanceof Callback) { callback = (Callback) context; @@ -109,7 +121,8 @@ public class PlaybackParameterDialog extends DialogFragment { } @Override - public void onCreate(@Nullable Bundle savedInstanceState) { + public void onCreate(@Nullable final Bundle savedInstanceState) { + assureCorrectAppLanguage(getContext()); super.onCreate(savedInstanceState); if (savedInstanceState != null) { initialTempo = savedInstanceState.getDouble(INITIAL_TEMPO_KEY, DEFAULT_TEMPO); @@ -122,7 +135,7 @@ public class PlaybackParameterDialog extends DialogFragment { } @Override - public void onSaveInstanceState(Bundle outState) { + public void onSaveInstanceState(final Bundle outState) { super.onSaveInstanceState(outState); outState.putDouble(INITIAL_TEMPO_KEY, initialTempo); outState.putDouble(INITIAL_PITCH_KEY, initialPitch); @@ -138,7 +151,8 @@ public class PlaybackParameterDialog extends DialogFragment { @NonNull @Override - public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) { + assureCorrectAppLanguage(getContext()); final View view = View.inflate(getContext(), R.layout.dialog_playback_parameter, null); setupControlViews(view); @@ -160,18 +174,18 @@ public class PlaybackParameterDialog extends DialogFragment { // Control Views //////////////////////////////////////////////////////////////////////////*/ - private void setupControlViews(@NonNull View rootView) { + private void setupControlViews(@NonNull final View rootView) { setupHookingControl(rootView); setupSkipSilenceControl(rootView); setupTempoControl(rootView); setupPitchControl(rootView); - changeStepSize(stepSize); + setStepSize(stepSize); setupStepSizeSelector(rootView); } - private void setupTempoControl(@NonNull View rootView) { + private void setupTempoControl(@NonNull final View rootView) { tempoSlider = rootView.findViewById(R.id.tempoSeekbar); TextView tempoMinimumText = rootView.findViewById(R.id.tempoMinimumText); TextView tempoMaximumText = rootView.findViewById(R.id.tempoMaximumText); @@ -179,12 +193,15 @@ public class PlaybackParameterDialog extends DialogFragment { tempoStepUpText = rootView.findViewById(R.id.tempoStepUp); tempoStepDownText = rootView.findViewById(R.id.tempoStepDown); - if (tempoCurrentText != null) + if (tempoCurrentText != null) { tempoCurrentText.setText(PlayerHelper.formatSpeed(tempo)); - if (tempoMaximumText != null) + } + if (tempoMaximumText != null) { tempoMaximumText.setText(PlayerHelper.formatSpeed(MAXIMUM_PLAYBACK_VALUE)); - if (tempoMinimumText != null) + } + if (tempoMinimumText != null) { tempoMinimumText.setText(PlayerHelper.formatSpeed(MINIMUM_PLAYBACK_VALUE)); + } if (tempoSlider != null) { tempoSlider.setMax(strategy.progressOf(MAXIMUM_PLAYBACK_VALUE)); @@ -193,7 +210,7 @@ public class PlaybackParameterDialog extends DialogFragment { } } - private void setupPitchControl(@NonNull View rootView) { + private void setupPitchControl(@NonNull final View rootView) { pitchSlider = rootView.findViewById(R.id.pitchSeekbar); TextView pitchMinimumText = rootView.findViewById(R.id.pitchMinimumText); TextView pitchMaximumText = rootView.findViewById(R.id.pitchMaximumText); @@ -201,12 +218,15 @@ public class PlaybackParameterDialog extends DialogFragment { pitchStepDownText = rootView.findViewById(R.id.pitchStepDown); pitchStepUpText = rootView.findViewById(R.id.pitchStepUp); - if (pitchCurrentText != null) + if (pitchCurrentText != null) { pitchCurrentText.setText(PlayerHelper.formatPitch(pitch)); - if (pitchMaximumText != null) + } + if (pitchMaximumText != null) { pitchMaximumText.setText(PlayerHelper.formatPitch(MAXIMUM_PLAYBACK_VALUE)); - if (pitchMinimumText != null) + } + if (pitchMinimumText != null) { pitchMinimumText.setText(PlayerHelper.formatPitch(MINIMUM_PLAYBACK_VALUE)); + } if (pitchSlider != null) { pitchSlider.setMax(strategy.progressOf(MAXIMUM_PLAYBACK_VALUE)); @@ -215,21 +235,31 @@ public class PlaybackParameterDialog extends DialogFragment { } } - private void setupHookingControl(@NonNull View rootView) { + private void setupHookingControl(@NonNull final View rootView) { unhookingCheckbox = rootView.findViewById(R.id.unhookCheckbox); if (unhookingCheckbox != null) { - unhookingCheckbox.setChecked(pitch != tempo); + // restore whether pitch and tempo are unhooked or not + unhookingCheckbox.setChecked(PreferenceManager.getDefaultSharedPreferences(getContext()) + .getBoolean(getString(R.string.playback_unhook_key), true)); + unhookingCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> { - if (isChecked) return; - // When unchecked, slide back to the minimum of current tempo or pitch - final double minimum = Math.min(getCurrentPitch(), getCurrentTempo()); - setSliders(minimum); - setCurrentPlaybackParameters(); + // save whether pitch and tempo are unhooked or not + PreferenceManager.getDefaultSharedPreferences(getContext()) + .edit() + .putBoolean(getString(R.string.playback_unhook_key), isChecked) + .apply(); + + if (!isChecked) { + // when unchecked, slide back to the minimum of current tempo or pitch + final double minimum = Math.min(getCurrentPitch(), getCurrentTempo()); + setSliders(minimum); + setCurrentPlaybackParameters(); + } }); } } - private void setupSkipSilenceControl(@NonNull View rootView) { + private void setupSkipSilenceControl(@NonNull final View rootView) { skipSilenceCheckbox = rootView.findViewById(R.id.skipSilenceCheckbox); if (skipSilenceCheckbox != null) { skipSilenceCheckbox.setChecked(initialSkipSilence); @@ -242,41 +272,45 @@ public class PlaybackParameterDialog extends DialogFragment { TextView stepSizeOnePercentText = rootView.findViewById(R.id.stepSizeOnePercent); TextView stepSizeFivePercentText = rootView.findViewById(R.id.stepSizeFivePercent); TextView stepSizeTenPercentText = rootView.findViewById(R.id.stepSizeTenPercent); - TextView stepSizeTwentyFivePercentText = rootView.findViewById(R.id.stepSizeTwentyFivePercent); - TextView stepSizeOneHundredPercentText = rootView.findViewById(R.id.stepSizeOneHundredPercent); + TextView stepSizeTwentyFivePercentText = rootView + .findViewById(R.id.stepSizeTwentyFivePercent); + TextView stepSizeOneHundredPercentText = rootView + .findViewById(R.id.stepSizeOneHundredPercent); if (stepSizeOnePercentText != null) { stepSizeOnePercentText.setText(getPercentString(STEP_ONE_PERCENT_VALUE)); - stepSizeOnePercentText.setOnClickListener(view -> - changeStepSize(STEP_ONE_PERCENT_VALUE)); + stepSizeOnePercentText + .setOnClickListener(view -> setStepSize(STEP_ONE_PERCENT_VALUE)); } if (stepSizeFivePercentText != null) { stepSizeFivePercentText.setText(getPercentString(STEP_FIVE_PERCENT_VALUE)); - stepSizeFivePercentText.setOnClickListener(view -> - changeStepSize(STEP_FIVE_PERCENT_VALUE)); + stepSizeFivePercentText + .setOnClickListener(view -> setStepSize(STEP_FIVE_PERCENT_VALUE)); } if (stepSizeTenPercentText != null) { stepSizeTenPercentText.setText(getPercentString(STEP_TEN_PERCENT_VALUE)); - stepSizeTenPercentText.setOnClickListener(view -> - changeStepSize(STEP_TEN_PERCENT_VALUE)); + stepSizeTenPercentText + .setOnClickListener(view -> setStepSize(STEP_TEN_PERCENT_VALUE)); } if (stepSizeTwentyFivePercentText != null) { - stepSizeTwentyFivePercentText.setText(getPercentString(STEP_TWENTY_FIVE_PERCENT_VALUE)); - stepSizeTwentyFivePercentText.setOnClickListener(view -> - changeStepSize(STEP_TWENTY_FIVE_PERCENT_VALUE)); + stepSizeTwentyFivePercentText + .setText(getPercentString(STEP_TWENTY_FIVE_PERCENT_VALUE)); + stepSizeTwentyFivePercentText + .setOnClickListener(view -> setStepSize(STEP_TWENTY_FIVE_PERCENT_VALUE)); } if (stepSizeOneHundredPercentText != null) { - stepSizeOneHundredPercentText.setText(getPercentString(STEP_ONE_HUNDRED_PERCENT_VALUE)); - stepSizeOneHundredPercentText.setOnClickListener(view -> - changeStepSize(STEP_ONE_HUNDRED_PERCENT_VALUE)); + stepSizeOneHundredPercentText + .setText(getPercentString(STEP_ONE_HUNDRED_PERCENT_VALUE)); + stepSizeOneHundredPercentText + .setOnClickListener(view -> setStepSize(STEP_ONE_HUNDRED_PERCENT_VALUE)); } } - private void changeStepSize(final double stepSize) { + private void setStepSize(final double stepSize) { this.stepSize = stepSize; if (tempoStepUpText != null) { @@ -319,7 +353,8 @@ public class PlaybackParameterDialog extends DialogFragment { private SeekBar.OnSeekBarChangeListener getOnTempoChangedListener() { return new SeekBar.OnSeekBarChangeListener() { @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + public void onProgressChanged(final SeekBar seekBar, final int progress, + final boolean fromUser) { final double currentTempo = strategy.valueOf(progress); if (fromUser) { onTempoSliderUpdated(currentTempo); @@ -328,12 +363,12 @@ public class PlaybackParameterDialog extends DialogFragment { } @Override - public void onStartTrackingTouch(SeekBar seekBar) { + public void onStartTrackingTouch(final SeekBar seekBar) { // Do Nothing. } @Override - public void onStopTrackingTouch(SeekBar seekBar) { + public void onStopTrackingTouch(final SeekBar seekBar) { // Do Nothing. } }; @@ -342,7 +377,8 @@ public class PlaybackParameterDialog extends DialogFragment { private SeekBar.OnSeekBarChangeListener getOnPitchChangedListener() { return new SeekBar.OnSeekBarChangeListener() { @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + public void onProgressChanged(final SeekBar seekBar, final int progress, + final boolean fromUser) { final double currentPitch = strategy.valueOf(progress); if (fromUser) { // this change is first in chain onPitchSliderUpdated(currentPitch); @@ -351,19 +387,21 @@ public class PlaybackParameterDialog extends DialogFragment { } @Override - public void onStartTrackingTouch(SeekBar seekBar) { + public void onStartTrackingTouch(final SeekBar seekBar) { // Do Nothing. } @Override - public void onStopTrackingTouch(SeekBar seekBar) { + public void onStopTrackingTouch(final SeekBar seekBar) { // Do Nothing. } }; } private void onTempoSliderUpdated(final double newTempo) { - if (unhookingCheckbox == null) return; + if (unhookingCheckbox == null) { + return; + } if (!unhookingCheckbox.isChecked()) { setSliders(newTempo); } else { @@ -372,7 +410,9 @@ public class PlaybackParameterDialog extends DialogFragment { } private void onPitchSliderUpdated(final double newPitch) { - if (unhookingCheckbox == null) return; + if (unhookingCheckbox == null) { + return; + } if (!unhookingCheckbox.isChecked()) { setSliders(newPitch); } else { @@ -386,12 +426,16 @@ public class PlaybackParameterDialog extends DialogFragment { } private void setTempoSlider(final double newTempo) { - if (tempoSlider == null) return; + if (tempoSlider == null) { + return; + } tempoSlider.setProgress(strategy.progressOf(newTempo)); } private void setPitchSlider(final double newPitch) { - if (pitchSlider == null) return; + if (pitchSlider == null) { + return; + } pitchSlider.setProgress(strategy.progressOf(newPitch)); } @@ -403,27 +447,27 @@ public class PlaybackParameterDialog extends DialogFragment { setPlaybackParameters(getCurrentTempo(), getCurrentPitch(), getCurrentSkipSilence()); } - private void setPlaybackParameters(final double tempo, final double pitch, + private void setPlaybackParameters(final double newTempo, final double newPitch, final boolean skipSilence) { if (callback != null && tempoCurrentText != null && pitchCurrentText != null) { - if (DEBUG) Log.d(TAG, "Setting playback parameters to " + - "tempo=[" + tempo + "], " + - "pitch=[" + pitch + "]"); + if (DEBUG) { + Log.d(TAG, "Setting playback parameters to " + + "tempo=[" + newTempo + "], " + + "pitch=[" + newPitch + "]"); + } - tempoCurrentText.setText(PlayerHelper.formatSpeed(tempo)); - pitchCurrentText.setText(PlayerHelper.formatPitch(pitch)); - callback.onPlaybackParameterChanged((float) tempo, (float) pitch, skipSilence); + tempoCurrentText.setText(PlayerHelper.formatSpeed(newTempo)); + pitchCurrentText.setText(PlayerHelper.formatPitch(newPitch)); + callback.onPlaybackParameterChanged((float) newTempo, (float) newPitch, skipSilence); } } private double getCurrentTempo() { - return tempoSlider == null ? tempo : strategy.valueOf( - tempoSlider.getProgress()); + return tempoSlider == null ? tempo : strategy.valueOf(tempoSlider.getProgress()); } private double getCurrentPitch() { - return pitchSlider == null ? pitch : strategy.valueOf( - pitchSlider.getProgress()); + return pitchSlider == null ? pitch : strategy.valueOf(pitchSlider.getProgress()); } private double getCurrentStepSize() { @@ -448,4 +492,9 @@ public class PlaybackParameterDialog extends DialogFragment { private static String getPercentString(final double percent) { return PlayerHelper.formatPitch(percent); } + + public interface Callback { + void onPlaybackParameterChanged(float playbackTempo, float playbackPitch, + boolean playbackSkipSilence); + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java index 5aa331dc5..5fea4761b 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java @@ -24,30 +24,33 @@ public class PlayerDataSource { private final DataSource.Factory cacheDataSourceFactory; private final DataSource.Factory cachelessDataSourceFactory; - public PlayerDataSource(@NonNull final Context context, - @NonNull final String userAgent, + public PlayerDataSource(@NonNull final Context context, @NonNull final String userAgent, @NonNull final TransferListener transferListener) { cacheDataSourceFactory = new CacheFactory(context, userAgent, transferListener); - cachelessDataSourceFactory = new DefaultDataSourceFactory(context, userAgent, transferListener); + cachelessDataSourceFactory + = new DefaultDataSourceFactory(context, userAgent, transferListener); } public SsMediaSource.Factory getLiveSsMediaSourceFactory() { return new SsMediaSource.Factory(new DefaultSsChunkSource.Factory( cachelessDataSourceFactory), cachelessDataSourceFactory) - .setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY)) + .setLoadErrorHandlingPolicy( + new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY)) .setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS); } public HlsMediaSource.Factory getLiveHlsMediaSourceFactory() { return new HlsMediaSource.Factory(cachelessDataSourceFactory) .setAllowChunklessPreparation(true) - .setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY)); + .setLoadErrorHandlingPolicy( + new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY)); } public DashMediaSource.Factory getLiveDashMediaSourceFactory() { return new DashMediaSource.Factory(new DefaultDashChunkSource.Factory( cachelessDataSourceFactory), cachelessDataSourceFactory) - .setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY)) + .setLoadErrorHandlingPolicy( + new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY)) .setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS, true); } @@ -67,10 +70,12 @@ public class PlayerDataSource { public ProgressiveMediaSource.Factory getExtractorMediaSourceFactory() { return new ProgressiveMediaSource.Factory(cacheDataSourceFactory) - .setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(EXTRACTOR_MINIMUM_RETRY)); + .setLoadErrorHandlingPolicy( + new DefaultLoadErrorHandlingPolicy(EXTRACTOR_MINIMUM_RETRY)); } - public ProgressiveMediaSource.Factory getExtractorMediaSourceFactory(@NonNull final String key) { + public ProgressiveMediaSource.Factory getExtractorMediaSourceFactory( + @NonNull final String key) { return getExtractorMediaSourceFactory().setCustomCacheKey(key); } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java index 82003231d..4c0301541 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java @@ -6,10 +6,11 @@ import android.content.res.Configuration; import android.os.Build; import android.preference.PreferenceManager; import android.provider.Settings; +import android.view.accessibility.CaptioningManager; + import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import android.view.accessibility.CaptioningManager; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.text.CaptionStyleCompat; @@ -53,22 +54,12 @@ import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZ 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() {} - - private static final StringBuilder stringBuilder = new StringBuilder(); - private static final Formatter stringFormatter = new Formatter(stringBuilder, Locale.getDefault()); - private static final NumberFormat speedFormatter = new DecimalFormat("0.##x"); - private static final NumberFormat pitchFormatter = new DecimalFormat("##%"); - - @Retention(SOURCE) - @IntDef({MINIMIZE_ON_EXIT_MODE_NONE, MINIMIZE_ON_EXIT_MODE_BACKGROUND, - MINIMIZE_ON_EXIT_MODE_POPUP}) - public @interface MinimizeMode { - int MINIMIZE_ON_EXIT_MODE_NONE = 0; - int MINIMIZE_ON_EXIT_MODE_BACKGROUND = 1; - int MINIMIZE_ON_EXIT_MODE_POPUP = 2; - } +public final class PlayerHelper { + private static final StringBuilder STRING_BUILDER = new StringBuilder(); + private static final Formatter STRING_FORMATTER + = new Formatter(STRING_BUILDER, Locale.getDefault()); + private static final NumberFormat SPEED_FORMATTER = new DecimalFormat("0.##x"); + private static final NumberFormat PITCH_FORMATTER = new DecimalFormat("##%"); @Retention(SOURCE) @IntDef({AUTOPLAY_TYPE_ALWAYS, AUTOPLAY_TYPE_WIFI, @@ -78,35 +69,44 @@ public class PlayerHelper { int AUTOPLAY_TYPE_WIFI = 1; int AUTOPLAY_TYPE_NEVER = 2; } + + private PlayerHelper() { } + //////////////////////////////////////////////////////////////////////////// // Exposed helpers //////////////////////////////////////////////////////////////////////////// - public static String getTimeString(int milliSeconds) { + public static String getTimeString(final int milliSeconds) { int seconds = (milliSeconds % 60000) / 1000; int minutes = (milliSeconds % 3600000) / 60000; int hours = (milliSeconds % 86400000) / 3600000; int days = (milliSeconds % (86400000 * 7)) / 86400000; - stringBuilder.setLength(0); - return days > 0 ? stringFormatter.format("%d:%02d:%02d:%02d", days, hours, minutes, seconds).toString() - : hours > 0 ? stringFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString() - : stringFormatter.format("%02d:%02d", minutes, seconds).toString(); + STRING_BUILDER.setLength(0); + return days > 0 + ? STRING_FORMATTER.format("%d:%02d:%02d:%02d", days, hours, minutes, seconds) + .toString() + : hours > 0 + ? STRING_FORMATTER.format("%d:%02d:%02d", hours, minutes, seconds).toString() + : STRING_FORMATTER.format("%02d:%02d", minutes, seconds).toString(); } - public static String formatSpeed(double speed) { - return speedFormatter.format(speed); + public static String formatSpeed(final double speed) { + return SPEED_FORMATTER.format(speed); } - public static String formatPitch(double pitch) { - return pitchFormatter.format(pitch); + public static String formatPitch(final double pitch) { + return PITCH_FORMATTER.format(pitch); } public static String subtitleMimeTypesOf(final MediaFormat format) { switch (format) { - case VTT: return MimeTypes.TEXT_VTT; - case TTML: return MimeTypes.APPLICATION_TTML; - default: throw new IllegalArgumentException("Unrecognized mime type: " + format.name()); + case VTT: + return MimeTypes.TEXT_VTT; + case TTML: + return MimeTypes.APPLICATION_TTML; + default: + throw new IllegalArgumentException("Unrecognized mime type: " + format.name()); } } @@ -114,42 +114,55 @@ public class PlayerHelper { public static String captionLanguageOf(@NonNull final Context context, @NonNull final SubtitlesStream subtitles) { final String displayName = subtitles.getDisplayLanguageName(); - return displayName + (subtitles.isAutoGenerated() ? " (" + context.getString(R.string.caption_auto_generated)+ ")" : ""); + return displayName + (subtitles.isAutoGenerated() + ? " (" + context.getString(R.string.caption_auto_generated) + ")" : ""); } @NonNull public static String resizeTypeOf(@NonNull final Context context, @AspectRatioFrameLayout.ResizeMode final int resizeMode) { switch (resizeMode) { - case RESIZE_MODE_FIT: return context.getResources().getString(R.string.resize_fit); - case RESIZE_MODE_FILL: return context.getResources().getString(R.string.resize_fill); - case RESIZE_MODE_ZOOM: return context.getResources().getString(R.string.resize_zoom); - default: throw new IllegalArgumentException("Unrecognized resize mode: " + resizeMode); + case RESIZE_MODE_FIT: + return context.getResources().getString(R.string.resize_fit); + case RESIZE_MODE_FILL: + return context.getResources().getString(R.string.resize_fill); + case RESIZE_MODE_ZOOM: + return context.getResources().getString(R.string.resize_zoom); + default: + throw new IllegalArgumentException("Unrecognized resize mode: " + resizeMode); } } @NonNull - public static String cacheKeyOf(@NonNull final StreamInfo info, @NonNull VideoStream video) { + public static String cacheKeyOf(@NonNull final StreamInfo info, + @NonNull final VideoStream video) { return info.getUrl() + video.getResolution() + video.getFormat().getName(); } @NonNull - public static String cacheKeyOf(@NonNull final StreamInfo info, @NonNull AudioStream audio) { + public static String cacheKeyOf(@NonNull final StreamInfo info, + @NonNull final AudioStream audio) { return info.getUrl() + audio.getAverageBitrate() + audio.getFormat().getName(); } /** * Given a {@link StreamInfo} and the existing queue items, provide the * {@link SinglePlayQueue} consisting of the next video for auto queuing. - *

+ *

* This method detects and prevents cycle by naively checking if a * candidate next video's url already exists in the existing items. - *

+ *

+ *

* To select the next video, {@link StreamInfo#getNextVideo()} is first * checked. If it is nonnull and is not part of the existing items, then * it will be used as the next video. Otherwise, an random item with * non-repeating url will be selected from the {@link StreamInfo#getRelatedStreams()}. - * */ + *

+ * + * @param info currently playing stream + * @param existingItems existing items in the queue + * @return {@link SinglePlayQueue} with the next stream to queue + */ @Nullable public static PlayQueue autoQueueOf(@NonNull final StreamInfo info, @NonNull final List existingItems) { @@ -164,7 +177,9 @@ public class PlayerHelper { } final List relatedItems = info.getRelatedStreams(); - if (relatedItems == null) return null; + if (relatedItems == null) { + return null; + } List autoQueueItems = new ArrayList<>(); for (final InfoItem item : info.getRelatedStreams()) { @@ -173,7 +188,8 @@ public class PlayerHelper { } } Collections.shuffle(autoQueueItems); - return autoQueueItems.isEmpty() ? null : getAutoQueuedSinglePlayQueue(autoQueueItems.get(0)); + return autoQueueItems.isEmpty() + ? null : getAutoQueuedSinglePlayQueue(autoQueueItems.get(0)); } //////////////////////////////////////////////////////////////////////////// @@ -234,44 +250,43 @@ public class PlayerHelper { @NonNull public static SeekParameters getSeekParameters(@NonNull final Context context) { - return isUsingInexactSeek(context) ? - SeekParameters.CLOSEST_SYNC : SeekParameters.EXACT; + return isUsingInexactSeek(context) ? SeekParameters.CLOSEST_SYNC : SeekParameters.EXACT; } - public static long getPreferredCacheSize(@NonNull final Context context) { + public static long getPreferredCacheSize() { return 64 * 1024 * 1024L; } - public static long getPreferredFileSize(@NonNull final Context context) { + public static long getPreferredFileSize() { return 512 * 1024L; } /** - * Returns the number of milliseconds the player buffers for before starting playback. - * */ - public static int getPlaybackStartBufferMs(@NonNull final Context context) { + * @return the number of milliseconds the player buffers for before starting playback + */ + public static int getPlaybackStartBufferMs() { return 500; } /** - * Returns the minimum number of milliseconds the player always buffers to after starting - * playback. - * */ - public static int getPlaybackMinimumBufferMs(@NonNull final Context context) { + * @return the minimum number of milliseconds the player always buffers to + * after starting playback. + */ + public static int getPlaybackMinimumBufferMs() { return 25000; } /** - * Returns the maximum/optimal number of milliseconds the player will buffer to once the buffer - * hits the point of {@link #getPlaybackMinimumBufferMs(Context)}. - * */ - public static int getPlaybackOptimalBufferMs(@NonNull final Context context) { + * @return the maximum/optimal number of milliseconds the player will buffer to once the buffer + * hits the point of {@link #getPlaybackMinimumBufferMs()}. + */ + public static int getPlaybackOptimalBufferMs() { return 60000; } public static TrackSelection.Factory getQualitySelector(@NonNull final Context context) { return new AdaptiveTrackSelection.Factory( - /*bufferDurationRequiredForQualityIncrease=*/1000, + 1000, AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, AdaptiveTrackSelection.DEFAULT_BANDWIDTH_FRACTION); @@ -287,7 +302,9 @@ public class PlayerHelper { @NonNull public static CaptionStyleCompat getCaptionStyle(@NonNull final Context context) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return CaptionStyleCompat.DEFAULT; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + return CaptionStyleCompat.DEFAULT; + } final CaptioningManager captioningManager = (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE); @@ -299,14 +316,26 @@ public class PlayerHelper { } /** - * System font scaling: - * Very small - 0.25f, Small - 0.5f, Normal - 1.0f, Large - 1.5f, Very Large - 2.0f - * */ + * Get scaling for captions based on system font scaling. + *

Options:

+ *
    + *
  • Very small: 0.25f
  • + *
  • Small: 0.5f
  • + *
  • Normal: 1.0f
  • + *
  • Large: 1.5f
  • + *
  • Very large: 2.0f
  • + *
+ * + * @param context Android app context + * @return caption scaling + */ public static float getCaptionScale(@NonNull final Context context) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return 1.0f; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + return 1f; + } - final CaptioningManager captioningManager = (CaptioningManager) - context.getSystemService(Context.CAPTIONING_SERVICE); + final CaptioningManager captioningManager + = (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE); if (captioningManager == null || !captioningManager.isEnabled()) { return 1.0f; } @@ -319,7 +348,8 @@ public class PlayerHelper { return getScreenBrightness(context, -1); } - public static void setScreenBrightness(@NonNull final Context context, final float setScreenBrightness) { + public static void setScreenBrightness(@NonNull final Context context, + final float setScreenBrightness) { setScreenBrightness(context, setScreenBrightness, System.currentTimeMillis()); } @@ -344,53 +374,67 @@ public class PlayerHelper { return PreferenceManager.getDefaultSharedPreferences(context); } - private static boolean isResumeAfterAudioFocusGain(@NonNull final Context context, final boolean b) { - return getPreferences(context).getBoolean(context.getString(R.string.resume_on_audio_focus_gain_key), b); + private static boolean isResumeAfterAudioFocusGain(@NonNull final Context context, + final boolean b) { + return getPreferences(context) + .getBoolean(context.getString(R.string.resume_on_audio_focus_gain_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 isVolumeGestureEnabled(@NonNull final Context context, + final boolean b) { + return getPreferences(context) + .getBoolean(context.getString(R.string.volume_gesture_control_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 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) { - return getPreferences(context).getBoolean(context.getString(R.string.popup_remember_size_pos_key), b); + private static boolean isRememberingPopupDimensions(@NonNull final Context context, + final boolean b) { + return getPreferences(context) + .getBoolean(context.getString(R.string.popup_remember_size_pos_key), b); } private static boolean isUsingInexactSeek(@NonNull final Context context) { - return getPreferences(context).getBoolean(context.getString(R.string.use_inexact_seek_key), false); + return getPreferences(context) + .getBoolean(context.getString(R.string.use_inexact_seek_key), false); } private static boolean isAutoQueueEnabled(@NonNull final Context context, final boolean b) { return getPreferences(context).getBoolean(context.getString(R.string.auto_queue_key), b); } - private static void setScreenBrightness(@NonNull final Context context, final float screenBrightness, final long timestamp) { + private static void setScreenBrightness(@NonNull final Context context, + final float screenBrightness, final long timestamp) { SharedPreferences.Editor editor = getPreferences(context).edit(); editor.putFloat(context.getString(R.string.screen_brightness_key), screenBrightness); editor.putLong(context.getString(R.string.screen_brightness_timestamp_key), timestamp); editor.apply(); } - private static float getScreenBrightness(@NonNull final Context context, final float screenBrightness) { + private static float getScreenBrightness(@NonNull final Context context, + final float screenBrightness) { SharedPreferences sp = getPreferences(context); - long timestamp = sp.getLong(context.getString(R.string.screen_brightness_timestamp_key), 0); - // hypothesis: 4h covers a viewing block, eg evening. External lightning conditions will change in the next + long timestamp = sp + .getLong(context.getString(R.string.screen_brightness_timestamp_key), 0); + // Hypothesis: 4h covers a viewing block, e.g. evening. + // External lightning conditions will change in the next // viewing block so we fall back to the default brightness if ((System.currentTimeMillis() - timestamp) > TimeUnit.HOURS.toMillis(4)) { return screenBrightness; } else { - return sp.getFloat(context.getString(R.string.screen_brightness_key), screenBrightness); + return sp + .getFloat(context.getString(R.string.screen_brightness_key), screenBrightness); } } private static String getMinimizeOnExitAction(@NonNull final Context context, final String key) { - return getPreferences(context).getString(context.getString(R.string.minimize_on_exit_key), - key); + return getPreferences(context) + .getString(context.getString(R.string.minimize_on_exit_key), key); } private static String getAutoplayType(@NonNull final Context context, @@ -399,9 +443,19 @@ public class PlayerHelper { key); } - private static SinglePlayQueue getAutoQueuedSinglePlayQueue(StreamInfoItem streamInfoItem) { + private static SinglePlayQueue getAutoQueuedSinglePlayQueue( + final StreamInfoItem streamInfoItem) { SinglePlayQueue singlePlayQueue = new SinglePlayQueue(streamInfoItem); singlePlayQueue.getItem().setAutoQueued(true); return singlePlayQueue; } + + @Retention(SOURCE) + @IntDef({MINIMIZE_ON_EXIT_MODE_NONE, MINIMIZE_ON_EXIT_MODE_BACKGROUND, + MINIMIZE_ON_EXIT_MODE_POPUP}) + public @interface MinimizeMode { + int MINIMIZE_ON_EXIT_MODE_NONE = 0; + int MINIMIZE_ON_EXIT_MODE_BACKGROUND = 1; + int MINIMIZE_ON_EXIT_MODE_POPUP = 2; + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionCallback.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionCallback.java index 498fb4a88..883d9bb4f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionCallback.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionCallback.java @@ -4,13 +4,18 @@ import android.support.v4.media.MediaDescriptionCompat; public interface MediaSessionCallback { void onSkipToPrevious(); + void onSkipToNext(); - void onSkipToIndex(final int index); + + void onSkipToIndex(int index); int getCurrentPlayingIndex(); + int getQueueSize(); - MediaDescriptionCompat getQueueMetadata(final int index); + + MediaDescriptionCompat getQueueMetadata(int index); void onPlay(); + void onPause(); } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java index ab0de08be..1f1152b62 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java @@ -20,7 +20,6 @@ import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_T import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS; import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM; - public class PlayQueueNavigator implements MediaSessionConnector.QueueNavigator { public static final int DEFAULT_MAX_QUEUE_SIZE = 10; @@ -40,17 +39,17 @@ public class PlayQueueNavigator implements MediaSessionConnector.QueueNavigator } @Override - public long getSupportedQueueNavigatorActions(@Nullable Player player) { + public long getSupportedQueueNavigatorActions(@Nullable final Player player) { return ACTION_SKIP_TO_NEXT | ACTION_SKIP_TO_PREVIOUS | ACTION_SKIP_TO_QUEUE_ITEM; } @Override - public void onTimelineChanged(Player player) { + public void onTimelineChanged(final Player player) { publishFloatingQueueWindow(); } @Override - public void onCurrentWindowIndexChanged(Player player) { + public void onCurrentWindowIndexChanged(final Player player) { if (activeQueueItemId == MediaSessionCompat.QueueItem.UNKNOWN_ID || player.getCurrentTimeline().getWindowCount() > maxQueueSize) { publishFloatingQueueWindow(); @@ -60,22 +59,23 @@ public class PlayQueueNavigator implements MediaSessionConnector.QueueNavigator } @Override - public long getActiveQueueItemId(@Nullable Player player) { + public long getActiveQueueItemId(@Nullable final Player player) { return callback.getCurrentPlayingIndex(); } @Override - public void onSkipToPrevious(Player player, ControlDispatcher controlDispatcher) { + public void onSkipToPrevious(final Player player, final ControlDispatcher controlDispatcher) { callback.onSkipToPrevious(); } @Override - public void onSkipToQueueItem(Player player, ControlDispatcher controlDispatcher, long id) { + public void onSkipToQueueItem(final Player player, final ControlDispatcher controlDispatcher, + final long id) { callback.onSkipToIndex((int) id); } @Override - public void onSkipToNext(Player player, ControlDispatcher controlDispatcher) { + public void onSkipToNext(final Player player, final ControlDispatcher controlDispatcher) { callback.onSkipToNext(); } @@ -102,7 +102,8 @@ public class PlayQueueNavigator implements MediaSessionConnector.QueueNavigator } @Override - public boolean onCommand(Player player, ControlDispatcher controlDispatcher, String command, Bundle extras, ResultReceiver cb) { + public boolean onCommand(final Player player, final ControlDispatcher controlDispatcher, + final String command, final Bundle extras, final ResultReceiver cb) { return false; } } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueuePlaybackController.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueuePlaybackController.java index b7f0638e3..21c99859c 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueuePlaybackController.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueuePlaybackController.java @@ -12,7 +12,7 @@ public class PlayQueuePlaybackController extends DefaultControlDispatcher { } @Override - public boolean dispatchSetPlayWhenReady(Player player, boolean playWhenReady) { + public boolean dispatchSetPlayWhenReady(final Player player, final boolean playWhenReady) { if (playWhenReady) { callback.onPlay(); } else { diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java index b99047417..c09a44c08 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java @@ -1,8 +1,9 @@ package org.schabi.newpipe.player.mediasource; +import android.util.Log; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import android.util.Log; import com.google.android.exoplayer2.source.BaseMediaSource; import com.google.android.exoplayer2.source.MediaPeriod; @@ -15,32 +16,8 @@ import java.io.IOException; public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSource { private final String TAG = "FailedMediaSource@" + Integer.toHexString(hashCode()); - - public static class FailedMediaSourceException extends Exception { - FailedMediaSourceException(String message) { - super(message); - } - - FailedMediaSourceException(Throwable cause) { - super(cause); - } - } - - public static final class MediaSourceResolutionException extends FailedMediaSourceException { - public MediaSourceResolutionException(String message) { - super(message); - } - } - - public static final class StreamInfoLoadException extends FailedMediaSourceException { - public StreamInfoLoadException(Throwable cause) { - super(cause); - } - } - private final PlayQueueItem playQueueItem; private final FailedMediaSourceException error; - private final long retryTimestamp; public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem, @@ -54,7 +31,10 @@ public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSo /** * Permanently fail the play queue item associated with this source, with no hope of retrying. * The error will always be propagated to ExoPlayer. - * */ + * + * @param playQueueItem play queue item + * @param error exception that was the reason to fail + */ public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem, @NonNull final FailedMediaSourceException error) { this.playQueueItem = playQueueItem; @@ -80,21 +60,21 @@ public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSo } @Override - public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { + public MediaPeriod createPeriod(final MediaPeriodId id, final Allocator allocator, + final long startPositionUs) { return null; } @Override - public void releasePeriod(MediaPeriod mediaPeriod) {} - + public void releasePeriod(final MediaPeriod mediaPeriod) { } @Override - protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected void prepareSourceInternal(@Nullable final TransferListener mediaTransferListener) { Log.e(TAG, "Loading failed source: ", error); } @Override - protected void releaseSourceInternal() {} + protected void releaseSourceInternal() { } @Override public boolean shouldBeReplacedWith(@NonNull final PlayQueueItem newIdentity, @@ -103,7 +83,29 @@ public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSo } @Override - public boolean isStreamEqual(@NonNull PlayQueueItem stream) { + public boolean isStreamEqual(@NonNull final PlayQueueItem stream) { return playQueueItem == stream; } + + public static class FailedMediaSourceException extends Exception { + FailedMediaSourceException(final String message) { + super(message); + } + + FailedMediaSourceException(final Throwable cause) { + super(cause); + } + } + + public static final class MediaSourceResolutionException extends FailedMediaSourceException { + public MediaSourceResolutionException(final String message) { + super(message); + } + } + + public static final class StreamInfoLoadException extends FailedMediaSourceException { + public StreamInfoLoadException(final Throwable cause) { + super(cause); + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java index 1519103c2..cdbf8609b 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java @@ -1,6 +1,7 @@ package org.schabi.newpipe.player.mediasource; import android.os.Handler; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -15,13 +16,11 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItem; import java.io.IOException; public class LoadedMediaSource implements ManagedMediaSource { - private final MediaSource source; private final PlayQueueItem stream; private final long expireTimestamp; - public LoadedMediaSource(@NonNull final MediaSource source, - @NonNull final PlayQueueItem stream, + public LoadedMediaSource(@NonNull final MediaSource source, @NonNull final PlayQueueItem stream, final long expireTimestamp) { this.source = source; this.stream = stream; @@ -37,8 +36,9 @@ public class LoadedMediaSource implements ManagedMediaSource { } @Override - public void prepareSource(SourceInfoRefreshListener listener, @Nullable TransferListener mediaTransferListener) { - source.prepareSource(listener, mediaTransferListener); + public void prepareSource(final MediaSourceCaller mediaSourceCaller, + @Nullable final TransferListener mediaTransferListener) { + source.prepareSource(mediaSourceCaller, mediaTransferListener); } @Override @@ -47,38 +47,50 @@ public class LoadedMediaSource implements ManagedMediaSource { } @Override - public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { + public void enable(final MediaSourceCaller caller) { + source.enable(caller); + } + + @Override + public MediaPeriod createPeriod(final MediaPeriodId id, final Allocator allocator, + final long startPositionUs) { return source.createPeriod(id, allocator, startPositionUs); } @Override - public void releasePeriod(MediaPeriod mediaPeriod) { + public void releasePeriod(final MediaPeriod mediaPeriod) { source.releasePeriod(mediaPeriod); } @Override - public void releaseSource(SourceInfoRefreshListener listener) { - source.releaseSource(listener); + public void disable(final MediaSourceCaller caller) { + source.disable(caller); } @Override - public void addEventListener(Handler handler, MediaSourceEventListener eventListener) { + public void releaseSource(final MediaSourceCaller mediaSourceCaller) { + source.releaseSource(mediaSourceCaller); + } + + @Override + public void addEventListener(final Handler handler, + final MediaSourceEventListener eventListener) { source.addEventListener(handler, eventListener); } @Override - public void removeEventListener(MediaSourceEventListener eventListener) { + public void removeEventListener(final MediaSourceEventListener eventListener) { source.removeEventListener(eventListener); } @Override - public boolean shouldBeReplacedWith(@NonNull PlayQueueItem newIdentity, + public boolean shouldBeReplacedWith(@NonNull final PlayQueueItem newIdentity, final boolean isInterruptable) { return newIdentity != stream || (isInterruptable && isExpired()); } @Override - public boolean isStreamEqual(@NonNull PlayQueueItem stream) { - return this.stream == stream; + public boolean isStreamEqual(@NonNull final PlayQueueItem otherStream) { + return this.stream == otherStream; } } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSource.java index b180ca9f2..21fddbe86 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSource.java @@ -1,6 +1,7 @@ package org.schabi.newpipe.player.mediasource; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.source.MediaSource; @@ -10,18 +11,27 @@ public interface ManagedMediaSource extends MediaSource { /** * Determines whether or not this {@link ManagedMediaSource} can be replaced. * - * @param newIdentity a stream the {@link ManagedMediaSource} should encapsulate over, if - * it is different from the existing stream in the - * {@link ManagedMediaSource}, then it should be replaced. + * @param newIdentity a stream the {@link ManagedMediaSource} should encapsulate over, if + * it is different from the existing stream in the + * {@link ManagedMediaSource}, then it should be replaced. * @param isInterruptable specifies if this {@link ManagedMediaSource} potentially * being played. - * */ - boolean shouldBeReplacedWith(@NonNull final PlayQueueItem newIdentity, - final boolean isInterruptable); + * @return whether this could be replaces + */ + boolean shouldBeReplacedWith(@NonNull PlayQueueItem newIdentity, boolean isInterruptable); /** * Determines if the {@link PlayQueueItem} is the one the * {@link ManagedMediaSource} encapsulates over. - * */ - boolean isStreamEqual(@NonNull final PlayQueueItem stream); + * + * @param stream play queue item to check + * @return whether this source is for the specified stream + */ + boolean isStreamEqual(@NonNull PlayQueueItem stream); + + @Nullable + @Override + default Object getTag() { + return this; + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSourcePlaylist.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSourcePlaylist.java index 76f097665..ff0cf21fa 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSourcePlaylist.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSourcePlaylist.java @@ -1,5 +1,7 @@ package org.schabi.newpipe.player.mediasource; + import android.os.Handler; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -7,7 +9,8 @@ import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.ShuffleOrder; public class ManagedMediaSourcePlaylist { - @NonNull private final ConcatenatingMediaSource internalSource; + @NonNull + private final ConcatenatingMediaSource internalSource; public ManagedMediaSourcePlaylist() { internalSource = new ConcatenatingMediaSource(/*isPlaylistAtomic=*/false, @@ -25,11 +28,14 @@ public class ManagedMediaSourcePlaylist { /** * Returns the {@link ManagedMediaSource} at the given index of the playlist. * If the index is invalid, then null is returned. - * */ + * + * @param index index of {@link ManagedMediaSource} to get from the playlist + * @return the {@link ManagedMediaSource} at the given index of the playlist + */ @Nullable public ManagedMediaSource get(final int index) { - return (index < 0 || index >= size()) ? - null : (ManagedMediaSource) internalSource.getMediaSource(index); + return (index < 0 || index >= size()) + ? null : (ManagedMediaSource) internalSource.getMediaSource(index).getTag(); } @NonNull @@ -46,15 +52,17 @@ public class ManagedMediaSourcePlaylist { * {@link PlaceholderMediaSource}. * * @see #append(ManagedMediaSource) - * */ + */ public synchronized void expand() { append(new PlaceholderMediaSource()); } /** * Appends a {@link ManagedMediaSource} to the end of {@link ConcatenatingMediaSource}. + * * @see ConcatenatingMediaSource#addMediaSource - * */ + * @param source {@link ManagedMediaSource} to append + */ public synchronized void append(@NonNull final ManagedMediaSource source) { internalSource.addMediaSource(source); } @@ -62,10 +70,14 @@ public class ManagedMediaSourcePlaylist { /** * Removes a {@link ManagedMediaSource} from {@link ConcatenatingMediaSource} * at the given index. If this index is out of bound, then the removal is ignored. + * * @see ConcatenatingMediaSource#removeMediaSource(int) - * */ + * @param index of {@link ManagedMediaSource} to be removed + */ public synchronized void remove(final int index) { - if (index < 0 || index > internalSource.getSize()) return; + if (index < 0 || index > internalSource.getSize()) { + return; + } internalSource.removeMediaSource(index); } @@ -74,11 +86,18 @@ public class ManagedMediaSourcePlaylist { * Moves a {@link ManagedMediaSource} in {@link ConcatenatingMediaSource} * from the given source index to the target index. If either index is out of bound, * then the call is ignored. + * * @see ConcatenatingMediaSource#moveMediaSource(int, int) - * */ + * @param source original index of {@link ManagedMediaSource} + * @param target new index of {@link ManagedMediaSource} + */ public synchronized void move(final int source, final int target) { - if (source < 0 || target < 0) return; - if (source >= internalSource.getSize() || target >= internalSource.getSize()) return; + if (source < 0 || target < 0) { + return; + } + if (source >= internalSource.getSize() || target >= internalSource.getSize()) { + return; + } internalSource.moveMediaSource(source, target); } @@ -86,20 +105,30 @@ public class ManagedMediaSourcePlaylist { /** * Invalidates the {@link ManagedMediaSource} at the given index by replacing it * with a {@link PlaceholderMediaSource}. + * * @see #update(int, ManagedMediaSource, Handler, Runnable) - * */ + * @param index index of {@link ManagedMediaSource} to invalidate + * @param handler the {@link Handler} to run {@code finalizingAction} + * @param finalizingAction a {@link Runnable} which is executed immediately + * after the media source has been removed from the playlist + */ public synchronized void invalidate(final int index, @Nullable final Handler handler, @Nullable final Runnable finalizingAction) { - if (get(index) instanceof PlaceholderMediaSource) return; + if (get(index) instanceof PlaceholderMediaSource) { + return; + } update(index, new PlaceholderMediaSource(), handler, finalizingAction); } /** * Updates the {@link ManagedMediaSource} in {@link ConcatenatingMediaSource} * at the given index with a given {@link ManagedMediaSource}. + * * @see #update(int, ManagedMediaSource, Handler, Runnable) - * */ + * @param index index of {@link ManagedMediaSource} to update + * @param source new {@link ManagedMediaSource} to use + */ public synchronized void update(final int index, @NonNull final ManagedMediaSource source) { update(index, source, null, /*doNothing=*/null); } @@ -108,13 +137,21 @@ public class ManagedMediaSourcePlaylist { * Updates the {@link ManagedMediaSource} in {@link ConcatenatingMediaSource} * at the given index with a given {@link ManagedMediaSource}. If the index is out of bound, * then the replacement is ignored. + * * @see ConcatenatingMediaSource#addMediaSource * @see ConcatenatingMediaSource#removeMediaSource(int, Handler, Runnable) - * */ + * @param index index of {@link ManagedMediaSource} to update + * @param source new {@link ManagedMediaSource} to use + * @param handler the {@link Handler} to run {@code finalizingAction} + * @param finalizingAction a {@link Runnable} which is executed immediately + * after the media source has been removed from the playlist + */ public synchronized void update(final int index, @NonNull final ManagedMediaSource source, @Nullable final Handler handler, @Nullable final Runnable finalizingAction) { - if (index < 0 || index >= internalSource.getSize()) return; + if (index < 0 || index >= internalSource.getSize()) { + return; + } // Add and remove are sequential on the same thread, therefore here, the exoplayer // message queue must receive and process add before remove, effectively treating them diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java index 48179aed5..f73a219d7 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java @@ -12,20 +12,32 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItem; public class PlaceholderMediaSource extends BaseMediaSource implements ManagedMediaSource { // Do nothing, so this will stall the playback - @Override public void maybeThrowSourceInfoRefreshError() {} - @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { return null; } - @Override public void releasePeriod(MediaPeriod mediaPeriod) {} - @Override protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {} - @Override protected void releaseSourceInternal() {} + @Override + public void maybeThrowSourceInfoRefreshError() { } @Override - public boolean shouldBeReplacedWith(@NonNull PlayQueueItem newIdentity, + public MediaPeriod createPeriod(final MediaPeriodId id, final Allocator allocator, + final long startPositionUs) { + return null; + } + + @Override + public void releasePeriod(final MediaPeriod mediaPeriod) { } + + @Override + protected void prepareSourceInternal(@Nullable final TransferListener mediaTransferListener) { } + + @Override + protected void releaseSourceInternal() { } + + @Override + public boolean shouldBeReplacedWith(@NonNull final PlayQueueItem newIdentity, final boolean isInterruptable) { return true; } @Override - public boolean isStreamEqual(@NonNull PlayQueueItem stream) { + public boolean isStreamEqual(@NonNull final PlayQueueItem stream) { return false; } } diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java b/app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java index 7b55629b8..0154716e0 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java @@ -27,25 +27,31 @@ public class BasePlayerMediaSession implements MediaSessionCallback { } @Override - public void onSkipToIndex(int index) { - if (player.getPlayQueue() == null) return; + public void onSkipToIndex(final int index) { + if (player.getPlayQueue() == null) { + return; + } player.onSelected(player.getPlayQueue().getItem(index)); } @Override public int getCurrentPlayingIndex() { - if (player.getPlayQueue() == null) return -1; + if (player.getPlayQueue() == null) { + return -1; + } return player.getPlayQueue().getIndex(); } @Override public int getQueueSize() { - if (player.getPlayQueue() == null) return -1; + if (player.getPlayQueue() == null) { + return -1; + } return player.getPlayQueue().size(); } @Override - public MediaDescriptionCompat getQueueMetadata(int index) { + public MediaDescriptionCompat getQueueMetadata(final int index) { if (player.getPlayQueue() == null || player.getPlayQueue().getItem(index) == null) { return null; } @@ -60,13 +66,17 @@ public class BasePlayerMediaSession implements MediaSessionCallback { Bundle additionalMetadata = new Bundle(); additionalMetadata.putString(MediaMetadataCompat.METADATA_KEY_TITLE, item.getTitle()); additionalMetadata.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, item.getUploader()); - additionalMetadata.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, item.getDuration() * 1000); + additionalMetadata + .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, item.getDuration() * 1000); additionalMetadata.putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, index + 1); - additionalMetadata.putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, player.getPlayQueue().size()); + additionalMetadata + .putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, player.getPlayQueue().size()); descriptionBuilder.setExtras(additionalMetadata); final Uri thumbnailUri = Uri.parse(item.getThumbnailUrl()); - if (thumbnailUri != null) descriptionBuilder.setIconUri(thumbnailUri); + if (thumbnailUri != null) { + descriptionBuilder.setIconUri(thumbnailUri); + } return descriptionBuilder.build(); } diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/CustomTrackSelector.java b/app/src/main/java/org/schabi/newpipe/player/playback/CustomTrackSelector.java index d51cf630d..e554059d9 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/CustomTrackSelector.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/CustomTrackSelector.java @@ -1,5 +1,6 @@ package org.schabi.newpipe.player.playback; +import android.content.Context; import android.text.TextUtils; import android.util.Pair; @@ -7,8 +8,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.RendererCapabilities.Capabilities; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; @@ -18,16 +19,21 @@ import com.google.android.exoplayer2.util.Assertions; /** * This class allows irregular text language labels for use when selecting text captions and * is mostly a copy-paste from {@link DefaultTrackSelector}. - * + *

* This is a hack and should be removed once ExoPlayer fixes language normalization to accept * a broader set of languages. - * */ + *

+ */ public class CustomTrackSelector extends DefaultTrackSelector { - private String preferredTextLanguage; - public CustomTrackSelector(TrackSelection.Factory adaptiveTrackSelectionFactory) { - super(adaptiveTrackSelectionFactory); + public CustomTrackSelector(final Context context, + final TrackSelection.Factory adaptiveTrackSelectionFactory) { + super(context, adaptiveTrackSelectionFactory); + } + + private static boolean formatHasLanguage(final Format format, final String language) { + return language != null && TextUtils.equals(language, format.language); } public String getPreferredTextLanguage() { @@ -42,40 +48,36 @@ public class CustomTrackSelector extends DefaultTrackSelector { } } - private static boolean formatHasLanguage(Format format, String language) { - return language != null && TextUtils.equals(language, format.language); - } - @Override @Nullable protected Pair selectTextTrack( - TrackGroupArray groups, - int[][] formatSupport, - Parameters params, - @Nullable String selectedAudioLanguage) - throws ExoPlaybackException { + final TrackGroupArray groups, + @NonNull final int[][] formatSupport, + @NonNull final Parameters params, + @Nullable final String selectedAudioLanguage) { TrackGroup selectedGroup = null; int selectedTrackIndex = C.INDEX_UNSET; - int newPipeTrackScore = 0; TextTrackScore selectedTrackScore = null; + for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { TrackGroup trackGroup = groups.get(groupIndex); - int[] trackFormatSupport = formatSupport[groupIndex]; + @Capabilities int[] trackFormatSupport = formatSupport[groupIndex]; + for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { if (isSupported(trackFormatSupport[trackIndex], params.exceedRendererCapabilitiesIfNecessary)) { Format format = trackGroup.getFormat(trackIndex); - TextTrackScore trackScore = - new TextTrackScore( - format, params, trackFormatSupport[trackIndex], selectedAudioLanguage); + TextTrackScore trackScore = new TextTrackScore(format, params, + trackFormatSupport[trackIndex], selectedAudioLanguage); + if (formatHasLanguage(format, preferredTextLanguage)) { selectedGroup = trackGroup; selectedTrackIndex = trackIndex; selectedTrackScore = trackScore; - // found user selected match (perfect!) - break; - } else if (trackScore.isWithinConstraints - && (selectedTrackScore == null || trackScore.compareTo(selectedTrackScore) > 0)) { + break; // found user selected match (perfect!) + + } else if (trackScore.isWithinConstraints && (selectedTrackScore == null + || trackScore.compareTo(selectedTrackScore) > 0)) { selectedGroup = trackGroup; selectedTrackIndex = trackIndex; selectedTrackScore = trackScore; @@ -83,10 +85,8 @@ public class CustomTrackSelector extends DefaultTrackSelector { } } } - return selectedGroup == null - ? null - : Pair.create( - new TrackSelection.Definition(selectedGroup, selectedTrackIndex), - Assertions.checkNotNull(selectedTrackScore)); + return selectedGroup == null ? null + : Pair.create(new TrackSelection.Definition(selectedGroup, selectedTrackIndex), + Assertions.checkNotNull(selectedTrackScore)); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java index 85c852f57..23e813c4b 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java @@ -1,9 +1,11 @@ package org.schabi.newpipe.player.playback; + import android.os.Handler; +import android.util.Log; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.collection.ArraySet; -import android.util.Log; import com.google.android.exoplayer2.source.MediaSource; @@ -42,50 +44,20 @@ import static org.schabi.newpipe.player.mediasource.FailedMediaSource.StreamInfo import static org.schabi.newpipe.player.playqueue.PlayQueue.DEBUG; public class MediaSourceManager { - @NonNull private final String TAG = "MediaSourceManager@" + hashCode(); + @NonNull + private final String TAG = "MediaSourceManager@" + hashCode(); /** * Determines how many streams before and after the current stream should be loaded. * The default value (1) ensures seamless playback under typical network settings. - *

+ *

* The streams after the current will be loaded into the playlist timeline while the * streams before will only be cached for future usage. + *

* * @see #onMediaSourceReceived(PlayQueueItem, ManagedMediaSource) - * */ - private final static int WINDOW_SIZE = 1; - - @NonNull private final PlaybackListener playbackListener; - @NonNull private final PlayQueue playQueue; - - /** - * Determines the gap time between the playback position and the playback duration which - * the {@link #getEdgeIntervalSignal()} begins to request loading. - * - * @see #progressUpdateIntervalMillis - * */ - private final long playbackNearEndGapMillis; - /** - * Determines the interval which the {@link #getEdgeIntervalSignal()} waits for between - * each request for loading, once {@link #playbackNearEndGapMillis} has reached. - * */ - private final long progressUpdateIntervalMillis; - @NonNull private final Observable nearEndIntervalSignal; - - /** - * Process only the last load order when receiving a stream of load orders (lessens I/O). - *

- * The higher it is, the less loading occurs during rapid noncritical timeline changes. - *

- * Not recommended to go below 100ms. - * - * @see #loadDebounced() - * */ - private final long loadDebounceMillis; - @NonNull private final Disposable debouncedLoader; - @NonNull private final PublishSubject debouncedSignal; - - @NonNull private Subscription playQueueReactor; + */ + private static final int WINDOW_SIZE = 1; /** * Determines the maximum number of disposables allowed in the {@link #loaderReactor}. @@ -94,20 +66,68 @@ public class MediaSourceManager { * * @see #loadImmediate() * @see #maybeLoadItem(PlayQueueItem) - * */ - private final static int MAXIMUM_LOADER_SIZE = WINDOW_SIZE * 2 + 1; - @NonNull private final CompositeDisposable loaderReactor; - @NonNull private final Set loadingItems; + */ + private static final int MAXIMUM_LOADER_SIZE = WINDOW_SIZE * 2 + 1; - @NonNull private final AtomicBoolean isBlocked; + @NonNull + private final PlaybackListener playbackListener; + @NonNull + private final PlayQueue playQueue; - @NonNull private ManagedMediaSourcePlaylist playlist; + /** + * Determines the gap time between the playback position and the playback duration which + * the {@link #getEdgeIntervalSignal()} begins to request loading. + * + * @see #progressUpdateIntervalMillis + */ + private final long playbackNearEndGapMillis; + + /** + * Determines the interval which the {@link #getEdgeIntervalSignal()} waits for between + * each request for loading, once {@link #playbackNearEndGapMillis} has reached. + */ + private final long progressUpdateIntervalMillis; + + @NonNull + private final Observable nearEndIntervalSignal; + + /** + * Process only the last load order when receiving a stream of load orders (lessens I/O). + *

+ * The higher it is, the less loading occurs during rapid noncritical timeline changes. + *

+ *

+ * Not recommended to go below 100ms. + *

+ * + * @see #loadDebounced() + */ + private final long loadDebounceMillis; + + @NonNull + private final Disposable debouncedLoader; + @NonNull + private final PublishSubject debouncedSignal; + + @NonNull + private Subscription playQueueReactor; + + @NonNull + private final CompositeDisposable loaderReactor; + @NonNull + private final Set loadingItems; + + @NonNull + private final AtomicBoolean isBlocked; + + @NonNull + private ManagedMediaSourcePlaylist playlist; private Handler removeMediaSourceHandler = new Handler(); public MediaSourceManager(@NonNull final PlaybackListener listener, @NonNull final PlayQueue playQueue) { - this(listener, playQueue, /*loadDebounceMillis=*/400L, + this(listener, playQueue, 400L, /*playbackNearEndGapMillis=*/TimeUnit.MILLISECONDS.convert(30, TimeUnit.SECONDS), /*progressUpdateIntervalMillis*/TimeUnit.MILLISECONDS.convert(2, TimeUnit.SECONDS)); } @@ -121,9 +141,9 @@ public class MediaSourceManager { throw new IllegalArgumentException("Play Queue has not been initialized."); } if (playbackNearEndGapMillis < progressUpdateIntervalMillis) { - throw new IllegalArgumentException("Playback end gap=[" + playbackNearEndGapMillis + - " ms] must be longer than update interval=[ " + progressUpdateIntervalMillis + - " ms] for them to be useful."); + throw new IllegalArgumentException("Playback end gap=[" + playbackNearEndGapMillis + + " ms] must be longer than update interval=[ " + progressUpdateIntervalMillis + + " ms] for them to be useful."); } this.playbackListener = listener; @@ -154,11 +174,14 @@ public class MediaSourceManager { /*////////////////////////////////////////////////////////////////////////// // Exposed Methods //////////////////////////////////////////////////////////////////////////*/ + /** * Dispose the manager and releases all message buses and loaders. - * */ + */ public void dispose() { - if (DEBUG) Log.d(TAG, "close() called."); + if (DEBUG) { + Log.d(TAG, "close() called."); + } debouncedSignal.onComplete(); debouncedLoader.dispose(); @@ -174,22 +197,22 @@ public class MediaSourceManager { private Subscriber getReactor() { return new Subscriber() { @Override - public void onSubscribe(@NonNull Subscription d) { + public void onSubscribe(@NonNull final Subscription d) { playQueueReactor.cancel(); playQueueReactor = d; playQueueReactor.request(1); } @Override - public void onNext(@NonNull PlayQueueEvent playQueueMessage) { + public void onNext(@NonNull final PlayQueueEvent playQueueMessage) { onPlayQueueChanged(playQueueMessage); } @Override - public void onError(@NonNull Throwable e) {} + public void onError(@NonNull final Throwable e) { } @Override - public void onComplete() {} + public void onComplete() { } }; } @@ -264,19 +287,27 @@ public class MediaSourceManager { } private boolean isPlaybackReady() { - if (playlist.size() != playQueue.size()) return false; + if (playlist.size() != playQueue.size()) { + return false; + } final ManagedMediaSource mediaSource = playlist.get(playQueue.getIndex()); - if (mediaSource == null) return false; + if (mediaSource == null) { + return false; + } final PlayQueueItem playQueueItem = playQueue.getItem(); return mediaSource.isStreamEqual(playQueueItem); } private void maybeBlock() { - if (DEBUG) Log.d(TAG, "maybeBlock() called."); + if (DEBUG) { + Log.d(TAG, "maybeBlock() called."); + } - if (isBlocked.get()) return; + if (isBlocked.get()) { + return; + } playbackListener.onPlaybackBlock(); resetSources(); @@ -285,7 +316,9 @@ public class MediaSourceManager { } private void maybeUnblock() { - if (DEBUG) Log.d(TAG, "maybeUnblock() called."); + if (DEBUG) { + Log.d(TAG, "maybeUnblock() called."); + } if (isBlocked.get()) { isBlocked.set(false); @@ -298,10 +331,14 @@ public class MediaSourceManager { //////////////////////////////////////////////////////////////////////////*/ private void maybeSync() { - if (DEBUG) Log.d(TAG, "maybeSync() called."); + if (DEBUG) { + Log.d(TAG, "maybeSync() called."); + } final PlayQueueItem currentItem = playQueue.getItem(); - if (isBlocked.get() || currentItem == null) return; + if (isBlocked.get() || currentItem == null) { + return; + } playbackListener.onPlaybackSynchronize(currentItem); } @@ -318,7 +355,8 @@ public class MediaSourceManager { //////////////////////////////////////////////////////////////////////////*/ private Observable getEdgeIntervalSignal() { - return Observable.interval(progressUpdateIntervalMillis, TimeUnit.MILLISECONDS) + return Observable.interval(progressUpdateIntervalMillis, + TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread()) .filter(ignored -> playbackListener.isApproachingPlaybackEdge(playbackNearEndGapMillis)); } @@ -336,9 +374,13 @@ public class MediaSourceManager { } private void loadImmediate() { - if (DEBUG) Log.d(TAG, "MediaSource - loadImmediate() called"); + if (DEBUG) { + Log.d(TAG, "MediaSource - loadImmediate() called"); + } final ItemsToLoad itemsToLoad = getItemsToLoad(playQueue); - if (itemsToLoad == null) return; + if (itemsToLoad == null) { + return; + } // Evict the previous items being loaded to free up memory, before start loading new ones maybeClearLoaders(); @@ -350,12 +392,18 @@ public class MediaSourceManager { } private void maybeLoadItem(@NonNull final PlayQueueItem item) { - if (DEBUG) Log.d(TAG, "maybeLoadItem() called."); - if (playQueue.indexOf(item) >= playlist.size()) return; + if (DEBUG) { + Log.d(TAG, "maybeLoadItem() called."); + } + if (playQueue.indexOf(item) >= playlist.size()) { + return; + } if (!loadingItems.contains(item) && isCorrectionNeeded(item)) { - if (DEBUG) Log.d(TAG, "MediaSource - Loading=[" + item.getTitle() + - "] with url=[" + item.getUrl() + "]"); + if (DEBUG) { + Log.d(TAG, "MediaSource - Loading=[" + item.getTitle() + "] " + + "with url=[" + item.getUrl() + "]"); + } loadingItems.add(item); final Disposable loader = getLoadedMediaSource(item) @@ -370,16 +418,16 @@ public class MediaSourceManager { return stream.getStream().map(streamInfo -> { final MediaSource source = playbackListener.sourceOf(stream, streamInfo); if (source == null) { - final String message = "Unable to resolve source from stream info." + - " URL: " + stream.getUrl() + - ", audio count: " + streamInfo.getAudioStreams().size() + - ", video count: " + streamInfo.getVideoOnlyStreams().size() + - streamInfo.getVideoStreams().size(); + final String message = "Unable to resolve source from stream info. " + + "URL: " + stream.getUrl() + ", " + + "audio count: " + streamInfo.getAudioStreams().size() + ", " + + "video count: " + streamInfo.getVideoOnlyStreams().size() + ", " + + streamInfo.getVideoStreams().size(); return new FailedMediaSource(stream, new MediaSourceResolutionException(message)); } - final long expiration = System.currentTimeMillis() + - ServiceHelper.getCacheExpirationMillis(streamInfo.getServiceId()); + final long expiration = System.currentTimeMillis() + + ServiceHelper.getCacheExpirationMillis(streamInfo.getServiceId()); return new LoadedMediaSource(source, stream, expiration); }).onErrorReturn(throwable -> new FailedMediaSource(stream, new StreamInfoLoadException(throwable))); @@ -387,17 +435,22 @@ public class MediaSourceManager { private void onMediaSourceReceived(@NonNull final PlayQueueItem item, @NonNull final ManagedMediaSource mediaSource) { - if (DEBUG) Log.d(TAG, "MediaSource - Loaded=[" + item.getTitle() + - "] with url=[" + item.getUrl() + "]"); + if (DEBUG) { + Log.d(TAG, "MediaSource - Loaded=[" + item.getTitle() + + "] with url=[" + item.getUrl() + "]"); + } loadingItems.remove(item); final int itemIndex = playQueue.indexOf(item); // Only update the playlist timeline for items at the current index or after. if (isCorrectionNeeded(item)) { - if (DEBUG) Log.d(TAG, "MediaSource - Updating index=[" + itemIndex + "] with " + - "title=[" + item.getTitle() + "] at url=[" + item.getUrl() + "]"); - playlist.update(itemIndex, mediaSource, removeMediaSourceHandler, this::maybeSynchronizePlayer); + if (DEBUG) { + Log.d(TAG, "MediaSource - Updating index=[" + itemIndex + "] with " + + "title=[" + item.getTitle() + "] at url=[" + item.getUrl() + "]"); + } + playlist.update(itemIndex, mediaSource, removeMediaSourceHandler, + this::maybeSynchronizePlayer); } } @@ -406,17 +459,21 @@ public class MediaSourceManager { * {@link com.google.android.exoplayer2.source.ConcatenatingMediaSource} * for a given {@link PlayQueueItem} needs replacement, either due to gapless playback * readiness or playlist desynchronization. - *

+ *

* If the given {@link PlayQueueItem} is currently being played and is already loaded, * then correction is not only needed if the playlist is desynchronized. Otherwise, the * check depends on the status (e.g. expiration or placeholder) of the * {@link ManagedMediaSource}. - * */ + *

+ * + * @param item {@link PlayQueueItem} to check + * @return whether a correction is needed + */ private boolean isCorrectionNeeded(@NonNull final PlayQueueItem item) { final int index = playQueue.indexOf(item); final ManagedMediaSource mediaSource = playlist.get(index); return mediaSource != null && mediaSource.shouldBeReplacedWith(item, - /*mightBeInProgress=*/index != playQueue.getIndex()); + index != playQueue.getIndex()); } /** @@ -429,42 +486,53 @@ public class MediaSourceManager { *

* Under both cases, {@link #maybeSync()} will be called to ensure the listener * is up-to-date. - * */ + */ private void maybeRenewCurrentIndex() { final int currentIndex = playQueue.getIndex(); final ManagedMediaSource currentSource = playlist.get(currentIndex); - if (currentSource == null) return; + if (currentSource == null) { + return; + } final PlayQueueItem currentItem = playQueue.getItem(); - if (!currentSource.shouldBeReplacedWith(currentItem, /*canInterruptOnRenew=*/true)) { + if (!currentSource.shouldBeReplacedWith(currentItem, true)) { maybeSynchronizePlayer(); return; } - if (DEBUG) Log.d(TAG, "MediaSource - Reloading currently playing, " + - "index=[" + currentIndex + "], item=[" + currentItem.getTitle() + "]"); + if (DEBUG) { + Log.d(TAG, "MediaSource - Reloading currently playing, " + + "index=[" + currentIndex + "], item=[" + currentItem.getTitle() + "]"); + } playlist.invalidate(currentIndex, removeMediaSourceHandler, this::loadImmediate); } private void maybeClearLoaders() { - if (DEBUG) Log.d(TAG, "MediaSource - maybeClearLoaders() called."); - if (!loadingItems.contains(playQueue.getItem()) && - loaderReactor.size() > MAXIMUM_LOADER_SIZE) { + if (DEBUG) { + Log.d(TAG, "MediaSource - maybeClearLoaders() called."); + } + if (!loadingItems.contains(playQueue.getItem()) + && loaderReactor.size() > MAXIMUM_LOADER_SIZE) { loaderReactor.clear(); loadingItems.clear(); } } + /*////////////////////////////////////////////////////////////////////////// // MediaSource Playlist Helpers //////////////////////////////////////////////////////////////////////////*/ private void resetSources() { - if (DEBUG) Log.d(TAG, "resetSources() called."); + if (DEBUG) { + Log.d(TAG, "resetSources() called."); + } playlist = new ManagedMediaSourcePlaylist(); } private void populateSources() { - if (DEBUG) Log.d(TAG, "populateSources() called."); + if (DEBUG) { + Log.d(TAG, "populateSources() called."); + } while (playlist.size() < playQueue.size()) { playlist.expand(); } @@ -473,12 +541,15 @@ public class MediaSourceManager { /*////////////////////////////////////////////////////////////////////////// // Manager Helpers //////////////////////////////////////////////////////////////////////////*/ + @Nullable private static ItemsToLoad getItemsToLoad(@NonNull final PlayQueue playQueue) { // The current item has higher priority final int currentIndex = playQueue.getIndex(); final PlayQueueItem currentItem = playQueue.getItem(currentIndex); - if (currentItem == null) return null; + if (currentItem == null) { + return null; + } // The rest are just for seamless playback // Although timeline is not updated prior to the current index, these sources are still @@ -487,12 +558,13 @@ public class MediaSourceManager { final int rightLimit = currentIndex + MediaSourceManager.WINDOW_SIZE + 1; final int rightBound = Math.min(playQueue.size(), rightLimit); final Set neighbors = new ArraySet<>( - playQueue.getStreams().subList(leftBound,rightBound)); + playQueue.getStreams().subList(leftBound, rightBound)); // Do a round robin final int excess = rightLimit - playQueue.size(); if (excess >= 0) { - neighbors.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess))); + neighbors.addAll(playQueue.getStreams() + .subList(0, Math.min(playQueue.size(), excess))); } neighbors.remove(currentItem); @@ -500,8 +572,10 @@ public class MediaSourceManager { } private static class ItemsToLoad { - @NonNull final private PlayQueueItem center; - @NonNull final private Collection neighbors; + @NonNull + private final PlayQueueItem center; + @NonNull + private final Collection neighbors; ItemsToLoad(@NonNull final PlayQueueItem center, @NonNull final Collection neighbors) { diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java b/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java index 9682ea15e..0755bdd7a 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java @@ -9,57 +9,72 @@ import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.player.playqueue.PlayQueueItem; public interface PlaybackListener { - /** * Called to check if the currently playing stream is approaching the end of its playback. * Implementation should return true when the current playback position is progressing within * timeToEndMillis or less to its playback during. - * + *

* May be called at any time. - * */ - boolean isApproachingPlaybackEdge(final long timeToEndMillis); + *

+ * + * @param timeToEndMillis + * @return whether the stream is approaching end of playback + */ + boolean isApproachingPlaybackEdge(long timeToEndMillis); /** * Called when the stream at the current queue index is not ready yet. * Signals to the listener to block the player from playing anything and notify the source * is now invalid. - * + *

* May be called at any time. - * */ + *

+ */ void onPlaybackBlock(); /** * Called when the stream at the current queue index is ready. * Signals to the listener to resume the player by preparing a new source. - * + *

* May be called only when the player is blocked. - * */ - void onPlaybackUnblock(final MediaSource mediaSource); + *

+ * + * @param mediaSource + */ + void onPlaybackUnblock(MediaSource mediaSource); /** * Called when the queue index is refreshed. * Signals to the listener to synchronize the player's window to the manager's * window. - * + *

* May be called anytime at any amount once unblock is called. - * */ - void onPlaybackSynchronize(@NonNull final PlayQueueItem item); + *

+ * + * @param item + */ + void onPlaybackSynchronize(@NonNull PlayQueueItem item); /** * Requests the listener to resolve a stream info into a media source * according to the listener's implementation (background, popup or main video player). - * + *

* May be called at any time. - * */ + *

+ * @param item + * @param info + * @return the corresponding {@link MediaSource} + */ @Nullable - MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info); + MediaSource sourceOf(PlayQueueItem item, StreamInfo info); /** * Called when the play queue can no longer to played or used. * Currently, this means the play queue is empty and complete. * Signals to the listener that it should shutdown. - * + *

* May be called at any time. - * */ + *

+ */ void onPlaybackShutdown(); } diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/AbstractInfoPlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/AbstractInfoPlayQueue.java index 676c0ca72..cde376f4f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/AbstractInfoPlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/AbstractInfoPlayQueue.java @@ -5,6 +5,7 @@ import android.util.Log; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.ListInfo; +import org.schabi.newpipe.extractor.Page; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import java.util.ArrayList; @@ -17,34 +18,31 @@ import io.reactivex.disposables.Disposable; abstract class AbstractInfoPlayQueue extends PlayQueue { boolean isInitial; - boolean isComplete; + private boolean isComplete; final int serviceId; final String baseUrl; - String nextUrl; + Page nextPage; - transient Disposable fetchReactor; + private transient Disposable fetchReactor; AbstractInfoPlayQueue(final U item) { this(item.getServiceId(), item.getUrl(), null, Collections.emptyList(), 0); } - AbstractInfoPlayQueue(final int serviceId, - final String url, - final String nextPageUrl, - final List streams, - final int index) { + AbstractInfoPlayQueue(final int serviceId, final String url, final Page nextPage, + final List streams, final int index) { super(index, extractListItems(streams)); this.baseUrl = url; - this.nextUrl = nextPageUrl; + this.nextPage = nextPage; this.serviceId = serviceId; this.isInitial = streams.isEmpty(); - this.isComplete = !isInitial && (nextPageUrl == null || nextPageUrl.isEmpty()); + this.isComplete = !isInitial && !Page.isValid(nextPage); } - abstract protected String getTag(); + protected abstract String getTag(); @Override public boolean isComplete() { @@ -54,8 +52,9 @@ abstract class AbstractInfoPlayQueue ext SingleObserver getHeadListObserver() { return new SingleObserver() { @Override - public void onSubscribe(@NonNull Disposable d) { - if (isComplete || !isInitial || (fetchReactor != null && !fetchReactor.isDisposed())) { + public void onSubscribe(@NonNull final Disposable d) { + if (isComplete || !isInitial || (fetchReactor != null + && !fetchReactor.isDisposed())) { d.dispose(); } else { fetchReactor = d; @@ -63,10 +62,12 @@ abstract class AbstractInfoPlayQueue ext } @Override - public void onSuccess(@NonNull T result) { + public void onSuccess(@NonNull final T result) { isInitial = false; - if (!result.hasNextPage()) isComplete = true; - nextUrl = result.getNextPageUrl(); + if (!result.hasNextPage()) { + isComplete = true; + } + nextPage = result.getNextPage(); append(extractListItems(result.getRelatedItems())); @@ -75,7 +76,7 @@ abstract class AbstractInfoPlayQueue ext } @Override - public void onError(@NonNull Throwable e) { + public void onError(@NonNull final Throwable e) { Log.e(getTag(), "Error fetching more playlist, marking playlist as complete.", e); isComplete = true; append(); // Notify change @@ -86,8 +87,9 @@ abstract class AbstractInfoPlayQueue ext SingleObserver getNextPageObserver() { return new SingleObserver() { @Override - public void onSubscribe(@NonNull Disposable d) { - if (isComplete || isInitial || (fetchReactor != null && !fetchReactor.isDisposed())) { + public void onSubscribe(@NonNull final Disposable d) { + if (isComplete || isInitial || (fetchReactor != null + && !fetchReactor.isDisposed())) { d.dispose(); } else { fetchReactor = d; @@ -95,9 +97,11 @@ abstract class AbstractInfoPlayQueue ext } @Override - public void onSuccess(@NonNull ListExtractor.InfoItemsPage result) { - if (!result.hasNextPage()) isComplete = true; - nextUrl = result.getNextPageUrl(); + public void onSuccess(@NonNull final ListExtractor.InfoItemsPage result) { + if (!result.hasNextPage()) { + isComplete = true; + } + nextPage = result.getNextPage(); append(extractListItems(result.getItems())); @@ -106,7 +110,7 @@ abstract class AbstractInfoPlayQueue ext } @Override - public void onError(@NonNull Throwable e) { + public void onError(@NonNull final Throwable e) { Log.e(getTag(), "Error fetching more playlist, marking playlist as complete.", e); isComplete = true; append(); // Notify change @@ -117,7 +121,9 @@ abstract class AbstractInfoPlayQueue ext @Override public void dispose() { super.dispose(); - if (fetchReactor != null) fetchReactor.dispose(); + if (fetchReactor != null) { + fetchReactor.dispose(); + } fetchReactor = null; } diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/ChannelPlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/ChannelPlayQueue.java index 5a2e34d31..9e0d2b694 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/ChannelPlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/ChannelPlayQueue.java @@ -1,6 +1,7 @@ package org.schabi.newpipe.player.playqueue; +import org.schabi.newpipe.extractor.Page; import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.channel.ChannelInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; @@ -17,15 +18,15 @@ public final class ChannelPlayQueue extends AbstractInfoPlayQueue streams, final int index) { - super(serviceId, url, nextPageUrl, streams, index); + super(serviceId, url, nextPage, streams, index); } @Override @@ -41,7 +42,7 @@ public final class ChannelPlayQueue extends AbstractInfoPlayQueue * This class contains basic manipulation of a playlist while also functions as a * message bus, providing all listeners with new updates to the play queue. - * + *

+ *

* This class can be serialized for passing intents, but in order to start the * message bus, it must be initialized. - * */ + *

+ */ public abstract class PlayQueue implements Serializable { private final String TAG = "PlayQueue@" + Integer.toHexString(hashCode()); - public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release"); private ArrayList backup; private ArrayList streams; + + @NonNull + private final AtomicInteger queueIndex; private final ArrayList history; - @NonNull private final AtomicInteger queueIndex; private transient BehaviorSubject eventBroadcast; private transient Flowable broadcastReceiver; @@ -71,9 +75,10 @@ public abstract class PlayQueue implements Serializable { /** * Initializes the play queue message buses. - * + *

* Also starts a self reporter for logging if debug mode is enabled. - * */ + *

+ */ public void init() { eventBroadcast = BehaviorSubject.create(); @@ -81,15 +86,21 @@ public abstract class PlayQueue implements Serializable { .observeOn(AndroidSchedulers.mainThread()) .startWith(new InitEvent()); - if (DEBUG) broadcastReceiver.subscribe(getSelfReporter()); + if (DEBUG) { + broadcastReceiver.subscribe(getSelfReporter()); + } } /** * Dispose the play queue by stopping all message buses. - * */ + */ public void dispose() { - if (eventBroadcast != null) eventBroadcast.onComplete(); - if (reportingReactor != null) reportingReactor.cancel(); + if (eventBroadcast != null) { + eventBroadcast.onComplete(); + } + if (reportingReactor != null) { + reportingReactor.cancel(); + } eventBroadcast = null; broadcastReceiver = null; @@ -99,15 +110,18 @@ public abstract class PlayQueue implements Serializable { /** * Checks if the queue is complete. - * + *

* A queue is complete if it has loaded all items in an external playlist * single stream or local queues are always complete. - * */ + *

+ * + * @return whether the queue is complete + */ public abstract boolean isComplete(); /** * Load partial queue in the background, does nothing if the queue is complete. - * */ + */ public abstract void fetch(); /*////////////////////////////////////////////////////////////////////////// @@ -115,93 +129,33 @@ public abstract class PlayQueue implements Serializable { //////////////////////////////////////////////////////////////////////////*/ /** - * Returns the current index that should be played. - * */ + * @return the current index that should be played + */ public int getIndex() { return queueIndex.get(); } - /** - * Returns the current item that should be played. - * */ - public PlayQueueItem getItem() { - return getItem(getIndex()); - } - - /** - * Returns the item at the given index. - * May throw {@link IndexOutOfBoundsException}. - * */ - public PlayQueueItem getItem(final int index) { - if (index < 0 || index >= streams.size() || streams.get(index) == null) return null; - return streams.get(index); - } - - /** - * Returns the index of the given item using referential equality. - * May be null despite play queue contains identical item. - * */ - public int indexOf(@NonNull final PlayQueueItem item) { - // referential equality, can't think of a better way to do this - // todo: better than this - return streams.indexOf(item); - } - - /** - * Returns the current size of play queue. - * */ - public int size() { - return streams.size(); - } - - /** - * Checks if the play queue is empty. - * */ - public boolean isEmpty() { - return streams.isEmpty(); - } - - /** - * Determines if the current play queue is shuffled. - * */ - public boolean isShuffled() { - return backup != null; - } - - /** - * Returns an immutable view of the play queue. - * */ - @NonNull - public List getStreams() { - return Collections.unmodifiableList(streams); - } - - /** - * Returns the play queue's update broadcast. - * May be null if the play queue message bus is not initialized. - * */ - @Nullable - public Flowable getBroadcastReceiver() { - return broadcastReceiver; - } - - /*////////////////////////////////////////////////////////////////////////// - // Write ops - //////////////////////////////////////////////////////////////////////////*/ - /** * Changes the current playing index to a new index. - * + *

* This method is guarded using in a circular manner for index exceeding the play queue size. - * + *

+ *

* Will emit a {@link SelectEvent} if the index is not the current playing index. - * */ + *

+ * + * @param index the index to be set + */ public synchronized void setIndex(final int index) { final int oldIndex = getIndex(); int newIndex = index; - if (index < 0) newIndex = 0; - if (index >= streams.size()) newIndex = isComplete() ? index % streams.size() : streams.size() - 1; + if (index < 0) { + newIndex = 0; + } + if (index >= streams.size()) { + newIndex = isComplete() ? index % streams.size() : streams.size() - 1; + } if (oldIndex != newIndex) history.add(streams.get(newIndex)); queueIndex.set(newIndex); @@ -209,10 +163,93 @@ public abstract class PlayQueue implements Serializable { } /** - * Changes the current playing index by an offset amount. + * @return the current item that should be played + */ + public PlayQueueItem getItem() { + return getItem(getIndex()); + } + + /** + * @param index the index of the item to return + * @return the item at the given index + * @throws IndexOutOfBoundsException + */ + public PlayQueueItem getItem(final int index) { + if (index < 0 || index >= streams.size() || streams.get(index) == null) { + return null; + } + return streams.get(index); + } + + /** + * Returns the index of the given item using referential equality. + * May be null despite play queue contains identical item. * + * @param item the item to find the index of + * @return the index of the given item + */ + public int indexOf(@NonNull final PlayQueueItem item) { + // referential equality, can't think of a better way to do this + // todo: better than this + return streams.indexOf(item); + } + + /** + * @return the current size of play queue. + */ + public int size() { + return streams.size(); + } + + /** + * Checks if the play queue is empty. + * + * @return whether the play queue is empty + */ + public boolean isEmpty() { + return streams.isEmpty(); + } + + /** + * Determines if the current play queue is shuffled. + * + * @return whether the play queue is shuffled + */ + public boolean isShuffled() { + return backup != null; + } + + /** + * @return an immutable view of the play queue + */ + @NonNull + public List getStreams() { + return Collections.unmodifiableList(streams); + } + + /*////////////////////////////////////////////////////////////////////////// + // Write ops + //////////////////////////////////////////////////////////////////////////*/ + + /** + * Returns the play queue's update broadcast. + * May be null if the play queue message bus is not initialized. + * + * @return the play queue's update broadcast + */ + @Nullable + public Flowable getBroadcastReceiver() { + return broadcastReceiver; + } + + /** + * Changes the current playing index by an offset amount. + *

* Will emit a {@link SelectEvent} if offset is non-zero. - * */ + *

+ * + * @param offset the offset relative to the current index + */ public synchronized void offsetIndex(final int offset) { setIndex(getIndex() + offset); } @@ -221,19 +258,24 @@ public abstract class PlayQueue implements Serializable { * Appends the given {@link PlayQueueItem}s to the current play queue. * * @see #append(List items) - * */ + * @param items {@link PlayQueueItem}s to append + */ public synchronized void append(@NonNull final PlayQueueItem... items) { append(Arrays.asList(items)); } /** * Appends the given {@link PlayQueueItem}s to the current play queue. - * + *

* If the play queue is shuffled, then append the items to the backup queue as is and * append the shuffle items to the play queue. - * + *

+ *

* Will emit a {@link AppendEvent} on any given context. - * */ + *

+ * + * @param items {@link PlayQueueItem}s to append + */ public synchronized void append(@NonNull final List items) { final List itemList = new ArrayList<>(items); @@ -241,7 +283,8 @@ 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()) { + if (!streams.isEmpty() && streams.get(streams.size() - 1).isAutoQueued() + && !itemList.get(0).isAutoQueued()) { streams.remove(streams.size() - 1); } streams.addAll(itemList); @@ -251,38 +294,38 @@ public abstract class PlayQueue implements Serializable { /** * Removes the item at the given index from the play queue. - * + *

* The current playing index will decrement if it is greater than the index being removed. * On cases where the current playing index exceeds the playlist range, it is set to 0. - * + *

+ *

* Will emit a {@link RemoveEvent} if the index is within the play queue index range. - * */ + *

+ * + * @param index the index of the item to remove + */ public synchronized void remove(final int index) { - if (index >= streams.size() || index < 0) return; + if (index >= streams.size() || index < 0) { + return; + } removeInternal(index); broadcast(new RemoveEvent(index, getIndex())); } /** - * Report an exception for the item at the current index in order and the course of action: - * if the error can be skipped or the current item should be removed. - * + * Report an exception for the item at the current index in order and skip to the next one + *

* This is done as a separate event as the underlying manager may have * different implementation regarding exceptions. - * */ - public synchronized void error(final boolean skippable) { - final int index = getIndex(); - - if (skippable) { - queueIndex.incrementAndGet(); - if (streams.size() > queueIndex.get()) { - history.add(streams.get(queueIndex.get())); - } - } else { - removeInternal(index); + *

+ */ + public synchronized void error() { + final int oldIndex = getIndex(); + queueIndex.incrementAndGet(); + if (streams.size() > queueIndex.get()) { + history.add(streams.get(queueIndex.get())); } - - broadcast(new ErrorEvent(index, getIndex(), skippable)); + broadcast(new ErrorEvent(oldIndex, getIndex())); } private synchronized void removeInternal(final int removeIndex) { @@ -295,7 +338,7 @@ public abstract class PlayQueue implements Serializable { } else if (currentIndex >= size) { queueIndex.set(currentIndex % (size - 1)); - } else if (currentIndex == removeIndex && currentIndex == size - 1){ + } else if (currentIndex == removeIndex && currentIndex == size - 1) { queueIndex.set(0); } @@ -311,16 +354,24 @@ public abstract class PlayQueue implements Serializable { /** * Moves a queue item at the source index to the target index. - * + *

* If the item being moved is the currently playing, then the current playing index is set * to that of the target. * If the moved item is not the currently playing and moves to an index AFTER the * current playing index, then the current playing index is decremented. * Vice versa if the an item after the currently playing is moved BEFORE. - * */ + *

+ * + * @param source the original index of the item + * @param target the new index of the item + */ public synchronized void move(final int source, final int target) { - if (source < 0 || target < 0) return; - if (source >= streams.size() || target >= streams.size()) return; + if (source < 0 || target < 0) { + return; + } + if (source >= streams.size() || target >= streams.size()) { + return; + } final int current = getIndex(); if (source == current) { @@ -339,11 +390,17 @@ public abstract class PlayQueue implements Serializable { /** * Sets the recovery record of the item at the index. - * + *

* Broadcasts a recovery event. - * */ + *

+ * + * @param index index of the item + * @param position the recovery position + */ public synchronized void setRecovery(final int index, final long position) { - if (index < 0 || index >= streams.size()) return; + if (index < 0 || index >= streams.size()) { + return; + } streams.get(index).setRecoveryPosition(position); broadcast(new RecoveryEvent(index, position)); @@ -351,22 +408,27 @@ public abstract class PlayQueue implements Serializable { /** * Revoke the recovery record of the item at the index. - * + *

* Broadcasts a recovery event. - * */ + *

+ * + * @param index index of the item + */ public synchronized void unsetRecovery(final int index) { setRecovery(index, PlayQueueItem.RECOVERY_UNSET); } /** * Shuffles the current play queue. - * + *

* This method first backs up the existing play queue and item being played. * Then a newly shuffled play queue will be generated along with currently * playing item placed at the beginning of the queue. - * + *

+ *

* Will emit a {@link ReorderEvent} in any context. - * */ + *

+ */ public synchronized void shuffle() { if (backup == null) { backup = new ArrayList<>(streams); @@ -389,14 +451,18 @@ public abstract class PlayQueue implements Serializable { /** * Unshuffles the current play queue if a backup play queue exists. - * + *

* This method undoes shuffling and index will be set to the previously playing item if found, * otherwise, the index will reset to 0. - * + *

+ *

* Will emit a {@link ReorderEvent} if a backup exists. - * */ + *

+ */ public synchronized void unshuffle() { - if (backup == null) return; + if (backup == null) { + return; + } final int originIndex = getIndex(); final PlayQueueItem current = getItem(); @@ -471,20 +537,23 @@ public abstract class PlayQueue implements Serializable { private Subscriber getSelfReporter() { return new Subscriber() { @Override - public void onSubscribe(Subscription s) { - if (reportingReactor != null) reportingReactor.cancel(); + public void onSubscribe(final Subscription s) { + if (reportingReactor != null) { + reportingReactor.cancel(); + } reportingReactor = s; reportingReactor.request(1); } @Override - public void onNext(PlayQueueEvent event) { - Log.d(TAG, "Received broadcast: " + event.type().name() + ". Current index: " + getIndex() + ", play queue length: " + size() + "."); + public void onNext(final PlayQueueEvent event) { + Log.d(TAG, "Received broadcast: " + event.type().name() + ". " + + "Current index: " + getIndex() + ", play queue length: " + size() + "."); reportingReactor.request(1); } @Override - public void onError(Throwable t) { + public void onError(final Throwable t) { Log.e(TAG, "Received broadcast error", t); } diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueAdapter.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueAdapter.java index b74736c49..f8777597a 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueAdapter.java @@ -1,12 +1,13 @@ package org.schabi.newpipe.player.playqueue; import android.content.Context; -import androidx.recyclerview.widget.RecyclerView; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import androidx.recyclerview.widget.RecyclerView; + import org.schabi.newpipe.R; import org.schabi.newpipe.player.playqueue.events.AppendEvent; import org.schabi.newpipe.player.playqueue.events.ErrorEvent; @@ -24,22 +25,26 @@ import io.reactivex.disposables.Disposable; /** * Created by Christian Schabesberger on 01.08.16. - * + *

* Copyright (C) Christian Schabesberger 2016 * InfoListAdapter.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 . + *

*/ public class PlayQueueAdapter extends RecyclerView.Adapter { @@ -55,14 +60,6 @@ public class PlayQueueAdapter extends RecyclerView.Adapter getReactor() { return new Observer() { @Override - public void onSubscribe(@NonNull Disposable d) { - if (playQueueReactor != null) playQueueReactor.dispose(); + public void onSubscribe(@NonNull final Disposable d) { + if (playQueueReactor != null) { + playQueueReactor.dispose(); + } playQueueReactor = d; } @Override - public void onNext(@NonNull PlayQueueEvent playQueueMessage) { - if (playQueueReactor != null) onPlayQueueChanged(playQueueMessage); + public void onNext(@NonNull final PlayQueueEvent playQueueMessage) { + if (playQueueReactor != null) { + onPlayQueueChanged(playQueueMessage); + } } @Override - public void onError(@NonNull Throwable e) {} + public void onError(@NonNull final Throwable e) { } @Override public void onComplete() { @@ -114,9 +115,6 @@ public class PlayQueueAdapter extends RecyclerView.Adapter 0) + if (info.getStartPosition() > 0) { setRecoveryPosition(info.getStartPosition() * 1000); + } } PlayQueueItem(@NonNull final StreamInfoItem item) { @@ -94,6 +101,10 @@ public class PlayQueueItem implements Serializable { return recoveryPosition; } + /*package-private*/ void setRecoveryPosition(final long recoveryPosition) { + this.recoveryPosition = recoveryPosition; + } + @Nullable public Throwable getError() { return error; @@ -110,15 +121,11 @@ public class PlayQueueItem implements Serializable { return isAutoQueued; } - public void setAutoQueued(boolean autoQueued) { - isAutoQueued = autoQueued; - } - //////////////////////////////////////////////////////////////////////////// // Item States, keep external access out //////////////////////////////////////////////////////////////////////////// - /*package-private*/ void setRecoveryPosition(final long recoveryPosition) { - this.recoveryPosition = recoveryPosition; + public void setAutoQueued(final boolean autoQueued) { + isAutoQueued = autoQueued; } } diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemBuilder.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemBuilder.java index c24eff81a..1c50dc6b4 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemBuilder.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemBuilder.java @@ -12,25 +12,20 @@ import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.Localization; public class PlayQueueItemBuilder { - private static final String TAG = PlayQueueItemBuilder.class.toString(); - - public interface OnSelectedListener { - void selected(PlayQueueItem item, View view); - void held(PlayQueueItem item, View view); - void onStartDrag(PlayQueueItemHolder viewHolder); - } - private OnSelectedListener onItemClickListener; - public PlayQueueItemBuilder(final Context context) {} + public PlayQueueItemBuilder(final Context context) { + } - public void setOnSelectedListener(OnSelectedListener listener) { + public void setOnSelectedListener(final OnSelectedListener listener) { this.onItemClickListener = listener; } public void buildStreamInfoItem(final PlayQueueItemHolder holder, final PlayQueueItem item) { - if (!TextUtils.isEmpty(item.getTitle())) holder.itemVideoTitleView.setText(item.getTitle()); + if (!TextUtils.isEmpty(item.getTitle())) { + holder.itemVideoTitleView.setText(item.getTitle()); + } holder.itemAdditionalDetailsView.setText(Localization.concatenateStrings(item.getUploader(), NewPipe.getNameOfService(item.getServiceId()))); @@ -71,4 +66,12 @@ public class PlayQueueItemBuilder { return false; }; } + + public interface OnSelectedListener { + void selected(PlayQueueItem item, View view); + + void held(PlayQueueItem item, View view); + + void onStartDrag(PlayQueueItemHolder viewHolder); + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemHolder.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemHolder.java index 7ad34b91e..c46410343 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemHolder.java @@ -1,10 +1,11 @@ package org.schabi.newpipe.player.playqueue; -import androidx.recyclerview.widget.RecyclerView; import android.view.View; import android.widget.ImageView; import android.widget.TextView; +import androidx.recyclerview.widget.RecyclerView; + import org.schabi.newpipe.R; /** @@ -12,29 +13,37 @@ import org.schabi.newpipe.R; *

* Copyright (C) Christian Schabesberger 2016 * StreamInfoItemHolder.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 . + * along with NewPipe. If not, see . + *

*/ public class PlayQueueItemHolder extends RecyclerView.ViewHolder { + public final TextView itemVideoTitleView; + public final TextView itemDurationView; + final TextView itemAdditionalDetailsView; - public final TextView itemVideoTitleView, itemDurationView, itemAdditionalDetailsView; - public final ImageView itemSelected, itemThumbnailView, itemHandle; + final ImageView itemSelected; + public final ImageView itemThumbnailView; + final ImageView itemHandle; public final View itemRoot; - public PlayQueueItemHolder(View v) { + PlayQueueItemHolder(final View v) { super(v); itemRoot = v.findViewById(R.id.itemRoot); itemVideoTitleView = v.findViewById(R.id.itemVideoTitleView); diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemTouchCallback.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemTouchCallback.java index 38e8e092a..5fee43659 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemTouchCallback.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemTouchCallback.java @@ -1,7 +1,7 @@ package org.schabi.newpipe.player.playqueue; -import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.RecyclerView; public abstract class PlayQueueItemTouchCallback extends ItemTouchHelper.SimpleCallback { private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 10; @@ -11,14 +11,14 @@ public abstract class PlayQueueItemTouchCallback extends ItemTouchHelper.SimpleC super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.RIGHT); } - public abstract void onMove(final int sourceIndex, final int targetIndex); + public abstract void onMove(int sourceIndex, int targetIndex); public abstract void onSwiped(int index); @Override - public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize, - int viewSizeOutOfBounds, int totalSize, - long msSinceStartScroll) { + public int interpolateOutOfBoundsScroll(final RecyclerView recyclerView, final int viewSize, + final int viewSizeOutOfBounds, final int totalSize, + final long msSinceStartScroll) { final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize, viewSizeOutOfBounds, totalSize, msSinceStartScroll); final int clampedAbsVelocity = Math.max(MINIMUM_INITIAL_DRAG_VELOCITY, @@ -27,8 +27,8 @@ public abstract class PlayQueueItemTouchCallback extends ItemTouchHelper.SimpleC } @Override - public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, - RecyclerView.ViewHolder target) { + public boolean onMove(final RecyclerView recyclerView, final RecyclerView.ViewHolder source, + final RecyclerView.ViewHolder target) { if (source.getItemViewType() != target.getItemViewType()) { return false; } @@ -50,7 +50,7 @@ public abstract class PlayQueueItemTouchCallback extends ItemTouchHelper.SimpleC } @Override - public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) { + public void onSwiped(final RecyclerView.ViewHolder viewHolder, final int swipeDir) { onSwiped(viewHolder.getAdapterPosition()); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlaylistPlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlaylistPlayQueue.java index fcb7080c5..077702747 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlaylistPlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlaylistPlayQueue.java @@ -1,5 +1,6 @@ package org.schabi.newpipe.player.playqueue; +import org.schabi.newpipe.extractor.Page; import org.schabi.newpipe.extractor.playlist.PlaylistInfo; import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; @@ -16,15 +17,15 @@ public final class PlaylistPlayQueue extends AbstractInfoPlayQueue streams, final int index) { - super(serviceId, url, nextPageUrl, streams, index); + super(serviceId, url, nextPage, streams, index); } @Override @@ -40,7 +41,7 @@ public final class PlaylistPlayQueue extends AbstractInfoPlayQueue playQueueItemsOf(List items) { + private static List playQueueItemsOf(final List items) { List playQueueItems = new ArrayList<>(items.size()); for (final StreamInfoItem item : items) { playQueueItems.add(new PlayQueueItem(item)); @@ -39,5 +39,6 @@ public final class SinglePlayQueue extends PlayQueue { } @Override - public void fetch() {} + public void fetch() { + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/AppendEvent.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/events/AppendEvent.java index 6ccd85f82..cc922dbb1 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/AppendEvent.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/events/AppendEvent.java @@ -1,18 +1,17 @@ package org.schabi.newpipe.player.playqueue.events; - public class AppendEvent implements PlayQueueEvent { - final private int amount; + private final int amount; + + public AppendEvent(final int amount) { + this.amount = amount; + } @Override public PlayQueueEventType type() { return PlayQueueEventType.APPEND; } - public AppendEvent(final int amount) { - this.amount = amount; - } - public int getAmount() { return amount; } diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/ErrorEvent.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/events/ErrorEvent.java index 570a8e337..7b7e39212 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/ErrorEvent.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/events/ErrorEvent.java @@ -1,22 +1,19 @@ package org.schabi.newpipe.player.playqueue.events; - public class ErrorEvent implements PlayQueueEvent { - final private int errorIndex; - final private int queueIndex; - final private boolean skippable; + private final int errorIndex; + private final int queueIndex; + + public ErrorEvent(final int errorIndex, final int queueIndex) { + this.errorIndex = errorIndex; + this.queueIndex = queueIndex; + } @Override public PlayQueueEventType type() { return PlayQueueEventType.ERROR; } - public ErrorEvent(final int errorIndex, final int queueIndex, final boolean skippable) { - this.errorIndex = errorIndex; - this.queueIndex = queueIndex; - this.skippable = skippable; - } - public int getErrorIndex() { return errorIndex; } @@ -24,8 +21,4 @@ public class ErrorEvent implements PlayQueueEvent { public int getQueueIndex() { return queueIndex; } - - public boolean isSkippable() { - return skippable; - } } diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/MoveEvent.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/events/MoveEvent.java index 69468be31..55d198923 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/MoveEvent.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/events/MoveEvent.java @@ -1,19 +1,19 @@ package org.schabi.newpipe.player.playqueue.events; public class MoveEvent implements PlayQueueEvent { - final private int fromIndex; - final private int toIndex; - - @Override - public PlayQueueEventType type() { - return PlayQueueEventType.MOVE; - } + private final int fromIndex; + private final int toIndex; public MoveEvent(final int oldIndex, final int newIndex) { this.fromIndex = oldIndex; this.toIndex = newIndex; } + @Override + public PlayQueueEventType type() { + return PlayQueueEventType.MOVE; + } + public int getFromIndex() { return fromIndex; } diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/RecoveryEvent.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/events/RecoveryEvent.java index 58d3fadfc..6f21b36cd 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/RecoveryEvent.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/events/RecoveryEvent.java @@ -1,20 +1,19 @@ package org.schabi.newpipe.player.playqueue.events; - public class RecoveryEvent implements PlayQueueEvent { - final private int index; - final private long position; - - @Override - public PlayQueueEventType type() { - return PlayQueueEventType.RECOVERY; - } + private final int index; + private final long position; public RecoveryEvent(final int index, final long position) { this.index = index; this.position = position; } + @Override + public PlayQueueEventType type() { + return PlayQueueEventType.RECOVERY; + } + public int getIndex() { return index; } diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/RemoveEvent.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/events/RemoveEvent.java index bb42ef109..a5872906d 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/RemoveEvent.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/events/RemoveEvent.java @@ -1,20 +1,19 @@ package org.schabi.newpipe.player.playqueue.events; - public class RemoveEvent implements PlayQueueEvent { - final private int removeIndex; - final private int queueIndex; - - @Override - public PlayQueueEventType type() { - return PlayQueueEventType.REMOVE; - } + private final int removeIndex; + private final int queueIndex; public RemoveEvent(final int removeIndex, final int queueIndex) { this.removeIndex = removeIndex; this.queueIndex = queueIndex; } + @Override + public PlayQueueEventType type() { + return PlayQueueEventType.REMOVE; + } + public int getQueueIndex() { return queueIndex; } diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/ReorderEvent.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/events/ReorderEvent.java index 738a89fcf..4f4f14756 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/ReorderEvent.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/events/ReorderEvent.java @@ -4,16 +4,16 @@ public class ReorderEvent implements PlayQueueEvent { private final int fromSelectedIndex; private final int toSelectedIndex; - @Override - public PlayQueueEventType type() { - return PlayQueueEventType.REORDER; - } - public ReorderEvent(final int fromSelectedIndex, final int toSelectedIndex) { this.fromSelectedIndex = fromSelectedIndex; this.toSelectedIndex = toSelectedIndex; } + @Override + public PlayQueueEventType type() { + return PlayQueueEventType.REORDER; + } + public int getFromSelectedIndex() { return fromSelectedIndex; } diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/SelectEvent.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/events/SelectEvent.java index 7dcc88794..95e344211 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/SelectEvent.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/events/SelectEvent.java @@ -1,20 +1,19 @@ package org.schabi.newpipe.player.playqueue.events; - public class SelectEvent implements PlayQueueEvent { - final private int oldIndex; - final private int newIndex; - - @Override - public PlayQueueEventType type() { - return PlayQueueEventType.SELECT; - } + private final int oldIndex; + private final int newIndex; public SelectEvent(final int oldIndex, final int newIndex) { this.oldIndex = oldIndex; this.newIndex = newIndex; } + @Override + public PlayQueueEventType type() { + return PlayQueueEventType.SELECT; + } + public int getOldIndex() { return oldIndex; } diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java index 7e9199040..29be402c5 100644 --- a/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java +++ b/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java @@ -1,6 +1,7 @@ package org.schabi.newpipe.player.resolver; import android.content.Context; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -14,9 +15,10 @@ import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.util.ListHelper; public class AudioPlaybackResolver implements PlaybackResolver { - - @NonNull private final Context context; - @NonNull private final PlayerDataSource dataSource; + @NonNull + private final Context context; + @NonNull + private final PlayerDataSource dataSource; public AudioPlaybackResolver(@NonNull final Context context, @NonNull final PlayerDataSource dataSource) { @@ -26,12 +28,16 @@ public class AudioPlaybackResolver implements PlaybackResolver { @Override @Nullable - public MediaSource resolve(@NonNull StreamInfo info) { + public MediaSource resolve(@NonNull final StreamInfo info) { final MediaSource liveSource = maybeBuildLiveMediaSource(dataSource, info); - if (liveSource != null) return liveSource; + if (liveSource != null) { + return liveSource; + } final int index = ListHelper.getDefaultAudioFormat(context, info.getAudioStreams()); - if (index < 0 || index >= info.getAudioStreams().size()) return null; + if (index < 0 || index >= info.getAudioStreams().size()) { + return null; + } final AudioStream audio = info.getAudioStreams().get(index); final MediaSourceTag tag = new MediaSourceTag(info); diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/MediaSourceTag.java b/app/src/main/java/org/schabi/newpipe/player/resolver/MediaSourceTag.java index d8c0c89b7..360e92e7f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/resolver/MediaSourceTag.java +++ b/app/src/main/java/org/schabi/newpipe/player/resolver/MediaSourceTag.java @@ -11,9 +11,11 @@ import java.util.Collections; import java.util.List; public class MediaSourceTag implements Serializable { - @NonNull private final StreamInfo metadata; + @NonNull + private final StreamInfo metadata; - @NonNull private final List sortedAvailableVideoStreams; + @NonNull + private final List sortedAvailableVideoStreams; private final int selectedVideoStreamIndex; public MediaSourceTag(@NonNull final StreamInfo metadata, @@ -44,8 +46,8 @@ public class MediaSourceTag implements Serializable { @Nullable public VideoStream getSelectedVideoStream() { - return selectedVideoStreamIndex < 0 || - selectedVideoStreamIndex >= sortedAvailableVideoStreams.size() ? null : - sortedAvailableVideoStreams.get(selectedVideoStreamIndex); + return selectedVideoStreamIndex < 0 + || selectedVideoStreamIndex >= sortedAvailableVideoStreams.size() + ? null : sortedAvailableVideoStreams.get(selectedVideoStreamIndex); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java index ef28f71ee..e06c0ff82 100644 --- a/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java +++ b/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java @@ -1,9 +1,10 @@ package org.schabi.newpipe.player.resolver; import android.net.Uri; +import android.text.TextUtils; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import android.text.TextUtils; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.source.MediaSource; @@ -61,8 +62,8 @@ public interface PlaybackResolver extends Resolver { @NonNull final String overrideExtension, @NonNull final MediaSourceTag metadata) { final Uri uri = Uri.parse(sourceUrl); - @C.ContentType final int type = TextUtils.isEmpty(overrideExtension) ? - Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension); + @C.ContentType final int type = TextUtils.isEmpty(overrideExtension) + ? Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension); switch (type) { case C.TYPE_SS: diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/Resolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/Resolver.java index d6af20ae2..a3e1db5b4 100644 --- a/app/src/main/java/org/schabi/newpipe/player/resolver/Resolver.java +++ b/app/src/main/java/org/schabi/newpipe/player/resolver/Resolver.java @@ -4,5 +4,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; public interface Resolver { - @Nullable Product resolve(@NonNull Source source); + @Nullable + Product resolve(@NonNull Source source); } diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java index c503fe596..2eb766769 100644 --- a/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java +++ b/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java @@ -2,6 +2,7 @@ package org.schabi.newpipe.player.resolver; import android.content.Context; import android.net.Uri; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -10,9 +11,9 @@ 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.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; @@ -25,18 +26,15 @@ import static com.google.android.exoplayer2.C.SELECTION_FLAG_AUTOSELECT; import static com.google.android.exoplayer2.C.TIME_UNSET; public class VideoPlaybackResolver implements PlaybackResolver { + @NonNull + private final Context context; + @NonNull + private final PlayerDataSource dataSource; + @NonNull + private final QualityResolver qualityResolver; - public interface QualityResolver { - int getDefaultResolutionIndex(final List sortedVideos); - int getOverrideResolutionIndex(final List sortedVideos, - final String playbackQuality); - } - - @NonNull private final Context context; - @NonNull private final PlayerDataSource dataSource; - @NonNull private final QualityResolver qualityResolver; - - @Nullable private String playbackQuality; + @Nullable + private String playbackQuality; public VideoPlaybackResolver(@NonNull final Context context, @NonNull final PlayerDataSource dataSource, @@ -48,9 +46,11 @@ public class VideoPlaybackResolver implements PlaybackResolver { @Override @Nullable - public MediaSource resolve(@NonNull StreamInfo info) { + public MediaSource resolve(@NonNull final StreamInfo info) { final MediaSource liveSource = maybeBuildLiveMediaSource(dataSource, info); - if (liveSource != null) return liveSource; + if (liveSource != null) { + return liveSource; + } List mediaSources = new ArrayList<>(); @@ -81,7 +81,7 @@ public class VideoPlaybackResolver implements PlaybackResolver { ListHelper.getDefaultAudioFormat(context, audioStreams)); // Use the audio stream if there is no video stream, or // Merge with audio stream in case if video does not contain audio - if (audio != null && ((video != null && video.isVideoOnly) || video == null)) { + if (audio != null && (video == null || video.isVideoOnly)) { final MediaSource audioSource = buildMediaSource(dataSource, audio.getUrl(), PlayerHelper.cacheKeyOf(info, audio), MediaFormat.getSuffixById(audio.getFormatId()), tag); @@ -89,17 +89,22 @@ public class VideoPlaybackResolver implements PlaybackResolver { } // If there is no audio or video sources, then this media source cannot be played back - if (mediaSources.isEmpty()) return null; + if (mediaSources.isEmpty()) { + return null; + } // Below are auxiliary media sources // Create subtitle sources - if(info.getSubtitles() != null) { + if (info.getSubtitles() != null) { for (final SubtitlesStream subtitle : info.getSubtitles()) { final String mimeType = PlayerHelper.subtitleMimeTypesOf(subtitle.getFormat()); - if (mimeType == null) continue; + if (mimeType == null) { + continue; + } final Format textFormat = Format.createTextSampleFormat(null, mimeType, - SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle)); + SELECTION_FLAG_AUTOSELECT, + PlayerHelper.captionLanguageOf(context, subtitle)); final MediaSource textSource = dataSource.getSampleMediaSourceFactory() .createMediaSource(Uri.parse(subtitle.getURL()), textFormat, TIME_UNSET); mediaSources.add(textSource); @@ -119,7 +124,13 @@ public class VideoPlaybackResolver implements PlaybackResolver { return playbackQuality; } - public void setPlaybackQuality(@Nullable String playbackQuality) { + public void setPlaybackQuality(@Nullable final String playbackQuality) { this.playbackQuality = playbackQuality; } + + public interface QualityResolver { + int getDefaultResolutionIndex(List sortedVideos); + + int getOverrideResolutionIndex(List sortedVideos, String playbackQuality); + } } diff --git a/app/src/main/java/org/schabi/newpipe/report/AcraReportSender.java b/app/src/main/java/org/schabi/newpipe/report/AcraReportSender.java index d8506fe6e..b31e3a31e 100644 --- a/app/src/main/java/org/schabi/newpipe/report/AcraReportSender.java +++ b/app/src/main/java/org/schabi/newpipe/report/AcraReportSender.java @@ -1,9 +1,10 @@ package org.schabi.newpipe.report; import android.content.Context; + import androidx.annotation.NonNull; -import org.acra.collector.CrashReportData; +import org.acra.data.CrashReportData; import org.acra.sender.ReportSender; import org.schabi.newpipe.R; @@ -30,9 +31,9 @@ import org.schabi.newpipe.R; public class AcraReportSender implements ReportSender { @Override - public void send(@NonNull Context context, @NonNull CrashReportData report) { + public void send(@NonNull final Context context, @NonNull final CrashReportData report) { ErrorActivity.reportError(context, report, - ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR,"none", + ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, "none", "App crash, UI failure", R.string.app_ui_crash)); } } diff --git a/app/src/main/java/org/schabi/newpipe/report/AcraReportSenderFactory.java b/app/src/main/java/org/schabi/newpipe/report/AcraReportSenderFactory.java index 94b2e84a5..f4c1c4ac8 100644 --- a/app/src/main/java/org/schabi/newpipe/report/AcraReportSenderFactory.java +++ b/app/src/main/java/org/schabi/newpipe/report/AcraReportSenderFactory.java @@ -1,14 +1,15 @@ package org.schabi.newpipe.report; import android.content.Context; + import androidx.annotation.NonNull; -import org.acra.config.ACRAConfiguration; +import org.acra.config.CoreConfiguration; import org.acra.sender.ReportSender; import org.acra.sender.ReportSenderFactory; /* - * Created by Christian Schabesberger on 13.09.16. + * Created by Christian Schabesberger on 13.09.16. * * Copyright (C) Christian Schabesberger 2015 * AcraReportSenderFactory.java is part of NewPipe. @@ -29,7 +30,8 @@ import org.acra.sender.ReportSenderFactory; public class AcraReportSenderFactory implements ReportSenderFactory { @NonNull - public ReportSender create(@NonNull Context context, @NonNull ACRAConfiguration config) { + public ReportSender create(@NonNull final Context context, + @NonNull final CoreConfiguration config) { return new AcraReportSender(); } } diff --git a/app/src/main/java/org/schabi/newpipe/report/ErrorActivity.java b/app/src/main/java/org/schabi/newpipe/report/ErrorActivity.java index e7a6319e3..52761e467 100644 --- a/app/src/main/java/org/schabi/newpipe/report/ErrorActivity.java +++ b/app/src/main/java/org/schabi/newpipe/report/ErrorActivity.java @@ -11,14 +11,6 @@ import android.os.Bundle; import android.os.Handler; import android.os.Parcel; import android.os.Parcelable; -import android.preference.PreferenceManager; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; -import com.google.android.material.snackbar.Snackbar; -import androidx.core.app.NavUtils; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; import android.util.Log; import android.view.Menu; import android.view.MenuInflater; @@ -27,25 +19,39 @@ import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.core.app.NavUtils; + +import com.google.android.material.snackbar.Snackbar; +import com.grack.nanojson.JsonWriter; import org.acra.ReportField; -import org.acra.collector.CrashReportData; -import org.json.JSONArray; -import org.json.JSONObject; +import org.acra.data.CrashReportData; import org.schabi.newpipe.ActivityCommunicator; import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; +import org.schabi.newpipe.util.Localization; +import org.schabi.newpipe.util.ShareUtils; import org.schabi.newpipe.util.ThemeHelper; import java.io.PrintWriter; import java.io.StringWriter; import java.text.SimpleDateFormat; +import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.TimeZone; import java.util.Vector; +import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; + /* * Created by Christian Schabesberger on 24.10.15. * @@ -74,7 +80,12 @@ public class ErrorActivity extends AppCompatActivity { public static final String ERROR_LIST = "error_list"; public static final String ERROR_EMAIL_ADDRESS = "crashreport@newpipe.schabi.org"; - public static final String ERROR_EMAIL_SUBJECT = "Exception in NewPipe " + BuildConfig.VERSION_NAME; + public static final String ERROR_EMAIL_SUBJECT + = "Exception in NewPipe " + BuildConfig.VERSION_NAME; + + public static final String ERROR_GITHUB_ISSUE_URL + = "https://github.com/TeamNewPipe/NewPipe/issues"; + private String[] errorList; private ErrorInfo errorInfo; private Class returnActivity; @@ -82,25 +93,27 @@ public class ErrorActivity extends AppCompatActivity { private EditText userCommentBox; public static void reportUiError(final AppCompatActivity activity, final Throwable el) { - reportError(activity, el, activity.getClass(), null, - ErrorInfo.make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash)); + reportError(activity, el, activity.getClass(), null, ErrorInfo.make(UserAction.UI_ERROR, + "none", "", R.string.app_ui_crash)); } public static void reportError(final Context context, final List el, - final Class returnActivity, View rootView, final ErrorInfo errorInfo) { + final Class returnActivity, final View rootView, + final ErrorInfo errorInfo) { if (rootView != null) { Snackbar.make(rootView, R.string.error_snackbar_message, 3 * 1000) .setActionTextColor(Color.YELLOW) - .setAction(R.string.error_snackbar_action, v -> + .setAction(context.getString(R.string.error_snackbar_action).toUpperCase(), v -> startErrorActivity(returnActivity, context, errorInfo, el)).show(); } else { startErrorActivity(returnActivity, context, errorInfo, el); } } - private static void startErrorActivity(Class returnActivity, Context context, ErrorInfo errorInfo, List el) { + private static void startErrorActivity(final Class returnActivity, final Context context, + final ErrorInfo errorInfo, final List el) { ActivityCommunicator ac = ActivityCommunicator.getCommunicator(); - ac.returnActivity = returnActivity; + ac.setReturnActivity(returnActivity); Intent intent = new Intent(context, ErrorActivity.class); intent.putExtra(ERROR_INFO, errorInfo); intent.putExtra(ERROR_LIST, elToSl(el)); @@ -109,7 +122,8 @@ public class ErrorActivity extends AppCompatActivity { } public static void reportError(final Context context, final Throwable e, - final Class returnActivity, View rootView, final ErrorInfo errorInfo) { + final Class returnActivity, final View rootView, + final ErrorInfo errorInfo) { List el = null; if (e != null) { el = new Vector<>(); @@ -119,8 +133,9 @@ public class ErrorActivity extends AppCompatActivity { } // async call - public static void reportError(Handler handler, final Context context, final Throwable e, - final Class returnActivity, final View rootView, final ErrorInfo errorInfo) { + public static void reportError(final Handler handler, final Context context, + final Throwable e, final Class returnActivity, + final View rootView, final ErrorInfo errorInfo) { List el = null; if (e != null) { @@ -131,20 +146,15 @@ public class ErrorActivity extends AppCompatActivity { } // async call - public static void reportError(Handler handler, final Context context, final List el, - final Class returnActivity, final View rootView, final ErrorInfo errorInfo) { + public static void reportError(final Handler handler, final Context context, + final List el, final Class returnActivity, + final View rootView, final ErrorInfo errorInfo) { handler.post(() -> reportError(context, el, returnActivity, rootView, errorInfo)); } - public static void reportError(final Context context, final CrashReportData report, final ErrorInfo errorInfo) { - // get key first (don't ask about this solution) - ReportField key = null; - for (ReportField k : report.keySet()) { - if (k.toString().equals("STACK_TRACE")) { - key = k; - } - } - String[] el = new String[]{report.get(key).toString()}; + public static void reportError(final Context context, final CrashReportData report, + final ErrorInfo errorInfo) { + String[] el = new String[]{report.getString(ReportField.STACK_TRACE)}; Intent intent = new Intent(context, ErrorActivity.class); intent.putExtra(ERROR_INFO, errorInfo); @@ -161,7 +171,7 @@ public class ErrorActivity extends AppCompatActivity { } // errorList to StringList - private static String[] elToSl(List stackTraces) { + private static String[] elToSl(final List stackTraces) { String[] out = new String[stackTraces.size()]; for (int i = 0; i < stackTraces.size(); i++) { out[i] = getStackTrace(stackTraces.get(i)); @@ -170,7 +180,8 @@ public class ErrorActivity extends AppCompatActivity { } @Override - protected void onCreate(Bundle savedInstanceState) { + protected void onCreate(final Bundle savedInstanceState) { + assureCorrectAppLanguage(this); super.onCreate(savedInstanceState); ThemeHelper.setTheme(this); setContentView(R.layout.activity_error); @@ -187,49 +198,38 @@ public class ErrorActivity extends AppCompatActivity { actionBar.setDisplayShowTitleEnabled(true); } - Button reportButton = findViewById(R.id.errorReportButton); + final Button reportEmailButton = findViewById(R.id.errorReportEmailButton); + final Button copyButton = findViewById(R.id.errorReportCopyButton); + final Button reportGithubButton = findViewById(R.id.errorReportGitHubButton); + userCommentBox = findViewById(R.id.errorCommentBox); TextView errorView = findViewById(R.id.errorView); TextView infoView = findViewById(R.id.errorInfosView); TextView errorMessageView = findViewById(R.id.errorMessageView); ActivityCommunicator ac = ActivityCommunicator.getCommunicator(); - returnActivity = ac.returnActivity; + returnActivity = ac.getReturnActivity(); errorInfo = intent.getParcelableExtra(ERROR_INFO); errorList = intent.getStringArrayExtra(ERROR_LIST); // important add guru meditation - addGuruMeditaion(); + addGuruMeditation(); currentTimeStamp = getCurrentTimeStamp(); - reportButton.setOnClickListener((View v) -> { - Context context = this; - new AlertDialog.Builder(context) - .setIcon(android.R.drawable.ic_dialog_alert) - .setTitle(R.string.privacy_policy_title) - .setMessage(R.string.start_accept_privacy_policy) - .setCancelable(false) - .setNeutralButton(R.string.read_privacy_policy, (dialog, which) -> { - Intent webIntent = new Intent(Intent.ACTION_VIEW, - Uri.parse(context.getString(R.string.privacy_policy_url)) - ); - context.startActivity(webIntent); - }) - .setPositiveButton(R.string.accept, (dialog, which) -> { - Intent i = new Intent(Intent.ACTION_SENDTO); - i.setData(Uri.parse("mailto:" + ERROR_EMAIL_ADDRESS)) - .putExtra(Intent.EXTRA_SUBJECT, ERROR_EMAIL_SUBJECT) - .putExtra(Intent.EXTRA_TEXT, buildJson()); - - startActivity(Intent.createChooser(i, "Send Email")); - }) - .setNegativeButton(R.string.decline, (dialog, which) -> { - // do nothing - }) - .show(); - + reportEmailButton.setOnClickListener((View v) -> { + openPrivacyPolicyDialog(this, "EMAIL"); }); + copyButton.setOnClickListener((View v) -> { + ShareUtils.copyToClipboard(this, buildMarkdown()); + Toast.makeText(this, R.string.msg_copied, Toast.LENGTH_SHORT).show(); + }); + + reportGithubButton.setOnClickListener((View v) -> { + openPrivacyPolicyDialog(this, "GITHUB"); + }); + + // normal bugreport buildInfo(errorInfo); if (errorInfo.message != 0) { @@ -241,39 +241,69 @@ public class ErrorActivity extends AppCompatActivity { errorView.setText(formErrorText(errorList)); - //print stack trace once again for debugging: + // print stack trace once again for debugging: for (String e : errorList) { Log.e(TAG, e); } } @Override - public boolean onCreateOptionsMenu(Menu menu) { + public boolean onCreateOptionsMenu(final Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.error_menu, menu); return true; } @Override - public boolean onOptionsItemSelected(MenuItem item) { + public boolean onOptionsItemSelected(final MenuItem item) { int id = item.getItemId(); switch (id) { case android.R.id.home: goToReturnActivity(); break; - case R.id.menu_item_share_error: { + case R.id.menu_item_share_error: Intent intent = new Intent(); intent.setAction(Intent.ACTION_SEND); intent.putExtra(Intent.EXTRA_TEXT, buildJson()); intent.setType("text/plain"); startActivity(Intent.createChooser(intent, getString(R.string.share_dialog_title))); - } - break; + break; } return false; } - private String formErrorText(String[] el) { + private void openPrivacyPolicyDialog(final Context context, final String action) { + new AlertDialog.Builder(context) + .setIcon(android.R.drawable.ic_dialog_alert) + .setTitle(R.string.privacy_policy_title) + .setMessage(R.string.start_accept_privacy_policy) + .setCancelable(false) + .setNeutralButton(R.string.read_privacy_policy, (dialog, which) -> { + ShareUtils.openUrlInBrowser(context, + context.getString(R.string.privacy_policy_url)); + }) + .setPositiveButton(R.string.accept, (dialog, which) -> { + if (action.equals("EMAIL")) { // send on email + final Intent i = new Intent(Intent.ACTION_SENDTO) + .setData(Uri.parse("mailto:")) // only email apps should handle this + .putExtra(Intent.EXTRA_EMAIL, new String[]{ERROR_EMAIL_ADDRESS}) + .putExtra(Intent.EXTRA_SUBJECT, ERROR_EMAIL_SUBJECT) + .putExtra(Intent.EXTRA_TEXT, buildJson()); + if (i.resolveActivity(getPackageManager()) != null) { + startActivity(i); + } + } else if (action.equals("GITHUB")) { // open the NewPipe issue page on GitHub + ShareUtils.openUrlInBrowser(this, ERROR_GITHUB_ISSUE_URL); + } + + }) + .setNegativeButton(R.string.decline, (dialog, which) -> { + // do nothing + }) + .show(); + } + + private String formErrorText(final String[] el) { StringBuilder text = new StringBuilder(); if (el != null) { for (String e : el) { @@ -291,7 +321,7 @@ public class ErrorActivity extends AppCompatActivity { * @return the casted return activity or null */ @Nullable - static Class getReturnActivity(Class returnActivity) { + static Class getReturnActivity(final Class returnActivity) { Class checkedReturnActivity = null; if (returnActivity != null) { if (Activity.class.isAssignableFrom(returnActivity)) { @@ -314,49 +344,45 @@ public class ErrorActivity extends AppCompatActivity { } } - private void buildInfo(ErrorInfo info) { + private void buildInfo(final ErrorInfo info) { TextView infoLabelView = findViewById(R.id.errorInfoLabelsView); TextView infoView = findViewById(R.id.errorInfosView); String text = ""; infoLabelView.setText(getString(R.string.info_labels).replace("\\n", "\n")); - text += getUserActionString(info.userAction) - + "\n" + info.request - + "\n" + getContentLangString() - + "\n" + info.serviceName - + "\n" + currentTimeStamp - + "\n" + getPackageName() - + "\n" + BuildConfig.VERSION_NAME - + "\n" + getOsString(); + text += getUserActionString(info.userAction) + "\n" + + info.request + "\n" + + getContentLanguageString() + "\n" + + getContentCountryString() + "\n" + + getAppLanguage() + "\n" + + info.serviceName + "\n" + + currentTimeStamp + "\n" + + getPackageName() + "\n" + + BuildConfig.VERSION_NAME + "\n" + + getOsString(); infoView.setText(text); } private String buildJson() { - JSONObject errorObject = new JSONObject(); - try { - errorObject.put("user_action", getUserActionString(errorInfo.userAction)) - .put("request", errorInfo.request) - .put("content_language", getContentLangString()) - .put("service", errorInfo.serviceName) - .put("package", getPackageName()) - .put("version", BuildConfig.VERSION_NAME) - .put("os", getOsString()) - .put("time", currentTimeStamp); - - JSONArray exceptionArray = new JSONArray(); - if (errorList != null) { - for (String e : errorList) { - exceptionArray.put(e); - } - } - - errorObject.put("exceptions", exceptionArray); - errorObject.put("user_comment", userCommentBox.getText().toString()); - - return errorObject.toString(3); + return JsonWriter.string() + .object() + .value("user_action", getUserActionString(errorInfo.userAction)) + .value("request", errorInfo.request) + .value("content_language", getContentLanguageString()) + .value("content_country", getContentCountryString()) + .value("app_language", getAppLanguage()) + .value("service", errorInfo.serviceName) + .value("package", getPackageName()) + .value("version", BuildConfig.VERSION_NAME) + .value("os", getOsString()) + .value("time", currentTimeStamp) + .array("exceptions", Arrays.asList(errorList)) + .value("user_comment", userCommentBox.getText().toString()) + .end() + .done(); } catch (Throwable e) { Log.e(TAG, "Error while erroring: Could not build json"); e.printStackTrace(); @@ -365,7 +391,64 @@ public class ErrorActivity extends AppCompatActivity { return ""; } - private String getUserActionString(UserAction userAction) { + private String buildMarkdown() { + try { + final StringBuilder htmlErrorReport = new StringBuilder(); + + final String userComment = userCommentBox.getText().toString(); + if (!userComment.isEmpty()) { + htmlErrorReport.append(userComment).append("\n"); + } + + // basic error info + htmlErrorReport + .append("## Exception") + .append("\n* __User Action:__ ") + .append(getUserActionString(errorInfo.userAction)) + .append("\n* __Request:__ ").append(errorInfo.request) + .append("\n* __Content Country:__ ").append(getContentCountryString()) + .append("\n* __Content Language:__ ").append(getContentLanguageString()) + .append("\n* __App Language:__ ").append(getAppLanguage()) + .append("\n* __Service:__ ").append(errorInfo.serviceName) + .append("\n* __Version:__ ").append(BuildConfig.VERSION_NAME) + .append("\n* __OS:__ ").append(getOsString()).append("\n"); + + + // Collapse all logs to a single paragraph when there are more than one + // to keep the GitHub issue clean. + if (errorList.length > 1) { + htmlErrorReport + .append("
Exceptions (") + .append(errorList.length) + .append(")

\n"); + } + + // add the logs + for (int i = 0; i < errorList.length; i++) { + htmlErrorReport.append("

Crash log "); + if (errorList.length > 1) { + htmlErrorReport.append(i + 1); + } + htmlErrorReport.append("") + .append("

\n") + .append("\n```\n").append(errorList[i]).append("\n```\n") + .append("

\n"); + } + + // make sure to close everything + if (errorList.length > 1) { + htmlErrorReport.append("

\n"); + } + htmlErrorReport.append("
\n"); + return htmlErrorReport.toString(); + } catch (Throwable e) { + Log.e(TAG, "Error while erroring: Could not build markdown"); + e.printStackTrace(); + return ""; + } + } + + private String getUserActionString(final UserAction userAction) { if (userAction == null) { return "Your description is in another castle."; } else { @@ -373,20 +456,27 @@ public class ErrorActivity extends AppCompatActivity { } } - private String getContentLangString() { - return PreferenceManager.getDefaultSharedPreferences(this) - .getString(this.getString(R.string.content_country_key), "none"); + private String getContentCountryString() { + return Localization.getPreferredContentCountry(this).getCountryCode(); + } + + private String getContentLanguageString() { + return Localization.getPreferredLocalization(this).getLocalizationCode(); + } + + private String getAppLanguage() { + return Localization.getAppLocale(getApplicationContext()).toString(); } private String getOsString() { - String osBase = Build.VERSION.SDK_INT >= 23 ? Build.VERSION.BASE_OS : "Android"; + final String osBase = Build.VERSION.SDK_INT >= 23 ? Build.VERSION.BASE_OS : "Android"; return System.getProperty("os.name") + " " + (osBase.isEmpty() ? "Android" : osBase) + " " + Build.VERSION.RELEASE - + " - " + Integer.toString(Build.VERSION.SDK_INT); + + " - " + Build.VERSION.SDK_INT; } - private void addGuruMeditaion() { + private void addGuruMeditation() { //just an easter egg TextView sorryView = findViewById(R.id.errorSorryView); String text = sorryView.getText().toString(); @@ -407,38 +497,42 @@ public class ErrorActivity extends AppCompatActivity { } public static class ErrorInfo implements Parcelable { - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { @Override - public ErrorInfo createFromParcel(Parcel source) { + public ErrorInfo createFromParcel(final Parcel source) { return new ErrorInfo(source); } @Override - public ErrorInfo[] newArray(int size) { + public ErrorInfo[] newArray(final int size) { return new ErrorInfo[size]; } }; - final public UserAction userAction; - final public String request; - final public String serviceName; - @StringRes - final public int message; - private ErrorInfo(UserAction userAction, String serviceName, String request, @StringRes int message) { + final UserAction userAction; + public final String request; + final String serviceName; + @StringRes + public final int message; + + private ErrorInfo(final UserAction userAction, final String serviceName, + final String request, @StringRes final int message) { this.userAction = userAction; this.serviceName = serviceName; this.request = request; this.message = message; } - protected ErrorInfo(Parcel in) { + protected ErrorInfo(final Parcel in) { this.userAction = UserAction.valueOf(in.readString()); this.request = in.readString(); this.serviceName = in.readString(); this.message = in.readInt(); } - public static ErrorInfo make(UserAction userAction, String serviceName, String request, @StringRes int message) { + public static ErrorInfo make(final UserAction userAction, final String serviceName, + final String request, @StringRes final int message) { return new ErrorInfo(userAction, serviceName, request, message); } @@ -448,7 +542,7 @@ public class ErrorActivity extends AppCompatActivity { } @Override - public void writeToParcel(Parcel dest, int flags) { + public void writeToParcel(final Parcel dest, final int flags) { dest.writeString(this.userAction.name()); dest.writeString(this.request); dest.writeString(this.serviceName); diff --git a/app/src/main/java/org/schabi/newpipe/report/UserAction.java b/app/src/main/java/org/schabi/newpipe/report/UserAction.java index 2cca9305a..faa5e7a44 100644 --- a/app/src/main/java/org/schabi/newpipe/report/UserAction.java +++ b/app/src/main/java/org/schabi/newpipe/report/UserAction.java @@ -16,6 +16,7 @@ public enum UserAction { REQUESTED_PLAYLIST("requested playlist"), REQUESTED_KIOSK("requested kiosk"), REQUESTED_COMMENTS("requested comments"), + REQUESTED_FEED("requested feed"), DELETE_FROM_HISTORY("delete from history"), PLAY_STREAM("Play stream"), DOWNLOAD_POSTPROCESSING("download post-processing"), @@ -24,7 +25,7 @@ public enum UserAction { private final String message; - UserAction(String message) { + UserAction(final String message) { this.message = message; } diff --git a/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java index ce22b84e9..a9531693c 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java @@ -1,9 +1,12 @@ package org.schabi.newpipe.settings; +import android.content.ActivityNotFoundException; import android.content.Intent; import android.os.Build; import android.os.Bundle; import android.provider.Settings; +import android.widget.Toast; + import androidx.annotation.Nullable; import androidx.preference.Preference; @@ -11,48 +14,20 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.util.Constants; public class AppearanceSettingsFragment extends BasePreferenceFragment { - private final static boolean CAPTIONING_SETTINGS_ACCESSIBLE = + private static final boolean CAPTIONING_SETTINGS_ACCESSIBLE = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; /** - * Theme that was applied when the settings was opened (or recreated after a theme change) + * Theme that was applied when the settings was opened (or recreated after a theme change). */ private String startThemeKey; - private String captionSettingsKey; - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - String themeKey = getString(R.string.theme_key); - startThemeKey = defaultPreferences.getString(themeKey, getString(R.string.default_theme_value)); - findPreference(themeKey).setOnPreferenceChangeListener(themePreferenceChange); - - captionSettingsKey = getString(R.string.caption_settings_key); - if (!CAPTIONING_SETTINGS_ACCESSIBLE) { - final Preference captionSettings = findPreference(captionSettingsKey); - getPreferenceScreen().removePreference(captionSettings); - } - } - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - addPreferencesFromResource(R.xml.appearance_settings); - } - - @Override - public boolean onPreferenceTreeClick(Preference preference) { - if (preference.getKey().equals(captionSettingsKey) && CAPTIONING_SETTINGS_ACCESSIBLE) { - startActivity(new Intent(Settings.ACTION_CAPTIONING_SETTINGS)); - } - - return super.onPreferenceTreeClick(preference); - } - - private final Preference.OnPreferenceChangeListener themePreferenceChange = new Preference.OnPreferenceChangeListener() { + private final Preference.OnPreferenceChangeListener themePreferenceChange + = new Preference.OnPreferenceChangeListener() { @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { + public boolean onPreferenceChange(final Preference preference, final Object newValue) { defaultPreferences.edit().putBoolean(Constants.KEY_THEME_CHANGE, true).apply(); - defaultPreferences.edit().putString(getString(R.string.theme_key), newValue.toString()).apply(); + defaultPreferences.edit() + .putString(getString(R.string.theme_key), newValue.toString()).apply(); if (!newValue.equals(startThemeKey) && getActivity() != null) { // If it's not the current theme @@ -62,4 +37,38 @@ public class AppearanceSettingsFragment extends BasePreferenceFragment { return false; } }; + private String captionSettingsKey; + + @Override + public void onCreate(@Nullable final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + String themeKey = getString(R.string.theme_key); + startThemeKey = defaultPreferences + .getString(themeKey, getString(R.string.default_theme_value)); + findPreference(themeKey).setOnPreferenceChangeListener(themePreferenceChange); + + captionSettingsKey = getString(R.string.caption_settings_key); + if (!CAPTIONING_SETTINGS_ACCESSIBLE) { + final Preference captionSettings = findPreference(captionSettingsKey); + getPreferenceScreen().removePreference(captionSettings); + } + } + + @Override + public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { + addPreferencesFromResource(R.xml.appearance_settings); + } + + @Override + public boolean onPreferenceTreeClick(final Preference preference) { + if (preference.getKey().equals(captionSettingsKey) && CAPTIONING_SETTINGS_ACCESSIBLE) { + try { + startActivity(new Intent(Settings.ACTION_CAPTIONING_SETTINGS)); + } catch (ActivityNotFoundException e) { + Toast.makeText(getActivity(), R.string.general_error, Toast.LENGTH_SHORT).show(); + } + } + + return super.onPreferenceTreeClick(preference); + } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java b/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java index 056e9942a..125931ee1 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java @@ -3,11 +3,12 @@ package org.schabi.newpipe.settings; import android.content.SharedPreferences; import android.os.Bundle; import android.preference.PreferenceManager; +import android.view.View; + import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; import androidx.preference.PreferenceFragmentCompat; -import android.view.View; import org.schabi.newpipe.MainActivity; @@ -15,16 +16,16 @@ public abstract class BasePreferenceFragment extends PreferenceFragmentCompat { protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode()); protected final boolean DEBUG = MainActivity.DEBUG; - protected SharedPreferences defaultPreferences; + SharedPreferences defaultPreferences; @Override - public void onCreate(@Nullable Bundle savedInstanceState) { + public void onCreate(@Nullable final Bundle savedInstanceState) { defaultPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); super.onCreate(savedInstanceState); } @Override - public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + public void onViewCreated(final View view, @Nullable final Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); setDivider(null); updateTitle(); @@ -39,7 +40,9 @@ public abstract class BasePreferenceFragment extends PreferenceFragmentCompat { private void updateTitle() { if (getActivity() instanceof AppCompatActivity) { ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); - if (actionBar != null) actionBar.setTitle(getPreferenceScreen().getTitle()); + if (actionBar != null) { + actionBar.setTitle(getPreferenceScreen().getTitle()); + } } } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java index 0c7a4b46e..b0bb30aa7 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java @@ -2,20 +2,24 @@ package org.schabi.newpipe.settings; import android.app.Activity; import android.app.AlertDialog; +import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.preference.PreferenceManager; +import android.util.Log; +import android.widget.Toast; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.preference.Preference; -import android.util.Log; -import android.widget.Toast; import com.nononsenseapps.filepicker.Utils; import com.nostra13.universalimageloader.core.ImageLoader; +import org.schabi.newpipe.DownloaderImpl; +import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.localization.ContentCountry; @@ -40,34 +44,42 @@ import java.util.Map; import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; -public class ContentSettingsFragment extends BasePreferenceFragment { +import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; +public class ContentSettingsFragment extends BasePreferenceFragment { private static final int REQUEST_IMPORT_PATH = 8945; private static final int REQUEST_EXPORT_PATH = 30945; private File databasesDir; - private File newpipe_db; - private File newpipe_db_journal; - private File newpipe_db_shm; - private File newpipe_db_wal; - private File newpipe_settings; + private File newpipeDb; + private File newpipeDbJournal; + private File newpipeDbShm; + private File newpipeDbWal; + private File newpipeSettings; private String thumbnailLoadToggleKey; + private String youtubeRestrictedModeEnabledKey; private Localization initialSelectedLocalization; private ContentCountry initialSelectedContentCountry; + private String initialLanguage; @Override - public void onCreate(@Nullable Bundle savedInstanceState) { + public void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); thumbnailLoadToggleKey = getString(R.string.download_thumbnail_key); + youtubeRestrictedModeEnabledKey = getString(R.string.youtube_restricted_mode_enabled); - initialSelectedLocalization = org.schabi.newpipe.util.Localization.getPreferredLocalization(requireContext()); - initialSelectedContentCountry = org.schabi.newpipe.util.Localization.getPreferredContentCountry(requireContext()); + initialSelectedLocalization = org.schabi.newpipe.util.Localization + .getPreferredLocalization(requireContext()); + initialSelectedContentCountry = org.schabi.newpipe.util.Localization + .getPreferredContentCountry(requireContext()); + initialLanguage = PreferenceManager + .getDefaultSharedPreferences(getContext()).getString("app_language_key", "en"); } @Override - public boolean onPreferenceTreeClick(Preference preference) { + public boolean onPreferenceTreeClick(final Preference preference) { if (preference.getKey().equals(thumbnailLoadToggleKey)) { final ImageLoader imageLoader = ImageLoader.getInstance(); imageLoader.stop(); @@ -78,21 +90,30 @@ public class ContentSettingsFragment extends BasePreferenceFragment { Toast.LENGTH_SHORT).show(); } + if (preference.getKey().equals(youtubeRestrictedModeEnabledKey)) { + Context context = getContext(); + if (context != null) { + DownloaderImpl.getInstance().updateYoutubeRestrictedModeCookies(context); + } else { + Log.w(TAG, "onPreferenceTreeClick: null context"); + } + } + return super.onPreferenceTreeClick(preference); } @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { String homeDir = getActivity().getApplicationInfo().dataDir; databasesDir = new File(homeDir + "/databases"); - newpipe_db = new File(homeDir + "/databases/newpipe.db"); - newpipe_db_journal = new File(homeDir + "/databases/newpipe.db-journal"); - newpipe_db_shm = new File(homeDir + "/databases/newpipe.db-shm"); - newpipe_db_wal = new File(homeDir + "/databases/newpipe.db-wal"); + newpipeDb = new File(homeDir + "/databases/newpipe.db"); + newpipeDbJournal = new File(homeDir + "/databases/newpipe.db-journal"); + newpipeDbShm = new File(homeDir + "/databases/newpipe.db-shm"); + newpipeDbWal = new File(homeDir + "/databases/newpipe.db-wal"); - newpipe_settings = new File(homeDir + "/databases/newpipe.settings"); - newpipe_settings.delete(); + newpipeSettings = new File(homeDir + "/databases/newpipe.settings"); + newpipeSettings.delete(); addPreferencesFromResource(R.xml.content_settings); @@ -101,7 +122,8 @@ public class ContentSettingsFragment extends BasePreferenceFragment { Intent i = new Intent(getActivity(), FilePickerActivityHelper.class) .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, false) - .putExtra(FilePickerActivityHelper.EXTRA_MODE, FilePickerActivityHelper.MODE_FILE); + .putExtra(FilePickerActivityHelper.EXTRA_MODE, + FilePickerActivityHelper.MODE_FILE); startActivityForResult(i, REQUEST_IMPORT_PATH); return true; }); @@ -111,7 +133,8 @@ public class ContentSettingsFragment extends BasePreferenceFragment { Intent i = new Intent(getActivity(), FilePickerActivityHelper.class) .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true) - .putExtra(FilePickerActivityHelper.EXTRA_MODE, FilePickerActivityHelper.MODE_DIR); + .putExtra(FilePickerActivityHelper.EXTRA_MODE, + FilePickerActivityHelper.MODE_DIR); startActivityForResult(i, REQUEST_EXPORT_PATH); return true; }); @@ -125,20 +148,29 @@ public class ContentSettingsFragment extends BasePreferenceFragment { .getPreferredLocalization(requireContext()); final ContentCountry selectedContentCountry = org.schabi.newpipe.util.Localization .getPreferredContentCountry(requireContext()); + final String selectedLanguage = PreferenceManager + .getDefaultSharedPreferences(getContext()).getString("app_language_key", "en"); if (!selectedLocalization.equals(initialSelectedLocalization) - || !selectedContentCountry.equals(initialSelectedContentCountry)) { - Toast.makeText(requireContext(), R.string.localization_changes_requires_app_restart, Toast.LENGTH_LONG).show(); + || !selectedContentCountry.equals(initialSelectedContentCountry) + || !selectedLanguage.equals(initialLanguage)) { + Toast.makeText(requireContext(), R.string.localization_changes_requires_app_restart, + Toast.LENGTH_LONG).show(); NewPipe.setupLocalization(selectedLocalization, selectedContentCountry); } } @Override - public void onActivityResult(int requestCode, int resultCode, @NonNull Intent data) { + public void onActivityResult(final int requestCode, final int resultCode, + @NonNull final Intent data) { + assureCorrectAppLanguage(getContext()); super.onActivityResult(requestCode, resultCode, data); if (DEBUG) { - Log.d(TAG, "onActivityResult() called with: requestCode = [" + requestCode + "], resultCode = [" + resultCode + "], data = [" + data + "]"); + Log.d(TAG, "onActivityResult() called with: " + + "requestCode = [" + requestCode + "], " + + "resultCode = [" + resultCode + "], " + + "data = [" + data + "]"); } if ((requestCode == REQUEST_IMPORT_PATH || requestCode == REQUEST_EXPORT_PATH) @@ -150,7 +182,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment { } else { AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setMessage(R.string.override_current_data) - .setPositiveButton(android.R.string.ok, + .setPositiveButton(getString(R.string.finish), (DialogInterface d, int id) -> importDatabase(path)) .setNegativeButton(android.R.string.cancel, (DialogInterface d, int id) -> d.cancel()); @@ -159,15 +191,18 @@ public class ContentSettingsFragment extends BasePreferenceFragment { } } - private void exportDatabase(String path) { + private void exportDatabase(final String path) { try { + //checkpoint before export + NewPipeDatabase.checkpoint(); + ZipOutputStream outZip = new ZipOutputStream( new BufferedOutputStream( new FileOutputStream(path))); - ZipHelper.addFileToZip(outZip, newpipe_db.getPath(), "newpipe.db"); + ZipHelper.addFileToZip(outZip, newpipeDb.getPath(), "newpipe.db"); - saveSharedPreferencesToFile(newpipe_settings); - ZipHelper.addFileToZip(outZip, newpipe_settings.getPath(), "newpipe.settings"); + saveSharedPreferencesToFile(newpipeSettings); + ZipHelper.addFileToZip(outZip, newpipeSettings.getPath(), "newpipe.settings"); outZip.close(); @@ -178,7 +213,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment { } } - private void saveSharedPreferencesToFile(File dst) { + private void saveSharedPreferencesToFile(final File dst) { ObjectOutputStream output = null; try { output = new ObjectOutputStream(new FileOutputStream(dst)); @@ -189,7 +224,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); - }finally { + } finally { try { if (output != null) { output.flush(); @@ -201,7 +236,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment { } } - private void importDatabase(String filePath) { + private void importDatabase(final String filePath) { // check if file is supported ZipFile zipFile = null; try { @@ -213,7 +248,8 @@ public class ContentSettingsFragment extends BasePreferenceFragment { } finally { try { zipFile.close(); - } catch (Exception ignored){} + } catch (Exception ignored) { + } } try { @@ -222,21 +258,20 @@ public class ContentSettingsFragment extends BasePreferenceFragment { } final boolean isDbFileExtracted = ZipHelper.extractFileFromZip(filePath, - newpipe_db.getPath(), "newpipe.db"); + newpipeDb.getPath(), "newpipe.db"); if (isDbFileExtracted) { - newpipe_db_journal.delete(); - newpipe_db_wal.delete(); - newpipe_db_shm.delete(); - + newpipeDbJournal.delete(); + newpipeDbWal.delete(); + newpipeDbShm.delete(); } else { - Toast.makeText(getContext(), R.string.could_not_import_all_files, Toast.LENGTH_LONG) .show(); } //If settings file exist, ask if it should be imported. - if(ZipHelper.extractFileFromZip(filePath, newpipe_settings.getPath(), "newpipe.settings")) { + if (ZipHelper.extractFileFromZip(filePath, newpipeSettings.getPath(), + "newpipe.settings")) { AlertDialog.Builder alert = new AlertDialog.Builder(getContext()); alert.setTitle(R.string.import_settings); @@ -245,9 +280,9 @@ public class ContentSettingsFragment extends BasePreferenceFragment { // restart app to properly load db System.exit(0); }); - alert.setPositiveButton(android.R.string.yes, (dialog, which) -> { + alert.setPositiveButton(getString(R.string.finish), (dialog, which) -> { dialog.dismiss(); - loadSharedPreferences(newpipe_settings); + loadSharedPreferences(newpipeSettings); // restart app to properly load db System.exit(0); }); @@ -256,33 +291,34 @@ public class ContentSettingsFragment extends BasePreferenceFragment { // restart app to properly load db System.exit(0); } - } catch (Exception e) { onError(e); } } - private void loadSharedPreferences(File src) { + private void loadSharedPreferences(final File src) { ObjectInputStream input = null; try { input = new ObjectInputStream(new FileInputStream(src)); - SharedPreferences.Editor prefEdit = PreferenceManager.getDefaultSharedPreferences(getContext()).edit(); + SharedPreferences.Editor prefEdit = PreferenceManager + .getDefaultSharedPreferences(getContext()).edit(); prefEdit.clear(); Map entries = (Map) input.readObject(); for (Map.Entry entry : entries.entrySet()) { Object v = entry.getValue(); String key = entry.getKey(); - if (v instanceof Boolean) + if (v instanceof Boolean) { prefEdit.putBoolean(key, (Boolean) v); - else if (v instanceof Float) + } else if (v instanceof Float) { prefEdit.putFloat(key, (Float) v); - else if (v instanceof Integer) + } else if (v instanceof Integer) { prefEdit.putInt(key, (Integer) v); - else if (v instanceof Long) + } else if (v instanceof Long) { prefEdit.putLong(key, (Long) v); - else if (v instanceof String) - prefEdit.putString(key, ((String) v)); + } else if (v instanceof String) { + prefEdit.putString(key, (String) v); + } } prefEdit.commit(); } catch (FileNotFoundException e) { @@ -291,7 +327,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); - }finally { + } finally { try { if (input != null) { input.close(); @@ -306,7 +342,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment { // Error //////////////////////////////////////////////////////////////////////////*/ - protected void onError(Throwable e) { + protected void onError(final Throwable e) { final Activity activity = getActivity(); ErrorActivity.reportError(activity, e, activity.getClass(), diff --git a/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java index 0956f47d6..af3e3f5a9 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java @@ -6,7 +6,7 @@ import org.schabi.newpipe.R; public class DebugSettingsFragment extends BasePreferenceFragment { @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { addPreferencesFromResource(R.xml.debug_settings); } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java index 8becc79a8..aaa572eab 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java @@ -8,11 +8,12 @@ import android.content.Intent; import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.util.Log; +import android.widget.Toast; + import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.preference.Preference; -import android.util.Log; -import android.widget.Toast; import com.nononsenseapps.filepicker.Utils; @@ -28,14 +29,15 @@ import java.nio.charset.StandardCharsets; import us.shandian.giga.io.StoredDirectoryHelper; +import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; + public class DownloadSettingsFragment extends BasePreferenceFragment { + public static final boolean IGNORE_RELEASE_ON_OLD_PATH = true; private static final int REQUEST_DOWNLOAD_VIDEO_PATH = 0x1235; private static final int REQUEST_DOWNLOAD_AUDIO_PATH = 0x1236; - public static final boolean IGNORE_RELEASE_ON_OLD_PATH = true; - - private String DOWNLOAD_PATH_VIDEO_PREFERENCE; - private String DOWNLOAD_PATH_AUDIO_PREFERENCE; - private String STORAGE_USE_SAF_PREFERENCE; + private String downloadPathVideoPreference; + private String downloadPathAudioPreference; + private String storageUseSafPreference; private Preference prefPathVideo; private Preference prefPathAudio; @@ -44,16 +46,16 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { private Context ctx; @Override - public void onCreate(@Nullable Bundle savedInstanceState) { + public void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - DOWNLOAD_PATH_VIDEO_PREFERENCE = getString(R.string.download_path_video_key); - DOWNLOAD_PATH_AUDIO_PREFERENCE = getString(R.string.download_path_audio_key); - STORAGE_USE_SAF_PREFERENCE = getString(R.string.storage_use_saf); + downloadPathVideoPreference = getString(R.string.download_path_video_key); + downloadPathAudioPreference = getString(R.string.download_path_audio_key); + storageUseSafPreference = getString(R.string.storage_use_saf); final String downloadStorageAsk = getString(R.string.downloads_storage_ask); - prefPathVideo = findPreference(DOWNLOAD_PATH_VIDEO_PREFERENCE); - prefPathAudio = findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE); + prefPathVideo = findPreference(downloadPathVideoPreference); + prefPathAudio = findPreference(downloadPathAudioPreference); prefStorageAsk = findPreference(downloadStorageAsk); updatePreferencesSummary(); @@ -63,7 +65,8 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { prefStorageAsk.setSummary(R.string.downloads_storage_ask_summary); } - if (hasInvalidPath(DOWNLOAD_PATH_VIDEO_PREFERENCE) || hasInvalidPath(DOWNLOAD_PATH_AUDIO_PREFERENCE)) { + if (hasInvalidPath(downloadPathVideoPreference) + || hasInvalidPath(downloadPathAudioPreference)) { updatePreferencesSummary(); } @@ -74,12 +77,12 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { } @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { addPreferencesFromResource(R.xml.download_settings); } @Override - public void onAttach(Context context) { + public void onAttach(final Context context) { super.onAttach(context); ctx = context; } @@ -92,11 +95,14 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { } private void updatePreferencesSummary() { - showPathInSummary(DOWNLOAD_PATH_VIDEO_PREFERENCE, R.string.download_path_summary, prefPathVideo); - showPathInSummary(DOWNLOAD_PATH_AUDIO_PREFERENCE, R.string.download_path_audio_summary, prefPathAudio); + showPathInSummary(downloadPathVideoPreference, R.string.download_path_summary, + prefPathVideo); + showPathInSummary(downloadPathAudioPreference, R.string.download_path_audio_summary, + prefPathAudio); } - private void showPathInSummary(String prefKey, @StringRes int defaultString, Preference target) { + private void showPathInSummary(final String prefKey, @StringRes final int defaultString, + final Preference target) { String rawUri = defaultPreferences.getString(prefKey, null); if (rawUri == null || rawUri.isEmpty()) { target.setSummary(getString(defaultString)); @@ -121,33 +127,36 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { target.setSummary(rawUri); } - private boolean isFileUri(String path) { + private boolean isFileUri(final String path) { return path.charAt(0) == File.separatorChar || path.startsWith(ContentResolver.SCHEME_FILE); } - private boolean hasInvalidPath(String prefKey) { + private boolean hasInvalidPath(final String prefKey) { String value = defaultPreferences.getString(prefKey, null); return value == null || value.isEmpty(); } - private void updatePathPickers(boolean enabled) { + private void updatePathPickers(final boolean enabled) { prefPathVideo.setEnabled(enabled); prefPathAudio.setEnabled(enabled); } // FIXME: after releasing the old path, all downloads created on the folder becomes inaccessible - private void forgetSAFTree(Context ctx, String oldPath) { + private void forgetSAFTree(final Context context, final String oldPath) { if (IGNORE_RELEASE_ON_OLD_PATH) { return; } - if (oldPath == null || oldPath.isEmpty() || isFileUri(oldPath)) return; + if (oldPath == null || oldPath.isEmpty() || isFileUri(oldPath)) { + return; + } try { Uri uri = Uri.parse(oldPath); - ctx.getContentResolver().releasePersistableUriPermission(uri, StoredDirectoryHelper.PERMISSION_FLAGS); - ctx.revokeUriPermission(uri, StoredDirectoryHelper.PERMISSION_FLAGS); + context.getContentResolver() + .releasePersistableUriPermission(uri, StoredDirectoryHelper.PERMISSION_FLAGS); + context.revokeUriPermission(uri, StoredDirectoryHelper.PERMISSION_FLAGS); Log.i(TAG, "Revoke old path permissions success on " + oldPath); } catch (Exception err) { @@ -155,44 +164,49 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { } } - private void showMessageDialog(@StringRes int title, @StringRes int message) { + private void showMessageDialog(@StringRes final int title, @StringRes final int message) { AlertDialog.Builder msg = new AlertDialog.Builder(ctx); msg.setTitle(title); msg.setMessage(message); - msg.setPositiveButton(android.R.string.ok, null); + msg.setPositiveButton(getString(R.string.finish), null); msg.show(); } @Override - public boolean onPreferenceTreeClick(Preference preference) { + public boolean onPreferenceTreeClick(final Preference preference) { if (DEBUG) { - Log.d(TAG, "onPreferenceTreeClick() called with: preference = [" + preference + "]"); + Log.d(TAG, "onPreferenceTreeClick() called with: " + + "preference = [" + preference + "]"); } String key = preference.getKey(); int request; - if (key.equals(STORAGE_USE_SAF_PREFERENCE)) { - Toast.makeText(getContext(), R.string.download_choose_new_path, Toast.LENGTH_LONG).show(); + if (key.equals(storageUseSafPreference)) { + Toast.makeText(getContext(), R.string.download_choose_new_path, + Toast.LENGTH_LONG).show(); return true; - } else if (key.equals(DOWNLOAD_PATH_VIDEO_PREFERENCE)) { + } else if (key.equals(downloadPathVideoPreference)) { request = REQUEST_DOWNLOAD_VIDEO_PATH; - } else if (key.equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) { + } else if (key.equals(downloadPathAudioPreference)) { request = REQUEST_DOWNLOAD_AUDIO_PATH; } else { return super.onPreferenceTreeClick(preference); } Intent i; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && NewPipeSettings.useStorageAccessFramework(ctx)) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP + && NewPipeSettings.useStorageAccessFramework(ctx)) { i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) .putExtra("android.content.extra.SHOW_ADVANCED", true) - .addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | StoredDirectoryHelper.PERMISSION_FLAGS); + .addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION + | StoredDirectoryHelper.PERMISSION_FLAGS); } else { i = new Intent(getActivity(), FilePickerActivityHelper.class) .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true) - .putExtra(FilePickerActivityHelper.EXTRA_MODE, FilePickerActivityHelper.MODE_DIR); + .putExtra(FilePickerActivityHelper.EXTRA_MODE, + FilePickerActivityHelper.MODE_DIR); } startActivityForResult(i, request); @@ -201,23 +215,28 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { } @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { + public void onActivityResult(final int requestCode, final int resultCode, final Intent data) { + assureCorrectAppLanguage(getContext()); super.onActivityResult(requestCode, resultCode, data); if (DEBUG) { - Log.d(TAG, "onActivityResult() called with: requestCode = [" + requestCode + "], " + - "resultCode = [" + resultCode + "], data = [" + data + "]" + Log.d(TAG, "onActivityResult() called with: " + + "requestCode = [" + requestCode + "], " + + "resultCode = [" + resultCode + "], data = [" + data + "]" ); } - if (resultCode != Activity.RESULT_OK) return; + if (resultCode != Activity.RESULT_OK) { + return; + } String key; - if (requestCode == REQUEST_DOWNLOAD_VIDEO_PATH) - key = DOWNLOAD_PATH_VIDEO_PREFERENCE; - else if (requestCode == REQUEST_DOWNLOAD_AUDIO_PATH) - key = DOWNLOAD_PATH_AUDIO_PREFERENCE; - else + if (requestCode == REQUEST_DOWNLOAD_VIDEO_PATH) { + key = downloadPathVideoPreference; + } else if (requestCode == REQUEST_DOWNLOAD_AUDIO_PATH) { + key = downloadPathAudioPreference; + } else { return; + } Uri uri = data.getData(); if (uri == null) { @@ -227,23 +246,28 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { // revoke permissions on the old save path (required for SAF only) - final Context ctx = getContext(); - if (ctx == null) throw new NullPointerException("getContext()"); + final Context context = getContext(); + if (context == null) { + throw new NullPointerException("getContext()"); + } - forgetSAFTree(ctx, defaultPreferences.getString(key, "")); + forgetSAFTree(context, defaultPreferences.getString(key, "")); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && !FilePickerActivityHelper.isOwnFileUri(ctx, uri)) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP + && !FilePickerActivityHelper.isOwnFileUri(context, uri)) { // steps to acquire the selected path: // 1. acquire permissions on the new save path // 2. save the new path, if step(2) was successful try { - ctx.grantUriPermission(ctx.getPackageName(), uri, StoredDirectoryHelper.PERMISSION_FLAGS); + context.grantUriPermission(context.getPackageName(), uri, + StoredDirectoryHelper.PERMISSION_FLAGS); - StoredDirectoryHelper mainStorage = new StoredDirectoryHelper(ctx, uri, null); + StoredDirectoryHelper mainStorage = new StoredDirectoryHelper(context, uri, null); Log.i(TAG, "Acquiring tree success from " + uri.toString()); - if (!mainStorage.canWrite()) + if (!mainStorage.canWrite()) { throw new IOException("No write permissions on " + uri.toString()); + } } catch (IOException err) { Log.e(TAG, "Error acquiring tree from " + uri.toString(), err); showMessageDialog(R.string.general_error, R.string.no_available_dir); @@ -252,7 +276,8 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { } else { File target = Utils.getFileForUri(uri); if (!target.canWrite()) { - showMessageDialog(R.string.download_to_sdcard_error_title, R.string.download_to_sdcard_error_message); + showMessageDialog(R.string.download_to_sdcard_error_title, + R.string.download_to_sdcard_error_message); return; } uri = Uri.fromFile(target); diff --git a/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java index cdfbf54a7..d9b404204 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java @@ -1,10 +1,11 @@ package org.schabi.newpipe.settings; import android.os.Bundle; +import android.widget.Toast; + import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.preference.Preference; -import android.widget.Toast; import org.schabi.newpipe.R; import org.schabi.newpipe.local.history.HistoryRecordManager; @@ -25,7 +26,7 @@ public class HistorySettingsFragment extends BasePreferenceFragment { private CompositeDisposable disposables; @Override - public void onCreate(@Nullable Bundle savedInstanceState) { + public void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); cacheWipeKey = getString(R.string.metadata_cache_wipe_key); viewsHistoryClearKey = getString(R.string.clear_views_history_key); @@ -36,12 +37,12 @@ public class HistorySettingsFragment extends BasePreferenceFragment { } @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { addPreferencesFromResource(R.xml.history_settings); } @Override - public boolean onPreferenceTreeClick(Preference preference) { + public boolean onPreferenceTreeClick(final Preference preference) { if (preference.getKey().equals(cacheWipeKey)) { InfoCache.getInstance().clearCache(); Toast.makeText(preference.getContext(), R.string.metadata_cache_wipe_complete_notice, @@ -53,7 +54,8 @@ public class HistorySettingsFragment extends BasePreferenceFragment { .setTitle(R.string.delete_view_history_alert) .setNegativeButton(R.string.cancel, ((dialog, which) -> dialog.dismiss())) .setPositiveButton(R.string.delete, ((dialog, which) -> { - final Disposable onDeletePlaybackStates = recordManager.deleteCompelteStreamStateHistory() + final Disposable onDeletePlaybackStates + = recordManager.deleteCompelteStreamStateHistory() .observeOn(AndroidSchedulers.mainThread()) .subscribe( howManyDeleted -> Toast.makeText(getActivity(), @@ -86,7 +88,8 @@ public class HistorySettingsFragment extends BasePreferenceFragment { final Disposable onClearOrphans = recordManager.removeOrphanedRecords() .observeOn(AndroidSchedulers.mainThread()) .subscribe( - howManyDeleted -> {}, + howManyDeleted -> { + }, throwable -> ErrorActivity.reportError(getContext(), throwable, SettingsActivity.class, null, @@ -109,7 +112,8 @@ public class HistorySettingsFragment extends BasePreferenceFragment { .setNegativeButton(R.string.cancel, ((dialog, which) -> dialog.dismiss())) .setPositiveButton(R.string.delete, ((dialog, which) -> { - final Disposable onDeletePlaybackStates = recordManager.deleteCompelteStreamStateHistory() + final Disposable onDeletePlaybackStates + = recordManager.deleteCompelteStreamStateHistory() .observeOn(AndroidSchedulers.mainThread()) .subscribe( howManyDeleted -> Toast.makeText(getActivity(), diff --git a/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java index 70460509d..159625c92 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java @@ -1,6 +1,7 @@ package org.schabi.newpipe.settings; import android.os.Bundle; + import androidx.preference.Preference; import org.schabi.newpipe.BuildConfig; @@ -11,7 +12,7 @@ public class MainSettingsFragment extends BasePreferenceFragment { public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release"); @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { addPreferencesFromResource(R.xml.main_settings); if (!CheckForNewAppVersionTask.isGithubApk()) { diff --git a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java index e0003ccaa..47a16f6f3 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java +++ b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java @@ -1,3 +1,16 @@ +package org.schabi.newpipe.settings; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Environment; + +import androidx.annotation.NonNull; +import androidx.preference.PreferenceManager; + +import org.schabi.newpipe.R; + +import java.io.File; + /* * Created by k3b on 07.01.2016. * @@ -18,46 +31,13 @@ * along with NewPipe. If not, see . */ -package org.schabi.newpipe.settings; - -import android.content.Context; -import android.content.SharedPreferences; -import android.os.Environment; -import android.preference.PreferenceManager; -import androidx.annotation.NonNull; - -import org.schabi.newpipe.R; - -import java.io.File; - /** - * Helper for global settings + * Helper class for global settings. */ +public final class NewPipeSettings { + private NewPipeSettings() { } -/* - * Copyright (C) Christian Schabesberger 2016 - * NewPipeSettings.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 . - */ - -public class NewPipeSettings { - - private NewPipeSettings() { - } - - public static void initSettings(Context context) { + public static void initSettings(final Context context) { PreferenceManager.setDefaultValues(context, R.xml.appearance_settings, true); PreferenceManager.setDefaultValues(context, R.xml.content_settings, true); PreferenceManager.setDefaultValues(context, R.xml.download_settings, true); @@ -70,19 +50,22 @@ public class NewPipeSettings { getAudioDownloadFolder(context); } - private static void getVideoDownloadFolder(Context context) { + private static void getVideoDownloadFolder(final Context context) { getDir(context, R.string.download_path_video_key, Environment.DIRECTORY_MOVIES); } - private static void getAudioDownloadFolder(Context context) { + private static void getAudioDownloadFolder(final Context context) { getDir(context, R.string.download_path_audio_key, Environment.DIRECTORY_MUSIC); } - private static void getDir(Context context, int keyID, String defaultDirectoryName) { + private static void getDir(final Context context, final int keyID, + final String defaultDirectoryName) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); final String key = context.getString(keyID); String downloadPath = prefs.getString(key, null); - if ((downloadPath != null) && (!downloadPath.isEmpty())) return; + if ((downloadPath != null) && (!downloadPath.isEmpty())) { + return; + } SharedPreferences.Editor spEditor = prefs.edit(); spEditor.putString(key, getNewPipeChildFolderPathForDir(getDir(defaultDirectoryName))); @@ -90,15 +73,15 @@ public class NewPipeSettings { } @NonNull - public static File getDir(String defaultDirectoryName) { + public static File getDir(final String defaultDirectoryName) { return new File(Environment.getExternalStorageDirectory(), defaultDirectoryName); } - private static String getNewPipeChildFolderPathForDir(File dir) { + private static String getNewPipeChildFolderPathForDir(final File dir) { return new File(dir, "NewPipe").toURI().toString(); } - public static boolean useStorageAccessFramework(Context context) { + public static boolean useStorageAccessFramework(final Context context) { final String key = context.getString(R.string.storage_use_saf); final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); diff --git a/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java b/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java index 1f8d552d0..03e246533 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java @@ -53,11 +53,12 @@ import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; public class PeertubeInstanceListFragment extends Fragment { + private static final int MENU_ITEM_RESTORE_ID = 123456; private List instanceList = new ArrayList<>(); private PeertubeInstance selectedInstance; private String savedInstanceListKey; - public InstanceListAdapter instanceListAdapter; + private InstanceListAdapter instanceListAdapter; private ProgressBar progressBar; private SharedPreferences sharedPreferences; @@ -69,7 +70,7 @@ public class PeertubeInstanceListFragment extends Fragment { //////////////////////////////////////////////////////////////////////////*/ @Override - public void onCreate(@Nullable Bundle savedInstanceState) { + public void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()); @@ -81,14 +82,24 @@ public class PeertubeInstanceListFragment extends Fragment { } @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container, + final Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_instance_list, container, false); } @Override - public void onViewCreated(@NonNull View rootView, @Nullable Bundle savedInstanceState) { + public void onViewCreated(@NonNull final View rootView, + @Nullable final Bundle savedInstanceState) { super.onViewCreated(rootView, savedInstanceState); + initViews(rootView); + } + + private void initViews(@NonNull final View rootView) { + TextView instanceHelpTV = rootView.findViewById(R.id.instanceHelpTV); + instanceHelpTV.setText(getString(R.string.peertube_instance_url_help, + getString(R.string.peertube_instance_list_url))); + initButton(rootView); RecyclerView listInstances = rootView.findViewById(R.id.instances); @@ -118,28 +129,31 @@ public class PeertubeInstanceListFragment extends Fragment { @Override public void onDestroy() { super.onDestroy(); - if (disposables != null) disposables.clear(); + if (disposables != null) { + disposables.clear(); + } disposables = null; } + /*////////////////////////////////////////////////////////////////////////// // Menu //////////////////////////////////////////////////////////////////////////*/ - private final int MENU_ITEM_RESTORE_ID = 123456; - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); - final MenuItem restoreItem = menu.add(Menu.NONE, MENU_ITEM_RESTORE_ID, Menu.NONE, R.string.restore_defaults); + final MenuItem restoreItem = menu + .add(Menu.NONE, MENU_ITEM_RESTORE_ID, Menu.NONE, R.string.restore_defaults); restoreItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); - final int restoreIcon = ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_restore_defaults); + final int restoreIcon = ThemeHelper + .resolveResourceIdFromAttr(requireContext(), R.attr.ic_restore_defaults); restoreItem.setIcon(AppCompatResources.getDrawable(requireContext(), restoreIcon)); } @Override - public boolean onOptionsItemSelected(MenuItem item) { + public boolean onOptionsItemSelected(final MenuItem item) { if (item.getItemId() == MENU_ITEM_RESTORE_ID) { restoreDefaults(); return true; @@ -157,7 +171,7 @@ public class PeertubeInstanceListFragment extends Fragment { instanceList.addAll(PeertubeHelper.getInstanceList(requireContext())); } - private void selectInstance(PeertubeInstance instance) { + private void selectInstance(final PeertubeInstance instance) { selectedInstance = PeertubeHelper.selectInstance(instance, requireContext()); sharedPreferences.edit().putBoolean(Constants.KEY_MAIN_PAGE_CHANGE, true).apply(); } @@ -165,7 +179,9 @@ public class PeertubeInstanceListFragment extends Fragment { private void updateTitle() { if (getActivity() instanceof AppCompatActivity) { ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); - if (actionBar != null) actionBar.setTitle(R.string.peertube_instance_url_title); + if (actionBar != null) { + actionBar.setTitle(R.string.peertube_instance_url_title); + } } } @@ -195,14 +211,14 @@ public class PeertubeInstanceListFragment extends Fragment { .show(); } - private void initButton(View rootView) { + private void initButton(final View rootView) { final FloatingActionButton fab = rootView.findViewById(R.id.addInstanceButton); fab.setOnClickListener(v -> { showAddItemDialog(requireContext()); }); } - private void showAddItemDialog(Context c) { + private void showAddItemDialog(final Context c) { final EditText urlET = new EditText(c); urlET.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI); urlET.setHint(R.string.peertube_instance_add_help); @@ -219,46 +235,52 @@ public class PeertubeInstanceListFragment extends Fragment { dialog.show(); } - private void addInstance(String url) { + private void addInstance(final String url) { String cleanUrl = cleanUrl(url); - if(null == cleanUrl) return; + if (cleanUrl == null) { + return; + } progressBar.setVisibility(View.VISIBLE); Disposable disposable = Single.fromCallable(() -> { PeertubeInstance instance = new PeertubeInstance(cleanUrl); instance.fetchInstanceMetaData(); return instance; - }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe((instance) -> { - progressBar.setVisibility(View.GONE); - add(instance); - }, e -> { - progressBar.setVisibility(View.GONE); - Toast.makeText(getActivity(), R.string.peertube_instance_add_fail, Toast.LENGTH_SHORT).show(); - }); + }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()) + .subscribe((instance) -> { + progressBar.setVisibility(View.GONE); + add(instance); + }, e -> { + progressBar.setVisibility(View.GONE); + Toast.makeText(getActivity(), R.string.peertube_instance_add_fail, + Toast.LENGTH_SHORT).show(); + }); disposables.add(disposable); } @Nullable - private String cleanUrl(String url){ - url = url.trim(); + private String cleanUrl(final String url) { + String cleanUrl = url.trim(); // if protocol not present, add https - if(!url.startsWith("http")){ - url = "https://" + url; + if (!cleanUrl.startsWith("http")) { + cleanUrl = "https://" + cleanUrl; } // remove trailing slash - url = url.replaceAll("/$", ""); + cleanUrl = cleanUrl.replaceAll("/$", ""); // only allow https - if (!url.startsWith("https://")) { - Toast.makeText(getActivity(), R.string.peertube_instance_add_https_only, Toast.LENGTH_SHORT).show(); + if (!cleanUrl.startsWith("https://")) { + Toast.makeText(getActivity(), R.string.peertube_instance_add_https_only, + Toast.LENGTH_SHORT).show(); return null; } // only allow if not already exists for (PeertubeInstance instance : instanceList) { - if (instance.getUrl().equals(url)) { - Toast.makeText(getActivity(), R.string.peertube_instance_add_exists, Toast.LENGTH_SHORT).show(); + if (instance.getUrl().equals(cleanUrl)) { + Toast.makeText(getActivity(), R.string.peertube_instance_add_exists, + Toast.LENGTH_SHORT).show(); return null; } } - return url; + return cleanUrl; } private void add(final PeertubeInstance instance) { @@ -266,34 +288,97 @@ public class PeertubeInstanceListFragment extends Fragment { instanceListAdapter.notifyDataSetChanged(); } + private ItemTouchHelper.SimpleCallback getItemTouchCallback() { + return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, + ItemTouchHelper.START | ItemTouchHelper.END) { + @Override + public int interpolateOutOfBoundsScroll(final RecyclerView recyclerView, + final int viewSize, + final int viewSizeOutOfBounds, + final int totalSize, + final long msSinceStartScroll) { + final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize, + viewSizeOutOfBounds, totalSize, msSinceStartScroll); + final int minimumAbsVelocity = Math.max(12, + Math.abs(standardSpeed)); + return minimumAbsVelocity * (int) Math.signum(viewSizeOutOfBounds); + } + + @Override + public boolean onMove(final RecyclerView recyclerView, + final RecyclerView.ViewHolder source, + final RecyclerView.ViewHolder target) { + if (source.getItemViewType() != target.getItemViewType() + || instanceListAdapter == null) { + return false; + } + + final int sourceIndex = source.getAdapterPosition(); + final int targetIndex = target.getAdapterPosition(); + instanceListAdapter.swapItems(sourceIndex, targetIndex); + return true; + } + + @Override + public boolean isLongPressDragEnabled() { + return false; + } + + @Override + public boolean isItemViewSwipeEnabled() { + return true; + } + + @Override + public void onSwiped(final RecyclerView.ViewHolder viewHolder, final int swipeDir) { + int position = viewHolder.getAdapterPosition(); + // do not allow swiping the selected instance + if (instanceList.get(position).getUrl().equals(selectedInstance.getUrl())) { + instanceListAdapter.notifyItemChanged(position); + return; + } + instanceList.remove(position); + instanceListAdapter.notifyItemRemoved(position); + + if (instanceList.isEmpty()) { + instanceList.add(selectedInstance); + instanceListAdapter.notifyItemInserted(0); + } + } + }; + } + /*////////////////////////////////////////////////////////////////////////// // List Handling //////////////////////////////////////////////////////////////////////////*/ - private class InstanceListAdapter extends RecyclerView.Adapter { - private ItemTouchHelper itemTouchHelper; + private class InstanceListAdapter + extends RecyclerView.Adapter { private final LayoutInflater inflater; + private ItemTouchHelper itemTouchHelper; private RadioButton lastChecked; - InstanceListAdapter(Context context, ItemTouchHelper itemTouchHelper) { + InstanceListAdapter(final Context context, final ItemTouchHelper itemTouchHelper) { this.itemTouchHelper = itemTouchHelper; this.inflater = LayoutInflater.from(context); } - public void swapItems(int fromPosition, int toPosition) { + public void swapItems(final int fromPosition, final int toPosition) { Collections.swap(instanceList, fromPosition, toPosition); notifyItemMoved(fromPosition, toPosition); } @NonNull @Override - public InstanceListAdapter.TabViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + public InstanceListAdapter.TabViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, + final int viewType) { View view = inflater.inflate(R.layout.item_instance, parent, false); return new InstanceListAdapter.TabViewHolder(view); } @Override - public void onBindViewHolder(@NonNull InstanceListAdapter.TabViewHolder holder, int position) { + public void onBindViewHolder(@NonNull final InstanceListAdapter.TabViewHolder holder, + final int position) { holder.bind(position, holder); } @@ -309,7 +394,7 @@ public class PeertubeInstanceListFragment extends Fragment { private RadioButton instanceRB; private ImageView handle; - TabViewHolder(View itemView) { + TabViewHolder(final View itemView) { super(itemView); instanceIconView = itemView.findViewById(R.id.instanceIcon); @@ -320,7 +405,7 @@ public class PeertubeInstanceListFragment extends Fragment { } @SuppressLint("ClickableViewAccessibility") - void bind(int position, TabViewHolder holder) { + void bind(final int position, final TabViewHolder holder) { handle.setOnTouchListener(getOnTouchListener(holder)); final PeertubeInstance instance = instanceList.get(position); @@ -360,61 +445,4 @@ public class PeertubeInstanceListFragment extends Fragment { } } } - - private ItemTouchHelper.SimpleCallback getItemTouchCallback() { - return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, - ItemTouchHelper.START | ItemTouchHelper.END) { - @Override - public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize, - int viewSizeOutOfBounds, int totalSize, - long msSinceStartScroll) { - final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize, - viewSizeOutOfBounds, totalSize, msSinceStartScroll); - final int minimumAbsVelocity = Math.max(12, - Math.abs(standardSpeed)); - return minimumAbsVelocity * (int) Math.signum(viewSizeOutOfBounds); - } - - @Override - public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, - RecyclerView.ViewHolder target) { - if (source.getItemViewType() != target.getItemViewType() || - instanceListAdapter == null) { - return false; - } - - final int sourceIndex = source.getAdapterPosition(); - final int targetIndex = target.getAdapterPosition(); - instanceListAdapter.swapItems(sourceIndex, targetIndex); - return true; - } - - @Override - public boolean isLongPressDragEnabled() { - return false; - } - - @Override - public boolean isItemViewSwipeEnabled() { - return true; - } - - @Override - public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) { - int position = viewHolder.getAdapterPosition(); - // do not allow swiping the selected instance - if(instanceList.get(position).getUrl().equals(selectedInstance.getUrl())) { - instanceListAdapter.notifyItemChanged(position); - return; - } - instanceList.remove(position); - instanceListAdapter.notifyItemRemoved(position); - - if (instanceList.isEmpty()) { - instanceList.add(selectedInstance); - instanceListAdapter.notifyItemInserted(0); - } - } - }; - } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java index 7064aec33..df529fee0 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java @@ -3,24 +3,27 @@ package org.schabi.newpipe.settings; import android.app.Activity; import android.content.DialogInterface; import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.fragment.app.DialogFragment; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ProgressBar; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.ImageLoader; import org.schabi.newpipe.R; import org.schabi.newpipe.database.subscription.SubscriptionEntity; +import org.schabi.newpipe.local.subscription.SubscriptionManager; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; -import org.schabi.newpipe.local.subscription.SubscriptionService; +import org.schabi.newpipe.util.ThemeHelper; import java.util.List; import java.util.Vector; @@ -31,51 +34,50 @@ import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; - /** * Created by Christian Schabesberger on 26.09.17. * SelectChannelFragment.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 . + *

*/ public class SelectChannelFragment extends DialogFragment { + /** + * This contains the base display options for images. + */ + private static final DisplayImageOptions DISPLAY_IMAGE_OPTIONS + = new DisplayImageOptions.Builder().cacheInMemory(true).build(); + private final ImageLoader imageLoader = ImageLoader.getInstance(); + private OnSelectedListener onSelectedListener = null; + private OnCancelListener onCancelListener = null; + private ProgressBar progressBar; private TextView emptyView; private RecyclerView recyclerView; private List subscriptions = new Vector<>(); - /*////////////////////////////////////////////////////////////////////////// - // Interfaces - //////////////////////////////////////////////////////////////////////////*/ - - public interface OnSelectedLisener { - void onChannelSelected(int serviceId, String url, String name); - } - OnSelectedLisener onSelectedLisener = null; - public void setOnSelectedLisener(OnSelectedLisener listener) { - onSelectedLisener = listener; + public void setOnSelectedListener(final OnSelectedListener listener) { + onSelectedListener = listener; } - public interface OnCancelListener { - void onCancel(); - } - OnCancelListener onCancelListener = null; - public void setOnCancelListener(OnCancelListener listener) { + public void setOnCancelListener(final OnCancelListener listener) { onCancelListener = listener; } @@ -83,9 +85,15 @@ public class SelectChannelFragment extends DialogFragment { // Init //////////////////////////////////////////////////////////////////////////*/ + @Override + public void onCreate(@Nullable final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setStyle(STYLE_NO_TITLE, ThemeHelper.getMinWidthDialogTheme(requireContext())); + } @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container, + final Bundle savedInstanceState) { View v = inflater.inflate(R.layout.select_channel_fragment, container, false); recyclerView = v.findViewById(R.id.items_list); recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); @@ -99,8 +107,8 @@ public class SelectChannelFragment extends DialogFragment { emptyView.setVisibility(View.GONE); - SubscriptionService subscriptionService = SubscriptionService.getInstance(getContext()); - subscriptionService.getSubscription().toObservable() + SubscriptionManager subscriptionManager = new SubscriptionManager(getContext()); + subscriptionManager.subscriptions().toObservable() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(getSubscriptionObserver()); @@ -108,7 +116,6 @@ public class SelectChannelFragment extends DialogFragment { return v; } - /*////////////////////////////////////////////////////////////////////////// // Handle actions //////////////////////////////////////////////////////////////////////////*/ @@ -116,15 +123,16 @@ public class SelectChannelFragment extends DialogFragment { @Override public void onCancel(final DialogInterface dialogInterface) { super.onCancel(dialogInterface); - if(onCancelListener != null) { + if (onCancelListener != null) { onCancelListener.onCancel(); } } - private void clickedItem(int position) { - if(onSelectedLisener != null) { + private void clickedItem(final int position) { + if (onSelectedListener != null) { SubscriptionEntity entry = subscriptions.get(position); - onSelectedLisener.onChannelSelected(entry.getServiceId(), entry.getUrl(), entry.getName()); + onSelectedListener + .onChannelSelected(entry.getServiceId(), entry.getUrl(), entry.getName()); } dismiss(); } @@ -133,10 +141,10 @@ public class SelectChannelFragment extends DialogFragment { // Item handling //////////////////////////////////////////////////////////////////////////*/ - private void displayChannels(List subscriptions) { - this.subscriptions = subscriptions; + private void displayChannels(final List newSubscriptions) { + this.subscriptions = newSubscriptions; progressBar.setVisibility(View.GONE); - if(subscriptions.isEmpty()) { + if (newSubscriptions.isEmpty()) { emptyView.setVisibility(View.VISIBLE); return; } @@ -147,46 +155,67 @@ public class SelectChannelFragment extends DialogFragment { private Observer> getSubscriptionObserver() { return new Observer>() { @Override - public void onSubscribe(Disposable d) { + public void onSubscribe(final Disposable d) { } + + @Override + public void onNext(final List newSubscriptions) { + displayChannels(newSubscriptions); } @Override - public void onNext(List subscriptions) { - displayChannels(subscriptions); - } - - @Override - public void onError(Throwable exception) { + public void onError(final Throwable exception) { SelectChannelFragment.this.onError(exception); } @Override - public void onComplete() { - } + public void onComplete() { } }; } - private class SelectChannelAdapter extends - RecyclerView.Adapter { + /*////////////////////////////////////////////////////////////////////////// + // Error + //////////////////////////////////////////////////////////////////////////*/ + protected void onError(final Throwable e) { + final Activity activity = getActivity(); + ErrorActivity.reportError(activity, e, activity.getClass(), null, ErrorActivity.ErrorInfo + .make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash)); + } + + /*////////////////////////////////////////////////////////////////////////// + // Interfaces + //////////////////////////////////////////////////////////////////////////*/ + + public interface OnSelectedListener { + void onChannelSelected(int serviceId, String url, String name); + } + + public interface OnCancelListener { + void onCancel(); + } + + private class SelectChannelAdapter + extends RecyclerView.Adapter { @Override - public SelectChannelItemHolder onCreateViewHolder(ViewGroup parent, int viewType) { + public SelectChannelItemHolder onCreateViewHolder(final ViewGroup parent, + final int viewType) { View item = LayoutInflater.from(parent.getContext()) .inflate(R.layout.select_channel_item, parent, false); return new SelectChannelItemHolder(item); } @Override - public void onBindViewHolder(SelectChannelItemHolder holder, final int position) { + public void onBindViewHolder(final SelectChannelItemHolder holder, final int position) { SubscriptionEntity entry = subscriptions.get(position); holder.titleView.setText(entry.getName()); holder.view.setOnClickListener(new View.OnClickListener() { @Override - public void onClick(View view) { + public void onClick(final View view) { clickedItem(position); } }); - imageLoader.displayImage(entry.getAvatarUrl(), holder.thumbnailView, DISPLAY_IMAGE_OPTIONS); + imageLoader.displayImage(entry.getAvatarUrl(), holder.thumbnailView, + DISPLAY_IMAGE_OPTIONS); } @Override @@ -195,41 +224,15 @@ public class SelectChannelFragment extends DialogFragment { } public class SelectChannelItemHolder extends RecyclerView.ViewHolder { - public SelectChannelItemHolder(View v) { + public final View view; + final CircleImageView thumbnailView; + final TextView titleView; + SelectChannelItemHolder(final View v) { super(v); this.view = v; thumbnailView = v.findViewById(R.id.itemThumbnailView); titleView = v.findViewById(R.id.itemTitleView); } - public final View view; - public final CircleImageView thumbnailView; - public final TextView titleView; } } - - /*////////////////////////////////////////////////////////////////////////// - // Error - //////////////////////////////////////////////////////////////////////////*/ - - protected void onError(Throwable e) { - final Activity activity = getActivity(); - ErrorActivity.reportError(activity, e, - activity.getClass(), - null, - ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, - "none", "", R.string.app_ui_crash)); - } - - - /*////////////////////////////////////////////////////////////////////////// - // ImageLoaderOptions - //////////////////////////////////////////////////////////////////////////*/ - - /** - * Base display options - */ - public static final DisplayImageOptions DISPLAY_IMAGE_OPTIONS = - new DisplayImageOptions.Builder() - .cacheInMemory(true) - .build(); } diff --git a/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java index d97e4f1b7..13d34dec8 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java @@ -3,17 +3,18 @@ package org.schabi.newpipe.settings; import android.app.Activity; import android.content.DialogInterface; import android.os.Bundle; -import androidx.fragment.app.DialogFragment; -import androidx.core.content.ContextCompat; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; -import org.schabi.newpipe.MainActivity; +import androidx.annotation.Nullable; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.fragment.app.DialogFragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; @@ -21,6 +22,7 @@ import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.KioskTranslator; import org.schabi.newpipe.util.ServiceHelper; +import org.schabi.newpipe.util.ThemeHelper; import java.util.List; import java.util.Vector; @@ -28,51 +30,52 @@ import java.util.Vector; /** * Created by Christian Schabesberger on 09.10.17. * SelectKioskFragment.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 . + * along with NewPipe. If not, see . + *

*/ public class SelectKioskFragment extends DialogFragment { + private RecyclerView recyclerView = null; + private SelectKioskAdapter selectKioskAdapter = null; - private static final boolean DEBUG = MainActivity.DEBUG; + private OnSelectedListener onSelectedListener = null; + private OnCancelListener onCancelListener = null; - RecyclerView recyclerView = null; - SelectKioskAdapter selectKioskAdapter = null; - - /*////////////////////////////////////////////////////////////////////////// - // Interfaces - //////////////////////////////////////////////////////////////////////////*/ - - public interface OnSelectedLisener { - void onKioskSelected(int serviceId, String kioskId, String kioskName); + public void setOnSelectedListener(final OnSelectedListener listener) { + onSelectedListener = listener; } - OnSelectedLisener onSelectedLisener = null; - public void setOnSelectedLisener(OnSelectedLisener listener) { - onSelectedLisener = listener; - } - - public interface OnCancelListener { - void onCancel(); - } - OnCancelListener onCancelListener = null; - public void setOnCancelListener(OnCancelListener listener) { + public void setOnCancelListener(final OnCancelListener listener) { onCancelListener = listener; } + /*////////////////////////////////////////////////////////////////////////// + // Init + //////////////////////////////////////////////////////////////////////////*/ + @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + public void onCreate(@Nullable final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setStyle(STYLE_NO_TITLE, ThemeHelper.getMinWidthDialogTheme(requireContext())); + } + + @Override + public View onCreateView(final LayoutInflater inflater, final ViewGroup container, + final Bundle savedInstanceState) { View v = inflater.inflate(R.layout.select_kiosk_fragment, container, false); recyclerView = v.findViewById(R.id.items_list); recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); @@ -93,45 +96,52 @@ public class SelectKioskFragment extends DialogFragment { @Override public void onCancel(final DialogInterface dialogInterface) { super.onCancel(dialogInterface); - if(onCancelListener != null) { + if (onCancelListener != null) { onCancelListener.onCancel(); } } - private void clickedItem(SelectKioskAdapter.Entry entry) { - if(onSelectedLisener != null) { - onSelectedLisener.onKioskSelected(entry.serviceId, entry.kioskId, entry.kioskName); + private void clickedItem(final SelectKioskAdapter.Entry entry) { + if (onSelectedListener != null) { + onSelectedListener.onKioskSelected(entry.serviceId, entry.kioskId, entry.kioskName); } dismiss(); } + /*////////////////////////////////////////////////////////////////////////// + // Error + //////////////////////////////////////////////////////////////////////////*/ + + protected void onError(final Throwable e) { + final Activity activity = getActivity(); + ErrorActivity.reportError(activity, e, activity.getClass(), null, ErrorActivity.ErrorInfo + .make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash)); + } + + /*////////////////////////////////////////////////////////////////////////// + // Interfaces + //////////////////////////////////////////////////////////////////////////*/ + + public interface OnSelectedListener { + void onKioskSelected(int serviceId, String kioskId, String kioskName); + } + + public interface OnCancelListener { + void onCancel(); + } + private class SelectKioskAdapter extends RecyclerView.Adapter { - public class Entry { - public Entry (int i, int si, String ki, String kn){ - icon = i; serviceId=si; kioskId=ki; kioskName = kn; - } - final int icon; - final int serviceId; - final String kioskId; - final String kioskName; - } - private final List kioskList = new Vector<>(); - public SelectKioskAdapter() - throws Exception { - - for(StreamingService service : NewPipe.getServices()) { - for(String kioskId : service.getKioskList().getAvailableKiosks()) { + SelectKioskAdapter() throws Exception { + for (StreamingService service : NewPipe.getServices()) { + for (String kioskId : service.getKioskList().getAvailableKiosks()) { String name = String.format(getString(R.string.service_kiosk_string), service.getServiceInfo().getName(), KioskTranslator.getTranslatedKioskName(kioskId, getContext())); - kioskList.add(new Entry( - ServiceHelper.getIcon(service.getServiceId()), - service.getServiceId(), - kioskId, - name)); + kioskList.add(new Entry(ServiceHelper.getIcon(service.getServiceId()), + service.getServiceId(), kioskId, name)); } } } @@ -140,47 +150,45 @@ public class SelectKioskFragment extends DialogFragment { return kioskList.size(); } - public SelectKioskItemHolder onCreateViewHolder(ViewGroup parent, int type) { + public SelectKioskItemHolder onCreateViewHolder(final ViewGroup parent, final int type) { View item = LayoutInflater.from(parent.getContext()) .inflate(R.layout.select_kiosk_item, parent, false); return new SelectKioskItemHolder(item); } + public void onBindViewHolder(final SelectKioskItemHolder holder, final int position) { + final Entry entry = kioskList.get(position); + holder.titleView.setText(entry.kioskName); + holder.thumbnailView + .setImageDrawable(AppCompatResources.getDrawable(requireContext(), entry.icon)); + holder.view.setOnClickListener(view -> clickedItem(entry)); + } + + class Entry { + final int icon; + final int serviceId; + final String kioskId; + final String kioskName; + + Entry(final int i, final int si, final String ki, final String kn) { + icon = i; + serviceId = si; + kioskId = ki; + kioskName = kn; + } + } + public class SelectKioskItemHolder extends RecyclerView.ViewHolder { - public SelectKioskItemHolder(View v) { + public final View view; + final ImageView thumbnailView; + final TextView titleView; + + SelectKioskItemHolder(final View v) { super(v); this.view = v; thumbnailView = v.findViewById(R.id.itemThumbnailView); titleView = v.findViewById(R.id.itemTitleView); } - public final View view; - public final ImageView thumbnailView; - public final TextView titleView; } - - public void onBindViewHolder(SelectKioskItemHolder holder, final int position) { - final Entry entry = kioskList.get(position); - holder.titleView.setText(entry.kioskName); - holder.thumbnailView.setImageDrawable(ContextCompat.getDrawable(getContext(), entry.icon)); - holder.view.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - clickedItem(entry); - } - }); - } - } - - /*////////////////////////////////////////////////////////////////////////// - // Error - //////////////////////////////////////////////////////////////////////////*/ - - protected void onError(Throwable e) { - final Activity activity = getActivity(); - ErrorActivity.reportError(activity, e, - activity.getClass(), - null, - ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, - "none", "", R.string.app_ui_crash)); } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java new file mode 100644 index 000000000..1d5c94421 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java @@ -0,0 +1,225 @@ +package org.schabi.newpipe.settings; + +import android.app.Activity; +import android.content.DialogInterface; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.fragment.app.DialogFragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.nostra13.universalimageloader.core.DisplayImageOptions; +import com.nostra13.universalimageloader.core.ImageLoader; + +import org.schabi.newpipe.NewPipeDatabase; +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.AppDatabase; +import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.database.playlist.PlaylistLocalItem; +import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; +import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; +import org.schabi.newpipe.local.playlist.LocalPlaylistManager; +import org.schabi.newpipe.local.playlist.RemotePlaylistManager; +import org.schabi.newpipe.report.ErrorActivity; +import org.schabi.newpipe.report.UserAction; + +import java.util.List; +import java.util.Vector; + +import io.reactivex.Flowable; +import io.reactivex.disposables.Disposable; + +public class SelectPlaylistFragment extends DialogFragment { + /** + * This contains the base display options for images. + */ + private static final DisplayImageOptions DISPLAY_IMAGE_OPTIONS + = new DisplayImageOptions.Builder().cacheInMemory(true).build(); + + private final ImageLoader imageLoader = ImageLoader.getInstance(); + + private OnSelectedListener onSelectedListener = null; + private OnCancelListener onCancelListener = null; + + private ProgressBar progressBar; + private TextView emptyView; + private RecyclerView recyclerView; + private Disposable playlistsSubscriber; + + private List playlists = new Vector<>(); + + public void setOnSelectedListener(final OnSelectedListener listener) { + onSelectedListener = listener; + } + + public void setOnCancelListener(final OnCancelListener listener) { + onCancelListener = listener; + } + + /*////////////////////////////////////////////////////////////////////////// + // Fragment's Lifecycle + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container, + final Bundle savedInstanceState) { + final View v = + inflater.inflate(R.layout.select_playlist_fragment, container, false); + recyclerView = v.findViewById(R.id.items_list); + recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + SelectPlaylistAdapter playlistAdapter = new SelectPlaylistAdapter(); + recyclerView.setAdapter(playlistAdapter); + + progressBar = v.findViewById(R.id.progressBar); + emptyView = v.findViewById(R.id.empty_state_view); + progressBar.setVisibility(View.VISIBLE); + recyclerView.setVisibility(View.GONE); + emptyView.setVisibility(View.GONE); + + final AppDatabase database = NewPipeDatabase.getInstance(requireContext()); + final LocalPlaylistManager localPlaylistManager = new LocalPlaylistManager(database); + final RemotePlaylistManager remotePlaylistManager = new RemotePlaylistManager(database); + + playlistsSubscriber = Flowable.combineLatest(localPlaylistManager.getPlaylists(), + remotePlaylistManager.getPlaylists(), PlaylistLocalItem::merge) + .subscribe(this::displayPlaylists, this::onError); + + return v; + } + + @Override + public void onDestroy() { + super.onDestroy(); + + if (playlistsSubscriber != null) { + playlistsSubscriber.dispose(); + playlistsSubscriber = null; + } + } + + /*////////////////////////////////////////////////////////////////////////// + // Handle actions + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onCancel(final DialogInterface dialogInterface) { + super.onCancel(dialogInterface); + if (onCancelListener != null) { + onCancelListener.onCancel(); + } + } + + private void clickedItem(final int position) { + if (onSelectedListener != null) { + final LocalItem selectedItem = playlists.get(position); + + if (selectedItem instanceof PlaylistMetadataEntry) { + final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem); + onSelectedListener + .onLocalPlaylistSelected(entry.uid, entry.name); + + } else if (selectedItem instanceof PlaylistRemoteEntity) { + final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem); + onSelectedListener.onRemotePlaylistSelected( + entry.getServiceId(), entry.getUrl(), entry.getName()); + } + } + dismiss(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Item handling + //////////////////////////////////////////////////////////////////////////*/ + + private void displayPlaylists(final List newPlaylists) { + this.playlists = newPlaylists; + progressBar.setVisibility(View.GONE); + if (newPlaylists.isEmpty()) { + emptyView.setVisibility(View.VISIBLE); + return; + } + recyclerView.setVisibility(View.VISIBLE); + + } + + /*////////////////////////////////////////////////////////////////////////// + // Error + //////////////////////////////////////////////////////////////////////////*/ + + protected void onError(final Throwable e) { + final Activity activity = getActivity(); + ErrorActivity.reportError(activity, e, activity.getClass(), null, ErrorActivity.ErrorInfo + .make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash)); + } + + /*////////////////////////////////////////////////////////////////////////// + // Interfaces + //////////////////////////////////////////////////////////////////////////*/ + + public interface OnSelectedListener { + void onLocalPlaylistSelected(long id, String name); + void onRemotePlaylistSelected(int serviceId, String url, String name); + } + + public interface OnCancelListener { + void onCancel(); + } + + private class SelectPlaylistAdapter + extends RecyclerView.Adapter { + @Override + public SelectPlaylistItemHolder onCreateViewHolder(final ViewGroup parent, + final int viewType) { + final View item = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.list_playlist_mini_item, parent, false); + return new SelectPlaylistItemHolder(item); + } + + @Override + public void onBindViewHolder(final SelectPlaylistItemHolder holder, final int position) { + final PlaylistLocalItem selectedItem = playlists.get(position); + + if (selectedItem instanceof PlaylistMetadataEntry) { + final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem); + + holder.titleView.setText(entry.name); + holder.view.setOnClickListener(view -> clickedItem(position)); + imageLoader.displayImage(entry.thumbnailUrl, holder.thumbnailView, + DISPLAY_IMAGE_OPTIONS); + + } else if (selectedItem instanceof PlaylistRemoteEntity) { + final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem); + + holder.titleView.setText(entry.getName()); + holder.view.setOnClickListener(view -> clickedItem(position)); + imageLoader.displayImage(entry.getThumbnailUrl(), holder.thumbnailView, + DISPLAY_IMAGE_OPTIONS); + } + } + + @Override + public int getItemCount() { + return playlists.size(); + } + + public class SelectPlaylistItemHolder extends RecyclerView.ViewHolder { + public final View view; + final ImageView thumbnailView; + final TextView titleView; + + SelectPlaylistItemHolder(final View v) { + super(v); + this.view = v; + thumbnailView = v.findViewById(R.id.itemThumbnailView); + titleView = v.findViewById(R.id.itemTitleView); + } + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java index a3f218074..18cbece6f 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java @@ -2,18 +2,22 @@ package org.schabi.newpipe.settings; import android.content.Context; import android.os.Bundle; -import androidx.fragment.app.Fragment; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AppCompatActivity; -import androidx.preference.Preference; -import androidx.preference.PreferenceFragmentCompat; -import androidx.appcompat.widget.Toolbar; import android.view.Menu; import android.view.MenuItem; -import org.schabi.newpipe.R; -import org.schabi.newpipe.util.ThemeHelper; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.fragment.app.Fragment; +import androidx.preference.Preference; +import androidx.preference.PreferenceFragmentCompat; +import org.schabi.newpipe.R; +import org.schabi.newpipe.util.AndroidTvUtils; +import org.schabi.newpipe.util.ThemeHelper; +import org.schabi.newpipe.views.FocusOverlayView; + +import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; /* * Created by Christian Schabesberger on 31.08.15. @@ -35,16 +39,17 @@ import org.schabi.newpipe.util.ThemeHelper; * along with NewPipe. If not, see . */ -public class SettingsActivity extends AppCompatActivity implements BasePreferenceFragment.OnPreferenceStartFragmentCallback { +public class SettingsActivity extends AppCompatActivity + implements BasePreferenceFragment.OnPreferenceStartFragmentCallback { - public static void initSettings(Context context) { + public static void initSettings(final Context context) { NewPipeSettings.initSettings(context); } @Override - protected void onCreate(Bundle savedInstanceBundle) { + protected void onCreate(final Bundle savedInstanceBundle) { setTheme(ThemeHelper.getSettingsThemeStyle(this)); - + assureCorrectAppLanguage(this); super.onCreate(savedInstanceBundle); setContentView(R.layout.settings_layout); @@ -56,10 +61,14 @@ public class SettingsActivity extends AppCompatActivity implements BasePreferenc .replace(R.id.fragment_holder, new MainSettingsFragment()) .commit(); } + + if (AndroidTvUtils.isTv(this)) { + FocusOverlayView.setupFocusObserver(this); + } } @Override - public boolean onCreateOptionsMenu(Menu menu) { + public boolean onCreateOptionsMenu(final Menu menu) { ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); @@ -70,22 +79,27 @@ public class SettingsActivity extends AppCompatActivity implements BasePreferenc } @Override - public boolean onOptionsItemSelected(MenuItem item) { + public boolean onOptionsItemSelected(final MenuItem item) { int id = item.getItemId(); if (id == android.R.id.home) { if (getSupportFragmentManager().getBackStackEntryCount() == 0) { finish(); - } else getSupportFragmentManager().popBackStack(); + } else { + getSupportFragmentManager().popBackStack(); + } } return super.onOptionsItemSelected(item); } @Override - public boolean onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference preference) { - Fragment fragment = Fragment.instantiate(this, preference.getFragment(), preference.getExtras()); + public boolean onPreferenceStartFragment(final PreferenceFragmentCompat caller, + final Preference preference) { + Fragment fragment = Fragment + .instantiate(this, preference.getFragment(), preference.getExtras()); getSupportFragmentManager().beginTransaction() - .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out) + .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, + R.animator.custom_fade_in, R.animator.custom_fade_out) .replace(R.id.fragment_holder, fragment) .addToBackStack(null) .commit(); diff --git a/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java index 9a4d59549..2b103e794 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java @@ -1,15 +1,22 @@ package org.schabi.newpipe.settings; import android.os.Bundle; + import androidx.annotation.Nullable; import androidx.preference.Preference; import org.schabi.newpipe.R; public class UpdateSettingsFragment extends BasePreferenceFragment { + private Preference.OnPreferenceChangeListener updatePreferenceChange + = (preference, newValue) -> { + defaultPreferences.edit() + .putBoolean(getString(R.string.update_app_key), (boolean) newValue).apply(); + return true; + }; @Override - public void onCreate(@Nullable Bundle savedInstanceState) { + public void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); String updateToggleKey = getString(R.string.update_app_key); @@ -17,16 +24,7 @@ public class UpdateSettingsFragment extends BasePreferenceFragment { } @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { addPreferencesFromResource(R.xml.update_settings); } - - private Preference.OnPreferenceChangeListener updatePreferenceChange - = (preference, newValue) -> { - - defaultPreferences.edit().putBoolean(getString(R.string.update_app_key), - (boolean) newValue).apply(); - - return true; - }; } diff --git a/app/src/main/java/org/schabi/newpipe/settings/VideoAudioSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/VideoAudioSettingsFragment.java index 9bbdd650d..bef9a7b56 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/VideoAudioSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/VideoAudioSettingsFragment.java @@ -1,12 +1,126 @@ package org.schabi.newpipe.settings; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.os.Build; import android.os.Bundle; +import android.provider.Settings; +import android.text.format.DateUtils; +import android.widget.Toast; + +import androidx.annotation.Nullable; +import androidx.preference.ListPreference; + +import com.google.android.material.snackbar.Snackbar; import org.schabi.newpipe.R; +import org.schabi.newpipe.util.PermissionHelper; + +import java.util.LinkedList; +import java.util.List; public class VideoAudioSettingsFragment extends BasePreferenceFragment { + private SharedPreferences.OnSharedPreferenceChangeListener listener; + @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + public void onCreate(@Nullable final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + updateSeekOptions(); + + listener = (sharedPreferences, s) -> { + + // on M and above, if user chooses to minimise to popup player on exit + // and the app doesn't have display over other apps permission, + // show a snackbar to let the user give permission + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + && s.equals(getString(R.string.minimize_on_exit_key))) { + String newSetting = sharedPreferences.getString(s, null); + if (newSetting != null + && newSetting.equals(getString(R.string.minimize_on_exit_popup_key)) + && !Settings.canDrawOverlays(getContext())) { + + Snackbar.make(getListView(), R.string.permission_display_over_apps, + Snackbar.LENGTH_INDEFINITE) + .setAction(R.string.settings, view -> + PermissionHelper.checkSystemAlertWindowPermission(getContext())) + .show(); + + } + } else if (s.equals(getString(R.string.use_inexact_seek_key))) { + updateSeekOptions(); + } + }; + } + + /** + * Update fast-forward/-rewind seek duration options + * according to language and inexact seek setting. + * Exoplayer can't seek 5 seconds in audio when using inexact seek. + */ + private void updateSeekOptions() { + // initializing R.array.seek_duration_description to display the translation of seconds + final Resources res = getResources(); + final String[] durationsValues = res.getStringArray(R.array.seek_duration_value); + final List displayedDurationValues = new LinkedList<>(); + final List displayedDescriptionValues = new LinkedList<>(); + int currentDurationValue; + final boolean inexactSeek = getPreferenceManager().getSharedPreferences() + .getBoolean(res.getString(R.string.use_inexact_seek_key), false); + + for (String durationsValue : durationsValues) { + currentDurationValue = + Integer.parseInt(durationsValue) / (int) DateUtils.SECOND_IN_MILLIS; + if (inexactSeek && currentDurationValue % 10 == 5) { + continue; + } + + displayedDurationValues.add(durationsValue); + try { + displayedDescriptionValues.add(String.format( + res.getQuantityString(R.plurals.seconds, + currentDurationValue), + currentDurationValue)); + } catch (Resources.NotFoundException ignored) { + // if this happens, the translation is missing, + // and the english string will be displayed instead + } + } + + final ListPreference durations = (ListPreference) findPreference( + getString(R.string.seek_duration_key)); + durations.setEntryValues(displayedDurationValues.toArray(new CharSequence[0])); + durations.setEntries(displayedDescriptionValues.toArray(new CharSequence[0])); + final int selectedDuration = Integer.parseInt(durations.getValue()); + if (inexactSeek && selectedDuration / (int) DateUtils.SECOND_IN_MILLIS % 10 == 5) { + final int newDuration = selectedDuration / (int) DateUtils.SECOND_IN_MILLIS + 5; + durations.setValue(Integer.toString(newDuration * (int) DateUtils.SECOND_IN_MILLIS)); + + Toast toast = Toast + .makeText(getContext(), + getString(R.string.new_seek_duration_toast, newDuration), + Toast.LENGTH_LONG); + toast.show(); + } + } + + @Override + public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { addPreferencesFromResource(R.xml.video_audio_settings); } + + @Override + public void onResume() { + super.onResume(); + getPreferenceManager().getSharedPreferences() + .registerOnSharedPreferenceChangeListener(listener); + + } + + @Override + public void onPause() { + super.onPause(); + getPreferenceManager().getSharedPreferences() + .unregisterOnSharedPreferenceChangeListener(listener); + } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/custom/DurationListPreference.kt b/app/src/main/java/org/schabi/newpipe/settings/custom/DurationListPreference.kt new file mode 100644 index 000000000..14801c01c --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/custom/DurationListPreference.kt @@ -0,0 +1,46 @@ +package org.schabi.newpipe.settings.custom + +import android.content.Context +import android.util.AttributeSet +import androidx.preference.ListPreference +import org.schabi.newpipe.util.Localization + +/** + * An extension of a common ListPreference where it sets the duration values to human readable strings. + * + * The values in the entry values array will be interpreted as seconds. If the value of a specific position + * is less than or equals to zero, its original entry title will be used. + * + * If the entry values array have anything other than numbers in it, an exception will be raised. + */ +class DurationListPreference : ListPreference { + constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) + constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) + constructor(context: Context?) : super(context) + + override fun onAttached() { + super.onAttached() + + val originalEntryTitles = entries + val originalEntryValues = entryValues + val newEntryTitles = arrayOfNulls(originalEntryValues.size) + + for (i in originalEntryValues.indices) { + val currentDurationValue: Int + try { + currentDurationValue = (originalEntryValues[i] as String).toInt() + } catch (e: NumberFormatException) { + throw RuntimeException("Invalid number was set in the preference entry values array", e) + } + + if (currentDurationValue <= 0) { + newEntryTitles[i] = originalEntryTitles[i] + } else { + newEntryTitles[i] = Localization.localizeDuration(context, currentDurationValue) + } + } + + entries = newEntryTitles + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/AddTabDialog.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/AddTabDialog.java index b93ec91d0..52e50fbba 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/tabs/AddTabDialog.java +++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/AddTabDialog.java @@ -3,23 +3,23 @@ package org.schabi.newpipe.settings.tabs; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; -import androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; -import androidx.appcompat.widget.AppCompatImageView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.TextView; +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.appcompat.widget.AppCompatImageView; + import org.schabi.newpipe.R; import org.schabi.newpipe.util.ThemeHelper; -public class AddTabDialog { +public final class AddTabDialog { private final AlertDialog dialog; - AddTabDialog(@NonNull final Context context, - @NonNull final ChooseTabListItem[] items, + AddTabDialog(@NonNull final Context context, @NonNull final ChooseTabListItem[] items, @NonNull final DialogInterface.OnClickListener actions) { dialog = new AlertDialog.Builder(context) @@ -32,32 +32,35 @@ public class AddTabDialog { dialog.show(); } - public static final class ChooseTabListItem { + static final class ChooseTabListItem { final int tabId; final String itemName; - @DrawableRes final int itemIcon; + @DrawableRes + final int itemIcon; - ChooseTabListItem(Context context, Tab tab) { + ChooseTabListItem(final Context context, final Tab tab) { this(tab.getTabId(), tab.getTabName(context), tab.getTabIconRes(context)); } - ChooseTabListItem(int tabId, String itemName, @DrawableRes int itemIcon) { + ChooseTabListItem(final int tabId, final String itemName, + @DrawableRes final int itemIcon) { this.tabId = tabId; this.itemName = itemName; this.itemIcon = itemIcon; } } - private static class DialogListAdapter extends BaseAdapter { + private static final class DialogListAdapter extends BaseAdapter { private final LayoutInflater inflater; private final ChooseTabListItem[] items; - @DrawableRes private final int fallbackIcon; + @DrawableRes + private final int fallbackIcon; - private DialogListAdapter(Context context, ChooseTabListItem[] items) { + private DialogListAdapter(final Context context, final ChooseTabListItem[] items) { this.inflater = LayoutInflater.from(context); this.items = items; - this.fallbackIcon = ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_hot); + this.fallbackIcon = ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_kiosk_hot); } @Override @@ -66,17 +69,18 @@ public class AddTabDialog { } @Override - public ChooseTabListItem getItem(int position) { + public ChooseTabListItem getItem(final int position) { return items[position]; } @Override - public long getItemId(int position) { + public long getItemId(final int position) { return getItem(position).tabId; } @Override - public View getView(int position, View convertView, ViewGroup parent) { + public View getView(final int position, final View view, final ViewGroup parent) { + View convertView = view; if (convertView == null) { convertView = inflater.inflate(R.layout.list_choose_tabs_dialog, parent, false); } diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java index 6aba2783f..1b26cd529 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java @@ -4,18 +4,6 @@ import android.annotation.SuppressLint; import android.app.Dialog; import android.content.Context; import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.google.android.material.floatingactionbutton.FloatingActionButton; -import androidx.fragment.app.Fragment; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.content.res.AppCompatResources; -import androidx.appcompat.widget.AppCompatImageView; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import androidx.recyclerview.widget.ItemTouchHelper; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -26,12 +14,27 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.appcompat.widget.AppCompatImageView; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.floatingactionbutton.FloatingActionButton; + import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.settings.SelectChannelFragment; import org.schabi.newpipe.settings.SelectKioskFragment; +import org.schabi.newpipe.settings.SelectPlaylistFragment; import org.schabi.newpipe.settings.tabs.AddTabDialog.ChooseTabListItem; import org.schabi.newpipe.util.ThemeHelper; @@ -42,17 +45,19 @@ import java.util.List; import static org.schabi.newpipe.settings.tabs.Tab.typeFrom; public class ChooseTabsFragment extends Fragment { + private static final int MENU_ITEM_RESTORE_ID = 123456; private TabsManager tabsManager; - private List tabList = new ArrayList<>(); - public ChooseTabsFragment.SelectedTabsAdapter selectedTabsAdapter; + + private final List tabList = new ArrayList<>(); + private ChooseTabsFragment.SelectedTabsAdapter selectedTabsAdapter; /*////////////////////////////////////////////////////////////////////////// // Lifecycle //////////////////////////////////////////////////////////////////////////*/ @Override - public void onCreate(@Nullable Bundle savedInstanceState) { + public void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); tabsManager = TabsManager.getManager(requireContext()); @@ -62,20 +67,22 @@ public class ChooseTabsFragment extends Fragment { } @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container, + final Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_choose_tabs, container, false); } @Override - public void onViewCreated(@NonNull View rootView, @Nullable Bundle savedInstanceState) { + public void onViewCreated(@NonNull final View rootView, + @Nullable final Bundle savedInstanceState) { super.onViewCreated(rootView, savedInstanceState); initButton(rootView); - RecyclerView listSelectedTabs = rootView.findViewById(R.id.selectedTabs); + final RecyclerView listSelectedTabs = rootView.findViewById(R.id.selectedTabs); listSelectedTabs.setLayoutManager(new LinearLayoutManager(requireContext())); - ItemTouchHelper itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); + final ItemTouchHelper itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); itemTouchHelper.attachToRecyclerView(listSelectedTabs); selectedTabsAdapter = new SelectedTabsAdapter(requireContext(), itemTouchHelper); @@ -98,21 +105,21 @@ public class ChooseTabsFragment extends Fragment { // Menu //////////////////////////////////////////////////////////////////////////*/ - private final int MENU_ITEM_RESTORE_ID = 123456; - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); - final MenuItem restoreItem = menu.add(Menu.NONE, MENU_ITEM_RESTORE_ID, Menu.NONE, R.string.restore_defaults); + final MenuItem restoreItem = menu.add(Menu.NONE, MENU_ITEM_RESTORE_ID, Menu.NONE, + R.string.restore_defaults); restoreItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); - final int restoreIcon = ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_restore_defaults); + final int restoreIcon = ThemeHelper.resolveResourceIdFromAttr(requireContext(), + R.attr.ic_restore_defaults); restoreItem.setIcon(AppCompatResources.getDrawable(requireContext(), restoreIcon)); } @Override - public boolean onOptionsItemSelected(MenuItem item) { + public boolean onOptionsItemSelected(final MenuItem item) { if (item.getItemId() == MENU_ITEM_RESTORE_ID) { restoreDefaults(); return true; @@ -132,8 +139,10 @@ public class ChooseTabsFragment extends Fragment { private void updateTitle() { if (getActivity() instanceof AppCompatActivity) { - ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); - if (actionBar != null) actionBar.setTitle(R.string.main_page_content); + final ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(R.string.main_page_content); + } } } @@ -154,7 +163,7 @@ public class ChooseTabsFragment extends Fragment { .show(); } - private void initButton(View rootView) { + private void initButton(final View rootView) { final FloatingActionButton fab = rootView.findViewById(R.id.addTabsButton); fab.setOnClickListener(v -> { final ChooseTabListItem[] availableTabs = getAvailableTabs(requireContext()); @@ -179,37 +188,54 @@ public class ChooseTabsFragment extends Fragment { selectedTabsAdapter.notifyDataSetChanged(); } - private void addTab(int tabId) { + private void addTab(final int tabId) { final Tab.Type type = typeFrom(tabId); if (type == null) { - ErrorActivity.reportError(requireContext(), new IllegalStateException("Tab id not found: " + tabId), null, null, - ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none", "Choosing tabs on settings", 0)); + ErrorActivity.reportError(requireContext(), + new IllegalStateException("Tab id not found: " + tabId), null, null, + ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none", + "Choosing tabs on settings", 0)); return; } switch (type) { - case KIOSK: { - SelectKioskFragment selectFragment = new SelectKioskFragment(); - selectFragment.setOnSelectedLisener((serviceId, kioskId, kioskName) -> + case KIOSK: + SelectKioskFragment selectKioskFragment = new SelectKioskFragment(); + selectKioskFragment.setOnSelectedListener((serviceId, kioskId, kioskName) -> addTab(new Tab.KioskTab(serviceId, kioskId))); - selectFragment.show(requireFragmentManager(), "select_kiosk"); + selectKioskFragment.show(requireFragmentManager(), "select_kiosk"); return; - } - case CHANNEL: { - SelectChannelFragment selectFragment = new SelectChannelFragment(); - selectFragment.setOnSelectedLisener((serviceId, url, name) -> + case CHANNEL: + SelectChannelFragment selectChannelFragment = new SelectChannelFragment(); + selectChannelFragment.setOnSelectedListener((serviceId, url, name) -> addTab(new Tab.ChannelTab(serviceId, url, name))); - selectFragment.show(requireFragmentManager(), "select_channel"); + selectChannelFragment.show(requireFragmentManager(), "select_channel"); + return; + case PLAYLIST: + SelectPlaylistFragment selectPlaylistFragment = new SelectPlaylistFragment(); + selectPlaylistFragment.setOnSelectedListener( + new SelectPlaylistFragment.OnSelectedListener() { + @Override + public void onLocalPlaylistSelected(final long id, final String name) { + addTab(new Tab.PlaylistTab(id, name)); + } + + @Override + public void onRemotePlaylistSelected( + final int serviceId, final String url, final String name) { + addTab(new Tab.PlaylistTab(serviceId, url, name)); + } + }); + selectPlaylistFragment.show(requireFragmentManager(), "select_playlist"); return; - } default: addTab(type.getTab()); break; } } - public ChooseTabListItem[] getAvailableTabs(Context context) { + private ChooseTabListItem[] getAvailableTabs(final Context context) { final ArrayList returnList = new ArrayList<>(); for (Tab.Type type : Tab.Type.values()) { @@ -217,24 +243,34 @@ public class ChooseTabsFragment extends Fragment { switch (type) { case BLANK: if (!tabList.contains(tab)) { - returnList.add(new ChooseTabListItem(tab.getTabId(), getString(R.string.blank_page_summary), + returnList.add(new ChooseTabListItem(tab.getTabId(), + getString(R.string.blank_page_summary), tab.getTabIconRes(context))); } break; case KIOSK: - returnList.add(new ChooseTabListItem(tab.getTabId(), getString(R.string.kiosk_page_summary), - ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_hot))); + returnList.add(new ChooseTabListItem(tab.getTabId(), + getString(R.string.kiosk_page_summary), + ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_kiosk_hot))); break; case CHANNEL: - returnList.add(new ChooseTabListItem(tab.getTabId(), getString(R.string.channel_page_summary), + returnList.add(new ChooseTabListItem(tab.getTabId(), + getString(R.string.channel_page_summary), tab.getTabIconRes(context))); break; case DEFAULT_KIOSK: if (!tabList.contains(tab)) { - returnList.add(new ChooseTabListItem(tab.getTabId(), getString(R.string.default_kiosk_page_summary), - ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_hot))); + returnList.add(new ChooseTabListItem(tab.getTabId(), + getString(R.string.default_kiosk_page_summary), + ThemeHelper.resolveResourceIdFromAttr(context, + R.attr.ic_kiosk_hot))); } break; + case PLAYLIST: + returnList.add(new ChooseTabListItem(tab.getTabId(), + getString(R.string.playlist_page_summary), + tab.getTabIconRes(context))); + break; default: if (!tabList.contains(tab)) { returnList.add(new ChooseTabListItem(context, tab)); @@ -250,29 +286,88 @@ public class ChooseTabsFragment extends Fragment { // List Handling //////////////////////////////////////////////////////////////////////////*/ - private class SelectedTabsAdapter extends RecyclerView.Adapter { - private ItemTouchHelper itemTouchHelper; - private final LayoutInflater inflater; + private ItemTouchHelper.SimpleCallback getItemTouchCallback() { + return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, + ItemTouchHelper.START | ItemTouchHelper.END) { + @Override + public int interpolateOutOfBoundsScroll(final RecyclerView recyclerView, + final int viewSize, + final int viewSizeOutOfBounds, + final int totalSize, + final long msSinceStartScroll) { + final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize, + viewSizeOutOfBounds, totalSize, msSinceStartScroll); + final int minimumAbsVelocity = Math.max(12, + Math.abs(standardSpeed)); + return minimumAbsVelocity * (int) Math.signum(viewSizeOutOfBounds); + } - SelectedTabsAdapter(Context context, ItemTouchHelper itemTouchHelper) { + @Override + public boolean onMove(final RecyclerView recyclerView, + final RecyclerView.ViewHolder source, + final RecyclerView.ViewHolder target) { + if (source.getItemViewType() != target.getItemViewType() + || selectedTabsAdapter == null) { + return false; + } + + final int sourceIndex = source.getAdapterPosition(); + final int targetIndex = target.getAdapterPosition(); + selectedTabsAdapter.swapItems(sourceIndex, targetIndex); + return true; + } + + @Override + public boolean isLongPressDragEnabled() { + return false; + } + + @Override + public boolean isItemViewSwipeEnabled() { + return true; + } + + @Override + public void onSwiped(final RecyclerView.ViewHolder viewHolder, final int swipeDir) { + int position = viewHolder.getAdapterPosition(); + tabList.remove(position); + selectedTabsAdapter.notifyItemRemoved(position); + + if (tabList.isEmpty()) { + tabList.add(Tab.Type.BLANK.getTab()); + selectedTabsAdapter.notifyItemInserted(0); + } + } + }; + } + + private class SelectedTabsAdapter + extends RecyclerView.Adapter { + private final LayoutInflater inflater; + private ItemTouchHelper itemTouchHelper; + + SelectedTabsAdapter(final Context context, final ItemTouchHelper itemTouchHelper) { this.itemTouchHelper = itemTouchHelper; this.inflater = LayoutInflater.from(context); } - public void swapItems(int fromPosition, int toPosition) { + public void swapItems(final int fromPosition, final int toPosition) { Collections.swap(tabList, fromPosition, toPosition); notifyItemMoved(fromPosition, toPosition); } @NonNull @Override - public ChooseTabsFragment.SelectedTabsAdapter.TabViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View view = inflater.inflate(R.layout.list_choose_tabs, parent, false); + public ChooseTabsFragment.SelectedTabsAdapter.TabViewHolder onCreateViewHolder( + @NonNull final ViewGroup parent, final int viewType) { + final View view = inflater.inflate(R.layout.list_choose_tabs, parent, false); return new ChooseTabsFragment.SelectedTabsAdapter.TabViewHolder(view); } @Override - public void onBindViewHolder(@NonNull ChooseTabsFragment.SelectedTabsAdapter.TabViewHolder holder, int position) { + public void onBindViewHolder( + @NonNull final ChooseTabsFragment.SelectedTabsAdapter.TabViewHolder holder, + final int position) { holder.bind(position, holder); } @@ -286,7 +381,7 @@ public class ChooseTabsFragment extends Fragment { private TextView tabNameView; private ImageView handle; - TabViewHolder(View itemView) { + TabViewHolder(final View itemView) { super(itemView); tabNameView = itemView.findViewById(R.id.tabName); @@ -295,7 +390,7 @@ public class ChooseTabsFragment extends Fragment { } @SuppressLint("ClickableViewAccessibility") - void bind(int position, TabViewHolder holder) { + void bind(final int position, final TabViewHolder holder) { handle.setOnTouchListener(getOnTouchListener(holder)); final Tab tab = tabList.get(position); @@ -314,10 +409,19 @@ public class ChooseTabsFragment extends Fragment { tabName = getString(R.string.default_kiosk_page_summary); break; case KIOSK: - tabName = NewPipe.getNameOfService(((Tab.KioskTab) tab).getKioskServiceId()) + "/" + tab.getTabName(requireContext()); + tabName = NewPipe.getNameOfService(((Tab.KioskTab) tab) + .getKioskServiceId()) + "/" + tab.getTabName(requireContext()); break; case CHANNEL: - tabName = NewPipe.getNameOfService(((Tab.ChannelTab) tab).getChannelServiceId()) + "/" + tab.getTabName(requireContext()); + tabName = NewPipe.getNameOfService(((Tab.ChannelTab) tab) + .getChannelServiceId()) + "/" + tab.getTabName(requireContext()); + break; + case PLAYLIST: + final int serviceId = ((Tab.PlaylistTab) tab).getPlaylistServiceId(); + final String serviceName = serviceId == -1 + ? getString(R.string.local) + : NewPipe.getNameOfService(serviceId); + tabName = serviceName + "/" + tab.getTabName(requireContext()); break; default: tabName = tab.getTabName(requireContext()); @@ -342,56 +446,4 @@ public class ChooseTabsFragment extends Fragment { } } } - - private ItemTouchHelper.SimpleCallback getItemTouchCallback() { - return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, - ItemTouchHelper.START | ItemTouchHelper.END) { - @Override - public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize, - int viewSizeOutOfBounds, int totalSize, - long msSinceStartScroll) { - final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize, - viewSizeOutOfBounds, totalSize, msSinceStartScroll); - final int minimumAbsVelocity = Math.max(12, - Math.abs(standardSpeed)); - return minimumAbsVelocity * (int) Math.signum(viewSizeOutOfBounds); - } - - @Override - public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, - RecyclerView.ViewHolder target) { - if (source.getItemViewType() != target.getItemViewType() || - selectedTabsAdapter == null) { - return false; - } - - final int sourceIndex = source.getAdapterPosition(); - final int targetIndex = target.getAdapterPosition(); - selectedTabsAdapter.swapItems(sourceIndex, targetIndex); - return true; - } - - @Override - public boolean isLongPressDragEnabled() { - return false; - } - - @Override - public boolean isItemViewSwipeEnabled() { - return true; - } - - @Override - public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) { - int position = viewHolder.getAdapterPosition(); - tabList.remove(position); - selectedTabsAdapter.notifyItemRemoved(position); - - if (tabList.isEmpty()) { - tabList.add(Tab.Type.BLANK.getTab()); - selectedTabsAdapter.notifyItemInserted(0); - } - } - }; - } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java index cba3c4534..b0511cd11 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java +++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java @@ -11,6 +11,7 @@ import com.grack.nanojson.JsonObject; import com.grack.nanojson.JsonSink; import org.schabi.newpipe.R; +import org.schabi.newpipe.database.LocalItem.LocalItemType; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.exceptions.ExtractionException; @@ -18,9 +19,11 @@ import org.schabi.newpipe.fragments.BlankFragment; import org.schabi.newpipe.fragments.list.channel.ChannelFragment; import org.schabi.newpipe.fragments.list.kiosk.DefaultKioskFragment; import org.schabi.newpipe.fragments.list.kiosk.KioskFragment; +import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment; import org.schabi.newpipe.local.bookmark.BookmarkFragment; import org.schabi.newpipe.local.feed.FeedFragment; import org.schabi.newpipe.local.history.StatisticsPlaylistFragment; +import org.schabi.newpipe.local.playlist.LocalPlaylistFragment; import org.schabi.newpipe.local.subscription.SubscriptionFragment; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; @@ -31,59 +34,21 @@ import org.schabi.newpipe.util.ThemeHelper; import java.util.Objects; public abstract class Tab { + private static final String JSON_TAB_ID_KEY = "tab_id"; + Tab() { } - Tab(@NonNull JsonObject jsonObject) { + Tab(@NonNull final JsonObject jsonObject) { readDataFromJson(jsonObject); } - public abstract int getTabId(); - public abstract String getTabName(Context context); - @DrawableRes public abstract int getTabIconRes(Context context); - - /** - * Return a instance of the fragment that this tab represent. - */ - public abstract Fragment getFragment(Context context) throws ExtractionException; - - @Override - public boolean equals(Object obj) { - if (obj == this) return true; - - return obj instanceof Tab && obj.getClass().equals(this.getClass()) - && ((Tab) obj).getTabId() == this.getTabId(); - } - - /*////////////////////////////////////////////////////////////////////////// - // JSON Handling - //////////////////////////////////////////////////////////////////////////*/ - - private static final String JSON_TAB_ID_KEY = "tab_id"; - - public void writeJsonOn(JsonSink jsonSink) { - jsonSink.object(); - - jsonSink.value(JSON_TAB_ID_KEY, getTabId()); - writeDataToJson(jsonSink); - - jsonSink.end(); - } - - protected void writeDataToJson(JsonSink writerSink) { - // No-op - } - - protected void readDataFromJson(JsonObject jsonObject) { - // No-op - } - /*////////////////////////////////////////////////////////////////////////// // Tab Handling //////////////////////////////////////////////////////////////////////////*/ @Nullable - public static Tab from(@NonNull JsonObject jsonObject) { + public static Tab from(@NonNull final JsonObject jsonObject) { final int tabId = jsonObject.getInt(Tab.JSON_TAB_ID_KEY, -1); if (tabId == -1) { @@ -99,7 +64,7 @@ public abstract class Tab { } @Nullable - public static Type typeFrom(int tabId) { + public static Type typeFrom(final int tabId) { for (Type available : Type.values()) { if (available.getTabId() == tabId) { return available; @@ -109,7 +74,7 @@ public abstract class Tab { } @Nullable - private static Tab from(final int tabId, @Nullable JsonObject jsonObject) { + private static Tab from(final int tabId, @Nullable final JsonObject jsonObject) { final Type type = typeFrom(tabId); if (type == null) { @@ -122,12 +87,60 @@ public abstract class Tab { return new KioskTab(jsonObject); case CHANNEL: return new ChannelTab(jsonObject); + case PLAYLIST: + return new PlaylistTab(jsonObject); } } return type.getTab(); } + public abstract int getTabId(); + + public abstract String getTabName(Context context); + + @DrawableRes + public abstract int getTabIconRes(Context context); + + /** + * Return a instance of the fragment that this tab represent. + * + * @param context Android app context + * @return the fragment this tab represents + */ + public abstract Fragment getFragment(Context context) throws ExtractionException; + + @Override + public boolean equals(final Object obj) { + if (obj == this) { + return true; + } + + return obj instanceof Tab && obj.getClass().equals(this.getClass()) + && ((Tab) obj).getTabId() == this.getTabId(); + } + + /*////////////////////////////////////////////////////////////////////////// + // JSON Handling + //////////////////////////////////////////////////////////////////////////*/ + + public void writeJsonOn(final JsonSink jsonSink) { + jsonSink.object(); + + jsonSink.value(JSON_TAB_ID_KEY, getTabId()); + writeDataToJson(jsonSink); + + jsonSink.end(); + } + + protected void writeDataToJson(final JsonSink writerSink) { + // No-op + } + + protected void readDataFromJson(final JsonObject jsonObject) { + // No-op + } + /*////////////////////////////////////////////////////////////////////////// // Implementations //////////////////////////////////////////////////////////////////////////*/ @@ -140,11 +153,12 @@ public abstract class Tab { BOOKMARKS(new BookmarksTab()), HISTORY(new HistoryTab()), KIOSK(new KioskTab()), - CHANNEL(new ChannelTab()); + CHANNEL(new ChannelTab()), + PLAYLIST(new PlaylistTab()); private Tab tab; - Type(Tab tab) { + Type(final Tab tab) { this.tab = tab; } @@ -166,18 +180,18 @@ public abstract class Tab { } @Override - public String getTabName(Context context) { + public String getTabName(final Context context) { return "NewPipe"; //context.getString(R.string.blank_page_summary); } @DrawableRes @Override - public int getTabIconRes(Context context) { + public int getTabIconRes(final Context context) { return ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_blank_page); } @Override - public BlankFragment getFragment(Context context) { + public BlankFragment getFragment(final Context context) { return new BlankFragment(); } } @@ -191,18 +205,18 @@ public abstract class Tab { } @Override - public String getTabName(Context context) { + public String getTabName(final Context context) { return context.getString(R.string.tab_subscriptions); } @DrawableRes @Override - public int getTabIconRes(Context context) { + public int getTabIconRes(final Context context) { return ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_channel); } @Override - public SubscriptionFragment getFragment(Context context) { + public SubscriptionFragment getFragment(final Context context) { return new SubscriptionFragment(); } @@ -217,18 +231,18 @@ public abstract class Tab { } @Override - public String getTabName(Context context) { - return context.getString(R.string.fragment_whats_new); + public String getTabName(final Context context) { + return context.getString(R.string.fragment_feed_title); } @DrawableRes @Override - public int getTabIconRes(Context context) { - return ThemeHelper.resolveResourceIdFromAttr(context, R.attr.rss); + public int getTabIconRes(final Context context) { + return ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_rss); } @Override - public FeedFragment getFragment(Context context) { + public FeedFragment getFragment(final Context context) { return new FeedFragment(); } } @@ -242,18 +256,18 @@ public abstract class Tab { } @Override - public String getTabName(Context context) { + public String getTabName(final Context context) { return context.getString(R.string.tab_bookmarks); } @DrawableRes @Override - public int getTabIconRes(Context context) { + public int getTabIconRes(final Context context) { return ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_bookmark); } @Override - public BookmarkFragment getFragment(Context context) { + public BookmarkFragment getFragment(final Context context) { return new BookmarkFragment(); } } @@ -267,41 +281,39 @@ public abstract class Tab { } @Override - public String getTabName(Context context) { + public String getTabName(final Context context) { return context.getString(R.string.title_activity_history); } @DrawableRes @Override - public int getTabIconRes(Context context) { - return ThemeHelper.resolveResourceIdFromAttr(context, R.attr.history); + public int getTabIconRes(final Context context) { + return ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_history); } @Override - public StatisticsPlaylistFragment getFragment(Context context) { + public StatisticsPlaylistFragment getFragment(final Context context) { return new StatisticsPlaylistFragment(); } } public static class KioskTab extends Tab { public static final int ID = 5; - - private int kioskServiceId; - private String kioskId; - private static final String JSON_KIOSK_SERVICE_ID_KEY = "service_id"; private static final String JSON_KIOSK_ID_KEY = "kiosk_id"; + private int kioskServiceId; + private String kioskId; private KioskTab() { this(-1, ""); } - public KioskTab(int kioskServiceId, String kioskId) { + public KioskTab(final int kioskServiceId, final String kioskId) { this.kioskServiceId = kioskServiceId; this.kioskId = kioskId; } - public KioskTab(JsonObject jsonObject) { + public KioskTab(final JsonObject jsonObject) { super(jsonObject); } @@ -311,14 +323,14 @@ public abstract class Tab { } @Override - public String getTabName(Context context) { + public String getTabName(final Context context) { return KioskTranslator.getTranslatedKioskName(kioskId, context); } @DrawableRes @Override - public int getTabIconRes(Context context) { - final int kioskIcon = KioskTranslator.getKioskIcons(kioskId, context); + public int getTabIconRes(final Context context) { + final int kioskIcon = KioskTranslator.getKioskIcon(kioskId, context); if (kioskIcon <= 0) { throw new IllegalStateException("Kiosk ID is not valid: \"" + kioskId + "\""); @@ -328,26 +340,25 @@ public abstract class Tab { } @Override - public KioskFragment getFragment(Context context) throws ExtractionException { + public KioskFragment getFragment(final Context context) throws ExtractionException { return KioskFragment.getInstance(kioskServiceId, kioskId); } @Override - protected void writeDataToJson(JsonSink writerSink) { + protected void writeDataToJson(final JsonSink writerSink) { writerSink.value(JSON_KIOSK_SERVICE_ID_KEY, kioskServiceId) .value(JSON_KIOSK_ID_KEY, kioskId); } @Override - protected void readDataFromJson(JsonObject jsonObject) { + protected void readDataFromJson(final JsonObject jsonObject) { kioskServiceId = jsonObject.getInt(JSON_KIOSK_SERVICE_ID_KEY, -1); kioskId = jsonObject.getString(JSON_KIOSK_ID_KEY, ""); } @Override - public boolean equals(Object obj) { - return super.equals(obj) && - kioskServiceId == ((KioskTab) obj).kioskServiceId + public boolean equals(final Object obj) { + return super.equals(obj) && kioskServiceId == ((KioskTab) obj).kioskServiceId && Objects.equals(kioskId, ((KioskTab) obj).kioskId); } @@ -362,26 +373,25 @@ public abstract class Tab { public static class ChannelTab extends Tab { public static final int ID = 6; - - private int channelServiceId; - private String channelUrl; - private String channelName; - private static final String JSON_CHANNEL_SERVICE_ID_KEY = "channel_service_id"; private static final String JSON_CHANNEL_URL_KEY = "channel_url"; private static final String JSON_CHANNEL_NAME_KEY = "channel_name"; + private int channelServiceId; + private String channelUrl; + private String channelName; private ChannelTab() { this(-1, "", ""); } - public ChannelTab(int channelServiceId, String channelUrl, String channelName) { + public ChannelTab(final int channelServiceId, final String channelUrl, + final String channelName) { this.channelServiceId = channelServiceId; this.channelUrl = channelUrl; this.channelName = channelName; } - public ChannelTab(JsonObject jsonObject) { + public ChannelTab(final JsonObject jsonObject) { super(jsonObject); } @@ -391,39 +401,38 @@ public abstract class Tab { } @Override - public String getTabName(Context context) { + public String getTabName(final Context context) { return channelName; } @DrawableRes @Override - public int getTabIconRes(Context context) { + public int getTabIconRes(final Context context) { return ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_channel); } @Override - public ChannelFragment getFragment(Context context) { + public ChannelFragment getFragment(final Context context) { return ChannelFragment.getInstance(channelServiceId, channelUrl, channelName); } @Override - protected void writeDataToJson(JsonSink writerSink) { + protected void writeDataToJson(final JsonSink writerSink) { writerSink.value(JSON_CHANNEL_SERVICE_ID_KEY, channelServiceId) .value(JSON_CHANNEL_URL_KEY, channelUrl) .value(JSON_CHANNEL_NAME_KEY, channelName); } @Override - protected void readDataFromJson(JsonObject jsonObject) { + protected void readDataFromJson(final JsonObject jsonObject) { channelServiceId = jsonObject.getInt(JSON_CHANNEL_SERVICE_ID_KEY, -1); channelUrl = jsonObject.getString(JSON_CHANNEL_URL_KEY, ""); channelName = jsonObject.getString(JSON_CHANNEL_NAME_KEY, ""); } @Override - public boolean equals(Object obj) { - return super.equals(obj) && - channelServiceId == ((ChannelTab) obj).channelServiceId + public boolean equals(final Object obj) { + return super.equals(obj) && channelServiceId == ((ChannelTab) obj).channelServiceId && Objects.equals(channelUrl, ((ChannelTab) obj).channelUrl) && Objects.equals(channelName, ((ChannelTab) obj).channelName); } @@ -450,22 +459,22 @@ public abstract class Tab { } @Override - public String getTabName(Context context) { + public String getTabName(final Context context) { return KioskTranslator.getTranslatedKioskName(getDefaultKioskId(context), context); } @DrawableRes @Override - public int getTabIconRes(Context context) { - return KioskTranslator.getKioskIcons(getDefaultKioskId(context), context); + public int getTabIconRes(final Context context) { + return KioskTranslator.getKioskIcon(getDefaultKioskId(context), context); } @Override - public DefaultKioskFragment getFragment(Context context) throws ExtractionException { + public DefaultKioskFragment getFragment(final Context context) { return new DefaultKioskFragment(); } - private String getDefaultKioskId(Context context) { + private String getDefaultKioskId(final Context context) { final int kioskServiceId = ServiceHelper.getSelectedServiceId(context); String kioskId = ""; @@ -474,9 +483,129 @@ public abstract class Tab { kioskId = service.getKioskList().getDefaultKioskId(); } catch (ExtractionException e) { ErrorActivity.reportError(context, e, null, null, - ErrorActivity.ErrorInfo.make(UserAction.REQUESTED_KIOSK, "none", "Loading default kiosk from selected service", 0)); + ErrorActivity.ErrorInfo.make(UserAction.REQUESTED_KIOSK, "none", + "Loading default kiosk from selected service", 0)); } return kioskId; } } + + public static class PlaylistTab extends Tab { + public static final int ID = 8; + private static final String JSON_PLAYLIST_SERVICE_ID_KEY = "playlist_service_id"; + private static final String JSON_PLAYLIST_URL_KEY = "playlist_url"; + private static final String JSON_PLAYLIST_NAME_KEY = "playlist_name"; + private static final String JSON_PLAYLIST_ID_KEY = "playlist_id"; + private static final String JSON_PLAYLIST_TYPE_KEY = "playlist_type"; + private int playlistServiceId; + private String playlistUrl; + private String playlistName; + private long playlistId; + private LocalItemType playlistType; + + private PlaylistTab() { + this(-1, ""); + } + + public PlaylistTab(final long playlistId, final String playlistName) { + this.playlistName = playlistName; + this.playlistId = playlistId; + this.playlistType = LocalItemType.PLAYLIST_LOCAL_ITEM; + this.playlistServiceId = -1; + this.playlistUrl = ""; + } + + public PlaylistTab(final int playlistServiceId, final String playlistUrl, + final String playlistName) { + this.playlistServiceId = playlistServiceId; + this.playlistUrl = playlistUrl; + this.playlistName = playlistName; + this.playlistType = LocalItemType.PLAYLIST_REMOTE_ITEM; + this.playlistId = -1; + } + + public PlaylistTab(final JsonObject jsonObject) { + super(jsonObject); + } + + @Override + public int getTabId() { + return ID; + } + + @Override + public String getTabName(final Context context) { + return playlistName; + } + + @DrawableRes + @Override + public int getTabIconRes(final Context context) { + return ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_bookmark); + } + + @Override + public Fragment getFragment(final Context context) { + if (playlistType == LocalItemType.PLAYLIST_LOCAL_ITEM) { + return LocalPlaylistFragment.getInstance(playlistId, playlistName); + + } else { // playlistType == LocalItemType.PLAYLIST_REMOTE_ITEM + return PlaylistFragment.getInstance(playlistServiceId, playlistUrl, playlistName); + } + } + + @Override + protected void writeDataToJson(final JsonSink writerSink) { + writerSink.value(JSON_PLAYLIST_SERVICE_ID_KEY, playlistServiceId) + .value(JSON_PLAYLIST_URL_KEY, playlistUrl) + .value(JSON_PLAYLIST_NAME_KEY, playlistName) + .value(JSON_PLAYLIST_ID_KEY, playlistId) + .value(JSON_PLAYLIST_TYPE_KEY, playlistType.toString()); + } + + @Override + protected void readDataFromJson(final JsonObject jsonObject) { + playlistServiceId = jsonObject.getInt(JSON_PLAYLIST_SERVICE_ID_KEY, -1); + playlistUrl = jsonObject.getString(JSON_PLAYLIST_URL_KEY, ""); + playlistName = jsonObject.getString(JSON_PLAYLIST_NAME_KEY, ""); + playlistId = jsonObject.getInt(JSON_PLAYLIST_ID_KEY, -1); + playlistType = LocalItemType.valueOf( + jsonObject.getString(JSON_PLAYLIST_TYPE_KEY, + LocalItemType.PLAYLIST_LOCAL_ITEM.toString()) + ); + } + + @Override + public boolean equals(final Object obj) { + if (!(super.equals(obj) + && Objects.equals(playlistType, ((PlaylistTab) obj).playlistType) + && Objects.equals(playlistName, ((PlaylistTab) obj).playlistName))) { + return false; // base objects are different + } + + return (playlistId == ((PlaylistTab) obj).playlistId) // local + || (playlistServiceId == ((PlaylistTab) obj).playlistServiceId // remote + && Objects.equals(playlistUrl, ((PlaylistTab) obj).playlistUrl)); + } + + public int getPlaylistServiceId() { + return playlistServiceId; + } + + public String getPlaylistUrl() { + return playlistUrl; + } + + public String getPlaylistName() { + return playlistName; + } + + public long getPlaylistId() { + return playlistId; + } + + public LocalItemType getPlaylistType() { + return playlistType; + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsJsonHelper.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsJsonHelper.java index 9f54d59f6..d18aad9d3 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsJsonHelper.java +++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsJsonHelper.java @@ -1,5 +1,7 @@ package org.schabi.newpipe.settings.tabs; +import androidx.annotation.Nullable; + import com.grack.nanojson.JsonArray; import com.grack.nanojson.JsonObject; import com.grack.nanojson.JsonParser; @@ -12,33 +14,19 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; -import androidx.annotation.Nullable; - /** * Class to get a JSON representation of a list of tabs, and the other way around. */ -public class TabsJsonHelper { +public final class TabsJsonHelper { private static final String JSON_TABS_ARRAY_KEY = "tabs"; - private static final List FALLBACK_INITIAL_TABS_LIST = Collections.unmodifiableList(Arrays.asList( - Tab.Type.DEFAULT_KIOSK.getTab(), - Tab.Type.SUBSCRIPTIONS.getTab(), - Tab.Type.BOOKMARKS.getTab() - )); + private static final List FALLBACK_INITIAL_TABS_LIST = Collections.unmodifiableList( + Arrays.asList( + Tab.Type.DEFAULT_KIOSK.getTab(), + Tab.Type.SUBSCRIPTIONS.getTab(), + Tab.Type.BOOKMARKS.getTab())); - public static class InvalidJsonException extends Exception { - private InvalidJsonException() { - super(); - } - - private InvalidJsonException(String message) { - super(message); - } - - private InvalidJsonException(Throwable cause) { - super(cause); - } - } + private TabsJsonHelper() { } /** * Try to reads the passed JSON and returns the list of tabs if no error were encountered. @@ -52,7 +40,8 @@ public class TabsJsonHelper { * @return a list of {@link Tab tabs}. * @throws InvalidJsonException if the JSON string is not valid */ - public static List getTabsFromJson(@Nullable String tabsJson) throws InvalidJsonException { + public static List getTabsFromJson(@Nullable final String tabsJson) + throws InvalidJsonException { if (tabsJson == null || tabsJson.isEmpty()) { return getDefaultTabs(); } @@ -62,14 +51,18 @@ public class TabsJsonHelper { final JsonObject outerJsonObject; try { outerJsonObject = JsonParser.object().from(tabsJson); - final JsonArray tabsArray = outerJsonObject.getArray(JSON_TABS_ARRAY_KEY); - if (tabsArray == null) { - throw new InvalidJsonException("JSON doesn't contain \"" + JSON_TABS_ARRAY_KEY + "\" array"); + if (!outerJsonObject.has(JSON_TABS_ARRAY_KEY)) { + throw new InvalidJsonException("JSON doesn't contain \"" + JSON_TABS_ARRAY_KEY + + "\" array"); } + final JsonArray tabsArray = outerJsonObject.getArray(JSON_TABS_ARRAY_KEY); + for (Object o : tabsArray) { - if (!(o instanceof JsonObject)) continue; + if (!(o instanceof JsonObject)) { + continue; + } final Tab tab = Tab.from((JsonObject) o); @@ -94,13 +87,15 @@ public class TabsJsonHelper { * @param tabList a list of {@link Tab tabs}. * @return a JSON string representing the list of tabs */ - public static String getJsonToSave(@Nullable List tabList) { + public static String getJsonToSave(@Nullable final List tabList) { final JsonStringWriter jsonWriter = JsonWriter.string(); jsonWriter.object(); jsonWriter.array(JSON_TABS_ARRAY_KEY); - if (tabList != null) for (Tab tab : tabList) { - tab.writeJsonOn(jsonWriter); + if (tabList != null) { + for (Tab tab : tabList) { + tab.writeJsonOn(jsonWriter); + } } jsonWriter.end(); @@ -108,7 +103,21 @@ public class TabsJsonHelper { return jsonWriter.done(); } - public static List getDefaultTabs(){ + public static List getDefaultTabs() { return FALLBACK_INITIAL_TABS_LIST; } -} \ No newline at end of file + + public static final class InvalidJsonException extends Exception { + private InvalidJsonException() { + super(); + } + + private InvalidJsonException(final String message) { + super(message); + } + + private InvalidJsonException(final Throwable cause) { + super(cause); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsManager.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsManager.java index 1c99775e5..c76df7047 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsManager.java +++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsManager.java @@ -9,21 +9,23 @@ import org.schabi.newpipe.R; import java.util.List; -public class TabsManager { +public final class TabsManager { private final SharedPreferences sharedPreferences; private final String savedTabsKey; private final Context context; + private SavedTabsChangeListener savedTabsChangeListener; + private SharedPreferences.OnSharedPreferenceChangeListener preferenceChangeListener; - public static TabsManager getManager(Context context) { - return new TabsManager(context); - } - - private TabsManager(Context context) { + private TabsManager(final Context context) { this.context = context; this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); this.savedTabsKey = context.getString(R.string.saved_tabs_key); } + public static TabsManager getManager(final Context context) { + return new TabsManager(context); + } + public List getTabs() { final String savedJson = sharedPreferences.getString(savedTabsKey, null); try { @@ -34,7 +36,7 @@ public class TabsManager { } } - public void saveTabs(List tabList) { + public void saveTabs(final List tabList) { final String jsonToSave = TabsJsonHelper.getJsonToSave(tabList); sharedPreferences.edit().putString(savedTabsKey, jsonToSave).apply(); } @@ -51,14 +53,7 @@ public class TabsManager { // Listener //////////////////////////////////////////////////////////////////////////*/ - public interface SavedTabsChangeListener { - void onTabsChanged(); - } - - private SavedTabsChangeListener savedTabsChangeListener; - private SharedPreferences.OnSharedPreferenceChangeListener preferenceChangeListener; - - public void setSavedTabsListener(SavedTabsChangeListener listener) { + public void setSavedTabsListener(final SavedTabsChangeListener listener) { if (preferenceChangeListener != null) { sharedPreferences.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener); } @@ -76,18 +71,16 @@ public class TabsManager { } private SharedPreferences.OnSharedPreferenceChangeListener getPreferenceChangeListener() { - return (sharedPreferences, key) -> { + return (sp, key) -> { if (key.equals(savedTabsKey)) { - if (savedTabsChangeListener != null) savedTabsChangeListener.onTabsChanged(); + if (savedTabsChangeListener != null) { + savedTabsChangeListener.onTabsChanged(); + } } }; } + public interface SavedTabsChangeListener { + void onTabsChanged(); + } } - - - - - - - diff --git a/app/src/main/java/org/schabi/newpipe/streams/DataReader.java b/app/src/main/java/org/schabi/newpipe/streams/DataReader.java index 0e62810c5..dcd751e81 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/DataReader.java +++ b/app/src/main/java/org/schabi/newpipe/streams/DataReader.java @@ -1,254 +1,266 @@ -package org.schabi.newpipe.streams; - -import org.schabi.newpipe.streams.io.SharpStream; - -import java.io.EOFException; -import java.io.IOException; -import java.io.InputStream; - -/** - * @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 final static int BUFFER_SIZE = 128 * 1024;// 128 KiB - - private long position = 0; - private final SharpStream stream; - - private InputStream view; - private int viewSize; - - public DataReader(SharpStream stream) { - this.stream = stream; - this.readOffset = this.readBuffer.length; - } - - public long position() { - return position; - } - - public int read() throws IOException { - if (fillBuffer()) { - return -1; - } - - position++; - readCount--; - - return readBuffer[readOffset++] & 0xFF; - } - - public long skipBytes(long amount) throws IOException { - if (readCount < 0) { - return 0; - } else if (readCount == 0) { - amount = stream.skip(amount); - } else { - if (readCount > amount) { - readCount -= (int) amount; - readOffset += (int) amount; - } else { - amount = readCount + stream.skip(amount - readCount); - readCount = 0; - readOffset = readBuffer.length; - } - } - - position += amount; - return amount; - } - - public int readInt() throws IOException { - primitiveRead(INTEGER_SIZE); - return primitive[0] << 24 | primitive[1] << 16 | primitive[2] << 8 | primitive[3]; - } - - public short readShort() throws IOException { - primitiveRead(SHORT_SIZE); - return (short) (primitive[0] << 8 | primitive[1]); - } - - public 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 int read(byte[] buffer) throws IOException { - return read(buffer, 0, buffer.length); - } - - public int read(byte[] buffer, int offset, int count) throws IOException { - if (readCount < 0) { - return -1; - } - int total = 0; - - if (count >= readBuffer.length) { - if (readCount > 0) { - System.arraycopy(readBuffer, readOffset, buffer, offset, readCount); - readOffset += readCount; - - offset += readCount; - count -= readCount; - - total = readCount; - readCount = 0; - } - total += Math.max(stream.read(buffer, offset, count), 0); - } else { - while (count > 0 && !fillBuffer()) { - int read = Math.min(readCount, count); - System.arraycopy(readBuffer, readOffset, buffer, offset, read); - - readOffset += read; - readCount -= read; - - offset += read; - count -= read; - - total += read; - } - } - - position += total; - return total; - } - - public boolean available() { - return readCount > 0 || stream.available() > 0; - } - - public void rewind() throws IOException { - stream.rewind(); - - if ((position - viewSize) > 0) { - viewSize = 0;// drop view - } else { - viewSize += position; - } - - position = 0; - readOffset = readBuffer.length; - } - - public boolean canRewind() { - return stream.canRewind(); - } - - /** - * Wraps this instance of {@code DataReader} into {@code InputStream} - * object. Note: Any read in the {@code DataReader} will not modify - * (decrease) the view size - * - * @param size the size of the view - * @return the view - */ - public InputStream getView(int size) { - if (view == null) { - view = new InputStream() { - @Override - public int read() throws IOException { - if (viewSize < 1) { - return -1; - } - int res = DataReader.this.read(); - if (res > 0) { - viewSize--; - } - 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 { - if (viewSize < 1) { - return -1; - } - - int res = DataReader.this.read(buffer, offset, Math.min(viewSize, count)); - viewSize -= res; - - return res; - } - - @Override - public long skip(long amount) throws IOException { - if (viewSize < 1) { - return 0; - } - int res = (int) DataReader.this.skipBytes(Math.min(amount, viewSize)); - viewSize -= res; - - return res; - } - - @Override - public int available() { - return viewSize; - } - - @Override - public void close() { - viewSize = 0; - } - - @Override - public boolean markSupported() { - return false; - } - - }; - } - viewSize = size; - - return view; - } - - private final short[] primitive = new short[LONG_SIZE]; - - private void primitiveRead(int amount) throws IOException { - byte[] buffer = new byte[amount]; - int read = read(buffer, 0, amount); - - if (read != amount) { - throw new EOFException("Truncated stream, missing " + String.valueOf(amount - read) + " bytes"); - } - - for (int i = 0; i < amount; i++) { - primitive[i] = (short) (buffer[i] & 0xFF);// the "byte" data type in java is signed and is very annoying - } - } - - private final byte[] readBuffer = new byte[BUFFER_SIZE]; - private int readOffset; - private int readCount; - - private boolean fillBuffer() throws IOException { - if (readCount < 0) { - return true; - } - if (readOffset >= readBuffer.length) { - readCount = stream.read(readBuffer); - if (readCount < 1) { - readCount = -1; - return true; - } - readOffset = 0; - } - - return readCount < 1; - } - -} +package org.schabi.newpipe.streams; + +import org.schabi.newpipe.streams.io.SharpStream; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; + +/** + * @author kapodamy + */ +public class DataReader { + public static final int SHORT_SIZE = 2; + public static final int LONG_SIZE = 8; + public static final int INTEGER_SIZE = 4; + public static final int FLOAT_SIZE = 4; + + private static final int BUFFER_SIZE = 128 * 1024; // 128 KiB + + private long position = 0; + private final SharpStream stream; + + private InputStream view; + private int viewSize; + + public DataReader(final SharpStream stream) { + this.stream = stream; + this.readOffset = this.readBuffer.length; + } + + public long position() { + return position; + } + + public int read() throws IOException { + if (fillBuffer()) { + return -1; + } + + position++; + readCount--; + + return readBuffer[readOffset++] & 0xFF; + } + + public long skipBytes(final long byteAmount) throws IOException { + long amount = byteAmount; + if (readCount < 0) { + return 0; + } else if (readCount == 0) { + amount = stream.skip(amount); + } else { + if (readCount > amount) { + readCount -= (int) amount; + readOffset += (int) amount; + } else { + amount = readCount + stream.skip(amount - readCount); + readCount = 0; + readOffset = readBuffer.length; + } + } + + position += amount; + return amount; + } + + public int readInt() throws IOException { + primitiveRead(INTEGER_SIZE); + return primitive[0] << 24 | primitive[1] << 16 | primitive[2] << 8 | primitive[3]; + } + + public long readUnsignedInt() throws IOException { + long value = readInt(); + return value & 0xffffffffL; + } + + + public short readShort() throws IOException { + primitiveRead(SHORT_SIZE); + return (short) (primitive[0] << 8 | primitive[1]); + } + + public 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 int read(final byte[] buffer) throws IOException { + return read(buffer, 0, buffer.length); + } + + public int read(final byte[] buffer, final int off, final int c) throws IOException { + int offset = off; + int count = c; + + if (readCount < 0) { + return -1; + } + int total = 0; + + if (count >= readBuffer.length) { + if (readCount > 0) { + System.arraycopy(readBuffer, readOffset, buffer, offset, readCount); + readOffset += readCount; + + offset += readCount; + count -= readCount; + + total = readCount; + readCount = 0; + } + total += Math.max(stream.read(buffer, offset, count), 0); + } else { + while (count > 0 && !fillBuffer()) { + int read = Math.min(readCount, count); + System.arraycopy(readBuffer, readOffset, buffer, offset, read); + + readOffset += read; + readCount -= read; + + offset += read; + count -= read; + + total += read; + } + } + + position += total; + return total; + } + + public boolean available() { + return readCount > 0 || stream.available() > 0; + } + + public void rewind() throws IOException { + stream.rewind(); + + if ((position - viewSize) > 0) { + viewSize = 0; // drop view + } else { + viewSize += position; + } + + position = 0; + readOffset = readBuffer.length; + readCount = 0; + } + + public boolean canRewind() { + return stream.canRewind(); + } + + /** + * Wraps this instance of {@code DataReader} into {@code InputStream} + * object. Note: Any read in the {@code DataReader} will not modify + * (decrease) the view size + * + * @param size the size of the view + * @return the view + */ + public InputStream getView(final int size) { + if (view == null) { + view = new InputStream() { + @Override + public int read() throws IOException { + if (viewSize < 1) { + return -1; + } + int res = DataReader.this.read(); + if (res > 0) { + viewSize--; + } + return res; + } + + @Override + public int read(final byte[] buffer) throws IOException { + return read(buffer, 0, buffer.length); + } + + @Override + public int read(final byte[] buffer, final int offset, final int count) + throws IOException { + if (viewSize < 1) { + return -1; + } + + int res = DataReader.this.read(buffer, offset, Math.min(viewSize, count)); + viewSize -= res; + + return res; + } + + @Override + public long skip(final long amount) throws IOException { + if (viewSize < 1) { + return 0; + } + int res = (int) DataReader.this.skipBytes(Math.min(amount, viewSize)); + viewSize -= res; + + return res; + } + + @Override + public int available() { + return viewSize; + } + + @Override + public void close() { + viewSize = 0; + } + + @Override + public boolean markSupported() { + return false; + } + + }; + } + viewSize = size; + + return view; + } + + private final short[] primitive = new short[LONG_SIZE]; + + private void primitiveRead(final int amount) throws IOException { + byte[] buffer = new byte[amount]; + int read = read(buffer, 0, amount); + + if (read != amount) { + throw new EOFException("Truncated stream, missing " + + String.valueOf(amount - read) + " bytes"); + } + + for (int i = 0; i < amount; i++) { + // the "byte" data type in java is signed and is very annoying + primitive[i] = (short) (buffer[i] & 0xFF); + } + } + + private final byte[] readBuffer = new byte[BUFFER_SIZE]; + private int readOffset; + private int readCount; + + private boolean fillBuffer() throws IOException { + if (readCount < 0) { + return true; + } + if (readOffset >= readBuffer.length) { + readCount = stream.read(readBuffer); + if (readCount < 1) { + readCount = -1; + return true; + } + readOffset = 0; + } + + return readCount < 1; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java b/app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java index 0cfd856e1..ff3aabd78 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java +++ b/app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java @@ -1,1014 +1,947 @@ -package org.schabi.newpipe.streams; - -import org.schabi.newpipe.streams.io.SharpStream; - -import java.io.EOFException; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.NoSuchElementException; - -/** - * @author kapodamy - */ -public class Mp4DashReader { - - private static final int ATOM_MOOF = 0x6D6F6F66; - private static final int ATOM_MFHD = 0x6D666864; - private static final int ATOM_TRAF = 0x74726166; - private static final int ATOM_TFHD = 0x74666864; - private static final int ATOM_TFDT = 0x74666474; - private static final int ATOM_TRUN = 0x7472756E; - private static final int ATOM_MDIA = 0x6D646961; - private static final int ATOM_FTYP = 0x66747970; - private static final int ATOM_SIDX = 0x73696478; - private static final int ATOM_MOOV = 0x6D6F6F76; - private static final int ATOM_MDAT = 0x6D646174; - private static final int ATOM_MVHD = 0x6D766864; - private static final int ATOM_TRAK = 0x7472616B; - private static final int ATOM_MVEX = 0x6D766578; - private static final int ATOM_TREX = 0x74726578; - private static final int ATOM_TKHD = 0x746B6864; - private static final int ATOM_MFRA = 0x6D667261; - private static final int ATOM_MDHD = 0x6D646864; - private static final int ATOM_EDTS = 0x65647473; - private static final int ATOM_ELST = 0x656C7374; - private static final int ATOM_HDLR = 0x68646C72; - private static final int ATOM_MINF = 0x6D696E66; - private static final int ATOM_DINF = 0x64696E66; - private static final int ATOM_STBL = 0x7374626C; - private static final int ATOM_STSD = 0x73747364; - private static final int ATOM_VMHD = 0x766D6864; - private static final int ATOM_SMHD = 0x736D6864; - - private static final int BRAND_DASH = 0x64617368; - private static final int BRAND_ISO5 = 0x69736F35; - - private static final int HANDLER_VIDE = 0x76696465; - private static final int HANDLER_SOUN = 0x736F756E; - private static final int HANDLER_SUBT = 0x73756274; - - - private final DataReader stream; - - private Mp4Track[] tracks = null; - private int[] brands = null; - - private Box box; - private Moof moof; - - private boolean chunkZero = false; - - private int selectedTrack = -1; - private Box backupBox = null; - - public enum TrackKind { - Audio, Video, Subtitles, Other - } - - public Mp4DashReader(SharpStream source) { - this.stream = new DataReader(source); - } - - public void parse() throws IOException, NoSuchElementException { - if (selectedTrack > -1) { - return; - } - - box = readBox(ATOM_FTYP); - brands = parse_ftyp(box); - switch (brands[0]) { - case BRAND_DASH: - case BRAND_ISO5:// ¿why not? - break; - default: - throw new NoSuchElementException( - "Not a MPEG-4 DASH container, major brand is not 'dash' or 'iso5' is " + boxName(brands[0]) - ); - } - - Moov moov = null; - int i; - - while (box.type != ATOM_MOOF) { - ensure(box); - box = readBox(); - - switch (box.type) { - case ATOM_MOOV: - moov = parse_moov(box); - break; - case ATOM_SIDX: - break; - case ATOM_MFRA: - break; - } - } - - if (moov == null) { - throw new IOException("The provided Mp4 doesn't have the 'moov' box"); - } - - tracks = new Mp4Track[moov.trak.length]; - - for (i = 0; i < tracks.length; i++) { - tracks[i] = new Mp4Track(); - tracks[i].trak = moov.trak[i]; - - if (moov.mvex_trex != null) { - for (Trex mvex_trex : moov.mvex_trex) { - if (tracks[i].trak.tkhd.trackId == mvex_trex.trackId) { - tracks[i].trex = mvex_trex; - } - } - } - - switch (moov.trak[i].mdia.hdlr.subType) { - case HANDLER_VIDE: - tracks[i].kind = TrackKind.Video; - break; - case HANDLER_SOUN: - tracks[i].kind = TrackKind.Audio; - break; - case HANDLER_SUBT: - tracks[i].kind = TrackKind.Subtitles; - break; - default: - tracks[i].kind = TrackKind.Other; - break; - } - } - - backupBox = box; - } - - Mp4Track selectTrack(int index) { - selectedTrack = index; - return tracks[index]; - } - - /** - * Count all fragments present. This operation requires a seekable stream - * - * @return list with a basic info - * @throws IOException if the source stream is not seekeable - */ - int getFragmentsCount() throws IOException { - if (selectedTrack < 0) { - throw new IllegalStateException("track no selected"); - } - if (!stream.canRewind()) { - throw new IOException("The provided stream doesn't allow seek"); - } - - Box tmp; - int count = 0; - - if (box.type == ATOM_MOOF) { - tmp = box; - } else { - ensure(box); - tmp = readBox(); - } - - do { - if (tmp.type == ATOM_MOOF) { - ensure(readBox(ATOM_MFHD)); - Box traf; - while ((traf = untilBox(tmp, ATOM_TRAF)) != null) { - Box tfhd = readBox(ATOM_TFHD); - if (parse_tfhd(tracks[selectedTrack].trak.tkhd.trackId) != null) { - count++; - break; - } - ensure(tfhd); - ensure(traf); - } - } - ensure(tmp); - } while (stream.available() && (tmp = readBox()) != null); - - rewind(); - - return count; - } - - public int[] getBrands() { - if (brands == null) throw new IllegalStateException("Not parsed"); - return brands; - } - - public void rewind() throws IOException { - if (!stream.canRewind()) { - throw new IOException("The provided stream doesn't allow seek"); - } - if (box == null) { - return; - } - - box = backupBox; - chunkZero = false; - - stream.rewind(); - stream.skipBytes(backupBox.offset + (DataReader.INTEGER_SIZE * 2)); - } - - public Mp4Track[] getAvailableTracks() { - return tracks; - } - - public Mp4DashChunk getNextChunk(boolean infoOnly) throws IOException { - Mp4Track track = tracks[selectedTrack]; - - while (stream.available()) { - - if (chunkZero) { - ensure(box); - if (!stream.available()) { - break; - } - box = readBox(); - } else { - chunkZero = true; - } - - switch (box.type) { - case ATOM_MOOF: - if (moof != null) { - throw new IOException("moof found without mdat"); - } - - moof = parse_moof(box, track.trak.tkhd.trackId); - - if (moof.traf != null) { - - if (hasFlag(moof.traf.trun.bFlags, 0x0001)) { - moof.traf.trun.dataOffset -= box.size + 8; - if (moof.traf.trun.dataOffset < 0) { - throw new IOException("trun box has wrong data offset, points outside of concurrent mdat box"); - } - } - - if (moof.traf.trun.chunkSize < 1) { - if (hasFlag(moof.traf.tfhd.bFlags, 0x10)) { - moof.traf.trun.chunkSize = moof.traf.tfhd.defaultSampleSize * moof.traf.trun.entryCount; - } else { - moof.traf.trun.chunkSize = (int) (box.size - 8); - } - } - if (!hasFlag(moof.traf.trun.bFlags, 0x900) && moof.traf.trun.chunkDuration == 0) { - if (hasFlag(moof.traf.tfhd.bFlags, 0x20)) { - moof.traf.trun.chunkDuration = moof.traf.tfhd.defaultSampleDuration * moof.traf.trun.entryCount; - } - } - } - break; - case ATOM_MDAT: - if (moof == null) { - throw new IOException("mdat found without moof"); - } - - if (moof.traf == null) { - moof = null; - continue;// find another chunk - } - - Mp4DashChunk chunk = new Mp4DashChunk(); - chunk.moof = moof; - if (!infoOnly) { - chunk.data = stream.getView(moof.traf.trun.chunkSize); - } - - moof = null; - - stream.skipBytes(chunk.moof.traf.trun.dataOffset); - return chunk; - default: - } - } - - return null; - } - - - - private long readUint() throws IOException { - return stream.readInt() & 0xffffffffL; - } - - public static boolean hasFlag(int flags, int mask) { - return (flags & mask) == mask; - } - - private String boxName(Box ref) { - return boxName(ref.type); - } - - private String boxName(int type) { - try { - return new String(ByteBuffer.allocate(4).putInt(type).array(), "UTF-8"); - } catch (UnsupportedEncodingException e) { - return "0x" + Integer.toHexString(type); - } - } - - private Box readBox() throws IOException { - Box b = new Box(); - b.offset = stream.position(); - b.size = stream.readInt(); - b.type = stream.readInt(); - - if (b.size == 1) { - b.size = stream.readLong(); - } - - return b; - } - - private Box readBox(int expected) throws IOException { - Box b = readBox(); - if (b.type != expected) { - throw new NoSuchElementException("expected " + boxName(expected) + " found " + boxName(b)); - } - return b; - } - - private byte[] readFullBox(Box ref) throws IOException { - // full box reading is limited to 2 GiB, and should be enough - int size = (int) ref.size; - - ByteBuffer buffer = ByteBuffer.allocate(size); - buffer.putInt(size); - buffer.putInt(ref.type); - - int read = size - 8; - - if (stream.read(buffer.array(), 8, read) != read) { - throw new EOFException( - String.format("EOF reached in box: type=%s offset=%s size=%s", boxName(ref.type), ref.offset, ref.size) - ); - } - - return buffer.array(); - } - - private void ensure(Box ref) throws IOException { - long skip = ref.offset + ref.size - stream.position(); - - if (skip == 0) { - return; - } else if (skip < 0) { - throw new EOFException(String.format( - "parser go beyond limits of the box. type=%s offset=%s size=%s position=%s", - boxName(ref), ref.offset, ref.size, stream.position() - )); - } - - stream.skipBytes((int) skip); - } - - private Box untilBox(Box ref, int... expected) throws IOException { - Box b; - while (stream.position() < (ref.offset + ref.size)) { - b = readBox(); - for (int type : expected) { - if (b.type == type) { - return b; - } - } - ensure(b); - } - - return null; - } - - private Box untilAnyBox(Box ref) throws IOException { - if (stream.position() >= (ref.offset + ref.size)) { - return null; - } - - return readBox(); - } - - - - private Moof parse_moof(Box ref, int trackId) throws IOException { - Moof obj = new Moof(); - - Box b = readBox(ATOM_MFHD); - obj.mfhd_SequenceNumber = parse_mfhd(); - ensure(b); - - while ((b = untilBox(ref, ATOM_TRAF)) != null) { - obj.traf = parse_traf(b, trackId); - ensure(b); - - if (obj.traf != null) { - return obj; - } - } - - return obj; - } - - private int parse_mfhd() throws IOException { - // version - // flags - stream.skipBytes(4); - - return stream.readInt(); - } - - private Traf parse_traf(Box ref, int trackId) throws IOException { - Traf traf = new Traf(); - - Box b = readBox(ATOM_TFHD); - traf.tfhd = parse_tfhd(trackId); - ensure(b); - - if (traf.tfhd == null) { - return null; - } - - b = untilBox(ref, ATOM_TRUN, ATOM_TFDT); - - if (b.type == ATOM_TFDT) { - traf.tfdt = parse_tfdt(); - ensure(b); - b = readBox(ATOM_TRUN); - } - - traf.trun = parse_trun(); - ensure(b); - - return traf; - } - - private Tfhd parse_tfhd(int trackId) throws IOException { - Tfhd obj = new Tfhd(); - - obj.bFlags = stream.readInt(); - obj.trackId = stream.readInt(); - - if (trackId != -1 && obj.trackId != trackId) { - return null; - } - - if (hasFlag(obj.bFlags, 0x01)) { - stream.skipBytes(8); - } - if (hasFlag(obj.bFlags, 0x02)) { - stream.skipBytes(4); - } - if (hasFlag(obj.bFlags, 0x08)) { - obj.defaultSampleDuration = stream.readInt(); - } - if (hasFlag(obj.bFlags, 0x10)) { - obj.defaultSampleSize = stream.readInt(); - } - if (hasFlag(obj.bFlags, 0x20)) { - obj.defaultSampleFlags = stream.readInt(); - } - - return obj; - } - - private long parse_tfdt() throws IOException { - int version = stream.read(); - stream.skipBytes(3);// flags - return version == 0 ? readUint() : stream.readLong(); - } - - private Trun parse_trun() throws IOException { - Trun obj = new Trun(); - obj.bFlags = stream.readInt(); - obj.entryCount = stream.readInt();// unsigned int - - obj.entries_rowSize = 0; - if (hasFlag(obj.bFlags, 0x0100)) { - obj.entries_rowSize += 4; - } - if (hasFlag(obj.bFlags, 0x0200)) { - obj.entries_rowSize += 4; - } - if (hasFlag(obj.bFlags, 0x0400)) { - obj.entries_rowSize += 4; - } - if (hasFlag(obj.bFlags, 0x0800)) { - obj.entries_rowSize += 4; - } - obj.bEntries = new byte[obj.entries_rowSize * obj.entryCount]; - - if (hasFlag(obj.bFlags, 0x0001)) { - obj.dataOffset = stream.readInt(); - } - if (hasFlag(obj.bFlags, 0x0004)) { - obj.bFirstSampleFlags = stream.readInt(); - } - - stream.read(obj.bEntries); - - for (int i = 0; i < obj.entryCount; i++) { - TrunEntry entry = obj.getEntry(i); - if (hasFlag(obj.bFlags, 0x0100)) { - obj.chunkDuration += entry.sampleDuration; - } - if (hasFlag(obj.bFlags, 0x0200)) { - obj.chunkSize += entry.sampleSize; - } - if (hasFlag(obj.bFlags, 0x0800)) { - if (!hasFlag(obj.bFlags, 0x0100)) { - obj.chunkDuration += entry.sampleCompositionTimeOffset; - } - } - } - - return obj; - } - - private int[] parse_ftyp(Box ref) throws IOException { - int i = 0; - int[] list = new int[(int) ((ref.offset + ref.size - stream.position() - 4) / 4)]; - - list[i++] = stream.readInt();// major brand - - stream.skipBytes(4);// minor version - - for (; i < list.length; i++) - list[i] = stream.readInt();// compatible brands - - return list; - } - - private Mvhd parse_mvhd() throws IOException { - int version = stream.read(); - stream.skipBytes(3);// flags - - // creation entries_time - // modification entries_time - stream.skipBytes(2 * (version == 0 ? 4 : 8)); - - Mvhd obj = new Mvhd(); - obj.timeScale = readUint(); - - // chunkDuration - stream.skipBytes(version == 0 ? 4 : 8); - - // rate - // volume - // reserved - // matrix array - // predefined - stream.skipBytes(76); - - obj.nextTrackId = readUint(); - - return obj; - } - - private Tkhd parse_tkhd() throws IOException { - int version = stream.read(); - - Tkhd obj = new Tkhd(); - - // flags - // creation entries_time - // modification entries_time - stream.skipBytes(3 + (2 * (version == 0 ? 4 : 8))); - - obj.trackId = stream.readInt(); - - stream.skipBytes(4);// reserved - - obj.duration = version == 0 ? readUint() : stream.readLong(); - - stream.skipBytes(2 * 4);// reserved - - obj.bLayer = stream.readShort(); - obj.bAlternateGroup = stream.readShort(); - obj.bVolume = stream.readShort(); - - stream.skipBytes(2);// reserved - - obj.matrix = new byte[9 * 4]; - stream.read(obj.matrix); - - obj.bWidth = stream.readInt(); - obj.bHeight = stream.readInt(); - - return obj; - } - - private Trak parse_trak(Box ref) throws IOException { - Trak trak = new Trak(); - - Box b = readBox(ATOM_TKHD); - trak.tkhd = parse_tkhd(); - ensure(b); - - while ((b = untilBox(ref, ATOM_MDIA, ATOM_EDTS)) != null) { - switch (b.type) { - case ATOM_MDIA: - trak.mdia = parse_mdia(b); - break; - case ATOM_EDTS: - trak.edst_elst = parse_edts(b); - break; - } - - ensure(b); - } - - return trak; - } - - private Mdia parse_mdia(Box ref) throws IOException { - Mdia obj = new Mdia(); - - Box b; - while ((b = untilBox(ref, ATOM_MDHD, ATOM_HDLR, ATOM_MINF)) != null) { - switch (b.type) { - case ATOM_MDHD: - obj.mdhd = readFullBox(b); - - // read time scale - ByteBuffer buffer = ByteBuffer.wrap(obj.mdhd); - byte version = buffer.get(8); - buffer.position(12 + ((version == 0 ? 4 : 8) * 2)); - obj.mdhd_timeScale = buffer.getInt(); - break; - case ATOM_HDLR: - obj.hdlr = parse_hdlr(b); - break; - case ATOM_MINF: - obj.minf = parse_minf(b); - break; - } - ensure(b); - } - - return obj; - } - - private Hdlr parse_hdlr(Box ref) throws IOException { - // version - // flags - stream.skipBytes(4); - - Hdlr obj = new Hdlr(); - obj.bReserved = new byte[12]; - - obj.type = stream.readInt(); - obj.subType = stream.readInt(); - stream.read(obj.bReserved); - - // component name (is a ansi/ascii string) - stream.skipBytes((ref.offset + ref.size) - stream.position()); - - return obj; - } - - private Moov parse_moov(Box ref) throws IOException { - Box b = readBox(ATOM_MVHD); - Moov moov = new Moov(); - moov.mvhd = parse_mvhd(); - ensure(b); - - ArrayList tmp = new ArrayList<>((int) moov.mvhd.nextTrackId); - while ((b = untilBox(ref, ATOM_TRAK, ATOM_MVEX)) != null) { - - switch (b.type) { - case ATOM_TRAK: - tmp.add(parse_trak(b)); - break; - case ATOM_MVEX: - moov.mvex_trex = parse_mvex(b, (int) moov.mvhd.nextTrackId); - break; - } - - ensure(b); - } - - moov.trak = tmp.toArray(new Trak[0]); - - return moov; - } - - private Trex[] parse_mvex(Box ref, int possibleTrackCount) throws IOException { - ArrayList tmp = new ArrayList<>(possibleTrackCount); - - Box b; - while ((b = untilBox(ref, ATOM_TREX)) != null) { - tmp.add(parse_trex()); - ensure(b); - } - - return tmp.toArray(new Trex[0]); - } - - private Trex parse_trex() throws IOException { - // version - // flags - stream.skipBytes(4); - - Trex obj = new Trex(); - obj.trackId = stream.readInt(); - obj.defaultSampleDescriptionIndex = stream.readInt(); - obj.defaultSampleDuration = stream.readInt(); - obj.defaultSampleSize = stream.readInt(); - obj.defaultSampleFlags = stream.readInt(); - - return obj; - } - - private Elst parse_edts(Box ref) throws IOException { - Box b = untilBox(ref, ATOM_ELST); - if (b == null) { - return null; - } - - Elst obj = new Elst(); - - boolean v1 = stream.read() == 1; - stream.skipBytes(3);// flags - - int entryCount = stream.readInt(); - if (entryCount < 1) { - obj.bMediaRate = 0x00010000;// default media rate (1.0) - return obj; - } - - if (v1) { - stream.skipBytes(DataReader.LONG_SIZE);// segment duration - obj.MediaTime = stream.readLong(); - // ignore all remain entries - stream.skipBytes((entryCount - 1) * (DataReader.LONG_SIZE * 2)); - } else { - stream.skipBytes(DataReader.INTEGER_SIZE);// segment duration - obj.MediaTime = stream.readInt(); - } - - obj.bMediaRate = stream.readInt(); - - return obj; - } - - private Minf parse_minf(Box ref) throws IOException { - Minf obj = new Minf(); - - Box b; - while ((b = untilAnyBox(ref)) != null) { - - switch (b.type) { - case ATOM_DINF: - obj.dinf = readFullBox(b); - break; - case ATOM_STBL: - obj.stbl_stsd = parse_stbl(b); - break; - case ATOM_VMHD: - case ATOM_SMHD: - obj.$mhd = readFullBox(b); - break; - - } - ensure(b); - } - - return obj; - } - - /** - * this only read the "stsd" box inside - */ - private byte[] parse_stbl(Box ref) throws IOException { - Box b = untilBox(ref, ATOM_STSD); - - if (b == null) { - return new byte[0];// this never should happens (missing codec startup data) - } - - return readFullBox(b); - } - - - - class Box { - - int type; - long offset; - long size; - } - - public class Moof { - - int mfhd_SequenceNumber; - public Traf traf; - } - - public class Traf { - - public Tfhd tfhd; - long tfdt; - public Trun trun; - } - - public class Tfhd { - - int bFlags; - public int trackId; - int defaultSampleDuration; - int defaultSampleSize; - int defaultSampleFlags; - } - - class TrunEntry { - - int sampleDuration; - int sampleSize; - int sampleFlags; - int sampleCompositionTimeOffset; - - boolean hasCompositionTimeOffset; - boolean isKeyframe; - - } - - public class Trun { - - public int chunkDuration; - public int chunkSize; - - public int bFlags; - int bFirstSampleFlags; - int dataOffset; - - public int entryCount; - byte[] bEntries; - int entries_rowSize; - - public TrunEntry getEntry(int i) { - ByteBuffer buffer = ByteBuffer.wrap(bEntries, i * entries_rowSize, entries_rowSize); - TrunEntry entry = new TrunEntry(); - - if (hasFlag(bFlags, 0x0100)) { - entry.sampleDuration = buffer.getInt(); - } - if (hasFlag(bFlags, 0x0200)) { - entry.sampleSize = buffer.getInt(); - } - if (hasFlag(bFlags, 0x0400)) { - entry.sampleFlags = buffer.getInt(); - } - if (hasFlag(bFlags, 0x0800)) { - entry.sampleCompositionTimeOffset = buffer.getInt(); - } - - entry.hasCompositionTimeOffset = hasFlag(bFlags, 0x0800); - entry.isKeyframe = !hasFlag(entry.sampleFlags, 0x10000); - - return entry; - } - - public TrunEntry getAbsoluteEntry(int i, Tfhd header) { - TrunEntry entry = getEntry(i); - - if (!hasFlag(bFlags, 0x0100) && hasFlag(header.bFlags, 0x20)) { - entry.sampleFlags = header.defaultSampleFlags; - } - - if (!hasFlag(bFlags, 0x0200) && hasFlag(header.bFlags, 0x10)) { - entry.sampleSize = header.defaultSampleSize; - } - - if (!hasFlag(bFlags, 0x0100) && hasFlag(header.bFlags, 0x08)) { - entry.sampleDuration = header.defaultSampleDuration; - } - - if (i == 0 && hasFlag(bFlags, 0x0004)) { - entry.sampleFlags = bFirstSampleFlags; - } - - return entry; - } - } - - public class Tkhd { - - int trackId; - long duration; - short bVolume; - int bWidth; - int bHeight; - byte[] matrix; - short bLayer; - short bAlternateGroup; - } - - public class Trak { - - public Tkhd tkhd; - public Elst edst_elst; - public Mdia mdia; - - } - - class Mvhd { - - long timeScale; - long nextTrackId; - } - - class Moov { - - Mvhd mvhd; - Trak[] trak; - Trex[] mvex_trex; - } - - public class Trex { - - private int trackId; - int defaultSampleDescriptionIndex; - int defaultSampleDuration; - int defaultSampleSize; - int defaultSampleFlags; - } - - public class Elst { - - public long MediaTime; - public int bMediaRate; - } - - public class Mdia { - - public int mdhd_timeScale; - public byte[] mdhd; - public Hdlr hdlr; - public Minf minf; - } - - public class Hdlr { - - public int type; - public int subType; - public byte[] bReserved; - } - - public class Minf { - - public byte[] dinf; - public byte[] stbl_stsd; - public byte[] $mhd; - } - - public class Mp4Track { - - public TrackKind kind; - public Trak trak; - public Trex trex; - } - - public class Mp4DashChunk { - - public InputStream data; - public Moof moof; - private int i = 0; - - public TrunEntry getNextSampleInfo() { - if (i >= moof.traf.trun.entryCount) { - return null; - } - return moof.traf.trun.getAbsoluteEntry(i++, moof.traf.tfhd); - } - - public Mp4DashSample getNextSample() throws IOException { - if (data == null) { - throw new IllegalStateException("This chunk has info only"); - } - if (i >= moof.traf.trun.entryCount) { - return null; - } - - Mp4DashSample sample = new Mp4DashSample(); - sample.info = moof.traf.trun.getAbsoluteEntry(i++, moof.traf.tfhd); - sample.data = new byte[sample.info.sampleSize]; - - if (data.read(sample.data) != sample.info.sampleSize) { - throw new EOFException("EOF reached while reading a sample"); - } - - return sample; - } - } - - public class Mp4DashSample { - - public TrunEntry info; - public byte[] data; - } - -} +package org.schabi.newpipe.streams; + +import org.schabi.newpipe.streams.io.SharpStream; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.NoSuchElementException; + +/** + * @author kapodamy + */ +public class Mp4DashReader { + private static final int ATOM_MOOF = 0x6D6F6F66; + private static final int ATOM_MFHD = 0x6D666864; + private static final int ATOM_TRAF = 0x74726166; + private static final int ATOM_TFHD = 0x74666864; + private static final int ATOM_TFDT = 0x74666474; + private static final int ATOM_TRUN = 0x7472756E; + private static final int ATOM_MDIA = 0x6D646961; + private static final int ATOM_FTYP = 0x66747970; + private static final int ATOM_SIDX = 0x73696478; + private static final int ATOM_MOOV = 0x6D6F6F76; + private static final int ATOM_MDAT = 0x6D646174; + private static final int ATOM_MVHD = 0x6D766864; + private static final int ATOM_TRAK = 0x7472616B; + private static final int ATOM_MVEX = 0x6D766578; + private static final int ATOM_TREX = 0x74726578; + private static final int ATOM_TKHD = 0x746B6864; + private static final int ATOM_MFRA = 0x6D667261; + private static final int ATOM_MDHD = 0x6D646864; + private static final int ATOM_EDTS = 0x65647473; + private static final int ATOM_ELST = 0x656C7374; + private static final int ATOM_HDLR = 0x68646C72; + private static final int ATOM_MINF = 0x6D696E66; + private static final int ATOM_DINF = 0x64696E66; + private static final int ATOM_STBL = 0x7374626C; + private static final int ATOM_STSD = 0x73747364; + private static final int ATOM_VMHD = 0x766D6864; + private static final int ATOM_SMHD = 0x736D6864; + + private static final int BRAND_DASH = 0x64617368; + private static final int BRAND_ISO5 = 0x69736F35; + + private static final int HANDLER_VIDE = 0x76696465; + private static final int HANDLER_SOUN = 0x736F756E; + private static final int HANDLER_SUBT = 0x73756274; + + private final DataReader stream; + + private Mp4Track[] tracks = null; + private int[] brands = null; + + private Box box; + private Moof moof; + + private boolean chunkZero = false; + + private int selectedTrack = -1; + private Box backupBox = null; + + public enum TrackKind { + Audio, Video, Subtitles, Other + } + + public Mp4DashReader(final SharpStream source) { + this.stream = new DataReader(source); + } + + public void parse() throws IOException, NoSuchElementException { + if (selectedTrack > -1) { + return; + } + + box = readBox(ATOM_FTYP); + brands = parseFtyp(box); + switch (brands[0]) { + case BRAND_DASH: + case BRAND_ISO5:// ¿why not? + break; + default: + throw new NoSuchElementException( + "Not a MPEG-4 DASH container, major brand is not 'dash' or 'iso5' is " + + boxName(brands[0]) + ); + } + + Moov moov = null; + int i; + + while (box.type != ATOM_MOOF) { + ensure(box); + box = readBox(); + + switch (box.type) { + case ATOM_MOOV: + moov = parseMoov(box); + break; + case ATOM_SIDX: + case ATOM_MFRA: + break; + } + } + + if (moov == null) { + throw new IOException("The provided Mp4 doesn't have the 'moov' box"); + } + + tracks = new Mp4Track[moov.trak.length]; + + for (i = 0; i < tracks.length; i++) { + tracks[i] = new Mp4Track(); + tracks[i].trak = moov.trak[i]; + + if (moov.mvexTrex != null) { + for (Trex mvexTrex : moov.mvexTrex) { + if (tracks[i].trak.tkhd.trackId == mvexTrex.trackId) { + tracks[i].trex = mvexTrex; + } + } + } + + switch (moov.trak[i].mdia.hdlr.subType) { + case HANDLER_VIDE: + tracks[i].kind = TrackKind.Video; + break; + case HANDLER_SOUN: + tracks[i].kind = TrackKind.Audio; + break; + case HANDLER_SUBT: + tracks[i].kind = TrackKind.Subtitles; + break; + default: + tracks[i].kind = TrackKind.Other; + break; + } + } + + backupBox = box; + } + + Mp4Track selectTrack(final int index) { + selectedTrack = index; + return tracks[index]; + } + + public int[] getBrands() { + if (brands == null) { + throw new IllegalStateException("Not parsed"); + } + return brands; + } + + public void rewind() throws IOException { + if (!stream.canRewind()) { + throw new IOException("The provided stream doesn't allow seek"); + } + if (box == null) { + return; + } + + box = backupBox; + chunkZero = false; + + stream.rewind(); + stream.skipBytes(backupBox.offset + (DataReader.INTEGER_SIZE * 2)); + } + + public Mp4Track[] getAvailableTracks() { + return tracks; + } + + public Mp4DashChunk getNextChunk(final boolean infoOnly) throws IOException { + Mp4Track track = tracks[selectedTrack]; + + while (stream.available()) { + + if (chunkZero) { + ensure(box); + if (!stream.available()) { + break; + } + box = readBox(); + } else { + chunkZero = true; + } + + switch (box.type) { + case ATOM_MOOF: + if (moof != null) { + throw new IOException("moof found without mdat"); + } + + moof = parseMoof(box, track.trak.tkhd.trackId); + + if (moof.traf != null) { + + if (hasFlag(moof.traf.trun.bFlags, 0x0001)) { + moof.traf.trun.dataOffset -= box.size + 8; + if (moof.traf.trun.dataOffset < 0) { + throw new IOException("trun box has wrong data offset, " + + "points outside of concurrent mdat box"); + } + } + + if (moof.traf.trun.chunkSize < 1) { + if (hasFlag(moof.traf.tfhd.bFlags, 0x10)) { + moof.traf.trun.chunkSize = moof.traf.tfhd.defaultSampleSize + * moof.traf.trun.entryCount; + } else { + moof.traf.trun.chunkSize = (int) (box.size - 8); + } + } + if (!hasFlag(moof.traf.trun.bFlags, 0x900) + && moof.traf.trun.chunkDuration == 0) { + if (hasFlag(moof.traf.tfhd.bFlags, 0x20)) { + moof.traf.trun.chunkDuration = moof.traf.tfhd.defaultSampleDuration + * moof.traf.trun.entryCount; + } + } + } + break; + case ATOM_MDAT: + if (moof == null) { + throw new IOException("mdat found without moof"); + } + + if (moof.traf == null) { + moof = null; + continue; // find another chunk + } + + Mp4DashChunk chunk = new Mp4DashChunk(); + chunk.moof = moof; + if (!infoOnly) { + chunk.data = stream.getView(moof.traf.trun.chunkSize); + } + + moof = null; + + stream.skipBytes(chunk.moof.traf.trun.dataOffset); + return chunk; + default: + } + } + + return null; + } + + public static boolean hasFlag(final int flags, final int mask) { + return (flags & mask) == mask; + } + + private String boxName(final Box ref) { + return boxName(ref.type); + } + + private String boxName(final int type) { + try { + return new String(ByteBuffer.allocate(4).putInt(type).array(), "UTF-8"); + } catch (UnsupportedEncodingException e) { + return "0x" + Integer.toHexString(type); + } + } + + private Box readBox() throws IOException { + Box b = new Box(); + b.offset = stream.position(); + b.size = stream.readUnsignedInt(); + b.type = stream.readInt(); + + if (b.size == 1) { + b.size = stream.readLong(); + } + + return b; + } + + private Box readBox(final int expected) throws IOException { + Box b = readBox(); + if (b.type != expected) { + throw new NoSuchElementException("expected " + boxName(expected) + + " found " + boxName(b)); + } + return b; + } + + private byte[] readFullBox(final Box ref) throws IOException { + // full box reading is limited to 2 GiB, and should be enough + int size = (int) ref.size; + + ByteBuffer buffer = ByteBuffer.allocate(size); + buffer.putInt(size); + buffer.putInt(ref.type); + + int read = size - 8; + + if (stream.read(buffer.array(), 8, read) != read) { + throw new EOFException(String.format("EOF reached in box: type=%s offset=%s size=%s", + boxName(ref.type), ref.offset, ref.size)); + } + + return buffer.array(); + } + + private void ensure(final Box ref) throws IOException { + long skip = ref.offset + ref.size - stream.position(); + + if (skip == 0) { + return; + } else if (skip < 0) { + throw new EOFException(String.format( + "parser go beyond limits of the box. type=%s offset=%s size=%s position=%s", + boxName(ref), ref.offset, ref.size, stream.position() + )); + } + + stream.skipBytes((int) skip); + } + + private Box untilBox(final Box ref, final int... expected) throws IOException { + Box b; + while (stream.position() < (ref.offset + ref.size)) { + b = readBox(); + for (int type : expected) { + if (b.type == type) { + return b; + } + } + ensure(b); + } + + return null; + } + + private Box untilAnyBox(final Box ref) throws IOException { + if (stream.position() >= (ref.offset + ref.size)) { + return null; + } + + return readBox(); + } + + private Moof parseMoof(final Box ref, final int trackId) throws IOException { + Moof obj = new Moof(); + + Box b = readBox(ATOM_MFHD); + obj.mfhdSequenceNumber = parseMfhd(); + ensure(b); + + while ((b = untilBox(ref, ATOM_TRAF)) != null) { + obj.traf = parseTraf(b, trackId); + ensure(b); + + if (obj.traf != null) { + return obj; + } + } + + return obj; + } + + private int parseMfhd() throws IOException { + // version + // flags + stream.skipBytes(4); + + return stream.readInt(); + } + + private Traf parseTraf(final Box ref, final int trackId) throws IOException { + Traf traf = new Traf(); + + Box b = readBox(ATOM_TFHD); + traf.tfhd = parseTfhd(trackId); + ensure(b); + + if (traf.tfhd == null) { + return null; + } + + b = untilBox(ref, ATOM_TRUN, ATOM_TFDT); + + if (b.type == ATOM_TFDT) { + traf.tfdt = parseTfdt(); + ensure(b); + b = readBox(ATOM_TRUN); + } + + traf.trun = parseTrun(); + ensure(b); + + return traf; + } + + private Tfhd parseTfhd(final int trackId) throws IOException { + Tfhd obj = new Tfhd(); + + obj.bFlags = stream.readInt(); + obj.trackId = stream.readInt(); + + if (trackId != -1 && obj.trackId != trackId) { + return null; + } + + if (hasFlag(obj.bFlags, 0x01)) { + stream.skipBytes(8); + } + if (hasFlag(obj.bFlags, 0x02)) { + stream.skipBytes(4); + } + if (hasFlag(obj.bFlags, 0x08)) { + obj.defaultSampleDuration = stream.readInt(); + } + if (hasFlag(obj.bFlags, 0x10)) { + obj.defaultSampleSize = stream.readInt(); + } + if (hasFlag(obj.bFlags, 0x20)) { + obj.defaultSampleFlags = stream.readInt(); + } + + return obj; + } + + private long parseTfdt() throws IOException { + int version = stream.read(); + stream.skipBytes(3); // flags + return version == 0 ? stream.readUnsignedInt() : stream.readLong(); + } + + private Trun parseTrun() throws IOException { + Trun obj = new Trun(); + obj.bFlags = stream.readInt(); + obj.entryCount = stream.readInt(); // unsigned int + + obj.entriesRowSize = 0; + if (hasFlag(obj.bFlags, 0x0100)) { + obj.entriesRowSize += 4; + } + if (hasFlag(obj.bFlags, 0x0200)) { + obj.entriesRowSize += 4; + } + if (hasFlag(obj.bFlags, 0x0400)) { + obj.entriesRowSize += 4; + } + if (hasFlag(obj.bFlags, 0x0800)) { + obj.entriesRowSize += 4; + } + obj.bEntries = new byte[obj.entriesRowSize * obj.entryCount]; + + if (hasFlag(obj.bFlags, 0x0001)) { + obj.dataOffset = stream.readInt(); + } + if (hasFlag(obj.bFlags, 0x0004)) { + obj.bFirstSampleFlags = stream.readInt(); + } + + stream.read(obj.bEntries); + + for (int i = 0; i < obj.entryCount; i++) { + TrunEntry entry = obj.getEntry(i); + if (hasFlag(obj.bFlags, 0x0100)) { + obj.chunkDuration += entry.sampleDuration; + } + if (hasFlag(obj.bFlags, 0x0200)) { + obj.chunkSize += entry.sampleSize; + } + if (hasFlag(obj.bFlags, 0x0800)) { + if (!hasFlag(obj.bFlags, 0x0100)) { + obj.chunkDuration += entry.sampleCompositionTimeOffset; + } + } + } + + return obj; + } + + private int[] parseFtyp(final Box ref) throws IOException { + int i = 0; + int[] list = new int[(int) ((ref.offset + ref.size - stream.position() - 4) / 4)]; + + list[i++] = stream.readInt(); // major brand + + stream.skipBytes(4); // minor version + + for (; i < list.length; i++) { + list[i] = stream.readInt(); // compatible brands + } + + return list; + } + + private Mvhd parseMvhd() throws IOException { + int version = stream.read(); + stream.skipBytes(3); // flags + + // creation entries_time + // modification entries_time + stream.skipBytes(2 * (version == 0 ? 4 : 8)); + + Mvhd obj = new Mvhd(); + obj.timeScale = stream.readUnsignedInt(); + + // chunkDuration + stream.skipBytes(version == 0 ? 4 : 8); + + // rate + // volume + // reserved + // matrix array + // predefined + stream.skipBytes(76); + + obj.nextTrackId = stream.readUnsignedInt(); + + return obj; + } + + private Tkhd parseTkhd() throws IOException { + int version = stream.read(); + + Tkhd obj = new Tkhd(); + + // flags + // creation entries_time + // modification entries_time + stream.skipBytes(3 + (2 * (version == 0 ? 4 : 8))); + + obj.trackId = stream.readInt(); + + stream.skipBytes(4); // reserved + + obj.duration = version == 0 ? stream.readUnsignedInt() : stream.readLong(); + + stream.skipBytes(2 * 4); // reserved + + obj.bLayer = stream.readShort(); + obj.bAlternateGroup = stream.readShort(); + obj.bVolume = stream.readShort(); + + stream.skipBytes(2); // reserved + + obj.matrix = new byte[9 * 4]; + stream.read(obj.matrix); + + obj.bWidth = stream.readInt(); + obj.bHeight = stream.readInt(); + + return obj; + } + + private Trak parseTrak(final Box ref) throws IOException { + Trak trak = new Trak(); + + Box b = readBox(ATOM_TKHD); + trak.tkhd = parseTkhd(); + ensure(b); + + while ((b = untilBox(ref, ATOM_MDIA, ATOM_EDTS)) != null) { + switch (b.type) { + case ATOM_MDIA: + trak.mdia = parseMdia(b); + break; + case ATOM_EDTS: + trak.edstElst = parseEdts(b); + break; + } + + ensure(b); + } + + return trak; + } + + private Mdia parseMdia(final Box ref) throws IOException { + Mdia obj = new Mdia(); + + Box b; + while ((b = untilBox(ref, ATOM_MDHD, ATOM_HDLR, ATOM_MINF)) != null) { + switch (b.type) { + case ATOM_MDHD: + obj.mdhd = readFullBox(b); + + // read time scale + ByteBuffer buffer = ByteBuffer.wrap(obj.mdhd); + byte version = buffer.get(8); + buffer.position(12 + ((version == 0 ? 4 : 8) * 2)); + obj.mdhdTimeScale = buffer.getInt(); + break; + case ATOM_HDLR: + obj.hdlr = parseHdlr(b); + break; + case ATOM_MINF: + obj.minf = parseMinf(b); + break; + } + ensure(b); + } + + return obj; + } + + private Hdlr parseHdlr(final Box ref) throws IOException { + // version + // flags + stream.skipBytes(4); + + Hdlr obj = new Hdlr(); + obj.bReserved = new byte[12]; + + obj.type = stream.readInt(); + obj.subType = stream.readInt(); + stream.read(obj.bReserved); + + // component name (is a ansi/ascii string) + stream.skipBytes((ref.offset + ref.size) - stream.position()); + + return obj; + } + + private Moov parseMoov(final Box ref) throws IOException { + Box b = readBox(ATOM_MVHD); + Moov moov = new Moov(); + moov.mvhd = parseMvhd(); + ensure(b); + + ArrayList tmp = new ArrayList<>((int) moov.mvhd.nextTrackId); + while ((b = untilBox(ref, ATOM_TRAK, ATOM_MVEX)) != null) { + + switch (b.type) { + case ATOM_TRAK: + tmp.add(parseTrak(b)); + break; + case ATOM_MVEX: + moov.mvexTrex = parseMvex(b, (int) moov.mvhd.nextTrackId); + break; + } + + ensure(b); + } + + moov.trak = tmp.toArray(new Trak[0]); + + return moov; + } + + private Trex[] parseMvex(final Box ref, final int possibleTrackCount) throws IOException { + ArrayList tmp = new ArrayList<>(possibleTrackCount); + + Box b; + while ((b = untilBox(ref, ATOM_TREX)) != null) { + tmp.add(parseTrex()); + ensure(b); + } + + return tmp.toArray(new Trex[0]); + } + + private Trex parseTrex() throws IOException { + // version + // flags + stream.skipBytes(4); + + Trex obj = new Trex(); + obj.trackId = stream.readInt(); + obj.defaultSampleDescriptionIndex = stream.readInt(); + obj.defaultSampleDuration = stream.readInt(); + obj.defaultSampleSize = stream.readInt(); + obj.defaultSampleFlags = stream.readInt(); + + return obj; + } + + private Elst parseEdts(final Box ref) throws IOException { + Box b = untilBox(ref, ATOM_ELST); + if (b == null) { + return null; + } + + Elst obj = new Elst(); + + boolean v1 = stream.read() == 1; + stream.skipBytes(3); // flags + + int entryCount = stream.readInt(); + if (entryCount < 1) { + obj.bMediaRate = 0x00010000; // default media rate (1.0) + return obj; + } + + if (v1) { + stream.skipBytes(DataReader.LONG_SIZE); // segment duration + obj.mediaTime = stream.readLong(); + // ignore all remain entries + stream.skipBytes((entryCount - 1) * (DataReader.LONG_SIZE * 2)); + } else { + stream.skipBytes(DataReader.INTEGER_SIZE); // segment duration + obj.mediaTime = stream.readInt(); + } + + obj.bMediaRate = stream.readInt(); + + return obj; + } + + private Minf parseMinf(final Box ref) throws IOException { + Minf obj = new Minf(); + + Box b; + while ((b = untilAnyBox(ref)) != null) { + + switch (b.type) { + case ATOM_DINF: + obj.dinf = readFullBox(b); + break; + case ATOM_STBL: + obj.stblStsd = parseStbl(b); + break; + case ATOM_VMHD: + case ATOM_SMHD: + obj.mhd = readFullBox(b); + break; + + } + ensure(b); + } + + return obj; + } + + /** + * This only reads the "stsd" box inside. + * + * @param ref stbl box + * @return stsd box inside + */ + private byte[] parseStbl(final Box ref) throws IOException { + Box b = untilBox(ref, ATOM_STSD); + + if (b == null) { + return new byte[0]; // this never should happens (missing codec startup data) + } + + return readFullBox(b); + } + + class Box { + int type; + long offset; + long size; + } + + public class Moof { + int mfhdSequenceNumber; + public Traf traf; + } + + public class Traf { + public Tfhd tfhd; + long tfdt; + public Trun trun; + } + + public class Tfhd { + int bFlags; + public int trackId; + int defaultSampleDuration; + int defaultSampleSize; + int defaultSampleFlags; + } + + class TrunEntry { + int sampleDuration; + int sampleSize; + int sampleFlags; + int sampleCompositionTimeOffset; + + boolean hasCompositionTimeOffset; + boolean isKeyframe; + + } + + public class Trun { + public int chunkDuration; + public int chunkSize; + + public int bFlags; + int bFirstSampleFlags; + int dataOffset; + + public int entryCount; + byte[] bEntries; + int entriesRowSize; + + public TrunEntry getEntry(final int i) { + ByteBuffer buffer = ByteBuffer.wrap(bEntries, i * entriesRowSize, entriesRowSize); + TrunEntry entry = new TrunEntry(); + + if (hasFlag(bFlags, 0x0100)) { + entry.sampleDuration = buffer.getInt(); + } + if (hasFlag(bFlags, 0x0200)) { + entry.sampleSize = buffer.getInt(); + } + if (hasFlag(bFlags, 0x0400)) { + entry.sampleFlags = buffer.getInt(); + } + if (hasFlag(bFlags, 0x0800)) { + entry.sampleCompositionTimeOffset = buffer.getInt(); + } + + entry.hasCompositionTimeOffset = hasFlag(bFlags, 0x0800); + entry.isKeyframe = !hasFlag(entry.sampleFlags, 0x10000); + + return entry; + } + + public TrunEntry getAbsoluteEntry(final int i, final Tfhd header) { + TrunEntry entry = getEntry(i); + + if (!hasFlag(bFlags, 0x0100) && hasFlag(header.bFlags, 0x20)) { + entry.sampleFlags = header.defaultSampleFlags; + } + + if (!hasFlag(bFlags, 0x0200) && hasFlag(header.bFlags, 0x10)) { + entry.sampleSize = header.defaultSampleSize; + } + + if (!hasFlag(bFlags, 0x0100) && hasFlag(header.bFlags, 0x08)) { + entry.sampleDuration = header.defaultSampleDuration; + } + + if (i == 0 && hasFlag(bFlags, 0x0004)) { + entry.sampleFlags = bFirstSampleFlags; + } + + return entry; + } + } + + public class Tkhd { + int trackId; + long duration; + short bVolume; + int bWidth; + int bHeight; + byte[] matrix; + short bLayer; + short bAlternateGroup; + } + + public class Trak { + public Tkhd tkhd; + public Elst edstElst; + public Mdia mdia; + + } + + class Mvhd { + long timeScale; + long nextTrackId; + } + + class Moov { + Mvhd mvhd; + Trak[] trak; + Trex[] mvexTrex; + } + + public class Trex { + private int trackId; + int defaultSampleDescriptionIndex; + int defaultSampleDuration; + int defaultSampleSize; + int defaultSampleFlags; + } + + public class Elst { + public long mediaTime; + public int bMediaRate; + } + + public class Mdia { + public int mdhdTimeScale; + public byte[] mdhd; + public Hdlr hdlr; + public Minf minf; + } + + public class Hdlr { + public int type; + public int subType; + public byte[] bReserved; + } + + public class Minf { + public byte[] dinf; + public byte[] stblStsd; + public byte[] mhd; + } + + public class Mp4Track { + public TrackKind kind; + public Trak trak; + public Trex trex; + } + + public class Mp4DashChunk { + public InputStream data; + public Moof moof; + private int i = 0; + + public TrunEntry getNextSampleInfo() { + if (i >= moof.traf.trun.entryCount) { + return null; + } + return moof.traf.trun.getAbsoluteEntry(i++, moof.traf.tfhd); + } + + public Mp4DashSample getNextSample() throws IOException { + if (data == null) { + throw new IllegalStateException("This chunk has info only"); + } + if (i >= moof.traf.trun.entryCount) { + return null; + } + + Mp4DashSample sample = new Mp4DashSample(); + sample.info = moof.traf.trun.getAbsoluteEntry(i++, moof.traf.tfhd); + sample.data = new byte[sample.info.sampleSize]; + + if (data.read(sample.data) != sample.info.sampleSize) { + throw new EOFException("EOF reached while reading a sample"); + } + + return sample; + } + } + + public class Mp4DashSample { + public TrunEntry info; + public byte[] data; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java b/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java index 818f6148e..2baf8fe55 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java @@ -5,25 +5,27 @@ import org.schabi.newpipe.streams.Mp4DashReader.Mdia; import org.schabi.newpipe.streams.Mp4DashReader.Mp4DashChunk; import org.schabi.newpipe.streams.Mp4DashReader.Mp4DashSample; import org.schabi.newpipe.streams.Mp4DashReader.Mp4Track; -import org.schabi.newpipe.streams.Mp4DashReader.TrunEntry; import org.schabi.newpipe.streams.Mp4DashReader.TrackKind; +import org.schabi.newpipe.streams.Mp4DashReader.TrunEntry; import org.schabi.newpipe.streams.io.SharpStream; import java.io.IOException; import java.nio.ByteBuffer; +import java.util.ArrayList; /** * @author kapodamy */ public class Mp4FromDashWriter { - - private final static int EPOCH_OFFSET = 2082844800; - private final static short DEFAULT_TIMESCALE = 1000; - private final static byte SAMPLES_PER_CHUNK_INIT = 2; - private final static byte SAMPLES_PER_CHUNK = 6;// ffmpeg uses 2, basic uses 1 (with 60fps uses 21 or 22). NewPipe will use 6 - private final static long THRESHOLD_FOR_CO64 = 0xFFFEFFFFL;// near 3.999 GiB - private final static int THRESHOLD_MOOV_LENGTH = (256 * 1024) + (2048 * 1024); // 2.2 MiB enough for: 1080p 60fps 00h35m00s - private final static short SINGLE_CHUNK_SAMPLE_BUFFER = 256; + private static final int EPOCH_OFFSET = 2082844800; + private static final short DEFAULT_TIMESCALE = 1000; + private static final byte SAMPLES_PER_CHUNK_INIT = 2; + // ffmpeg uses 2, basic uses 1 (with 60fps uses 21 or 22). NewPipe will use 6 + private static final byte SAMPLES_PER_CHUNK = 6; + // near 3.999 GiB + private static final long THRESHOLD_FOR_CO64 = 0xFFFEFFFFL; + // 2.2 MiB enough for: 1080p 60fps 00h35m00s + private static final int THRESHOLD_MOOV_LENGTH = (256 * 1024) + (2048 * 1024); private final long time; @@ -46,7 +48,9 @@ public class Mp4FromDashWriter { private int overrideMainBrand = 0x00; - public Mp4FromDashWriter(SharpStream... sources) throws IOException { + private final ArrayList compatibleBrands = new ArrayList<>(5); + + public Mp4FromDashWriter(final SharpStream... sources) throws IOException { for (SharpStream src : sources) { if (!src.canRewind() && !src.canRead()) { throw new IOException("All sources must be readable and allow rewind"); @@ -57,9 +61,13 @@ public class Mp4FromDashWriter { readers = new Mp4DashReader[sourceTracks.length]; readersChunks = new Mp4DashChunk[readers.length]; time = (System.currentTimeMillis() / 1000L) + EPOCH_OFFSET; + + compatibleBrands.add(0x6D703431); // mp41 + compatibleBrands.add(0x69736F6D); // isom + compatibleBrands.add(0x69736F32); // iso2 } - public Mp4Track[] getTracksFromSource(int sourceIndex) throws IllegalStateException { + public Mp4Track[] getTracksFromSource(final int sourceIndex) throws IllegalStateException { if (!parsed) { throw new IllegalStateException("All sources must be parsed first"); } @@ -86,7 +94,7 @@ public class Mp4FromDashWriter { } } - public void selectTracks(int... trackIndex) throws IOException { + public void selectTracks(final int... trackIndex) throws IOException { if (done) { throw new IOException("already done"); } @@ -104,8 +112,8 @@ public class Mp4FromDashWriter { } } - public void setMainBrand(int brandId) { - overrideMainBrand = brandId; + public void setMainBrand(final int brand) { + overrideMainBrand = brand; } public boolean isDone() { @@ -134,7 +142,7 @@ public class Mp4FromDashWriter { outStream = null; } - public void build(SharpStream output) throws IOException { + public void build(final SharpStream output) throws IOException { if (done) { throw new RuntimeException("already done"); } @@ -147,7 +155,7 @@ public class Mp4FromDashWriter { // not allowed for very short tracks (less than 0.5 seconds) // outStream = output; - long read = 8;// mdat box header size + long read = 8; // mdat box header size long totalSampleSize = 0; int[] sampleExtra = new int[readers.length]; int[] defaultMediaTime = new int[readers.length]; @@ -159,7 +167,13 @@ public class Mp4FromDashWriter { tablesInfo[i] = new TablesInfo(); } - boolean singleChunk = tracks.length == 1 && tracks[0].kind == TrackKind.Audio; + int singleSampleBuffer; + if (tracks.length == 1 && tracks[0].kind == TrackKind.Audio) { + // near 1 second of audio data per chunk, avoid split the audio stream in large chunks + singleSampleBuffer = tracks[0].trak.mdia.mdhdTimeScale / 1000; + } else { + singleSampleBuffer = -1; + } for (int i = 0; i < readers.length; i++) { @@ -175,7 +189,7 @@ public class Mp4FromDashWriter { } read += chunk.moof.traf.trun.chunkSize; - sampleExtra[i] += chunk.moof.traf.trun.chunkDuration;// calculate track duration + sampleExtra[i] += chunk.moof.traf.trun.chunkDuration; // calculate track duration TrunEntry info; while ((info = chunk.getNextSampleInfo()) != null) { @@ -210,76 +224,50 @@ public class Mp4FromDashWriter { readers[i].rewind(); - int tmp = tablesInfo[i].stsz - SAMPLES_PER_CHUNK_INIT; - tablesInfo[i].stco = (tmp / SAMPLES_PER_CHUNK) + 1;// +1 for samples in first chunk - - tmp = tmp % SAMPLES_PER_CHUNK; - if (singleChunk) { - // avoid split audio streams in chunks - tablesInfo[i].stsc = 1; - tablesInfo[i].stsc_bEntries = new int[]{ - 1, tablesInfo[i].stsz, 1 - }; - tablesInfo[i].stco = 1; - } else if (tmp == 0) { - tablesInfo[i].stsc = 2;// first chunk (init) and succesive chunks - tablesInfo[i].stsc_bEntries = new int[]{ - 1, SAMPLES_PER_CHUNK_INIT, 1, - 2, SAMPLES_PER_CHUNK, 1 - }; + if (singleSampleBuffer > 0) { + initChunkTables(tablesInfo[i], singleSampleBuffer, singleSampleBuffer); } else { - tablesInfo[i].stsc = 3;// first chunk (init) and successive chunks and remain chunk - tablesInfo[i].stsc_bEntries = new int[]{ - 1, SAMPLES_PER_CHUNK_INIT, 1, - 2, SAMPLES_PER_CHUNK, 1, - tablesInfo[i].stco + 1, tmp, 1 - }; - tablesInfo[i].stco++; + initChunkTables(tablesInfo[i], SAMPLES_PER_CHUNK_INIT, SAMPLES_PER_CHUNK); } sampleCount[i] = tablesInfo[i].stsz; if (sampleSizeChanges == 1) { tablesInfo[i].stsz = 0; - tablesInfo[i].stsz_default = samplesSize; + tablesInfo[i].stszDefault = samplesSize; } else { - tablesInfo[i].stsz_default = 0; + tablesInfo[i].stszDefault = 0; } if (tablesInfo[i].stss == tablesInfo[i].stsz) { - tablesInfo[i].stss = -1;// for audio tracks (all samples are keyframes) + tablesInfo[i].stss = -1; // for audio tracks (all samples are keyframes) } // ensure track duration if (tracks[i].trak.tkhd.duration < 1) { - tracks[i].trak.tkhd.duration = sampleExtra[i];// this never should happen + tracks[i].trak.tkhd.duration = sampleExtra[i]; // this never should happen } } boolean is64 = read > THRESHOLD_FOR_CO64; - // calculate the moov size; - int auxSize = make_moov(defaultMediaTime, tablesInfo, is64); + // calculate the moov size + int auxSize = makeMoov(defaultMediaTime, tablesInfo, is64); if (auxSize < THRESHOLD_MOOV_LENGTH) { - auxBuffer = ByteBuffer.allocate(auxSize);// cache moov in the memory + auxBuffer = ByteBuffer.allocate(auxSize); // cache moov in the memory } moovSimulation = false; writeOffset = 0; - final int ftyp_size = make_ftyp(); + final int ftypSize = makeFtyp(); // reserve moov space in the output stream - /*if (outStream.canSetLength()) { - long length = writeOffset + auxSize; - outStream.setLength(length); - outSeek(length); - } else {*/ if (auxSize > 0) { int length = auxSize; - byte[] buffer = new byte[64 * 1024];// 64 KiB + byte[] buffer = new byte[64 * 1024]; // 64 KiB while (length > 0) { int count = Math.min(length, buffer.length); outWrite(buffer, count); @@ -288,34 +276,38 @@ public class Mp4FromDashWriter { } if (auxBuffer == null) { - outSeek(ftyp_size); + outSeek(ftypSize); } // tablesInfo contains row counts - // and after returning from make_moov() will contain table offsets - make_moov(defaultMediaTime, tablesInfo, is64); + // and after returning from makeMoov() will contain those table offsets + makeMoov(defaultMediaTime, tablesInfo, is64); - // write tables: stts stsc + // write tables: stts stsc sbgp // reset for ctts table: sampleCount sampleExtra for (int i = 0; i < readers.length; i++) { writeEntryArray(tablesInfo[i].stts, 2, sampleCount[i], defaultSampleDuration[i]); - writeEntryArray(tablesInfo[i].stsc, tablesInfo[i].stsc_bEntries.length, tablesInfo[i].stsc_bEntries); - tablesInfo[i].stsc_bEntries = null; + writeEntryArray(tablesInfo[i].stsc, tablesInfo[i].stscBEntries.length, + tablesInfo[i].stscBEntries); + tablesInfo[i].stscBEntries = null; if (tablesInfo[i].ctts > 0) { - sampleCount[i] = 1;// the index is not base zero + sampleCount[i] = 1; // the index is not base zero sampleExtra[i] = -1; } + if (tablesInfo[i].sbgp > 0) { + writeEntryArray(tablesInfo[i].sbgp, 1, sampleCount[i]); + } } if (auxBuffer == null) { outRestore(); } - outWrite(make_mdat(totalSampleSize, is64)); + outWrite(makeMdat(totalSampleSize, is64)); int[] sampleIndex = new int[readers.length]; - int[] sizes = new int[singleChunk ? SINGLE_CHUNK_SAMPLE_BUFFER : SAMPLES_PER_CHUNK]; - int[] sync = new int[singleChunk ? SINGLE_CHUNK_SAMPLE_BUFFER : SAMPLES_PER_CHUNK]; + int[] sizes = new int[singleSampleBuffer > 0 ? singleSampleBuffer : SAMPLES_PER_CHUNK]; + int[] sync = new int[singleSampleBuffer > 0 ? singleSampleBuffer : SAMPLES_PER_CHUNK]; int written = readers.length; while (written > 0) { @@ -323,14 +315,14 @@ public class Mp4FromDashWriter { for (int i = 0; i < readers.length; i++) { if (sampleIndex[i] < 0) { - continue;// track is done + continue; // track is done } long chunkOffset = writeOffset; int syncCount = 0; int limit; - if (singleChunk) { - limit = SINGLE_CHUNK_SAMPLE_BUFFER; + if (singleSampleBuffer > 0) { + limit = singleSampleBuffer; } else { limit = sampleIndex[i] == 0 ? SAMPLES_PER_CHUNK_INIT : SAMPLES_PER_CHUNK; } @@ -341,7 +333,9 @@ public class Mp4FromDashWriter { if (sample == null) { if (tablesInfo[i].ctts > 0 && sampleExtra[i] >= 0) { - writeEntryArray(tablesInfo[i].ctts, 1, sampleCount[i], sampleExtra[i]);// flush last entries + writeEntryArray(tablesInfo[i].ctts, 1, sampleCount[i], + sampleExtra[i]); // flush last entries + outRestore(); } sampleIndex[i] = -1; break; @@ -354,7 +348,8 @@ public class Mp4FromDashWriter { sampleCount[i]++; } else { if (sampleExtra[i] >= 0) { - tablesInfo[i].ctts = writeEntryArray(tablesInfo[i].ctts, 2, sampleCount[i], sampleExtra[i]); + tablesInfo[i].ctts = writeEntryArray(tablesInfo[i].ctts, 2, + sampleCount[i], sampleExtra[i]); outRestore(); } sampleCount[i] = 1; @@ -388,11 +383,8 @@ public class Mp4FromDashWriter { if (is64) { tablesInfo[i].stco = writeEntry64(tablesInfo[i].stco, chunkOffset); } else { - tablesInfo[i].stco = writeEntryArray(tablesInfo[i].stco, 1, (int) chunkOffset); - } - - if (singleChunk) { - tablesInfo[i].stco = -1; + tablesInfo[i].stco = writeEntryArray(tablesInfo[i].stco, 1, + (int) chunkOffset); } } @@ -403,17 +395,17 @@ public class Mp4FromDashWriter { if (auxBuffer != null) { // dump moov - outSeek(ftyp_size); + outSeek(ftypSize); outStream.write(auxBuffer.array(), 0, auxBuffer.capacity()); auxBuffer = null; } } - private Mp4DashSample getNextSample(int track) throws IOException { + private Mp4DashSample getNextSample(final int track) throws IOException { if (readersChunks[track] == null) { readersChunks[track] = readers[track].getNextChunk(false); if (readersChunks[track] == null) { - return null;// EOF reached + return null; // EOF reached } } @@ -427,7 +419,7 @@ public class Mp4FromDashWriter { } - private int writeEntry64(int offset, long value) throws IOException { + private int writeEntry64(final int offset, final long value) throws IOException { outBackup(); auxSeek(offset); @@ -436,7 +428,8 @@ public class Mp4FromDashWriter { return offset + 8; } - private int writeEntryArray(int offset, int count, int... values) throws IOException { + private int writeEntryArray(final int offset, final int count, final int... values) + throws IOException { outBackup(); auxSeek(offset); @@ -470,18 +463,54 @@ public class Mp4FromDashWriter { } } + private void initChunkTables(final TablesInfo tables, final int firstCount, + final int successiveCount) { + // tables.stsz holds amount of samples of the track (total) + int totalSamples = (tables.stsz - firstCount); + float chunkAmount = totalSamples / (float) successiveCount; + int remainChunkOffset = (int) Math.ceil(chunkAmount); + boolean remain = remainChunkOffset != (int) chunkAmount; + int index = 0; + tables.stsc = 1; + if (firstCount != successiveCount) { + tables.stsc++; + } + if (remain) { + tables.stsc++; + } - private void outWrite(byte[] buffer) throws IOException { + // stsc_table_entry = [first_chunk, samples_per_chunk, sample_description_index] + tables.stscBEntries = new int[tables.stsc * 3]; + tables.stco = remainChunkOffset + 1; // total entrys in chunk offset box + + tables.stscBEntries[index++] = 1; + tables.stscBEntries[index++] = firstCount; + tables.stscBEntries[index++] = 1; + + if (firstCount != successiveCount) { + tables.stscBEntries[index++] = 2; + tables.stscBEntries[index++] = successiveCount; + tables.stscBEntries[index++] = 1; + } + + if (remain) { + tables.stscBEntries[index++] = remainChunkOffset + 1; + tables.stscBEntries[index++] = totalSamples % successiveCount; + tables.stscBEntries[index] = 1; + } + } + + private void outWrite(final byte[] buffer) throws IOException { outWrite(buffer, buffer.length); } - private void outWrite(byte[] buffer, int count) throws IOException { + private void outWrite(final byte[] buffer, final int count) throws IOException { writeOffset += count; outStream.write(buffer, 0, count); } - private void outSeek(long offset) throws IOException { + private void outSeek(final long offset) throws IOException { if (outStream.canSeek()) { outStream.seek(offset); writeOffset = offset; @@ -494,12 +523,12 @@ public class Mp4FromDashWriter { } } - private void outSkip(long amount) throws IOException { + private void outSkip(final long amount) throws IOException { outStream.skip(amount); writeOffset += amount; } - private int lengthFor(int offset) throws IOException { + private int lengthFor(final int offset) throws IOException { int size = auxOffset() - offset; if (moovSimulation) { @@ -513,7 +542,8 @@ public class Mp4FromDashWriter { return size; } - private int make(int type, int extra, int columns, int rows) throws IOException { + private int make(final int type, final int extra, final int columns, final int rows) + throws IOException { final byte base = 16; int size = columns * rows * 4; int total = size + base; @@ -541,14 +571,14 @@ public class Mp4FromDashWriter { return offset + base; } - private void auxWrite(int value) throws IOException { + private void auxWrite(final int value) throws IOException { auxWrite(ByteBuffer.allocate(4) .putInt(value) .array() ); } - private void auxWrite(byte[] buffer) throws IOException { + private void auxWrite(final byte[] buffer) throws IOException { if (moovSimulation) { writeOffset += buffer.length; } else if (auxBuffer == null) { @@ -558,7 +588,7 @@ public class Mp4FromDashWriter { } } - private void auxSeek(int offset) throws IOException { + private void auxSeek(final int offset) throws IOException { if (moovSimulation) { writeOffset = offset; } else if (auxBuffer == null) { @@ -568,7 +598,7 @@ public class Mp4FromDashWriter { } } - private void auxSkip(int amount) throws IOException { + private void auxSkip(final int amount) throws IOException { if (moovSimulation) { writeOffset += amount; } else if (auxBuffer == null) { @@ -582,43 +612,54 @@ public class Mp4FromDashWriter { return auxBuffer == null ? (int) writeOffset : auxBuffer.position(); } + private int makeFtyp() throws IOException { + int size = 16 + (compatibleBrands.size() * 4); + if (overrideMainBrand != 0) { + size += 4; + } + ByteBuffer buffer = ByteBuffer.allocate(size); + buffer.putInt(size); + buffer.putInt(0x66747970); // "ftyp" - private int make_ftyp() throws IOException { - byte[] buffer = new byte[]{ - 0x00, 0x00, 0x00, 0x1C, 0x66, 0x74, 0x79, 0x70,// ftyp - 0x6D, 0x70, 0x34, 0x32,// mayor brand (mp42) - 0x00, 0x00, 0x02, 0x00,// default minor version (512) - 0x6D, 0x70, 0x34, 0x31, 0x69, 0x73, 0x6F, 0x6D, 0x69, 0x73, 0x6F, 0x32// compatible brands: mp41 isom iso2 - }; + if (overrideMainBrand == 0) { + buffer.putInt(0x6D703432); // mayor brand "mp42" + buffer.putInt(512); // default minor version + } else { + buffer.putInt(overrideMainBrand); + buffer.putInt(0); + buffer.putInt(0x6D703432); // "mp42" compatible brand + } - if (overrideMainBrand != 0) - ByteBuffer.wrap(buffer).putInt(8, overrideMainBrand); + for (Integer brand : compatibleBrands) { + buffer.putInt(brand); // compatible brand + } - outWrite(buffer); + outWrite(buffer.array()); - return buffer.length; + return size; } - private byte[] make_mdat(long refSize, boolean is64) { + private byte[] makeMdat(final long refSize, final boolean is64) { + long size = refSize; if (is64) { - refSize += 16; + size += 16; } else { - refSize += 8; + size += 8; } ByteBuffer buffer = ByteBuffer.allocate(is64 ? 16 : 8) - .putInt(is64 ? 0x01 : (int) refSize) - .putInt(0x6D646174);// mdat + .putInt(is64 ? 0x01 : (int) size) + .putInt(0x6D646174); // mdat if (is64) { - buffer.putLong(refSize); + buffer.putLong(size); } return buffer.array(); } - private void make_mvhd(long longestTrack) throws IOException { + private void makeMvhd(final long longestTrack) throws IOException { auxWrite(new byte[]{ 0x00, 0x00, 0x00, 0x78, 0x6D, 0x76, 0x68, 0x64, 0x01, 0x00, 0x00, 0x00 }); @@ -631,21 +672,23 @@ public class Mp4FromDashWriter { ); auxWrite(new byte[]{ - 0x00, 0x01, 0x00, 0x00, 0x01, 0x00,// default volume and rate - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,// reserved values + 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, // default volume and rate + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // reserved values // default matrix - 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00 }); - auxWrite(new byte[24]);// predefined + auxWrite(new byte[24]); // predefined auxWrite(ByteBuffer.allocate(4) .putInt(tracks.length + 1) .array() ); } - private int make_moov(int[] defaultMediaTime, TablesInfo[] tablesInfo, boolean is64) throws RuntimeException, IOException { + private int makeMoov(final int[] defaultMediaTime, final TablesInfo[] tablesInfo, + final boolean is64) throws RuntimeException, IOException { int start = auxOffset(); auxWrite(new byte[]{ @@ -657,43 +700,36 @@ public class Mp4FromDashWriter { for (int i = 0; i < durations.length; i++) { durations[i] = (long) Math.ceil( - ((double) tracks[i].trak.tkhd.duration / tracks[i].trak.mdia.mdhd_timeScale) * DEFAULT_TIMESCALE - ); + ((double) tracks[i].trak.tkhd.duration / tracks[i].trak.mdia.mdhdTimeScale) + * DEFAULT_TIMESCALE); if (durations[i] > longestTrack) { longestTrack = durations[i]; } } - make_mvhd(longestTrack); + makeMvhd(longestTrack); for (int i = 0; i < tracks.length; i++) { if (tracks[i].trak.tkhd.matrix.length != 36) { - throw new RuntimeException("bad track matrix length (expected 36) in track n°" + i); + throw + new RuntimeException("bad track matrix length (expected 36) in track n°" + i); } - make_trak(i, durations[i], defaultMediaTime[i], tablesInfo[i], is64); + makeTrak(i, durations[i], defaultMediaTime[i], tablesInfo[i], is64); } - // udta/meta/ilst/©too - auxWrite(new byte[]{ - 0x00, 0x00, 0x00, 0x5C, 0x75, 0x64, 0x74, 0x61, 0x00, 0x00, 0x00, 0x54, 0x6D, 0x65, 0x74, 0x61, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x68, 0x64, 0x6C, 0x72, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x69, 0x72, 0x61, 0x70, 0x70, 0x6C, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0x69, 0x6C, 0x73, 0x74, 0x00, 0x00, 0x00, - 0x1F, (byte) 0xA9, 0x74, 0x6F, 0x6F, 0x00, 0x00, 0x00, 0x17, 0x64, 0x61, 0x74, 0x61, 0x00, 0x00, - 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65// "NewPipe" binary string - }); - return lengthFor(start); } - private void make_trak(int index, long duration, int defaultMediaTime, TablesInfo tables, boolean is64) throws IOException { + private void makeTrak(final int index, final long duration, final int defaultMediaTime, + final TablesInfo tables, final boolean is64) throws IOException { int start = auxOffset(); auxWrite(new byte[]{ - 0x00, 0x00, 0x00, 0x00, 0x74, 0x72, 0x61, 0x6B,// trak header - 0x00, 0x00, 0x00, 0x68, 0x74, 0x6B, 0x68, 0x64, 0x01, 0x00, 0x00, 0x03 // tkhd header + // trak header + 0x00, 0x00, 0x00, 0x00, 0x74, 0x72, 0x61, 0x6B, + // tkhd header + 0x00, 0x00, 0x00, 0x68, 0x74, 0x6B, 0x68, 0x64, 0x01, 0x00, 0x00, 0x03 }); ByteBuffer buffer = ByteBuffer.allocate(48); @@ -716,20 +752,21 @@ public class Mp4FromDashWriter { ); auxWrite(new byte[]{ - 0x00, 0x00, 0x00, 0x24, 0x65, 0x64, 0x74, 0x73,// edts header - 0x00, 0x00, 0x00, 0x1C, 0x65, 0x6C, 0x73, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01// elst header + 0x00, 0x00, 0x00, 0x24, 0x65, 0x64, 0x74, 0x73, // edts header + 0x00, 0x00, 0x00, 0x1C, 0x65, 0x6C, 0x73, 0x74, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 // elst header }); int bMediaRate; int mediaTime; - if (tracks[index].trak.edst_elst == null) { + if (tracks[index].trak.edstElst == null) { // is a audio track ¿is edst/elst optional for audio tracks? - mediaTime = 0x00;// ffmpeg set this value as zero, instead of defaultMediaTime + mediaTime = 0x00; // ffmpeg set this value as zero, instead of defaultMediaTime bMediaRate = 0x00010000; } else { - mediaTime = (int) tracks[index].trak.edst_elst.MediaTime; - bMediaRate = tracks[index].trak.edst_elst.bMediaRate; + mediaTime = (int) tracks[index].trak.edstElst.mediaTime; + bMediaRate = tracks[index].trak.edstElst.bMediaRate; } auxWrite(ByteBuffer @@ -740,33 +777,33 @@ public class Mp4FromDashWriter { .array() ); - make_mdia(tracks[index].trak.mdia, tables, is64); + makeMdia(tracks[index].trak.mdia, tables, is64, tracks[index].kind == TrackKind.Audio); lengthFor(start); } - private void make_mdia(Mdia mdia, TablesInfo tablesInfo, boolean is64) throws IOException { - - int start_mdia = auxOffset(); - auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x69, 0x61});// mdia + private void makeMdia(final Mdia mdia, final TablesInfo tablesInfo, final boolean is64, + final boolean isAudio) throws IOException { + int startMdia = auxOffset(); + auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x69, 0x61}); // mdia auxWrite(mdia.mdhd); - auxWrite(make_hdlr(mdia.hdlr)); + auxWrite(makeHdlr(mdia.hdlr)); - int start_minf = auxOffset(); - auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x6D, 0x69, 0x6E, 0x66});// minf - auxWrite(mdia.minf.$mhd); + int startMinf = auxOffset(); + auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x6D, 0x69, 0x6E, 0x66}); // minf + auxWrite(mdia.minf.mhd); auxWrite(mdia.minf.dinf); - int start_stbl = auxOffset(); - auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x73, 0x74, 0x62, 0x6C});// stbl - auxWrite(mdia.minf.stbl_stsd); + int startStbl = auxOffset(); + auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x73, 0x74, 0x62, 0x6C}); // stbl + auxWrite(mdia.minf.stblStsd); // // In audio tracks the following tables is not required: ssts ctts // And stsz can be empty if has a default sample size // if (moovSimulation) { - make(0x73747473, -1, 2, 1); + make(0x73747473, -1, 2, 1); // stts if (tablesInfo.stss > 0) { make(0x73747373, -1, 1, tablesInfo.stss); } @@ -774,7 +811,7 @@ public class Mp4FromDashWriter { make(0x63747473, -1, 2, tablesInfo.ctts); } make(0x73747363, -1, 3, tablesInfo.stsc); - make(0x7374737A, tablesInfo.stsz_default, 1, tablesInfo.stsz); + make(0x7374737A, tablesInfo.stszDefault, 1, tablesInfo.stsz); make(is64 ? 0x636F3634 : 0x7374636F, -1, is64 ? 2 : 1, tablesInfo.stco); } else { tablesInfo.stts = make(0x73747473, -1, 2, 1); @@ -785,47 +822,89 @@ public class Mp4FromDashWriter { tablesInfo.ctts = make(0x63747473, -1, 2, tablesInfo.ctts); } tablesInfo.stsc = make(0x73747363, -1, 3, tablesInfo.stsc); - tablesInfo.stsz = make(0x7374737A, tablesInfo.stsz_default, 1, tablesInfo.stsz); - tablesInfo.stco = make(is64 ? 0x636F3634 : 0x7374636F, -1, is64 ? 2 : 1, tablesInfo.stco); + tablesInfo.stsz = make(0x7374737A, tablesInfo.stszDefault, 1, tablesInfo.stsz); + tablesInfo.stco = make(is64 ? 0x636F3634 : 0x7374636F, -1, is64 ? 2 : 1, + tablesInfo.stco); } - lengthFor(start_stbl); - lengthFor(start_minf); - lengthFor(start_mdia); + if (isAudio) { + auxWrite(makeSgpd()); + tablesInfo.sbgp = makeSbgp(); // during simulation the returned offset is ignored + } + + lengthFor(startStbl); + lengthFor(startMinf); + lengthFor(startMdia); } - private byte[] make_hdlr(Hdlr hdlr) { + private byte[] makeHdlr(final Hdlr hdlr) { ByteBuffer buffer = ByteBuffer.wrap(new byte[]{ - 0x00, 0x00, 0x00, 0x77, 0x68, 0x64, 0x6C, 0x72,// hdlr - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // binary string "ISO Media file created in NewPipe (A libre lightweight streaming frontend for Android)." - 0x49, 0x53, 0x4F, 0x20, 0x4D, 0x65, 0x64, 0x69, 0x61, 0x20, 0x66, 0x69, 0x6C, 0x65, 0x20, 0x63, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x20, 0x69, 0x6E, 0x20, 0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, - 0x65, 0x20, 0x28, 0x41, 0x20, 0x6C, 0x69, 0x62, 0x72, 0x65, 0x20, 0x6C, 0x69, 0x67, 0x68, 0x74, - 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x20, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6D, 0x69, 0x6E, 0x67, - 0x20, 0x66, 0x72, 0x6F, 0x6E, 0x74, 0x65, 0x6E, 0x64, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x41, 0x6E, - 0x64, 0x72, 0x6F, 0x69, 0x64, 0x29, 0x2E + 0x00, 0x00, 0x00, 0x21, 0x68, 0x64, 0x6C, 0x72, // hdlr + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00// null string character }); buffer.position(12); buffer.putInt(hdlr.type); buffer.putInt(hdlr.subType); - buffer.put(hdlr.bReserved);// always is a zero array + buffer.put(hdlr.bReserved); // always is a zero array return buffer.array(); } + private int makeSbgp() throws IOException { + int offset = auxOffset(); + + auxWrite(new byte[] { + 0x00, 0x00, 0x00, 0x1C, // box size + 0x73, 0x62, 0x67, 0x70, // "sbpg" + 0x00, 0x00, 0x00, 0x00, // default box flags + 0x72, 0x6F, 0x6C, 0x6C, // group type "roll" + 0x00, 0x00, 0x00, 0x01, // group table size + 0x00, 0x00, 0x00, 0x00, // group[0] total samples (to be set later) + 0x00, 0x00, 0x00, 0x01 // group[0] description index + }); + + return offset + 0x14; + } + + private byte[] makeSgpd() { + /* + * Sample Group Description Box + * + * ¿whats does? + * the table inside of this box gives information about the + * characteristics of sample groups. The descriptive information is any other + * information needed to define or characterize the sample group. + * + * ¿is replicable this box? + * NO due lacks of documentation about this box but... + * most of m4a encoders and ffmpeg uses this box with dummy values (same values) + */ + + ByteBuffer buffer = ByteBuffer.wrap(new byte[] { + 0x00, 0x00, 0x00, 0x1A, // box size + 0x73, 0x67, 0x70, 0x64, // "sgpd" + 0x01, 0x00, 0x00, 0x00, // box flags (unknown flag sets) + 0x72, 0x6F, 0x6C, 0x6C, // ¿¿group type?? + 0x00, 0x00, 0x00, 0x02, // ¿¿?? + 0x00, 0x00, 0x00, 0x01, // ¿¿?? + (byte) 0xFF, (byte) 0xFF // ¿¿?? + }); + + return buffer.array(); + } class TablesInfo { - int stts; int stsc; - int[] stsc_bEntries; + int[] stscBEntries; int ctts; int stsz; - int stsz_default; + int stszDefault; int stss; int stco; + int sbgp; } } diff --git a/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java b/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java index 5a5a9e1fd..9542dbc05 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java @@ -1,6 +1,7 @@ package org.schabi.newpipe.streams; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import org.schabi.newpipe.streams.WebMReader.Cluster; import org.schabi.newpipe.streams.WebMReader.Segment; @@ -13,22 +14,19 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import androidx.annotation.Nullable; - /** * @author kapodamy */ public class OggFromWebMWriter implements Closeable { - private static final byte FLAG_UNSET = 0x00; //private static final byte FLAG_CONTINUED = 0x01; private static final byte FLAG_FIRST = 0x02; private static final byte FLAG_LAST = 0x04; - private final static byte HEADER_CHECKSUM_OFFSET = 22; - private final static byte HEADER_SIZE = 27; + private static final byte HEADER_CHECKSUM_OFFSET = 22; + private static final byte HEADER_SIZE = 27; - private final static int TIME_SCALE_NS = 1000000000; + private static final int TIME_SCALE_NS = 1000000000; private boolean done = false; private boolean parsed = false; @@ -36,26 +34,26 @@ public class OggFromWebMWriter implements Closeable { private SharpStream source; private SharpStream output; - private int sequence_count = 0; - private final int STREAM_ID; - private byte packet_flag = FLAG_FIRST; + private int sequenceCount = 0; + private final int streamId; + private byte packetFlag = FLAG_FIRST; private WebMReader webm = null; - private WebMTrack webm_track = null; - private Segment webm_segment = null; - private Cluster webm_cluster = null; - private SimpleBlock webm_block = null; + private WebMTrack webmTrack = null; + private Segment webmSegment = null; + private Cluster webmCluster = null; + private SimpleBlock webmBlock = null; - private long webm_block_last_timecode = 0; - private long webm_block_near_duration = 0; + private long webmBlockLastTimecode = 0; + private long webmBlockNearDuration = 0; - private short segment_table_size = 0; - private final byte[] segment_table = new byte[255]; - private long segment_table_next_timestamp = TIME_SCALE_NS; + private short segmentTableSize = 0; + private final byte[] segmentTable = new byte[255]; + private long segmentTableNextTimestamp = TIME_SCALE_NS; - private final int[] crc32_table = new int[256]; + private final int[] crc32Table = new int[256]; - public OggFromWebMWriter(@NonNull SharpStream source, @NonNull SharpStream target) { + public OggFromWebMWriter(@NonNull final SharpStream source, @NonNull final SharpStream target) { if (!source.canRead() || !source.canRewind()) { throw new IllegalArgumentException("source stream must be readable and allows seeking"); } @@ -66,9 +64,9 @@ public class OggFromWebMWriter implements Closeable { this.source = source; this.output = target; - this.STREAM_ID = (int) System.currentTimeMillis(); + this.streamId = (int) System.currentTimeMillis(); - populate_crc32_table(); + populateCrc32Table(); } public boolean isDone() { @@ -98,20 +96,20 @@ public class OggFromWebMWriter implements Closeable { try { webm = new WebMReader(source); webm.parse(); - webm_segment = webm.getNextSegment(); + webmSegment = webm.getNextSegment(); } finally { parsed = true; } } - public void selectTrack(int trackIndex) throws IOException { + public void selectTrack(final int trackIndex) throws IOException { if (!parsed) { throw new IllegalStateException("source must be parsed first"); } if (done) { throw new IOException("already done"); } - if (webm_track != null) { + if (webmTrack != null) { throw new IOException("tracks already selected"); } @@ -124,7 +122,7 @@ public class OggFromWebMWriter implements Closeable { } try { - webm_track = webm.selectTrack(trackIndex); + webmTrack = webm.selectTrack(trackIndex); } finally { parsed = true; } @@ -135,7 +133,7 @@ public class OggFromWebMWriter implements Closeable { done = true; parsed = true; - webm_track = null; + webmTrack = null; webm = null; if (!output.isClosed()) { @@ -155,43 +153,44 @@ public class OggFromWebMWriter implements Closeable { header.order(ByteOrder.LITTLE_ENDIAN); /* step 1: get the amount of frames per seconds */ - switch (webm_track.kind) { + switch (webmTrack.kind) { case Audio: - resolution = getSampleFrequencyFromTrack(webm_track.bMetadata); - if (resolution == 0.0f) { + resolution = getSampleFrequencyFromTrack(webmTrack.bMetadata); + if (resolution == 0f) { throw new RuntimeException("cannot get the audio sample rate"); } break; case Video: // WARNING: untested - if (webm_track.defaultDuration == 0) { + if (webmTrack.defaultDuration == 0) { throw new RuntimeException("missing default frame time"); } - resolution = 1000.0f / ((float) webm_track.defaultDuration / webm_segment.info.timecodeScale); + resolution = 1000f / ((float) webmTrack.defaultDuration + / webmSegment.info.timecodeScale); break; default: throw new RuntimeException("not implemented"); } /* step 2: create packet with code init data */ - if (webm_track.codecPrivate != null) { - addPacketSegment(webm_track.codecPrivate.length); - make_packetHeader(0x00, header, webm_track.codecPrivate); + if (webmTrack.codecPrivate != null) { + addPacketSegment(webmTrack.codecPrivate.length); + makePacketheader(0x00, header, webmTrack.codecPrivate); write(header); - output.write(webm_track.codecPrivate); + output.write(webmTrack.codecPrivate); } /* step 3: create packet with metadata */ - byte[] buffer = make_metadata(); + byte[] buffer = makeMetadata(); if (buffer != null) { addPacketSegment(buffer.length); - make_packetHeader(0x00, header, buffer); + makePacketheader(0x00, header, buffer); write(header); output.write(buffer); } /* step 4: calculate amount of packets */ - while (webm_segment != null) { + while (webmSegment != null) { bloq = getNextBlock(); if (bloq != null && addPacketSegment(bloq)) { @@ -203,29 +202,29 @@ public class OggFromWebMWriter implements Closeable { } // calculate the current packet duration using the next block - double elapsed_ns = webm_track.codecDelay; + double elapsedNs = webmTrack.codecDelay; if (bloq == null) { - packet_flag = FLAG_LAST;// note: if the flag is FLAG_CONTINUED, is changed - elapsed_ns += webm_block_last_timecode; + packetFlag = FLAG_LAST; // note: if the flag is FLAG_CONTINUED, is changed + elapsedNs += webmBlockLastTimecode; - if (webm_track.defaultDuration > 0) { - elapsed_ns += webm_track.defaultDuration; + if (webmTrack.defaultDuration > 0) { + elapsedNs += webmTrack.defaultDuration; } else { // hardcoded way, guess the sample duration - elapsed_ns += webm_block_near_duration; + elapsedNs += webmBlockNearDuration; } } else { - elapsed_ns += bloq.absoluteTimeCodeNs; + elapsedNs += bloq.absoluteTimeCodeNs; } // get the sample count in the page - elapsed_ns = elapsed_ns / TIME_SCALE_NS; - elapsed_ns = Math.ceil(elapsed_ns * resolution); + elapsedNs = elapsedNs / TIME_SCALE_NS; + elapsedNs = Math.ceil(elapsedNs * resolution); // create header and calculate page checksum - int checksum = make_packetHeader((long) elapsed_ns, header, null); - checksum = calc_crc32(checksum, page.array(), page.position()); + int checksum = makePacketheader((long) elapsedNs, header, null); + checksum = calcCrc32(checksum, page.array(), page.position()); header.putInt(HEADER_CHECKSUM_OFFSET, checksum); @@ -233,69 +232,57 @@ public class OggFromWebMWriter implements Closeable { write(header); write(page); - webm_block = bloq; + webmBlock = bloq; } } - private int make_packetHeader(long gran_pos, @NonNull ByteBuffer buffer, byte[] immediate_page) { + private int makePacketheader(final long granPos, @NonNull final ByteBuffer buffer, + final byte[] immediatePage) { short length = HEADER_SIZE; - buffer.putInt(0x5367674f);// "OggS" binary string in little-endian - buffer.put((byte) 0x00);// version - buffer.put(packet_flag);// type + buffer.putInt(0x5367674f); // "OggS" binary string in little-endian + buffer.put((byte) 0x00); // version + buffer.put(packetFlag); // type - buffer.putLong(gran_pos);// granulate position + buffer.putLong(granPos); // granulate position - buffer.putInt(STREAM_ID);// bitstream serial number - buffer.putInt(sequence_count++);// page sequence number + buffer.putInt(streamId); // bitstream serial number + buffer.putInt(sequenceCount++); // page sequence number - buffer.putInt(0x00);// page checksum + buffer.putInt(0x00); // page checksum - buffer.put((byte) segment_table_size);// segment table - buffer.put(segment_table, 0, segment_table_size);// segment size + buffer.put((byte) segmentTableSize); // segment table + buffer.put(segmentTable, 0, segmentTableSize); // segment size - length += segment_table_size; + length += segmentTableSize; - clearSegmentTable();// clear segment table for next header + clearSegmentTable(); // clear segment table for next header - int checksum_crc32 = calc_crc32(0x00, buffer.array(), length); + int checksumCrc32 = calcCrc32(0x00, buffer.array(), length); - if (immediate_page != null) { - checksum_crc32 = calc_crc32(checksum_crc32, immediate_page, immediate_page.length); - buffer.putInt(HEADER_CHECKSUM_OFFSET, checksum_crc32); - segment_table_next_timestamp -= TIME_SCALE_NS; + if (immediatePage != null) { + checksumCrc32 = calcCrc32(checksumCrc32, immediatePage, immediatePage.length); + buffer.putInt(HEADER_CHECKSUM_OFFSET, checksumCrc32); + segmentTableNextTimestamp -= TIME_SCALE_NS; } - return checksum_crc32; + return checksumCrc32; } @Nullable - private byte[] make_metadata() { - if ("A_OPUS".equals(webm_track.codecId)) { + private byte[] makeMetadata() { + if ("A_OPUS".equals(webmTrack.codecId)) { return new byte[]{ - 0x4F, 0x70, 0x75, 0x73, 0x54, 0x61, 0x67, 0x73,// "OpusTags" binary string - 0x07, 0x00, 0x00, 0x00,// writting application string size - 0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65,// "NewPipe" binary string - 0x00, 0x00, 0x00, 0x00// additional tags count (zero means no tags) + 0x4F, 0x70, 0x75, 0x73, 0x54, 0x61, 0x67, 0x73, // "OpusTags" binary string + 0x00, 0x00, 0x00, 0x00, // writing application string size (not present) + 0x00, 0x00, 0x00, 0x00 // additional tags count (zero means no tags) }; - } else if ("A_VORBIS".equals(webm_track.codecId)) { + } else if ("A_VORBIS".equals(webmTrack.codecId)) { return new byte[]{ - 0x03,// ???????? - 0x76, 0x6f, 0x72, 0x62, 0x69, 0x73,// "vorbis" binary string - 0x07, 0x00, 0x00, 0x00,// writting application string size - 0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65,// "NewPipe" binary string - 0x01, 0x00, 0x00, 0x00,// additional tags count (zero means no tags) - - /* - // whole file duration (not implemented) - 0x44,// tag string size - 0x55, 0x52, 0x41, 0x54, 0x49, 0x4F, 0x4E, 0x3D, 0x30, 0x30, 0x3A, 0x30, 0x30, 0x3A, 0x30, - 0x30, 0x2E, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30 - */ - 0x0F,// tag string size - 0x00, 0x00, 0x00, 0x45, 0x4E, 0x43, 0x4F, 0x44, 0x45, 0x52, 0x3D,// "ENCODER=" binary string - 0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65,// "NewPipe" binary string - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00// ???????? + 0x03, // ¿¿¿??? + 0x76, 0x6f, 0x72, 0x62, 0x69, 0x73, // "vorbis" binary string + 0x00, 0x00, 0x00, 0x00, // writing application string size (not present) + 0x00, 0x00, 0x00, 0x00 // additional tags count (zero means no tags) }; } @@ -303,51 +290,49 @@ public class OggFromWebMWriter implements Closeable { return null; } - private void write(ByteBuffer buffer) throws IOException { + private void write(final ByteBuffer buffer) throws IOException { output.write(buffer.array(), 0, buffer.position()); buffer.position(0); } - - @Nullable private SimpleBlock getNextBlock() throws IOException { SimpleBlock res; - if (webm_block != null) { - res = webm_block; - webm_block = null; + if (webmBlock != null) { + res = webmBlock; + webmBlock = null; return res; } - if (webm_segment == null) { - webm_segment = webm.getNextSegment(); - if (webm_segment == null) { - return null;// no more blocks in the selected track + if (webmSegment == null) { + webmSegment = webm.getNextSegment(); + if (webmSegment == null) { + return null; // no more blocks in the selected track } } - if (webm_cluster == null) { - webm_cluster = webm_segment.getNextCluster(); - if (webm_cluster == null) { - webm_segment = null; + if (webmCluster == null) { + webmCluster = webmSegment.getNextCluster(); + if (webmCluster == null) { + webmSegment = null; return getNextBlock(); } } - res = webm_cluster.getNextSimpleBlock(); + res = webmCluster.getNextSimpleBlock(); if (res == null) { - webm_cluster = null; + webmCluster = null; return getNextBlock(); } - webm_block_near_duration = res.absoluteTimeCodeNs - webm_block_last_timecode; - webm_block_last_timecode = res.absoluteTimeCodeNs; + webmBlockNearDuration = res.absoluteTimeCodeNs - webmBlockLastTimecode; + webmBlockLastTimecode = res.absoluteTimeCodeNs; return res; } - private float getSampleFrequencyFromTrack(byte[] bMetadata) { + private float getSampleFrequencyFromTrack(final byte[] bMetadata) { // hardcoded way ByteBuffer buffer = ByteBuffer.wrap(bMetadata); @@ -362,27 +347,27 @@ public class OggFromWebMWriter implements Closeable { } private void clearSegmentTable() { - segment_table_next_timestamp += TIME_SCALE_NS; - packet_flag = FLAG_UNSET; - segment_table_size = 0; + segmentTableNextTimestamp += TIME_SCALE_NS; + packetFlag = FLAG_UNSET; + segmentTableSize = 0; } - private boolean addPacketSegment(SimpleBlock block) { - long timestamp = block.absoluteTimeCodeNs + webm_track.codecDelay; + private boolean addPacketSegment(final SimpleBlock block) { + long timestamp = block.absoluteTimeCodeNs + webmTrack.codecDelay; - if (timestamp >= segment_table_next_timestamp) { + if (timestamp >= segmentTableNextTimestamp) { return false; } return addPacketSegment(block.dataSize); } - private boolean addPacketSegment(int size) { + private boolean addPacketSegment(final int size) { if (size > 65025) { throw new UnsupportedOperationException("page size cannot be larger than 65025"); } - int available = (segment_table.length - segment_table_size) * 255; + int available = (segmentTable.length - segmentTableSize) * 255; boolean extra = (size % 255) == 0; if (extra) { @@ -393,21 +378,21 @@ public class OggFromWebMWriter implements Closeable { // check if possible add the segment, without overflow the table if (available < size) { - return false;// not enough space on the page + return false; // not enough space on the page } - for (; size > 0; size -= 255) { - segment_table[segment_table_size++] = (byte) Math.min(size, 255); + for (int seg = size; seg > 0; seg -= 255) { + segmentTable[segmentTableSize++] = (byte) Math.min(seg, 255); } if (extra) { - segment_table[segment_table_size++] = 0x00; + segmentTable[segmentTableSize++] = 0x00; } return true; } - private void populate_crc32_table() { + private void populateCrc32Table() { for (int i = 0; i < 0x100; i++) { int crc = i << 24; for (int j = 0; j < 8; j++) { @@ -415,17 +400,17 @@ public class OggFromWebMWriter implements Closeable { crc <<= 1; crc ^= (int) (0x100000000L - b) & 0x04c11db7; } - crc32_table[i] = crc; + crc32Table[i] = crc; } } - private int calc_crc32(int initial_crc, byte[] buffer, int size) { + private int calcCrc32(final int initialCrc, final byte[] buffer, final int size) { + int crc = initialCrc; for (int i = 0; i < size; i++) { - int reg = (initial_crc >>> 24) & 0xff; - initial_crc = (initial_crc << 8) ^ crc32_table[reg ^ (buffer[i] & 0xff)]; + int reg = (crc >>> 24) & 0xff; + crc = (crc << 8) ^ crc32Table[reg ^ (buffer[i] & 0xff)]; } - return initial_crc; + return crc; } - } diff --git a/app/src/main/java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java b/app/src/main/java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java new file mode 100644 index 000000000..eddb951e5 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java @@ -0,0 +1,103 @@ +package org.schabi.newpipe.streams; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.nodes.Node; +import org.jsoup.nodes.TextNode; +import org.jsoup.parser.Parser; +import org.jsoup.select.Elements; +import org.schabi.newpipe.streams.io.SharpStream; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +/** + * @author kapodamy + */ +public class SrtFromTtmlWriter { + private static final String NEW_LINE = "\r\n"; + + private SharpStream out; + private boolean ignoreEmptyFrames; + private final Charset charset = StandardCharsets.UTF_8; + + private int frameIndex = 0; + + public SrtFromTtmlWriter(final SharpStream out, final boolean ignoreEmptyFrames) { + this.out = out; + this.ignoreEmptyFrames = ignoreEmptyFrames; + } + + private static String getTimestamp(final Element frame, final String attr) { + return frame + .attr(attr) + .replace('.', ','); // SRT subtitles uses comma as decimal separator + } + + private void writeFrame(final String begin, final String end, final StringBuilder text) + throws IOException { + writeString(String.valueOf(frameIndex++)); + writeString(NEW_LINE); + writeString(begin); + writeString(" --> "); + writeString(end); + writeString(NEW_LINE); + writeString(text.toString()); + writeString(NEW_LINE); + writeString(NEW_LINE); + } + + private void writeString(final String text) throws IOException { + out.write(text.getBytes(charset)); + } + + public void build(final SharpStream ttml) throws IOException { + /* + * TTML 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 + */ + + // parse XML + byte[] buffer = new byte[(int) ttml.available()]; + ttml.read(buffer); + Document doc = Jsoup.parse(new ByteArrayInputStream(buffer), "UTF-8", "", + Parser.xmlParser()); + + StringBuilder text = new StringBuilder(128); + Elements paragraphList = doc.select("body > div > p"); + + // check if has frames + if (paragraphList.size() < 1) { + return; + } + + for (Element paragraph : paragraphList) { + text.setLength(0); + + for (Node children : paragraph.childNodes()) { + if (children instanceof TextNode) { + text.append(((TextNode) children).text()); + } else if (children instanceof Element + && ((Element) children).tagName().equalsIgnoreCase("br")) { + text.append(NEW_LINE); + } + } + + if (ignoreEmptyFrames && text.length() < 1) { + continue; + } + + String begin = getTimestamp(paragraph, "begin"); + String end = getTimestamp(paragraph, "end"); + + writeFrame(begin, end, text); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/streams/SubtitleConverter.java b/app/src/main/java/org/schabi/newpipe/streams/SubtitleConverter.java index 9c6fa977d..e69de29bb 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/SubtitleConverter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/SubtitleConverter.java @@ -1,369 +0,0 @@ -package org.schabi.newpipe.streams; - -import org.schabi.newpipe.streams.io.SharpStream; -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 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[(int) 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) { - 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]) * 1000.0f;// 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; - } - } - -} diff --git a/app/src/main/java/org/schabi/newpipe/streams/WebMReader.java b/app/src/main/java/org/schabi/newpipe/streams/WebMReader.java index 42875c364..56cea9f2d 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/WebMReader.java +++ b/app/src/main/java/org/schabi/newpipe/streams/WebMReader.java @@ -1,540 +1,538 @@ -package org.schabi.newpipe.streams; - -import org.schabi.newpipe.streams.io.SharpStream; - -import java.io.EOFException; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.NoSuchElementException; - -/** - * - * @author kapodamy - */ -public class WebMReader { - - private final static int ID_EMBL = 0x0A45DFA3; - private final static int ID_EMBLReadVersion = 0x02F7; - private final static int ID_EMBLDocType = 0x0282; - private final static int ID_EMBLDocTypeReadVersion = 0x0285; - - private final static int ID_Segment = 0x08538067; - - private final static int ID_Info = 0x0549A966; - private final static int ID_TimecodeScale = 0x0AD7B1; - private final static int ID_Duration = 0x489; - - private final static int ID_Tracks = 0x0654AE6B; - private final static int ID_TrackEntry = 0x2E; - private final static int ID_TrackNumber = 0x57; - private final static int ID_TrackType = 0x03; - private final static int ID_CodecID = 0x06; - private final static int ID_CodecPrivate = 0x23A2; - private final static int ID_Video = 0x60; - private final static int ID_Audio = 0x61; - private final static int ID_DefaultDuration = 0x3E383; - private final static int ID_FlagLacing = 0x1C; - private final static int ID_CodecDelay = 0x16AA; - - private final static int ID_Cluster = 0x0F43B675; - private final static int ID_Timecode = 0x67; - private final static int ID_SimpleBlock = 0x23; - private final static int ID_Block = 0x21; - private final static int ID_GroupBlock = 0x20; - - - public enum TrackKind { - Audio/*2*/, Video/*1*/, Other - } - - private DataReader stream; - private Segment segment; - private WebMTrack[] tracks; - private int selectedTrack; - private boolean done; - private boolean firstSegment; - - public WebMReader(SharpStream source) { - this.stream = new DataReader(source); - } - - public void parse() throws IOException { - Element elem = readElement(ID_EMBL); - if (!readEbml(elem, 1, 2)) { - throw new UnsupportedOperationException("Unsupported EBML data (WebM)"); - } - ensure(elem); - - elem = untilElement(null, ID_Segment); - if (elem == null) { - throw new IOException("Fragment element not found"); - } - segment = readSegment(elem, 0, true); - tracks = segment.tracks; - selectedTrack = -1; - done = false; - firstSegment = true; - } - - public WebMTrack[] getAvailableTracks() { - return tracks; - } - - public WebMTrack selectTrack(int index) { - selectedTrack = index; - return tracks[index]; - } - - public Segment getNextSegment() throws IOException { - if (done) { - return null; - } - - if (firstSegment && segment != null) { - firstSegment = false; - return segment; - } - - ensure(segment.ref); - // WARNING: track cannot be the same or have different index in new segments - Element elem = untilElement(null, ID_Segment); - if (elem == null) { - done = true; - return null; - } - segment = readSegment(elem, 0, false); - - return segment; - } - - - - private long readNumber(Element parent) throws IOException { - int length = (int) parent.contentSize; - long value = 0; - while (length-- > 0) { - int read = stream.read(); - if (read == -1) { - throw new EOFException(); - } - value = (value << 8) | read; - } - return value; - } - - private String readString(Element parent) throws IOException { - return new String(readBlob(parent), StandardCharsets.UTF_8);// or use "utf-8" - } - - private byte[] readBlob(Element parent) throws IOException { - long length = parent.contentSize; - byte[] buffer = new byte[(int) length]; - int read = stream.read(buffer); - if (read < length) { - throw new EOFException(); - } - return buffer; - } - - private long readEncodedNumber() throws IOException { - int value = stream.read(); - - if (value > 0) { - byte size = 1; - int mask = 0x80; - - while (size < 9) { - if ((value & mask) == mask) { - mask = 0xFF; - mask >>= size; - - long number = value & mask; - - for (int i = 1; i < size; i++) { - value = stream.read(); - number <<= 8; - number |= value; - } - - return number; - } - - mask >>= 1; - size++; - } - } - - throw new IOException("Invalid encoded length"); - } - - private Element readElement() throws IOException { - Element elem = new Element(); - elem.offset = stream.position(); - elem.type = (int) readEncodedNumber(); - elem.contentSize = readEncodedNumber(); - elem.size = elem.contentSize + stream.position() - elem.offset; - - return elem; - } - - private Element readElement(int expected) throws IOException { - Element elem = readElement(); - if (expected != 0 && elem.type != expected) { - throw new NoSuchElementException("expected " + elementID(expected) + " found " + elementID(elem.type)); - } - - return elem; - } - - private Element untilElement(Element ref, int... expected) throws IOException { - Element elem; - while (ref == null ? stream.available() : (stream.position() < (ref.offset + ref.size))) { - elem = readElement(); - if (expected.length < 1) { - return elem; - } - for (int type : expected) { - if (elem.type == type) { - return elem; - } - } - - ensure(elem); - } - - return null; - } - - private String elementID(long type) { - return "0x".concat(Long.toHexString(type)); - } - - private void ensure(Element ref) throws IOException { - long skip = (ref.offset + ref.size) - stream.position(); - - if (skip == 0) { - return; - } else if (skip < 0) { - throw new EOFException(String.format( - "parser go beyond limits of the Element. type=%s offset=%s size=%s position=%s", - elementID(ref.type), ref.offset, ref.size, stream.position() - )); - } - - stream.skipBytes(skip); - } - - - - private boolean readEbml(Element ref, int minReadVersion, int minDocTypeVersion) throws IOException { - Element elem = untilElement(ref, ID_EMBLReadVersion); - if (elem == null) { - return false; - } - if (readNumber(elem) > minReadVersion) { - return false; - } - - elem = untilElement(ref, ID_EMBLDocType); - if (elem == null) { - return false; - } - if (!readString(elem).equals("webm")) { - return false; - } - elem = untilElement(ref, ID_EMBLDocTypeReadVersion); - - return elem != null && readNumber(elem) <= minDocTypeVersion; - } - - private Info readInfo(Element ref) throws IOException { - Element elem; - Info info = new Info(); - - while ((elem = untilElement(ref, ID_TimecodeScale, ID_Duration)) != null) { - switch (elem.type) { - case ID_TimecodeScale: - info.timecodeScale = readNumber(elem); - break; - case ID_Duration: - info.duration = readNumber(elem); - break; - } - ensure(elem); - } - - if (info.timecodeScale == 0) { - throw new NoSuchElementException("Element Timecode not found"); - } - - return info; - } - - private Segment readSegment(Element ref, int trackLacingExpected, boolean metadataExpected) throws IOException { - Segment obj = new Segment(ref); - Element elem; - while ((elem = untilElement(ref, ID_Info, ID_Tracks, ID_Cluster)) != null) { - if (elem.type == ID_Cluster) { - obj.currentCluster = elem; - break; - } - switch (elem.type) { - case ID_Info: - obj.info = readInfo(elem); - break; - case ID_Tracks: - obj.tracks = readTracks(elem, trackLacingExpected); - break; - } - ensure(elem); - } - - if (metadataExpected && (obj.info == null || obj.tracks == null)) { - throw new RuntimeException("Cluster element found without Info and/or Tracks element at position " + String.valueOf(ref.offset)); - } - - return obj; - } - - private WebMTrack[] readTracks(Element ref, int lacingExpected) throws IOException { - ArrayList trackEntries = new ArrayList<>(2); - Element elem_trackEntry; - - while ((elem_trackEntry = untilElement(ref, ID_TrackEntry)) != null) { - WebMTrack entry = new WebMTrack(); - boolean drop = false; - Element elem; - while ((elem = untilElement(elem_trackEntry)) != null) { - switch (elem.type) { - case ID_TrackNumber: - entry.trackNumber = readNumber(elem); - break; - case ID_TrackType: - entry.trackType = (int) readNumber(elem); - break; - case ID_CodecID: - entry.codecId = readString(elem); - break; - case ID_CodecPrivate: - entry.codecPrivate = readBlob(elem); - break; - case ID_Audio: - case ID_Video: - entry.bMetadata = readBlob(elem); - break; - case ID_DefaultDuration: - entry.defaultDuration = readNumber(elem); - break; - case ID_FlagLacing: - drop = readNumber(elem) != lacingExpected; - break; - case ID_CodecDelay: - entry.codecDelay = readNumber(elem); - default: - break; - } - ensure(elem); - } - if (!drop) { - trackEntries.add(entry); - } - ensure(elem_trackEntry); - } - - WebMTrack[] entries = new WebMTrack[trackEntries.size()]; - trackEntries.toArray(entries); - - for (WebMTrack entry : entries) { - switch (entry.trackType) { - case 1: - entry.kind = TrackKind.Video; - break; - case 2: - entry.kind = TrackKind.Audio; - break; - default: - entry.kind = TrackKind.Other; - break; - } - } - - return entries; - } - - private SimpleBlock readSimpleBlock(Element ref) throws IOException { - SimpleBlock obj = new SimpleBlock(ref); - obj.trackNumber = readEncodedNumber(); - obj.relativeTimeCode = stream.readShort(); - obj.flags = (byte) stream.read(); - obj.dataSize = (int) ((ref.offset + ref.size) - stream.position()); - obj.createdFromBlock = ref.type == ID_Block; - - // NOTE: lacing is not implemented, and will be mixed with the stream data - if (obj.dataSize < 0) { - throw new IOException(String.format("Unexpected SimpleBlock element size, missing %s bytes", -obj.dataSize)); - } - return obj; - } - - private Cluster readCluster(Element ref) throws IOException { - Cluster obj = new Cluster(ref); - - Element elem = untilElement(ref, ID_Timecode); - if (elem == null) { - throw new NoSuchElementException("Cluster at " + String.valueOf(ref.offset) + " without Timecode element"); - } - obj.timecode = readNumber(elem); - - return obj; - } - - - - class Element { - - int type; - long offset; - long contentSize; - long size; - } - - public class Info { - - public long timecodeScale; - public long duration; - } - - public class WebMTrack { - - public long trackNumber; - protected int trackType; - public String codecId; - public byte[] codecPrivate; - public byte[] bMetadata; - public TrackKind kind; - public long defaultDuration; - public long codecDelay; - } - - public class Segment { - - Segment(Element ref) { - this.ref = ref; - this.firstClusterInSegment = true; - } - - public Info info; - WebMTrack[] tracks; - private Element currentCluster; - private final Element ref; - boolean firstClusterInSegment; - - public Cluster getNextCluster() throws IOException { - if (done) { - return null; - } - if (firstClusterInSegment && segment.currentCluster != null) { - firstClusterInSegment = false; - return readCluster(segment.currentCluster); - } - ensure(segment.currentCluster); - - Element elem = untilElement(segment.ref, ID_Cluster); - if (elem == null) { - return null; - } - - segment.currentCluster = elem; - - return readCluster(segment.currentCluster); - } - } - - public class SimpleBlock { - - public InputStream data; - public boolean createdFromBlock; - - SimpleBlock(Element ref) { - this.ref = ref; - } - - public long trackNumber; - public short relativeTimeCode; - public long absoluteTimeCodeNs; - public byte flags; - public int dataSize; - private final Element ref; - - public boolean isKeyframe() { - return (flags & 0x80) == 0x80; - } - } - - public class Cluster { - - Element ref; - SimpleBlock currentSimpleBlock = null; - Element currentBlockGroup = null; - public long timecode; - - Cluster(Element ref) { - this.ref = ref; - } - - boolean insideClusterBounds() { - return stream.position() >= (ref.offset + ref.size); - } - - public SimpleBlock getNextSimpleBlock() throws IOException { - if (insideClusterBounds()) { - return null; - } - - if (currentBlockGroup != null) { - ensure(currentBlockGroup); - currentBlockGroup = null; - currentSimpleBlock = null; - } else if (currentSimpleBlock != null) { - ensure(currentSimpleBlock.ref); - } - - while (!insideClusterBounds()) { - Element elem = untilElement(ref, ID_SimpleBlock, ID_GroupBlock); - if (elem == null) { - return null; - } - - if (elem.type == ID_GroupBlock) { - currentBlockGroup = elem; - elem = untilElement(currentBlockGroup, ID_Block); - - if (elem == null) { - ensure(currentBlockGroup); - currentBlockGroup = null; - continue; - } - } - - currentSimpleBlock = readSimpleBlock(elem); - if (currentSimpleBlock.trackNumber == tracks[selectedTrack].trackNumber) { - currentSimpleBlock.data = stream.getView((int) currentSimpleBlock.dataSize); - - // calculate the timestamp in nanoseconds - currentSimpleBlock.absoluteTimeCodeNs = currentSimpleBlock.relativeTimeCode + this.timecode; - currentSimpleBlock.absoluteTimeCodeNs *= segment.info.timecodeScale; - - return currentSimpleBlock; - } - - ensure(elem); - } - - return null; - } - - } - -} +package org.schabi.newpipe.streams; + +import org.schabi.newpipe.streams.io.SharpStream; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.NoSuchElementException; + +/** + * + * @author kapodamy + */ +public class WebMReader { + private static final int ID_EMBL = 0x0A45DFA3; + private static final int ID_EMBL_READ_VERSION = 0x02F7; + private static final int ID_EMBL_DOC_TYPE = 0x0282; + private static final int ID_EMBL_DOC_TYPE_READ_VERSION = 0x0285; + + private static final int ID_SEGMENT = 0x08538067; + + private static final int ID_INFO = 0x0549A966; + private static final int ID_TIMECODE_SCALE = 0x0AD7B1; + private static final int ID_DURATION = 0x489; + + private static final int ID_TRACKS = 0x0654AE6B; + private static final int ID_TRACK_ENTRY = 0x2E; + private static final int ID_TRACK_NUMBER = 0x57; + private static final int ID_TRACK_TYPE = 0x03; + private static final int ID_CODEC_ID = 0x06; + private static final int ID_CODEC_PRIVATE = 0x23A2; + private static final int ID_VIDEO = 0x60; + private static final int ID_AUDIO = 0x61; + private static final int ID_DEFAULT_DURATION = 0x3E383; + private static final int ID_FLAG_LACING = 0x1C; + private static final int ID_CODEC_DELAY = 0x16AA; + private static final int ID_SEEK_PRE_ROLL = 0x16BB; + + private static final int ID_CLUSTER = 0x0F43B675; + private static final int ID_TIMECODE = 0x67; + private static final int ID_SIMPLE_BLOCK = 0x23; + private static final int ID_BLOCK = 0x21; + private static final int ID_GROUP_BLOCK = 0x20; + + + public enum TrackKind { + Audio/*2*/, Video/*1*/, Other + } + + private DataReader stream; + private Segment segment; + private WebMTrack[] tracks; + private int selectedTrack; + private boolean done; + private boolean firstSegment; + + public WebMReader(final SharpStream source) { + this.stream = new DataReader(source); + } + + public void parse() throws IOException { + Element elem = readElement(ID_EMBL); + if (!readEbml(elem, 1, 2)) { + throw new UnsupportedOperationException("Unsupported EBML data (WebM)"); + } + ensure(elem); + + elem = untilElement(null, ID_SEGMENT); + if (elem == null) { + throw new IOException("Fragment element not found"); + } + segment = readSegment(elem, 0, true); + tracks = segment.tracks; + selectedTrack = -1; + done = false; + firstSegment = true; + } + + public WebMTrack[] getAvailableTracks() { + return tracks; + } + + public WebMTrack selectTrack(final int index) { + selectedTrack = index; + return tracks[index]; + } + + public Segment getNextSegment() throws IOException { + if (done) { + return null; + } + + if (firstSegment && segment != null) { + firstSegment = false; + return segment; + } + + ensure(segment.ref); + // WARNING: track cannot be the same or have different index in new segments + Element elem = untilElement(null, ID_SEGMENT); + if (elem == null) { + done = true; + return null; + } + segment = readSegment(elem, 0, false); + + return segment; + } + + private long readNumber(final Element parent) throws IOException { + int length = (int) parent.contentSize; + long value = 0; + while (length-- > 0) { + int read = stream.read(); + if (read == -1) { + throw new EOFException(); + } + value = (value << 8) | read; + } + return value; + } + + private String readString(final Element parent) throws IOException { + return new String(readBlob(parent), StandardCharsets.UTF_8); // or use "utf-8" + } + + private byte[] readBlob(final Element parent) throws IOException { + long length = parent.contentSize; + byte[] buffer = new byte[(int) length]; + int read = stream.read(buffer); + if (read < length) { + throw new EOFException(); + } + return buffer; + } + + private long readEncodedNumber() throws IOException { + int value = stream.read(); + + if (value > 0) { + byte size = 1; + int mask = 0x80; + + while (size < 9) { + if ((value & mask) == mask) { + mask = 0xFF; + mask >>= size; + + long number = value & mask; + + for (int i = 1; i < size; i++) { + value = stream.read(); + number <<= 8; + number |= value; + } + + return number; + } + + mask >>= 1; + size++; + } + } + + throw new IOException("Invalid encoded length"); + } + + private Element readElement() throws IOException { + Element elem = new Element(); + elem.offset = stream.position(); + elem.type = (int) readEncodedNumber(); + elem.contentSize = readEncodedNumber(); + elem.size = elem.contentSize + stream.position() - elem.offset; + + return elem; + } + + private Element readElement(final int expected) throws IOException { + Element elem = readElement(); + if (expected != 0 && elem.type != expected) { + throw new NoSuchElementException("expected " + elementID(expected) + + " found " + elementID(elem.type)); + } + + return elem; + } + + private Element untilElement(final Element ref, final int... expected) throws IOException { + Element elem; + while (ref == null ? stream.available() : (stream.position() < (ref.offset + ref.size))) { + elem = readElement(); + if (expected.length < 1) { + return elem; + } + for (int type : expected) { + if (elem.type == type) { + return elem; + } + } + + ensure(elem); + } + + return null; + } + + private String elementID(final long type) { + return "0x".concat(Long.toHexString(type)); + } + + private void ensure(final Element ref) throws IOException { + long skip = (ref.offset + ref.size) - stream.position(); + + if (skip == 0) { + return; + } else if (skip < 0) { + throw new EOFException(String.format( + "parser go beyond limits of the Element. type=%s offset=%s size=%s position=%s", + elementID(ref.type), ref.offset, ref.size, stream.position() + )); + } + + stream.skipBytes(skip); + } + + private boolean readEbml(final Element ref, final int minReadVersion, + final int minDocTypeVersion) throws IOException { + Element elem = untilElement(ref, ID_EMBL_READ_VERSION); + if (elem == null) { + return false; + } + if (readNumber(elem) > minReadVersion) { + return false; + } + + elem = untilElement(ref, ID_EMBL_DOC_TYPE); + if (elem == null) { + return false; + } + if (!readString(elem).equals("webm")) { + return false; + } + elem = untilElement(ref, ID_EMBL_DOC_TYPE_READ_VERSION); + + return elem != null && readNumber(elem) <= minDocTypeVersion; + } + + private Info readInfo(final Element ref) throws IOException { + Element elem; + Info info = new Info(); + + while ((elem = untilElement(ref, ID_TIMECODE_SCALE, ID_DURATION)) != null) { + switch (elem.type) { + case ID_TIMECODE_SCALE: + info.timecodeScale = readNumber(elem); + break; + case ID_DURATION: + info.duration = readNumber(elem); + break; + } + ensure(elem); + } + + if (info.timecodeScale == 0) { + throw new NoSuchElementException("Element Timecode not found"); + } + + return info; + } + + private Segment readSegment(final Element ref, final int trackLacingExpected, + final boolean metadataExpected) throws IOException { + Segment obj = new Segment(ref); + Element elem; + while ((elem = untilElement(ref, ID_INFO, ID_TRACKS, ID_CLUSTER)) != null) { + if (elem.type == ID_CLUSTER) { + obj.currentCluster = elem; + break; + } + switch (elem.type) { + case ID_INFO: + obj.info = readInfo(elem); + break; + case ID_TRACKS: + obj.tracks = readTracks(elem, trackLacingExpected); + break; + } + ensure(elem); + } + + if (metadataExpected && (obj.info == null || obj.tracks == null)) { + throw new RuntimeException( + "Cluster element found without Info and/or Tracks element at position " + + String.valueOf(ref.offset)); + } + + return obj; + } + + private WebMTrack[] readTracks(final Element ref, final int lacingExpected) throws IOException { + ArrayList trackEntries = new ArrayList<>(2); + Element elemTrackEntry; + + while ((elemTrackEntry = untilElement(ref, ID_TRACK_ENTRY)) != null) { + WebMTrack entry = new WebMTrack(); + boolean drop = false; + Element elem; + while ((elem = untilElement(elemTrackEntry)) != null) { + switch (elem.type) { + case ID_TRACK_NUMBER: + entry.trackNumber = readNumber(elem); + break; + case ID_TRACK_TYPE: + entry.trackType = (int) readNumber(elem); + break; + case ID_CODEC_ID: + entry.codecId = readString(elem); + break; + case ID_CODEC_PRIVATE: + entry.codecPrivate = readBlob(elem); + break; + case ID_AUDIO: + case ID_VIDEO: + entry.bMetadata = readBlob(elem); + break; + case ID_DEFAULT_DURATION: + entry.defaultDuration = readNumber(elem); + break; + case ID_FLAG_LACING: + drop = readNumber(elem) != lacingExpected; + break; + case ID_CODEC_DELAY: + entry.codecDelay = readNumber(elem); + break; + case ID_SEEK_PRE_ROLL: + entry.seekPreRoll = readNumber(elem); + break; + default: + break; + } + ensure(elem); + } + if (!drop) { + trackEntries.add(entry); + } + ensure(elemTrackEntry); + } + + WebMTrack[] entries = new WebMTrack[trackEntries.size()]; + trackEntries.toArray(entries); + + for (WebMTrack entry : entries) { + switch (entry.trackType) { + case 1: + entry.kind = TrackKind.Video; + break; + case 2: + entry.kind = TrackKind.Audio; + break; + default: + entry.kind = TrackKind.Other; + break; + } + } + + return entries; + } + + private SimpleBlock readSimpleBlock(final Element ref) throws IOException { + SimpleBlock obj = new SimpleBlock(ref); + obj.trackNumber = readEncodedNumber(); + obj.relativeTimeCode = stream.readShort(); + obj.flags = (byte) stream.read(); + obj.dataSize = (int) ((ref.offset + ref.size) - stream.position()); + obj.createdFromBlock = ref.type == ID_BLOCK; + + // NOTE: lacing is not implemented, and will be mixed with the stream data + if (obj.dataSize < 0) { + throw new IOException(String.format( + "Unexpected SimpleBlock element size, missing %s bytes", -obj.dataSize)); + } + return obj; + } + + private Cluster readCluster(final Element ref) throws IOException { + Cluster obj = new Cluster(ref); + + Element elem = untilElement(ref, ID_TIMECODE); + if (elem == null) { + throw new NoSuchElementException("Cluster at " + String.valueOf(ref.offset) + + " without Timecode element"); + } + obj.timecode = readNumber(elem); + + return obj; + } + + class Element { + int type; + long offset; + long contentSize; + long size; + } + + public class Info { + public long timecodeScale; + public long duration; + } + + public class WebMTrack { + public long trackNumber; + protected int trackType; + public String codecId; + public byte[] codecPrivate; + public byte[] bMetadata; + public TrackKind kind; + public long defaultDuration = -1; + public long codecDelay = -1; + public long seekPreRoll = -1; + } + + public class Segment { + Segment(final Element ref) { + this.ref = ref; + this.firstClusterInSegment = true; + } + + public Info info; + WebMTrack[] tracks; + private Element currentCluster; + private final Element ref; + boolean firstClusterInSegment; + + public Cluster getNextCluster() throws IOException { + if (done) { + return null; + } + if (firstClusterInSegment && segment.currentCluster != null) { + firstClusterInSegment = false; + return readCluster(segment.currentCluster); + } + ensure(segment.currentCluster); + + Element elem = untilElement(segment.ref, ID_CLUSTER); + if (elem == null) { + return null; + } + + segment.currentCluster = elem; + + return readCluster(segment.currentCluster); + } + } + + public class SimpleBlock { + public InputStream data; + public boolean createdFromBlock; + + SimpleBlock(final Element ref) { + this.ref = ref; + } + + public long trackNumber; + public short relativeTimeCode; + public long absoluteTimeCodeNs; + public byte flags; + public int dataSize; + private final Element ref; + + public boolean isKeyframe() { + return (flags & 0x80) == 0x80; + } + } + + public class Cluster { + Element ref; + SimpleBlock currentSimpleBlock = null; + Element currentBlockGroup = null; + public long timecode; + + Cluster(final Element ref) { + this.ref = ref; + } + + boolean insideClusterBounds() { + return stream.position() >= (ref.offset + ref.size); + } + + public SimpleBlock getNextSimpleBlock() throws IOException { + if (insideClusterBounds()) { + return null; + } + + if (currentBlockGroup != null) { + ensure(currentBlockGroup); + currentBlockGroup = null; + currentSimpleBlock = null; + } else if (currentSimpleBlock != null) { + ensure(currentSimpleBlock.ref); + } + + while (!insideClusterBounds()) { + Element elem = untilElement(ref, ID_SIMPLE_BLOCK, ID_GROUP_BLOCK); + if (elem == null) { + return null; + } + + if (elem.type == ID_GROUP_BLOCK) { + currentBlockGroup = elem; + elem = untilElement(currentBlockGroup, ID_BLOCK); + + if (elem == null) { + ensure(currentBlockGroup); + currentBlockGroup = null; + continue; + } + } + + currentSimpleBlock = readSimpleBlock(elem); + if (currentSimpleBlock.trackNumber == tracks[selectedTrack].trackNumber) { + currentSimpleBlock.data = stream.getView((int) currentSimpleBlock.dataSize); + + // calculate the timestamp in nanoseconds + currentSimpleBlock.absoluteTimeCodeNs = currentSimpleBlock.relativeTimeCode + + this.timecode; + currentSimpleBlock.absoluteTimeCodeNs *= segment.info.timecodeScale; + + return currentSimpleBlock; + } + + ensure(elem); + } + return null; + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java b/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java index fa2cc43e2..02b22965d 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java @@ -1,720 +1,761 @@ -package org.schabi.newpipe.streams; - -import androidx.annotation.NonNull; - -import org.schabi.newpipe.streams.WebMReader.Cluster; -import org.schabi.newpipe.streams.WebMReader.Segment; -import org.schabi.newpipe.streams.WebMReader.SimpleBlock; -import org.schabi.newpipe.streams.WebMReader.WebMTrack; -import org.schabi.newpipe.streams.io.SharpStream; - -import java.io.Closeable; -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; - -/** - * @author kapodamy - */ -public class WebMWriter implements Closeable { - - private final static int BUFFER_SIZE = 8 * 1024; - private final static int DEFAULT_TIMECODE_SCALE = 1000000; - private final static int INTERV = 100;// 100ms on 1000000us timecode scale - private final static int DEFAULT_CUES_EACH_MS = 5000;// 100ms on 1000000us timecode scale - - private WebMReader.WebMTrack[] infoTracks; - private SharpStream[] sourceTracks; - - private WebMReader[] readers; - - private boolean done = false; - private boolean parsed = false; - - private long written = 0; - - private Segment[] readersSegment; - private Cluster[] readersCluster; - - private int[] predefinedDurations; - - private byte[] outBuffer; - - public WebMWriter(SharpStream... source) { - sourceTracks = source; - readers = new WebMReader[sourceTracks.length]; - infoTracks = new WebMTrack[sourceTracks.length]; - outBuffer = new byte[BUFFER_SIZE]; - } - - public WebMTrack[] getTracksFromSource(int sourceIndex) throws IllegalStateException { - if (done) { - throw new IllegalStateException("already done"); - } - if (!parsed) { - throw new IllegalStateException("All sources must be parsed first"); - } - - return readers[sourceIndex].getAvailableTracks(); - } - - public void parseSources() throws IOException, IllegalStateException { - if (done) { - throw new IllegalStateException("already done"); - } - if (parsed) { - throw new IllegalStateException("already parsed"); - } - - try { - for (int i = 0; i < readers.length; i++) { - readers[i] = new WebMReader(sourceTracks[i]); - readers[i].parse(); - } - - } finally { - parsed = true; - } - } - - public void selectTracks(int... trackIndex) throws IOException { - try { - readersSegment = new Segment[readers.length]; - readersCluster = new Cluster[readers.length]; - predefinedDurations = new int[readers.length]; - - for (int i = 0; i < readers.length; i++) { - infoTracks[i] = readers[i].selectTrack(trackIndex[i]); - predefinedDurations[i] = -1; - readersSegment[i] = readers[i].getNextSegment(); - } - } finally { - parsed = true; - } - } - - public boolean isDone() { - return done; - } - - public boolean isParsed() { - return parsed; - } - - @Override - public void close() { - done = true; - parsed = true; - - for (SharpStream src : sourceTracks) { - src.close(); - } - - sourceTracks = null; - readers = null; - infoTracks = null; - readersSegment = null; - readersCluster = null; - outBuffer = null; - } - - public void build(SharpStream out) throws IOException, RuntimeException { - if (!out.canRewind()) { - throw new IOException("The output stream must be allow seek"); - } - - makeEBML(out); - - long offsetSegmentSizeSet = written + 5; - long offsetInfoDurationSet = written + 94; - long offsetClusterSet = written + 58; - long offsetCuesSet = written + 75; - - ArrayList listBuffer = new ArrayList<>(4); - - /* segment */ - listBuffer.add(new byte[]{ - 0x18, 0x53, (byte) 0x80, 0x67, 0x01, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00// segment content size - }); - - long baseSegmentOffset = written + listBuffer.get(0).length; - - /* seek head */ - listBuffer.add(new byte[]{ - 0x11, 0x4d, (byte) 0x9b, 0x74, (byte) 0xbe, - 0x4d, (byte) 0xbb, (byte) 0x8b, - 0x53, (byte) 0xab, (byte) 0x84, 0x15, 0x49, (byte) 0xa9, 0x66, 0x53, - (byte) 0xac, (byte) 0x81, /*info offset*/ 0x43, - 0x4d, (byte) 0xbb, (byte) 0x8b, 0x53, (byte) 0xab, - (byte) 0x84, 0x16, 0x54, (byte) 0xae, 0x6b, 0x53, (byte) 0xac, (byte) 0x81, - /*tracks offset*/ 0x6a, - 0x4d, (byte) 0xbb, (byte) 0x8e, 0x53, (byte) 0xab, (byte) 0x84, 0x1f, - 0x43, (byte) 0xb6, 0x75, 0x53, (byte) 0xac, (byte) 0x84, /*cluster offset [2]*/ 0x00, 0x00, 0x00, 0x00, - 0x4d, (byte) 0xbb, (byte) 0x8e, 0x53, (byte) 0xab, (byte) 0x84, 0x1c, 0x53, - (byte) 0xbb, 0x6b, 0x53, (byte) 0xac, (byte) 0x84, /*cues offset [7]*/ 0x00, 0x00, 0x00, 0x00 - }); - - /* info */ - listBuffer.add(new byte[]{ - 0x15, 0x49, (byte) 0xa9, 0x66, (byte) 0xa2, 0x2a, (byte) 0xd7, (byte) 0xb1 - }); - listBuffer.add(encode(DEFAULT_TIMECODE_SCALE, true));// this value MUST NOT exceed 4 bytes - listBuffer.add(new byte[]{0x44, (byte) 0x89, (byte) 0x84, - 0x00, 0x00, 0x00, 0x00,// info.duration - - /* MuxingApp */ - 0x4d, (byte) 0x80, (byte) 0x87, 0x4E, - 0x65, 0x77, 0x50, 0x69, 0x70, 0x65, // "NewPipe" binary string - - /* WritingApp */ - 0x57, 0x41, (byte) 0x87, 0x4E, - 0x65, 0x77, 0x50, 0x69, 0x70, 0x65// "NewPipe" binary string - }); - - /* tracks */ - listBuffer.addAll(makeTracks()); - - for (byte[] buff : listBuffer) { - dump(buff, out); - } - - // reserve space for Cues element, but is a waste of space (actually is 64 KiB) - // TODO: better Cue maker - long cueReservedOffset = written; - dump(new byte[]{(byte) 0xec, 0x20, (byte) 0xff, (byte) 0xfb}, out); - int reserved = (1024 * 63) - 4; - while (reserved > 0) { - int write = Math.min(reserved, outBuffer.length); - out.write(outBuffer, 0, write); - reserved -= write; - written += write; - } - - // Select a track for the cue - int cuesForTrackId = selectTrackForCue(); - long nextCueTime = infoTracks[cuesForTrackId].trackType == 1 ? -1 : 0; - ArrayList keyFrames = new ArrayList<>(32); - - ArrayList clusterOffsets = new ArrayList<>(32); - ArrayList clusterSizes = new ArrayList<>(32); - - long duration = 0; - int durationFromTrackId = 0; - - byte[] bTimecode = makeTimecode(0); - - int firstClusterOffset = (int) written; - long currentClusterOffset = makeCluster(out, bTimecode, 0, clusterOffsets, clusterSizes); - - long baseTimecode = 0; - long limitTimecode = -1; - int limitTimecodeByTrackId = cuesForTrackId; - - int blockWritten = Integer.MAX_VALUE; - - int newClusterByTrackId = -1; - - while (blockWritten > 0) { - blockWritten = 0; - int i = 0; - while (i < readers.length) { - Block bloq = getNextBlockFrom(i); - if (bloq == null) { - i++; - continue; - } - - if (bloq.data == null) { - blockWritten = 1;// fake block - newClusterByTrackId = i; - i++; - continue; - } - - if (newClusterByTrackId == i) { - limitTimecodeByTrackId = i; - newClusterByTrackId = -1; - baseTimecode = bloq.absoluteTimecode; - limitTimecode = baseTimecode + INTERV; - bTimecode = makeTimecode(baseTimecode); - currentClusterOffset = makeCluster(out, bTimecode, currentClusterOffset, clusterOffsets, clusterSizes); - } - - if (cuesForTrackId == i) { - if ((nextCueTime > -1 && bloq.absoluteTimecode >= nextCueTime) || (nextCueTime < 0 && bloq.isKeyframe())) { - if (nextCueTime > -1) { - nextCueTime += DEFAULT_CUES_EACH_MS; - } - keyFrames.add( - new KeyFrame(baseSegmentOffset, currentClusterOffset - 8, written, bTimecode.length, bloq.absoluteTimecode) - ); - } - } - - writeBlock(out, bloq, baseTimecode); - blockWritten++; - - if (bloq.absoluteTimecode > duration) { - duration = bloq.absoluteTimecode; - durationFromTrackId = bloq.trackNumber; - } - - if (limitTimecode < 0) { - limitTimecode = bloq.absoluteTimecode + INTERV; - continue; - } - - if (bloq.absoluteTimecode >= limitTimecode) { - if (limitTimecodeByTrackId != i) { - limitTimecode += INTERV - (bloq.absoluteTimecode - limitTimecode); - } - i++; - } - } - } - - makeCluster(out, null, currentClusterOffset, null, clusterSizes); - - long segmentSize = written - offsetSegmentSizeSet - 7; - - /* ---- final step write offsets and sizes ---- */ - seekTo(out, offsetSegmentSizeSet); - writeLong(out, segmentSize); - - if (predefinedDurations[durationFromTrackId] > -1) { - duration += predefinedDurations[durationFromTrackId];// this value is full-filled in makeTrackEntry() method - } - seekTo(out, offsetInfoDurationSet); - writeFloat(out, duration); - - firstClusterOffset -= baseSegmentOffset; - seekTo(out, offsetClusterSet); - writeInt(out, firstClusterOffset); - - seekTo(out, cueReservedOffset); - - /* Cue */ - dump(new byte[]{0x1c, 0x53, (byte) 0xbb, 0x6b, 0x20, 0x00, 0x00}, out); - - for (KeyFrame keyFrame : keyFrames) { - for (byte[] buffer : makeCuePoint(cuesForTrackId, keyFrame)) { - dump(buffer, out); - if (written >= (cueReservedOffset + 65535 - 16)) { - throw new IOException("Too many Cues"); - } - } - } - short cueSize = (short) (written - cueReservedOffset - 7); - - /* EBML Void */ - ByteBuffer voidBuffer = ByteBuffer.allocate(4); - voidBuffer.putShort((short) 0xec20); - voidBuffer.putShort((short) (firstClusterOffset - written - 4)); - dump(voidBuffer.array(), out); - - seekTo(out, offsetCuesSet); - writeInt(out, (int) (cueReservedOffset - baseSegmentOffset)); - - seekTo(out, cueReservedOffset + 5); - writeShort(out, cueSize); - - for (int i = 0; i < clusterSizes.size(); i++) { - seekTo(out, clusterOffsets.get(i)); - byte[] buffer = ByteBuffer.allocate(4).putInt(clusterSizes.get(i) | 0x10000000).array(); - dump(buffer, out); - } - } - - private Block getNextBlockFrom(int internalTrackId) throws IOException { - if (readersSegment[internalTrackId] == null) { - readersSegment[internalTrackId] = readers[internalTrackId].getNextSegment(); - if (readersSegment[internalTrackId] == null) { - return null;// no more blocks in the selected track - } - } - - if (readersCluster[internalTrackId] == null) { - readersCluster[internalTrackId] = readersSegment[internalTrackId].getNextCluster(); - if (readersCluster[internalTrackId] == null) { - readersSegment[internalTrackId] = null; - return getNextBlockFrom(internalTrackId); - } - } - - SimpleBlock res = readersCluster[internalTrackId].getNextSimpleBlock(); - if (res == null) { - readersCluster[internalTrackId] = null; - return new Block();// fake block to indicate the end of the cluster - } - - Block bloq = new Block(); - bloq.data = res.data; - bloq.dataSize = (int) res.dataSize; - bloq.trackNumber = internalTrackId; - bloq.flags = res.flags; - bloq.absoluteTimecode = res.absoluteTimeCodeNs / DEFAULT_TIMECODE_SCALE; - - return bloq; - } - - private void seekTo(SharpStream stream, long offset) throws IOException { - if (stream.canSeek()) { - stream.seek(offset); - } else { - if (offset > written) { - stream.skip(offset - written); - } else { - stream.rewind(); - stream.skip(offset); - } - } - - written = offset; - } - - private void writeLong(SharpStream stream, long number) throws IOException { - byte[] buffer = ByteBuffer.allocate(DataReader.LONG_SIZE).putLong(number).array(); - stream.write(buffer, 1, buffer.length - 1); - written += buffer.length - 1; - } - - private void writeFloat(SharpStream stream, float number) throws IOException { - byte[] buffer = ByteBuffer.allocate(DataReader.FLOAT_SIZE).putFloat(number).array(); - dump(buffer, stream); - } - - private void writeShort(SharpStream stream, short number) throws IOException { - byte[] buffer = ByteBuffer.allocate(DataReader.SHORT_SIZE).putShort(number).array(); - dump(buffer, stream); - } - - private void writeInt(SharpStream stream, int number) throws IOException { - byte[] buffer = ByteBuffer.allocate(DataReader.INTEGER_SIZE).putInt(number).array(); - dump(buffer, stream); - } - - private void writeBlock(SharpStream stream, Block bloq, long clusterTimecode) throws IOException { - long relativeTimeCode = bloq.absoluteTimecode - clusterTimecode; - - if (relativeTimeCode < Short.MIN_VALUE || relativeTimeCode > Short.MAX_VALUE) { - throw new IndexOutOfBoundsException("SimpleBlock timecode overflow."); - } - - ArrayList listBuffer = new ArrayList<>(5); - listBuffer.add(new byte[]{(byte) 0xa3}); - listBuffer.add(null);// block size - listBuffer.add(encode(bloq.trackNumber + 1, false)); - listBuffer.add(ByteBuffer.allocate(DataReader.SHORT_SIZE).putShort((short) relativeTimeCode).array()); - listBuffer.add(new byte[]{bloq.flags}); - - int blockSize = bloq.dataSize; - for (int i = 2; i < listBuffer.size(); i++) { - blockSize += listBuffer.get(i).length; - } - listBuffer.set(1, encode(blockSize, false)); - - for (byte[] buff : listBuffer) { - dump(buff, stream); - } - - int read; - while ((read = bloq.data.read(outBuffer)) > 0) { - stream.write(outBuffer, 0, read); - written += read; - } - } - - private byte[] makeTimecode(long timecode) { - ByteBuffer buffer = ByteBuffer.allocate(9); - buffer.put((byte) 0xe7); - buffer.put(encode(timecode, true)); - - byte[] res = new byte[buffer.position()]; - System.arraycopy(buffer.array(), 0, res, 0, res.length); - - return res; - } - - private long makeCluster(SharpStream stream, byte[] bTimecode, long startOffset, ArrayList clusterOffsets, ArrayList clusterSizes) throws IOException { - if (startOffset > 0) { - clusterSizes.add((int) (written - startOffset));// size for last offset - } - - if (clusterOffsets != null) { - /* cluster */ - dump(new byte[]{0x1f, 0x43, (byte) 0xb6, 0x75}, stream); - clusterOffsets.add(written);// warning: max cluster size is 256 MiB - dump(new byte[]{0x10, 0x00, 0x00, 0x00}, stream); - - startOffset = written;// size for the this cluster - - dump(bTimecode, stream); - - return startOffset; - } - - return -1; - } - - private void makeEBML(SharpStream stream) throws IOException { - // deafult values - dump(new byte[]{ - 0x1A, 0x45, (byte) 0xDF, (byte) 0xA3, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x1F, 0x42, (byte) 0x86, (byte) 0x81, 0x01, - 0x42, (byte) 0xF7, (byte) 0x81, 0x01, 0x42, (byte) 0xF2, (byte) 0x81, 0x04, - 0x42, (byte) 0xF3, (byte) 0x81, 0x08, 0x42, (byte) 0x82, (byte) 0x84, 0x77, - 0x65, 0x62, 0x6D, 0x42, (byte) 0x87, (byte) 0x81, 0x02, - 0x42, (byte) 0x85, (byte) 0x81, 0x02 - }, stream); - } - - private ArrayList makeTracks() { - ArrayList buffer = new ArrayList<>(1); - buffer.add(new byte[]{0x16, 0x54, (byte) 0xae, 0x6b}); - buffer.add(null); - - for (int i = 0; i < infoTracks.length; i++) { - buffer.addAll(makeTrackEntry(i, infoTracks[i])); - } - - return lengthFor(buffer); - } - - private ArrayList makeTrackEntry(int internalTrackId, WebMTrack track) { - byte[] id = encode(internalTrackId + 1, true); - ArrayList buffer = new ArrayList<>(12); - - /* track */ - buffer.add(new byte[]{(byte) 0xae}); - buffer.add(null); - - /* track number */ - buffer.add(new byte[]{(byte) 0xd7}); - buffer.add(id); - - /* track uid */ - buffer.add(new byte[]{0x73, (byte) 0xc5}); - buffer.add(id); - - /* flag lacing */ - buffer.add(new byte[]{(byte) 0x9c, (byte) 0x81, 0x00}); - - /* lang */ - buffer.add(new byte[]{0x22, (byte) 0xb5, (byte) 0x9c, (byte) 0x83, 0x75, 0x6e, 0x64}); - - /* codec id */ - buffer.add(new byte[]{(byte) 0x86}); - buffer.addAll(encode(track.codecId)); - - /* type */ - buffer.add(new byte[]{(byte) 0x83}); - buffer.add(encode(track.trackType, true)); - - /* default duration */ - if (track.defaultDuration != 0) { - predefinedDurations[internalTrackId] = (int) Math.ceil(track.defaultDuration / (float) DEFAULT_TIMECODE_SCALE); - buffer.add(new byte[]{0x23, (byte) 0xe3, (byte) 0x83}); - buffer.add(encode(track.defaultDuration, true)); - } - - /* audio/video */ - if ((track.trackType == 1 || track.trackType == 2) && valid(track.bMetadata)) { - buffer.add(new byte[]{(byte) (track.trackType == 1 ? 0xe0 : 0xe1)}); - buffer.add(encode(track.bMetadata.length, false)); - buffer.add(track.bMetadata); - } - - /* codec private*/ - if (valid(track.codecPrivate)) { - buffer.add(new byte[]{0x63, (byte) 0xa2}); - buffer.add(encode(track.codecPrivate.length, false)); - buffer.add(track.codecPrivate); - } - - return lengthFor(buffer); - - } - - private ArrayList makeCuePoint(int internalTrackId, KeyFrame keyFrame) { - ArrayList buffer = new ArrayList<>(5); - - /* CuePoint */ - buffer.add(new byte[]{(byte) 0xbb}); - buffer.add(null); - - /* CueTime */ - buffer.add(new byte[]{(byte) 0xb3}); - buffer.add(encode(keyFrame.atTimecode, true)); - - /* CueTrackPosition */ - buffer.addAll(makeCueTrackPosition(internalTrackId, keyFrame)); - - return lengthFor(buffer); - } - - private ArrayList makeCueTrackPosition(int internalTrackId, KeyFrame keyFrame) { - ArrayList buffer = new ArrayList<>(8); - - /* CueTrackPositions */ - buffer.add(new byte[]{(byte) 0xb7}); - buffer.add(null); - - /* CueTrack */ - buffer.add(new byte[]{(byte) 0xf7}); - buffer.add(encode(internalTrackId + 1, true)); - - /* CueClusterPosition */ - buffer.add(new byte[]{(byte) 0xf1}); - buffer.add(encode(keyFrame.atCluster, true)); - - /* CueRelativePosition */ - if (keyFrame.atBlock > 0) { - buffer.add(new byte[]{(byte) 0xf0}); - buffer.add(encode(keyFrame.atBlock, true)); - } - - return lengthFor(buffer); - } - - private void dump(byte[] buffer, SharpStream stream) throws IOException { - stream.write(buffer); - written += buffer.length; - } - - private ArrayList lengthFor(ArrayList buffer) { - long size = 0; - for (int i = 2; i < buffer.size(); i++) { - size += buffer.get(i).length; - } - buffer.set(1, encode(size, false)); - return buffer; - } - - private byte[] encode(long number, boolean withLength) { - int length = -1; - for (int i = 1; i <= 7; i++) { - if (number < Math.pow(2, 7 * i)) { - length = i; - break; - } - } - - if (length < 1) { - throw new ArithmeticException("Can't encode a number of bigger than 7 bytes"); - } - - if (number == (Math.pow(2, 7 * length)) - 1) { - length++; - } - - int offset = withLength ? 1 : 0; - byte[] buffer = new byte[offset + length]; - long marker = (long) Math.floor((length - 1.0f) / 8.0f); - - float mul = 1; - for (int i = length - 1; i >= 0; i--, mul *= 0x100) { - long b = (long) Math.floor(number / mul); - if (!withLength && i == marker) { - b = b | (0x80 >> (length - 1)); - } - buffer[offset + i] = (byte) b; - } - - if (withLength) { - buffer[0] = (byte) (0x80 | length); - } - - return buffer; - } - - private ArrayList encode(String value) { - byte[] str; - str = value.getBytes(StandardCharsets.UTF_8);// or use "utf-8" - - ArrayList buffer = new ArrayList<>(2); - buffer.add(encode(str.length, false)); - buffer.add(str); - - return buffer; - } - - private boolean valid(byte[] buffer) { - return buffer != null && buffer.length > 0; - } - - private int selectTrackForCue() { - int i = 0; - int videoTracks = 0; - int audioTracks = 0; - - for (; i < infoTracks.length; i++) { - switch (infoTracks[i].trackType) { - case 1: - videoTracks++; - break; - case 2: - audioTracks++; - break; - } - } - - int kind; - if (audioTracks == infoTracks.length) { - kind = 2; - } else if (videoTracks == infoTracks.length) { - kind = 1; - } else if (videoTracks > 0) { - kind = 1; - } else if (audioTracks > 0) { - kind = 2; - } else { - return 0; - } - - // TODO: in the adove code, find and select the shortest track for the desired kind - for (i = 0; i < infoTracks.length; i++) { - if (kind == infoTracks[i].trackType) { - return i; - } - } - - return 0; - } - - class KeyFrame { - - KeyFrame(long segment, long cluster, long block, int bTimecodeLength, long timecode) { - atCluster = cluster - segment; - if ((block - bTimecodeLength) > cluster) { - atBlock = (int) (block - cluster); - } - atTimecode = timecode; - } - - long atCluster; - int atBlock; - long atTimecode; - } - - class Block { - - InputStream data; - int trackNumber; - byte flags; - int dataSize; - long absoluteTimecode; - - boolean isKeyframe() { - return (flags & 0x80) == 0x80; - } - - @NonNull - @Override - public String toString() { - return String.format("trackNumber=%s isKeyFrame=%S absoluteTimecode=%s", trackNumber, isKeyframe(), absoluteTimecode); - } - } -} +package org.schabi.newpipe.streams; + +import androidx.annotation.NonNull; + +import org.schabi.newpipe.streams.WebMReader.Cluster; +import org.schabi.newpipe.streams.WebMReader.Segment; +import org.schabi.newpipe.streams.WebMReader.SimpleBlock; +import org.schabi.newpipe.streams.WebMReader.WebMTrack; +import org.schabi.newpipe.streams.io.SharpStream; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; + +/** + * @author kapodamy + */ +public class WebMWriter implements Closeable { + private static final int BUFFER_SIZE = 8 * 1024; + private static final int DEFAULT_TIMECODE_SCALE = 1000000; + private static final int INTERV = 100; // 100ms on 1000000us timecode scale + private static final int DEFAULT_CUES_EACH_MS = 5000; // 5000ms on 1000000us timecode scale + private static final byte CLUSTER_HEADER_SIZE = 8; + private static final int CUE_RESERVE_SIZE = 65535; + private static final byte MINIMUM_EBML_VOID_SIZE = 4; + + private WebMReader.WebMTrack[] infoTracks; + private SharpStream[] sourceTracks; + + private WebMReader[] readers; + + private boolean done = false; + private boolean parsed = false; + + private long written = 0; + + private Segment[] readersSegment; + private Cluster[] readersCluster; + + private ArrayList clustersOffsetsSizes; + + private byte[] outBuffer; + private ByteBuffer outByteBuffer; + + public WebMWriter(final SharpStream... source) { + sourceTracks = source; + readers = new WebMReader[sourceTracks.length]; + infoTracks = new WebMTrack[sourceTracks.length]; + outBuffer = new byte[BUFFER_SIZE]; + outByteBuffer = ByteBuffer.wrap(outBuffer); + clustersOffsetsSizes = new ArrayList<>(256); + } + + public WebMTrack[] getTracksFromSource(final int sourceIndex) throws IllegalStateException { + if (done) { + throw new IllegalStateException("already done"); + } + if (!parsed) { + throw new IllegalStateException("All sources must be parsed first"); + } + + return readers[sourceIndex].getAvailableTracks(); + } + + public void parseSources() throws IOException, IllegalStateException { + if (done) { + throw new IllegalStateException("already done"); + } + if (parsed) { + throw new IllegalStateException("already parsed"); + } + + try { + for (int i = 0; i < readers.length; i++) { + readers[i] = new WebMReader(sourceTracks[i]); + readers[i].parse(); + } + + } finally { + parsed = true; + } + } + + public void selectTracks(final int... trackIndex) throws IOException { + try { + readersSegment = new Segment[readers.length]; + readersCluster = new Cluster[readers.length]; + + for (int i = 0; i < readers.length; i++) { + infoTracks[i] = readers[i].selectTrack(trackIndex[i]); + readersSegment[i] = readers[i].getNextSegment(); + } + } finally { + parsed = true; + } + } + + public boolean isDone() { + return done; + } + + @Override + public void close() { + done = true; + parsed = true; + + for (SharpStream src : sourceTracks) { + src.close(); + } + + sourceTracks = null; + readers = null; + infoTracks = null; + readersSegment = null; + readersCluster = null; + outBuffer = null; + outByteBuffer = null; + clustersOffsetsSizes = null; + } + + public void build(final SharpStream out) throws IOException, RuntimeException { + if (!out.canRewind()) { + throw new IOException("The output stream must be allow seek"); + } + + makeEBML(out); + + long offsetSegmentSizeSet = written + 5; + long offsetInfoDurationSet = written + 94; + long offsetClusterSet = written + 58; + long offsetCuesSet = written + 75; + + ArrayList listBuffer = new ArrayList<>(4); + + /* segment */ + listBuffer.add(new byte[]{ + 0x18, 0x53, (byte) 0x80, 0x67, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00// segment content size + }); + + long segmentOffset = written + listBuffer.get(0).length; + + /* seek head */ + listBuffer.add(new byte[]{ + 0x11, 0x4d, (byte) 0x9b, 0x74, (byte) 0xbe, + 0x4d, (byte) 0xbb, (byte) 0x8b, + 0x53, (byte) 0xab, (byte) 0x84, 0x15, 0x49, (byte) 0xa9, 0x66, 0x53, + (byte) 0xac, (byte) 0x81, + /*info offset*/ 0x43, + 0x4d, (byte) 0xbb, (byte) 0x8b, 0x53, (byte) 0xab, + (byte) 0x84, 0x16, 0x54, (byte) 0xae, 0x6b, 0x53, (byte) 0xac, (byte) 0x81, + /*tracks offset*/ 0x56, + 0x4d, (byte) 0xbb, (byte) 0x8e, 0x53, (byte) 0xab, (byte) 0x84, 0x1f, + 0x43, (byte) 0xb6, 0x75, 0x53, (byte) 0xac, (byte) 0x84, + /*cluster offset [2]*/ 0x00, 0x00, 0x00, 0x00, + 0x4d, (byte) 0xbb, (byte) 0x8e, 0x53, (byte) 0xab, (byte) 0x84, 0x1c, 0x53, + (byte) 0xbb, 0x6b, 0x53, (byte) 0xac, (byte) 0x84, + /*cues offset [7]*/ 0x00, 0x00, 0x00, 0x00 + }); + + /* info */ + listBuffer.add(new byte[]{ + 0x15, 0x49, (byte) 0xa9, 0x66, (byte) 0x8e, 0x2a, (byte) 0xd7, (byte) 0xb1 + }); + // the segment duration MUST NOT exceed 4 bytes + listBuffer.add(encode(DEFAULT_TIMECODE_SCALE, true)); + listBuffer.add(new byte[]{0x44, (byte) 0x89, (byte) 0x84, + 0x00, 0x00, 0x00, 0x00, // info.duration + }); + + /* tracks */ + listBuffer.addAll(makeTracks()); + + dump(listBuffer, out); + + // reserve space for Cues element + long cueOffset = written; + makeEbmlVoid(out, CUE_RESERVE_SIZE, true); + + int[] defaultSampleDuration = new int[infoTracks.length]; + long[] duration = new long[infoTracks.length]; + + for (int i = 0; i < infoTracks.length; i++) { + if (infoTracks[i].defaultDuration < 0) { + defaultSampleDuration[i] = -1; // not available + } else { + defaultSampleDuration[i] = (int) Math.ceil(infoTracks[i].defaultDuration + / (float) DEFAULT_TIMECODE_SCALE); + } + duration[i] = -1; + } + + // Select a track for the cue + int cuesForTrackId = selectTrackForCue(); + long nextCueTime = infoTracks[cuesForTrackId].trackType == 1 ? -1 : 0; + ArrayList keyFrames = new ArrayList<>(32); + + int firstClusterOffset = (int) written; + long currentClusterOffset = makeCluster(out, 0, 0, true); + + long baseTimecode = 0; + long limitTimecode = -1; + int limitTimecodeByTrackId = cuesForTrackId; + + int blockWritten = Integer.MAX_VALUE; + + int newClusterByTrackId = -1; + + while (blockWritten > 0) { + blockWritten = 0; + int i = 0; + while (i < readers.length) { + Block bloq = getNextBlockFrom(i); + if (bloq == null) { + i++; + continue; + } + + if (bloq.data == null) { + blockWritten = 1; // fake block + newClusterByTrackId = i; + i++; + continue; + } + + if (newClusterByTrackId == i) { + limitTimecodeByTrackId = i; + newClusterByTrackId = -1; + baseTimecode = bloq.absoluteTimecode; + limitTimecode = baseTimecode + INTERV; + currentClusterOffset = makeCluster(out, baseTimecode, currentClusterOffset, + true); + } + + if (cuesForTrackId == i) { + if ((nextCueTime > -1 && bloq.absoluteTimecode >= nextCueTime) + || (nextCueTime < 0 && bloq.isKeyframe())) { + if (nextCueTime > -1) { + nextCueTime += DEFAULT_CUES_EACH_MS; + } + keyFrames.add(new KeyFrame(segmentOffset, currentClusterOffset, written, + bloq.absoluteTimecode)); + } + } + + writeBlock(out, bloq, baseTimecode); + blockWritten++; + + if (defaultSampleDuration[i] < 0 && duration[i] >= 0) { + // if the sample duration in unknown, + // calculate using current_duration - previous_duration + defaultSampleDuration[i] = (int) (bloq.absoluteTimecode - duration[i]); + } + duration[i] = bloq.absoluteTimecode; + + if (limitTimecode < 0) { + limitTimecode = bloq.absoluteTimecode + INTERV; + continue; + } + + if (bloq.absoluteTimecode >= limitTimecode) { + if (limitTimecodeByTrackId != i) { + limitTimecode += INTERV - (bloq.absoluteTimecode - limitTimecode); + } + i++; + } + } + } + + makeCluster(out, -1, currentClusterOffset, false); + + long segmentSize = written - offsetSegmentSizeSet - 7; + + /* Segment size */ + seekTo(out, offsetSegmentSizeSet); + outByteBuffer.putLong(0, segmentSize); + out.write(outBuffer, 1, DataReader.LONG_SIZE - 1); + + /* Segment duration */ + long longestDuration = 0; + for (int i = 0; i < duration.length; i++) { + if (defaultSampleDuration[i] > 0) { + duration[i] += defaultSampleDuration[i]; + } + if (duration[i] > longestDuration) { + longestDuration = duration[i]; + } + } + seekTo(out, offsetInfoDurationSet); + outByteBuffer.putFloat(0, longestDuration); + dump(outBuffer, DataReader.FLOAT_SIZE, out); + + /* first Cluster offset */ + firstClusterOffset -= segmentOffset; + writeInt(out, offsetClusterSet, firstClusterOffset); + + seekTo(out, cueOffset); + + /* Cue */ + short cueSize = 0; + dump(new byte[]{0x1c, 0x53, (byte) 0xbb, 0x6b, 0x20, 0x00, 0x00}, out); // header size is 7 + + for (KeyFrame keyFrame : keyFrames) { + int size = makeCuePoint(cuesForTrackId, keyFrame, outBuffer); + + if ((cueSize + size + 7 + MINIMUM_EBML_VOID_SIZE) > CUE_RESERVE_SIZE) { + break; // no space left + } + + cueSize += size; + dump(outBuffer, size, out); + } + + makeEbmlVoid(out, CUE_RESERVE_SIZE - cueSize - 7, false); + + seekTo(out, cueOffset + 5); + outByteBuffer.putShort(0, cueSize); + dump(outBuffer, DataReader.SHORT_SIZE, out); + + /* seek head, seek for cues element */ + writeInt(out, offsetCuesSet, (int) (cueOffset - segmentOffset)); + + for (ClusterInfo cluster : clustersOffsetsSizes) { + writeInt(out, cluster.offset, cluster.size | 0x10000000); + } + } + + private Block getNextBlockFrom(final int internalTrackId) throws IOException { + if (readersSegment[internalTrackId] == null) { + readersSegment[internalTrackId] = readers[internalTrackId].getNextSegment(); + if (readersSegment[internalTrackId] == null) { + return null; // no more blocks in the selected track + } + } + + if (readersCluster[internalTrackId] == null) { + readersCluster[internalTrackId] = readersSegment[internalTrackId].getNextCluster(); + if (readersCluster[internalTrackId] == null) { + readersSegment[internalTrackId] = null; + return getNextBlockFrom(internalTrackId); + } + } + + SimpleBlock res = readersCluster[internalTrackId].getNextSimpleBlock(); + if (res == null) { + readersCluster[internalTrackId] = null; + return new Block(); // fake block to indicate the end of the cluster + } + + Block bloq = new Block(); + bloq.data = res.data; + bloq.dataSize = res.dataSize; + bloq.trackNumber = internalTrackId; + bloq.flags = res.flags; + bloq.absoluteTimecode = res.absoluteTimeCodeNs / DEFAULT_TIMECODE_SCALE; + + return bloq; + } + + private void seekTo(final SharpStream stream, final long offset) throws IOException { + if (stream.canSeek()) { + stream.seek(offset); + } else { + if (offset > written) { + stream.skip(offset - written); + } else { + stream.rewind(); + stream.skip(offset); + } + } + + written = offset; + } + + private void writeInt(final SharpStream stream, final long offset, final int number) + throws IOException { + seekTo(stream, offset); + outByteBuffer.putInt(0, number); + dump(outBuffer, DataReader.INTEGER_SIZE, stream); + } + + private void writeBlock(final SharpStream stream, final Block bloq, final long clusterTimecode) + throws IOException { + long relativeTimeCode = bloq.absoluteTimecode - clusterTimecode; + + if (relativeTimeCode < Short.MIN_VALUE || relativeTimeCode > Short.MAX_VALUE) { + throw new IndexOutOfBoundsException("SimpleBlock timecode overflow."); + } + + ArrayList listBuffer = new ArrayList<>(5); + listBuffer.add(new byte[]{(byte) 0xa3}); + listBuffer.add(null); // block size + listBuffer.add(encode(bloq.trackNumber + 1, false)); + listBuffer.add(ByteBuffer.allocate(DataReader.SHORT_SIZE).putShort((short) relativeTimeCode) + .array()); + listBuffer.add(new byte[]{bloq.flags}); + + int blockSize = bloq.dataSize; + for (int i = 2; i < listBuffer.size(); i++) { + blockSize += listBuffer.get(i).length; + } + listBuffer.set(1, encode(blockSize, false)); + + dump(listBuffer, stream); + + int read; + while ((read = bloq.data.read(outBuffer)) > 0) { + dump(outBuffer, read, stream); + } + } + + private long makeCluster(final SharpStream stream, final long timecode, final long offsetStart, + final boolean create) throws IOException { + ClusterInfo cluster; + long offset = offsetStart; + + if (offset > 0) { + // save the size of the previous cluster (maximum 256 MiB) + cluster = clustersOffsetsSizes.get(clustersOffsetsSizes.size() - 1); + cluster.size = (int) (written - offset - CLUSTER_HEADER_SIZE); + } + + offset = written; + + if (create) { + /* cluster */ + dump(new byte[]{0x1f, 0x43, (byte) 0xb6, 0x75}, stream); + + cluster = new ClusterInfo(); + cluster.offset = written; + clustersOffsetsSizes.add(cluster); + + dump(new byte[]{ + 0x10, 0x00, 0x00, 0x00, + /* timestamp */ + (byte) 0xe7 + }, stream); + + dump(encode(timecode, true), stream); + } + + return offset; + } + + private void makeEBML(final SharpStream stream) throws IOException { + // default values + dump(new byte[]{ + 0x1A, 0x45, (byte) 0xDF, (byte) 0xA3, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x1F, 0x42, (byte) 0x86, (byte) 0x81, 0x01, + 0x42, (byte) 0xF7, (byte) 0x81, 0x01, 0x42, (byte) 0xF2, (byte) 0x81, 0x04, + 0x42, (byte) 0xF3, (byte) 0x81, 0x08, 0x42, (byte) 0x82, (byte) 0x84, 0x77, + 0x65, 0x62, 0x6D, 0x42, (byte) 0x87, (byte) 0x81, 0x02, + 0x42, (byte) 0x85, (byte) 0x81, 0x02 + }, stream); + } + + private ArrayList makeTracks() { + ArrayList buffer = new ArrayList<>(1); + buffer.add(new byte[]{0x16, 0x54, (byte) 0xae, 0x6b}); + buffer.add(null); + + for (int i = 0; i < infoTracks.length; i++) { + buffer.addAll(makeTrackEntry(i, infoTracks[i])); + } + + return lengthFor(buffer); + } + + private ArrayList makeTrackEntry(final int internalTrackId, final WebMTrack track) { + byte[] id = encode(internalTrackId + 1, true); + ArrayList buffer = new ArrayList<>(12); + + /* track */ + buffer.add(new byte[]{(byte) 0xae}); + buffer.add(null); + + /* track number */ + buffer.add(new byte[]{(byte) 0xd7}); + buffer.add(id); + + /* track uid */ + buffer.add(new byte[]{0x73, (byte) 0xc5}); + buffer.add(id); + + /* flag lacing */ + buffer.add(new byte[]{(byte) 0x9c, (byte) 0x81, 0x00}); + + /* lang */ + buffer.add(new byte[]{0x22, (byte) 0xb5, (byte) 0x9c, (byte) 0x83, 0x75, 0x6e, 0x64}); + + /* codec id */ + buffer.add(new byte[]{(byte) 0x86}); + buffer.addAll(encode(track.codecId)); + + /* codec delay*/ + if (track.codecDelay >= 0) { + buffer.add(new byte[]{0x56, (byte) 0xAA}); + buffer.add(encode(track.codecDelay, true)); + } + + /* codec seek pre-roll*/ + if (track.seekPreRoll >= 0) { + buffer.add(new byte[]{0x56, (byte) 0xBB}); + buffer.add(encode(track.seekPreRoll, true)); + } + + /* type */ + buffer.add(new byte[]{(byte) 0x83}); + buffer.add(encode(track.trackType, true)); + + /* default duration */ + if (track.defaultDuration >= 0) { + buffer.add(new byte[]{0x23, (byte) 0xe3, (byte) 0x83}); + buffer.add(encode(track.defaultDuration, true)); + } + + /* audio/video */ + if ((track.trackType == 1 || track.trackType == 2) && valid(track.bMetadata)) { + buffer.add(new byte[]{(byte) (track.trackType == 1 ? 0xe0 : 0xe1)}); + buffer.add(encode(track.bMetadata.length, false)); + buffer.add(track.bMetadata); + } + + /* codec private*/ + if (valid(track.codecPrivate)) { + buffer.add(new byte[]{0x63, (byte) 0xa2}); + buffer.add(encode(track.codecPrivate.length, false)); + buffer.add(track.codecPrivate); + } + + return lengthFor(buffer); + } + + private int makeCuePoint(final int internalTrackId, final KeyFrame keyFrame, + final byte[] buffer) { + ArrayList cue = new ArrayList<>(5); + + /* CuePoint */ + cue.add(new byte[]{(byte) 0xbb}); + cue.add(null); + + /* CueTime */ + cue.add(new byte[]{(byte) 0xb3}); + cue.add(encode(keyFrame.duration, true)); + + /* CueTrackPosition */ + cue.addAll(makeCueTrackPosition(internalTrackId, keyFrame)); + + int size = 0; + lengthFor(cue); + + for (byte[] buff : cue) { + System.arraycopy(buff, 0, buffer, size, buff.length); + size += buff.length; + } + + return size; + } + + private ArrayList makeCueTrackPosition(final int internalTrackId, + final KeyFrame keyFrame) { + ArrayList buffer = new ArrayList<>(8); + + /* CueTrackPositions */ + buffer.add(new byte[]{(byte) 0xb7}); + buffer.add(null); + + /* CueTrack */ + buffer.add(new byte[]{(byte) 0xf7}); + buffer.add(encode(internalTrackId + 1, true)); + + /* CueClusterPosition */ + buffer.add(new byte[]{(byte) 0xf1}); + buffer.add(encode(keyFrame.clusterPosition, true)); + + /* CueRelativePosition */ + if (keyFrame.relativePosition > 0) { + buffer.add(new byte[]{(byte) 0xf0}); + buffer.add(encode(keyFrame.relativePosition, true)); + } + + return lengthFor(buffer); + } + + private void makeEbmlVoid(final SharpStream out, final int amount, final boolean wipe) + throws IOException { + int size = amount; + + /* ebml void */ + outByteBuffer.putShort(0, (short) 0xec20); + outByteBuffer.putShort(2, (short) (size - 4)); + + dump(outBuffer, 4, out); + + if (wipe) { + size -= 4; + while (size > 0) { + int write = Math.min(size, outBuffer.length); + dump(outBuffer, write, out); + size -= write; + } + } + } + + private void dump(final byte[] buffer, final SharpStream stream) throws IOException { + dump(buffer, buffer.length, stream); + } + + private void dump(final byte[] buffer, final int count, final SharpStream stream) + throws IOException { + stream.write(buffer, 0, count); + written += count; + } + + private void dump(final ArrayList buffers, final SharpStream stream) + throws IOException { + for (byte[] buffer : buffers) { + stream.write(buffer); + written += buffer.length; + } + } + + private ArrayList lengthFor(final ArrayList buffer) { + long size = 0; + for (int i = 2; i < buffer.size(); i++) { + size += buffer.get(i).length; + } + buffer.set(1, encode(size, false)); + return buffer; + } + + private byte[] encode(final long number, final boolean withLength) { + int length = -1; + for (int i = 1; i <= 7; i++) { + if (number < Math.pow(2, 7 * i)) { + length = i; + break; + } + } + + if (length < 1) { + throw new ArithmeticException("Can't encode a number of bigger than 7 bytes"); + } + + if (number == (Math.pow(2, 7 * length)) - 1) { + length++; + } + + int offset = withLength ? 1 : 0; + byte[] buffer = new byte[offset + length]; + long marker = (long) Math.floor((length - 1f) / 8f); + + int shift = 0; + for (int i = length - 1; i >= 0; i--, shift += 8) { + long b = number >>> shift; + if (!withLength && i == marker) { + b = b | (0x80 >>> (length - 1)); + } + buffer[offset + i] = (byte) b; + } + + if (withLength) { + buffer[0] = (byte) (0x80 | length); + } + + return buffer; + } + + private ArrayList encode(final String value) { + byte[] str; + str = value.getBytes(StandardCharsets.UTF_8); // or use "utf-8" + + ArrayList buffer = new ArrayList<>(2); + buffer.add(encode(str.length, false)); + buffer.add(str); + + return buffer; + } + + private boolean valid(final byte[] buffer) { + return buffer != null && buffer.length > 0; + } + + private int selectTrackForCue() { + int i = 0; + int videoTracks = 0; + int audioTracks = 0; + + for (; i < infoTracks.length; i++) { + switch (infoTracks[i].trackType) { + case 1: + videoTracks++; + break; + case 2: + audioTracks++; + break; + } + } + + int kind; + if (audioTracks == infoTracks.length) { + kind = 2; + } else if (videoTracks == infoTracks.length) { + kind = 1; + } else if (videoTracks > 0) { + kind = 1; + } else if (audioTracks > 0) { + kind = 2; + } else { + return 0; + } + + // TODO: in the adove code, find and select the shortest track for the desired kind + for (i = 0; i < infoTracks.length; i++) { + if (kind == infoTracks[i].trackType) { + return i; + } + } + + return 0; + } + + static class KeyFrame { + KeyFrame(final long segment, final long cluster, final long block, final long timecode) { + clusterPosition = cluster - segment; + relativePosition = (int) (block - cluster - CLUSTER_HEADER_SIZE); + duration = timecode; + } + + final long clusterPosition; + final int relativePosition; + final long duration; + } + + static class Block { + InputStream data; + int trackNumber; + byte flags; + int dataSize; + long absoluteTimecode; + + boolean isKeyframe() { + return (flags & 0x80) == 0x80; + } + + @NonNull + @Override + public String toString() { + return String.format("trackNumber=%s isKeyFrame=%S absoluteTimecode=%s", trackNumber, + isKeyframe(), absoluteTimecode); + } + } + + static class ClusterInfo { + long offset; + int size; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java b/app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java index 5950ba3dd..46ec68d9e 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java +++ b/app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java @@ -1,63 +1,62 @@ -package org.schabi.newpipe.streams.io; - -import java.io.Closeable; -import java.io.IOException; - -/** - * based on c# - */ -public abstract class SharpStream implements Closeable { - - 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 long available(); - - public abstract void rewind() throws IOException; - - public abstract boolean isClosed(); - - @Override - public abstract void close(); - - public abstract boolean canRewind(); - - public abstract boolean canRead(); - - public abstract boolean canWrite(); - - public boolean canSetLength() { - return false; - } - - public boolean canSeek() { - return false; - } - - 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 void flush() throws IOException { - // STUB - } - - public void setLength(long length) throws IOException { - throw new IOException("Not implemented"); - } - - public void seek(long offset) throws IOException { - throw new IOException("Not implemented"); - } - - public long length() throws IOException { - throw new UnsupportedOperationException("Unsupported operation"); - } -} +package org.schabi.newpipe.streams.io; + +import java.io.Closeable; +import java.io.IOException; + +/** + * Based on C#'s Stream class. + */ +public abstract class SharpStream implements Closeable { + 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 long available(); + + public abstract void rewind() throws IOException; + + public abstract boolean isClosed(); + + @Override + public abstract void close(); + + public abstract boolean canRewind(); + + public abstract boolean canRead(); + + public abstract boolean canWrite(); + + public boolean canSetLength() { + return false; + } + + public boolean canSeek() { + return false; + } + + 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 void flush() throws IOException { + // STUB + } + + public void setLength(final long length) throws IOException { + throw new IOException("Not implemented"); + } + + public void seek(final long offset) throws IOException { + throw new IOException("Not implemented"); + } + + public long length() throws IOException { + throw new UnsupportedOperationException("Unsupported operation"); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/util/AndroidTvUtils.java b/app/src/main/java/org/schabi/newpipe/util/AndroidTvUtils.java new file mode 100644 index 000000000..db2ab4aa7 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/AndroidTvUtils.java @@ -0,0 +1,66 @@ +package org.schabi.newpipe.util; + +import android.app.UiModeManager; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.os.BatteryManager; +import android.os.Build; +import android.view.KeyEvent; + +import org.schabi.newpipe.App; + +import static android.content.Context.BATTERY_SERVICE; +import static android.content.Context.UI_MODE_SERVICE; + +public final class AndroidTvUtils { + + private static final String AMAZON_FEATURE_FIRE_TV = "amazon.hardware.fire_tv"; + private static Boolean isTV = null; + + private AndroidTvUtils() { + } + + public static boolean isTv(final Context context) { + if (AndroidTvUtils.isTV != null) { + return AndroidTvUtils.isTV; + } + + PackageManager pm = App.getApp().getPackageManager(); + + // from doc: https://developer.android.com/training/tv/start/hardware.html#runtime-check + boolean isTv = ((UiModeManager) context.getSystemService(UI_MODE_SERVICE)) + .getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION + || pm.hasSystemFeature(AMAZON_FEATURE_FIRE_TV) + || pm.hasSystemFeature(PackageManager.FEATURE_TELEVISION); + + // from https://stackoverflow.com/a/58932366 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + boolean isBatteryAbsent = ((BatteryManager) context.getSystemService(BATTERY_SERVICE)) + .getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY) == 0; + isTv = isTv || (isBatteryAbsent + && !pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN) + && pm.hasSystemFeature(PackageManager.FEATURE_USB_HOST) + && pm.hasSystemFeature(PackageManager.FEATURE_ETHERNET)); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + isTv = isTv || pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK); + } + + AndroidTvUtils.isTV = isTv; + return AndroidTvUtils.isTV; + } + + public static boolean isConfirmKey(final int keyCode) { + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_ENTER: + case KeyEvent.KEYCODE_SPACE: + case KeyEvent.KEYCODE_NUMPAD_ENTER: + return true; + default: + return false; + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/util/AnimationUtils.java b/app/src/main/java/org/schabi/newpipe/util/AnimationUtils.java index e47e14483..4fa14ed01 100644 --- a/app/src/main/java/org/schabi/newpipe/util/AnimationUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/AnimationUtils.java @@ -24,48 +24,51 @@ import android.animation.AnimatorListenerAdapter; import android.animation.ArgbEvaluator; import android.animation.ValueAnimator; import android.content.res.ColorStateList; -import androidx.annotation.ColorInt; -import androidx.annotation.FloatRange; -import androidx.core.view.ViewCompat; -import androidx.interpolator.view.animation.FastOutSlowInInterpolator; import android.util.Log; import android.view.View; import android.widget.TextView; +import androidx.annotation.ColorInt; +import androidx.annotation.FloatRange; +import androidx.core.view.ViewCompat; +import androidx.interpolator.view.animation.FastOutSlowInInterpolator; + import org.schabi.newpipe.MainActivity; -public class AnimationUtils { +public final class AnimationUtils { private static final String TAG = "AnimationUtils"; private static final boolean DEBUG = MainActivity.DEBUG; - public enum Type { - ALPHA, - SCALE_AND_ALPHA, LIGHT_SCALE_AND_ALPHA, - SLIDE_AND_ALPHA, LIGHT_SLIDE_AND_ALPHA - } + private AnimationUtils() { } - public static void animateView(View view, boolean enterOrExit, long duration) { + public static void animateView(final View view, final boolean enterOrExit, + final long duration) { animateView(view, Type.ALPHA, enterOrExit, duration, 0, null); } - public static void animateView(View view, boolean enterOrExit, long duration, long delay) { + public static void animateView(final View view, final boolean enterOrExit, + final long duration, final long delay) { animateView(view, Type.ALPHA, enterOrExit, duration, delay, null); } - public static void animateView(View view, boolean enterOrExit, long duration, long delay, Runnable execOnEnd) { + public static void animateView(final View view, final boolean enterOrExit, final long duration, + final long delay, final Runnable execOnEnd) { animateView(view, Type.ALPHA, enterOrExit, duration, delay, execOnEnd); } - public static void animateView(View view, Type animationType, boolean enterOrExit, long duration) { + public static void animateView(final View view, final Type animationType, + final boolean enterOrExit, final long duration) { animateView(view, animationType, enterOrExit, duration, 0, null); } - public static void animateView(View view, Type animationType, boolean enterOrExit, long duration, long delay) { + public static void animateView(final View view, final Type animationType, + final boolean enterOrExit, final long duration, + final long delay) { animateView(view, animationType, enterOrExit, duration, delay, null); } /** - * Animate the view + * Animate the view. * * @param view view that will be animated * @param animationType {@link Type} of the animation @@ -74,7 +77,9 @@ public class AnimationUtils { * @param delay how long the animation will wait to start, in milliseconds * @param execOnEnd runnable that will be executed when the animation ends */ - public static void animateView(final View view, Type animationType, boolean enterOrExit, long duration, long delay, Runnable execOnEnd) { + public static void animateView(final View view, final Type animationType, + final boolean enterOrExit, final long duration, + final long delay, final Runnable execOnEnd) { if (DEBUG) { String id; try { @@ -83,24 +88,33 @@ public class AnimationUtils { id = view.getId() + ""; } - String msg = String.format("%8s → [%s:%s] [%s %s:%s] execOnEnd=%s", - enterOrExit, view.getClass().getSimpleName(), id, animationType, duration, delay, execOnEnd); + String msg = String.format("%8s → [%s:%s] [%s %s:%s] execOnEnd=%s", enterOrExit, + view.getClass().getSimpleName(), id, animationType, duration, delay, execOnEnd); Log.d(TAG, "animateView()" + msg); } if (view.getVisibility() == View.VISIBLE && enterOrExit) { - if (DEBUG) Log.d(TAG, "animateView() view was already visible > view = [" + view + "]"); + if (DEBUG) { + Log.d(TAG, "animateView() view was already visible > view = [" + view + "]"); + } view.animate().setListener(null).cancel(); view.setVisibility(View.VISIBLE); view.setAlpha(1f); - if (execOnEnd != null) execOnEnd.run(); + if (execOnEnd != null) { + execOnEnd.run(); + } return; - } else if ((view.getVisibility() == View.GONE || view.getVisibility() == View.INVISIBLE) && !enterOrExit) { - if (DEBUG) Log.d(TAG, "animateView() view was already gone > view = [" + view + "]"); + } else if ((view.getVisibility() == View.GONE || view.getVisibility() == View.INVISIBLE) + && !enterOrExit) { + if (DEBUG) { + Log.d(TAG, "animateView() view was already gone > view = [" + view + "]"); + } view.animate().setListener(null).cancel(); view.setVisibility(View.GONE); view.setAlpha(0f); - if (execOnEnd != null) execOnEnd.run(); + if (execOnEnd != null) { + execOnEnd.run(); + } return; } @@ -126,33 +140,44 @@ public class AnimationUtils { } } - /** - * Animate the background color of a view + * Animate the background color of a view. + * + * @param view the view to animate + * @param duration the duration of the animation + * @param colorStart the background color to start with + * @param colorEnd the background color to end with */ - public static void animateBackgroundColor(final View view, long duration, @ColorInt final int colorStart, @ColorInt final int colorEnd) { + public static void animateBackgroundColor(final View view, final long duration, + @ColorInt final int colorStart, + @ColorInt final int colorEnd) { if (DEBUG) { - Log.d(TAG, "animateBackgroundColor() called with: view = [" + view + "], duration = [" + duration + "], colorStart = [" + colorStart + "], colorEnd = [" + colorEnd + "]"); + Log.d(TAG, "animateBackgroundColor() called with: " + + "view = [" + view + "], duration = [" + duration + "], " + + "colorStart = [" + colorStart + "], colorEnd = [" + colorEnd + "]"); } - final int[][] EMPTY = new int[][]{new int[0]}; - ValueAnimator viewPropertyAnimator = ValueAnimator.ofObject(new ArgbEvaluator(), colorStart, colorEnd); + final int[][] empty = new int[][]{new int[0]}; + ValueAnimator viewPropertyAnimator = ValueAnimator + .ofObject(new ArgbEvaluator(), colorStart, colorEnd); viewPropertyAnimator.setInterpolator(new FastOutSlowInInterpolator()); viewPropertyAnimator.setDuration(duration); viewPropertyAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override - public void onAnimationUpdate(ValueAnimator animation) { - ViewCompat.setBackgroundTintList(view, new ColorStateList(EMPTY, new int[]{(int) animation.getAnimatedValue()})); + public void onAnimationUpdate(final ValueAnimator animation) { + ViewCompat.setBackgroundTintList(view, + new ColorStateList(empty, new int[]{(int) animation.getAnimatedValue()})); } }); viewPropertyAnimator.addListener(new AnimatorListenerAdapter() { @Override - public void onAnimationEnd(Animator animation) { - ViewCompat.setBackgroundTintList(view, new ColorStateList(EMPTY, new int[]{colorEnd})); + public void onAnimationEnd(final Animator animation) { + ViewCompat.setBackgroundTintList(view, + new ColorStateList(empty, new int[]{colorEnd})); } @Override - public void onAnimationCancel(Animator animation) { + public void onAnimationCancel(final Animator animation) { onAnimationEnd(animation); } }); @@ -160,40 +185,52 @@ public class AnimationUtils { } /** - * Animate the text color of any view that extends {@link TextView} (Buttons, EditText...) + * Animate the text color of any view that extends {@link TextView} (Buttons, EditText...). + * + * @param view the text view to animate + * @param duration the duration of the animation + * @param colorStart the text color to start with + * @param colorEnd the text color to end with */ - public static void animateTextColor(final TextView view, long duration, @ColorInt final int colorStart, @ColorInt final int colorEnd) { + public static void animateTextColor(final TextView view, final long duration, + @ColorInt final int colorStart, + @ColorInt final int colorEnd) { if (DEBUG) { - Log.d(TAG, "animateTextColor() called with: view = [" + view + "], duration = [" + duration + "], colorStart = [" + colorStart + "], colorEnd = [" + colorEnd + "]"); + Log.d(TAG, "animateTextColor() called with: " + + "view = [" + view + "], duration = [" + duration + "], " + + "colorStart = [" + colorStart + "], colorEnd = [" + colorEnd + "]"); } - ValueAnimator viewPropertyAnimator = ValueAnimator.ofObject(new ArgbEvaluator(), colorStart, colorEnd); + ValueAnimator viewPropertyAnimator = ValueAnimator + .ofObject(new ArgbEvaluator(), colorStart, colorEnd); viewPropertyAnimator.setInterpolator(new FastOutSlowInInterpolator()); viewPropertyAnimator.setDuration(duration); viewPropertyAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override - public void onAnimationUpdate(ValueAnimator animation) { + public void onAnimationUpdate(final ValueAnimator animation) { view.setTextColor((int) animation.getAnimatedValue()); } }); viewPropertyAnimator.addListener(new AnimatorListenerAdapter() { @Override - public void onAnimationEnd(Animator animation) { + public void onAnimationEnd(final Animator animation) { view.setTextColor(colorEnd); } @Override - public void onAnimationCancel(Animator animation) { + public void onAnimationCancel(final Animator animation) { view.setTextColor(colorEnd); } }); viewPropertyAnimator.start(); } - public static ValueAnimator animateHeight(final View view, long duration, int targetHeight) { + public static ValueAnimator animateHeight(final View view, final long duration, + final int targetHeight) { final int height = view.getHeight(); if (DEBUG) { - Log.d(TAG, "animateHeight: duration = [" + duration + "], from " + height + " to → " + targetHeight + " in: " + view); + Log.d(TAG, "animateHeight: duration = [" + duration + "], " + + "from " + height + " to → " + targetHeight + " in: " + view); } ValueAnimator animator = ValueAnimator.ofFloat(height, targetHeight); @@ -206,13 +243,13 @@ public class AnimationUtils { }); animator.addListener(new AnimatorListenerAdapter() { @Override - public void onAnimationEnd(Animator animation) { + public void onAnimationEnd(final Animator animation) { view.getLayoutParams().height = targetHeight; view.requestLayout(); } @Override - public void onAnimationCancel(Animator animation) { + public void onAnimationCancel(final Animator animation) { view.getLayoutParams().height = targetHeight; view.requestLayout(); } @@ -222,155 +259,211 @@ public class AnimationUtils { return animator; } - public static void animateRotation(final View view, long duration, int targetRotation) { + public static void animateRotation(final View view, final long duration, + final int targetRotation) { if (DEBUG) { - Log.d(TAG, "animateRotation: duration = [" + duration + "], from " + view.getRotation() + " to → " + targetRotation + " in: " + view); + Log.d(TAG, "animateRotation: duration = [" + duration + "], " + + "from " + view.getRotation() + " to → " + targetRotation + " in: " + view); } view.animate().setListener(null).cancel(); - view.animate().rotation(targetRotation).setDuration(duration).setInterpolator(new FastOutSlowInInterpolator()) + view.animate() + .rotation(targetRotation).setDuration(duration) + .setInterpolator(new FastOutSlowInInterpolator()) .setListener(new AnimatorListenerAdapter() { @Override - public void onAnimationCancel(Animator animation) { + public void onAnimationCancel(final Animator animation) { view.setRotation(targetRotation); } @Override - public void onAnimationEnd(Animator animation) { + public void onAnimationEnd(final Animator animation) { view.setRotation(targetRotation); } }).start(); } + private static void animateAlpha(final View view, final boolean enterOrExit, + final long duration, final long delay, + final Runnable execOnEnd) { + if (enterOrExit) { + view.animate().setInterpolator(new FastOutSlowInInterpolator()).alpha(1f) + .setDuration(duration).setStartDelay(delay) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(final Animator animation) { + if (execOnEnd != null) { + execOnEnd.run(); + } + } + }).start(); + } else { + view.animate().setInterpolator(new FastOutSlowInInterpolator()).alpha(0f) + .setDuration(duration).setStartDelay(delay) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(final Animator animation) { + view.setVisibility(View.GONE); + if (execOnEnd != null) { + execOnEnd.run(); + } + } + }).start(); + } + } + /*////////////////////////////////////////////////////////////////////////// // Internals //////////////////////////////////////////////////////////////////////////*/ - private static void animateAlpha(final View view, boolean enterOrExit, long duration, long delay, final Runnable execOnEnd) { - if (enterOrExit) { - view.animate().setInterpolator(new FastOutSlowInInterpolator()).alpha(1f) - .setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - if (execOnEnd != null) execOnEnd.run(); - } - }).start(); - } else { - view.animate().setInterpolator(new FastOutSlowInInterpolator()).alpha(0f) - .setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - view.setVisibility(View.GONE); - if (execOnEnd != null) execOnEnd.run(); - } - }).start(); - } - } - - private static void animateScaleAndAlpha(final View view, boolean enterOrExit, long duration, long delay, final Runnable execOnEnd) { + private static void animateScaleAndAlpha(final View view, final boolean enterOrExit, + final long duration, final long delay, + final Runnable execOnEnd) { if (enterOrExit) { view.setScaleX(.8f); view.setScaleY(.8f); - view.animate().setInterpolator(new FastOutSlowInInterpolator()).alpha(1f).scaleX(1f).scaleY(1f) - .setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() { + view.animate() + .setInterpolator(new FastOutSlowInInterpolator()) + .alpha(1f).scaleX(1f).scaleY(1f) + .setDuration(duration).setStartDelay(delay) + .setListener(new AnimatorListenerAdapter() { @Override - public void onAnimationEnd(Animator animation) { - if (execOnEnd != null) execOnEnd.run(); + public void onAnimationEnd(final Animator animation) { + if (execOnEnd != null) { + execOnEnd.run(); + } } }).start(); } else { view.setScaleX(1f); view.setScaleY(1f); - view.animate().setInterpolator(new FastOutSlowInInterpolator()).alpha(0f).scaleX(.8f).scaleY(.8f) - .setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() { + view.animate() + .setInterpolator(new FastOutSlowInInterpolator()) + .alpha(0f).scaleX(.8f).scaleY(.8f) + .setDuration(duration).setStartDelay(delay) + .setListener(new AnimatorListenerAdapter() { @Override - public void onAnimationEnd(Animator animation) { + public void onAnimationEnd(final Animator animation) { view.setVisibility(View.GONE); - if (execOnEnd != null) execOnEnd.run(); + if (execOnEnd != null) { + execOnEnd.run(); + } } }).start(); } } - private static void animateLightScaleAndAlpha(final View view, boolean enterOrExit, long duration, long delay, final Runnable execOnEnd) { + private static void animateLightScaleAndAlpha(final View view, final boolean enterOrExit, + final long duration, final long delay, + final Runnable execOnEnd) { if (enterOrExit) { view.setAlpha(.5f); view.setScaleX(.95f); view.setScaleY(.95f); - view.animate().setInterpolator(new FastOutSlowInInterpolator()).alpha(1f).scaleX(1f).scaleY(1f) - .setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() { + view.animate() + .setInterpolator(new FastOutSlowInInterpolator()) + .alpha(1f).scaleX(1f).scaleY(1f) + .setDuration(duration).setStartDelay(delay) + .setListener(new AnimatorListenerAdapter() { @Override - public void onAnimationEnd(Animator animation) { - if (execOnEnd != null) execOnEnd.run(); + public void onAnimationEnd(final Animator animation) { + if (execOnEnd != null) { + execOnEnd.run(); + } } }).start(); } else { view.setAlpha(1f); view.setScaleX(1f); view.setScaleY(1f); - view.animate().setInterpolator(new FastOutSlowInInterpolator()).alpha(0f).scaleX(.95f).scaleY(.95f) - .setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() { + view.animate() + .setInterpolator(new FastOutSlowInInterpolator()) + .alpha(0f).scaleX(.95f).scaleY(.95f) + .setDuration(duration).setStartDelay(delay) + .setListener(new AnimatorListenerAdapter() { @Override - public void onAnimationEnd(Animator animation) { + public void onAnimationEnd(final Animator animation) { view.setVisibility(View.GONE); - if (execOnEnd != null) execOnEnd.run(); + if (execOnEnd != null) { + execOnEnd.run(); + } } }).start(); } } - private static void animateSlideAndAlpha(final View view, boolean enterOrExit, long duration, long delay, final Runnable execOnEnd) { + private static void animateSlideAndAlpha(final View view, final boolean enterOrExit, + final long duration, final long delay, + final Runnable execOnEnd) { if (enterOrExit) { view.setTranslationY(-view.getHeight()); view.setAlpha(0f); - view.animate().setInterpolator(new FastOutSlowInInterpolator()).alpha(1f).translationY(0) - .setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() { + view.animate() + .setInterpolator(new FastOutSlowInInterpolator()).alpha(1f).translationY(0) + .setDuration(duration).setStartDelay(delay) + .setListener(new AnimatorListenerAdapter() { @Override - public void onAnimationEnd(Animator animation) { - if (execOnEnd != null) execOnEnd.run(); + public void onAnimationEnd(final Animator animation) { + if (execOnEnd != null) { + execOnEnd.run(); + } } }).start(); } else { - view.animate().setInterpolator(new FastOutSlowInInterpolator()).alpha(0f).translationY(-view.getHeight()) - .setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() { + view.animate() + .setInterpolator(new FastOutSlowInInterpolator()) + .alpha(0f).translationY(-view.getHeight()) + .setDuration(duration).setStartDelay(delay) + .setListener(new AnimatorListenerAdapter() { @Override - public void onAnimationEnd(Animator animation) { + public void onAnimationEnd(final Animator animation) { view.setVisibility(View.GONE); - if (execOnEnd != null) execOnEnd.run(); + if (execOnEnd != null) { + execOnEnd.run(); + } } }).start(); } } - private static void animateLightSlideAndAlpha(final View view, boolean enterOrExit, long duration, long delay, final Runnable execOnEnd) { + private static void animateLightSlideAndAlpha(final View view, final boolean enterOrExit, + final long duration, final long delay, + final Runnable execOnEnd) { if (enterOrExit) { view.setTranslationY(-view.getHeight() / 2); view.setAlpha(0f); - view.animate().setInterpolator(new FastOutSlowInInterpolator()).alpha(1f).translationY(0) - .setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() { + view.animate() + .setInterpolator(new FastOutSlowInInterpolator()).alpha(1f).translationY(0) + .setDuration(duration).setStartDelay(delay) + .setListener(new AnimatorListenerAdapter() { @Override - public void onAnimationEnd(Animator animation) { - if (execOnEnd != null) execOnEnd.run(); + public void onAnimationEnd(final Animator animation) { + if (execOnEnd != null) { + execOnEnd.run(); + } } }).start(); } else { - view.animate().setInterpolator(new FastOutSlowInInterpolator()).alpha(0f).translationY(-view.getHeight() / 2) - .setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() { + view.animate().setInterpolator(new FastOutSlowInInterpolator()) + .alpha(0f).translationY(-view.getHeight() / 2) + .setDuration(duration).setStartDelay(delay) + .setListener(new AnimatorListenerAdapter() { @Override - public void onAnimationEnd(Animator animation) { + public void onAnimationEnd(final Animator animation) { view.setVisibility(View.GONE); - if (execOnEnd != null) execOnEnd.run(); + if (execOnEnd != null) { + execOnEnd.run(); + } } }).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)); + public static void slideUp(final View view, final long duration, final long delay, + @FloatRange(from = 0.0f, to = 1.0f) + final float translationPercent) { + int translationY = (int) (view.getResources().getDisplayMetrics().heightPixels + * (translationPercent)); view.animate().setListener(null).cancel(); view.setAlpha(0f); @@ -384,4 +477,10 @@ public class AnimationUtils { .setInterpolator(new FastOutSlowInInterpolator()) .start(); } + + public enum Type { + ALPHA, + SCALE_AND_ALPHA, LIGHT_SCALE_AND_ALPHA, + SLIDE_AND_ALPHA, LIGHT_SLIDE_AND_ALPHA + } } diff --git a/app/src/main/java/org/schabi/newpipe/util/BitmapUtils.java b/app/src/main/java/org/schabi/newpipe/util/BitmapUtils.java new file mode 100644 index 000000000..5b1c46372 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/BitmapUtils.java @@ -0,0 +1,45 @@ +package org.schabi.newpipe.util; + +import android.graphics.Bitmap; + +import androidx.annotation.Nullable; + +public final class BitmapUtils { + private BitmapUtils() { } + + @Nullable + public static Bitmap centerCrop(final Bitmap inputBitmap, final int newWidth, + final int newHeight) { + if (inputBitmap == null || inputBitmap.isRecycled()) { + return null; + } + + float sourceWidth = inputBitmap.getWidth(); + float sourceHeight = inputBitmap.getHeight(); + + float xScale = newWidth / sourceWidth; + float yScale = newHeight / sourceHeight; + + float newXScale; + float newYScale; + + if (yScale > xScale) { + newXScale = xScale / yScale; + newYScale = 1.0f; + } else { + newXScale = 1.0f; + newYScale = yScale / xScale; + } + + float scaledWidth = newXScale * sourceWidth; + float scaledHeight = newYScale * sourceHeight; + + int left = (int) ((sourceWidth - scaledWidth) / 2); + int top = (int) ((sourceHeight - scaledHeight) / 2); + int width = (int) scaledWidth; + int height = (int) scaledHeight; + + return Bitmap.createBitmap(inputBitmap, left, top, width, height); + } + +} diff --git a/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java b/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java index ac79fee23..770592537 100644 --- a/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java +++ b/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java @@ -28,14 +28,13 @@ import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; public class CommentTextOnTouchListener implements View.OnTouchListener { - public static final CommentTextOnTouchListener INSTANCE = new CommentTextOnTouchListener(); - private static final Pattern timestampPattern = Pattern.compile("(.*)#timestamp=(\\d+)"); + private static final Pattern TIMESTAMP_PATTERN = Pattern.compile("(.*)#timestamp=(\\d+)"); @Override - public boolean onTouch(View v, MotionEvent event) { - if(!(v instanceof TextView)){ + public boolean onTouch(final View v, final MotionEvent event) { + if (!(v instanceof TextView)) { return false; } TextView widget = (TextView) v; @@ -66,10 +65,12 @@ public class CommentTextOnTouchListener implements View.OnTouchListener { if (link.length != 0) { if (action == MotionEvent.ACTION_UP) { boolean handled = false; - if(link[0] instanceof URLSpan){ + if (link[0] instanceof URLSpan) { handled = handleUrl(v.getContext(), (URLSpan) link[0]); } - if(!handled) link[0].onClick(widget); + if (!handled) { + link[0].onClick(widget); + } } else if (action == MotionEvent.ACTION_DOWN) { Selection.setSelection(buffer, buffer.getSpanStart(link[0]), @@ -78,17 +79,15 @@ public class CommentTextOnTouchListener implements View.OnTouchListener { return true; } } - } - return false; } - private boolean handleUrl(Context context, URLSpan urlSpan) { + private boolean handleUrl(final Context context, final URLSpan urlSpan) { String url = urlSpan.getURL(); int seconds = -1; - Matcher matcher = timestampPattern.matcher(url); - if(matcher.matches()){ + Matcher matcher = TIMESTAMP_PATTERN.matcher(url); + if (matcher.matches()) { url = matcher.group(1); seconds = Integer.parseInt(matcher.group(2)); } @@ -100,18 +99,19 @@ public class CommentTextOnTouchListener implements View.OnTouchListener { } catch (ExtractionException e) { return false; } - if(linkType == StreamingService.LinkType.NONE){ + if (linkType == StreamingService.LinkType.NONE) { return false; } - if(linkType == StreamingService.LinkType.STREAM && seconds != -1){ + if (linkType == StreamingService.LinkType.STREAM && seconds != -1) { return playOnPopup(context, url, service, seconds); - }else{ + } else { NavigationHelper.openRouterActivity(context, url); return true; } } - private boolean playOnPopup(Context context, String url, StreamingService service, int seconds) { + private boolean playOnPopup(final Context context, final String url, + final StreamingService service, final int seconds) { LinkHandlerFactory factory = service.getStreamLHFactory(); String cleanUrl = null; try { @@ -123,7 +123,7 @@ public class CommentTextOnTouchListener implements View.OnTouchListener { single.subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(info -> { - PlayQueue playQueue = new SinglePlayQueue((StreamInfo) info, seconds*1000); + PlayQueue playQueue = new SinglePlayQueue((StreamInfo) info, seconds * 1000); NavigationHelper.playOnPopupPlayer(context, playQueue, false); }); return true; diff --git a/app/src/main/java/org/schabi/newpipe/util/Constants.java b/app/src/main/java/org/schabi/newpipe/util/Constants.java index b01b6df6a..e71dd16f9 100644 --- a/app/src/main/java/org/schabi/newpipe/util/Constants.java +++ b/app/src/main/java/org/schabi/newpipe/util/Constants.java @@ -1,6 +1,6 @@ package org.schabi.newpipe.util; -public class Constants { +public final class Constants { public static final String KEY_SERVICE_ID = "key_service_id"; public static final String KEY_URL = "key_url"; public static final String KEY_TITLE = "key_title"; @@ -12,4 +12,6 @@ public class Constants { public static final String KEY_MAIN_PAGE_CHANGE = "key_main_page_change"; public static final int NO_SERVICE_ID = -1; + + private Constants() { } } diff --git a/app/src/main/java/org/schabi/newpipe/util/ConstantsKt.kt b/app/src/main/java/org/schabi/newpipe/util/ConstantsKt.kt new file mode 100644 index 000000000..8d24cb04e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/ConstantsKt.kt @@ -0,0 +1,6 @@ +package org.schabi.newpipe.util + +/** + * Default duration when using throttle functions across the app, in milliseconds. + */ +const val DEFAULT_THROTTLE_TIMEOUT = 120L diff --git a/app/src/main/java/org/schabi/newpipe/util/CookieUtils.java b/app/src/main/java/org/schabi/newpipe/util/CookieUtils.java new file mode 100644 index 000000000..d8b81b4ce --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/CookieUtils.java @@ -0,0 +1,25 @@ +package org.schabi.newpipe.util; + +import android.text.TextUtils; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +public final class CookieUtils { + private CookieUtils() { + } + + public static String concatCookies(final Collection cookieStrings) { + Set cookieSet = new HashSet<>(); + for (String cookies : cookieStrings) { + cookieSet.addAll(splitCookies(cookies)); + } + return TextUtils.join("; ", cookieSet).trim(); + } + + public static Set splitCookies(final String cookies) { + return new HashSet<>(Arrays.asList(cookies.split("; *"))); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/util/ExceptionUtils.kt b/app/src/main/java/org/schabi/newpipe/util/ExceptionUtils.kt new file mode 100644 index 000000000..528912ceb --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/ExceptionUtils.kt @@ -0,0 +1,82 @@ +package org.schabi.newpipe.util + +import java.io.IOException +import java.io.InterruptedIOException + +class ExceptionUtils { + companion object { + /** + * @return if throwable is related to Interrupted exceptions, or one of its causes is. + */ + @JvmStatic + fun isInterruptedCaused(throwable: Throwable): Boolean { + return hasExactCause(throwable, + InterruptedIOException::class.java, + InterruptedException::class.java) + } + + /** + * @return if throwable is related to network issues, or one of its causes is. + */ + @JvmStatic + fun isNetworkRelated(throwable: Throwable): Boolean { + return hasAssignableCause(throwable, + IOException::class.java) + } + + /** + * Calls [hasCause] with the `checkSubtypes` parameter set to false. + */ + @JvmStatic + fun hasExactCause(throwable: Throwable, vararg causesToCheck: Class<*>): Boolean { + return hasCause(throwable, false, *causesToCheck) + } + + /** + * Calls [hasCause] with the `checkSubtypes` parameter set to true. + */ + @JvmStatic + fun hasAssignableCause(throwable: Throwable?, vararg causesToCheck: Class<*>): Boolean { + return hasCause(throwable, true, *causesToCheck) + } + + /** + * Check if throwable has some cause from the causes to check, or is itself in it. + * + * If `checkIfAssignable` is true, not only the exact type will be considered equals, but also its subtypes. + * + * @param throwable throwable that will be checked. + * @param checkSubtypes if subtypes are also checked. + * @param causesToCheck an array of causes to check. + * + * @see Class.isAssignableFrom + */ + @JvmStatic + tailrec fun hasCause(throwable: Throwable?, checkSubtypes: Boolean, vararg causesToCheck: Class<*>): Boolean { + if (throwable == null) { + return false + } + + // Check if throwable is a subtype of any of the causes to check + causesToCheck.forEach { causeClass -> + if (checkSubtypes) { + if (causeClass.isAssignableFrom(throwable.javaClass)) { + return true + } + } else { + if (causeClass == throwable.javaClass) { + return true + } + } + } + + val currentCause: Throwable? = throwable.cause + // Check if cause is not pointing to the same instance, to avoid infinite loops. + if (throwable !== currentCause) { + return hasCause(currentCause, checkSubtypes, *causesToCheck) + } + + return false + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java index 0cebe5af3..9b8b2494e 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java @@ -31,23 +31,28 @@ import org.schabi.newpipe.ReCaptchaActivity; import org.schabi.newpipe.extractor.Info; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage; +import org.schabi.newpipe.extractor.ListInfo; import org.schabi.newpipe.extractor.NewPipe; -import org.schabi.newpipe.extractor.suggestion.SuggestionExtractor; +import org.schabi.newpipe.extractor.Page; +import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.comments.CommentsInfo; import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; +import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException; import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; +import org.schabi.newpipe.extractor.feed.FeedExtractor; +import org.schabi.newpipe.extractor.feed.FeedInfo; import org.schabi.newpipe.extractor.kiosk.KioskInfo; import org.schabi.newpipe.extractor.playlist.PlaylistInfo; import org.schabi.newpipe.extractor.search.SearchInfo; import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor; import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.extractor.suggestion.SuggestionExtractor; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; -import java.io.IOException; -import java.io.InterruptedIOException; import java.util.Collections; import java.util.List; @@ -56,47 +61,44 @@ import io.reactivex.Single; public final class ExtractorHelper { private static final String TAG = ExtractorHelper.class.getSimpleName(); - private static final InfoCache cache = InfoCache.getInstance(); + private static final InfoCache CACHE = InfoCache.getInstance(); private ExtractorHelper() { //no instance } - private static void checkServiceId(int serviceId) { + private static void checkServiceId(final int serviceId) { if (serviceId == Constants.NO_SERVICE_ID) { throw new IllegalArgumentException("serviceId is NO_SERVICE_ID"); } } - public static Single searchFor(final int serviceId, - final String searchString, + public static Single searchFor(final int serviceId, final String searchString, final List contentFilter, final String sortFilter) { checkServiceId(serviceId); return Single.fromCallable(() -> - SearchInfo.getInfo(NewPipe.getService(serviceId), - NewPipe.getService(serviceId) - .getSearchQHFactory() - .fromQuery(searchString, contentFilter, sortFilter))); + SearchInfo.getInfo(NewPipe.getService(serviceId), + NewPipe.getService(serviceId) + .getSearchQHFactory() + .fromQuery(searchString, contentFilter, sortFilter))); } public static Single getMoreSearchItems(final int serviceId, final String searchString, final List contentFilter, final String sortFilter, - final String pageUrl) { + final Page page) { checkServiceId(serviceId); return Single.fromCallable(() -> SearchInfo.getMoreItems(NewPipe.getService(serviceId), NewPipe.getService(serviceId) - .getSearchQHFactory() - .fromQuery(searchString, contentFilter, sortFilter), - pageUrl)); + .getSearchQHFactory() + .fromQuery(searchString, contentFilter, sortFilter), page)); } - public static Single> suggestionsFor(final int serviceId, - final String query) { + public static Single> suggestionsFor(final int serviceId, final String query) { checkServiceId(serviceId); return Single.fromCallable(() -> { SuggestionExtractor extractor = NewPipe.getService(serviceId) @@ -107,75 +109,85 @@ public final class ExtractorHelper { }); } - public static Single getStreamInfo(final int serviceId, - final String url, - boolean forceLoad) { + public static Single getStreamInfo(final int serviceId, final String url, + final boolean forceLoad) { checkServiceId(serviceId); - return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.STREAM, Single.fromCallable(() -> - StreamInfo.getInfo(NewPipe.getService(serviceId), url))); + return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.STREAM, + Single.fromCallable(() -> StreamInfo.getInfo(NewPipe.getService(serviceId), url))); } - public static Single getChannelInfo(final int serviceId, - final String url, - boolean forceLoad) { + public static Single getChannelInfo(final int serviceId, final String url, + final boolean forceLoad) { checkServiceId(serviceId); - return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.CHANNEL, Single.fromCallable(() -> - ChannelInfo.getInfo(NewPipe.getService(serviceId), url))); + return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.CHANNEL, + Single.fromCallable(() -> + ChannelInfo.getInfo(NewPipe.getService(serviceId), url))); } - public static Single getMoreChannelItems(final int serviceId, - final String url, - final String nextStreamsUrl) { + public static Single getMoreChannelItems(final int serviceId, final String url, + final Page nextPage) { checkServiceId(serviceId); return Single.fromCallable(() -> - ChannelInfo.getMoreItems(NewPipe.getService(serviceId), url, nextStreamsUrl)); + ChannelInfo.getMoreItems(NewPipe.getService(serviceId), url, nextPage)); } - public static Single getCommentsInfo(final int serviceId, - final String url, - boolean forceLoad) { + public static Single> getFeedInfoFallbackToChannelInfo( + final int serviceId, final String url) { + final Maybe> maybeFeedInfo = Maybe.fromCallable(() -> { + final StreamingService service = NewPipe.getService(serviceId); + final FeedExtractor feedExtractor = service.getFeedExtractor(url); + + if (feedExtractor == null) { + return null; + } + + return FeedInfo.getInfo(feedExtractor); + }); + + return maybeFeedInfo.switchIfEmpty(getChannelInfo(serviceId, url, true)); + } + + public static Single getCommentsInfo(final int serviceId, final String url, + final boolean forceLoad) { checkServiceId(serviceId); - return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.COMMENT, Single.fromCallable(() -> - CommentsInfo.getInfo(NewPipe.getService(serviceId), url))); + return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.COMMENT, + Single.fromCallable(() -> + CommentsInfo.getInfo(NewPipe.getService(serviceId), url))); } public static Single getMoreCommentItems(final int serviceId, final CommentsInfo info, - final String nextPageUrl) { + final Page nextPage) { checkServiceId(serviceId); return Single.fromCallable(() -> - CommentsInfo.getMoreItems(NewPipe.getService(serviceId), info, nextPageUrl)); + CommentsInfo.getMoreItems(NewPipe.getService(serviceId), info, nextPage)); } - public static Single getPlaylistInfo(final int serviceId, - final String url, - boolean forceLoad) { + public static Single getPlaylistInfo(final int serviceId, final String url, + final boolean forceLoad) { checkServiceId(serviceId); - return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.PLAYLIST, Single.fromCallable(() -> - PlaylistInfo.getInfo(NewPipe.getService(serviceId), url))); + return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.PLAYLIST, + Single.fromCallable(() -> + PlaylistInfo.getInfo(NewPipe.getService(serviceId), url))); } - public static Single getMorePlaylistItems(final int serviceId, - final String url, - final String nextStreamsUrl) { + public static Single getMorePlaylistItems(final int serviceId, final String url, + final Page nextPage) { checkServiceId(serviceId); return Single.fromCallable(() -> - PlaylistInfo.getMoreItems(NewPipe.getService(serviceId), url, nextStreamsUrl)); + PlaylistInfo.getMoreItems(NewPipe.getService(serviceId), url, nextPage)); } - public static Single getKioskInfo(final int serviceId, - final String url, - boolean forceLoad) { - return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.PLAYLIST, Single.fromCallable(() -> - KioskInfo.getInfo(NewPipe.getService(serviceId), url))); + public static Single getKioskInfo(final int serviceId, final String url, + final boolean forceLoad) { + return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.PLAYLIST, + Single.fromCallable(() -> KioskInfo.getInfo(NewPipe.getService(serviceId), url))); } - public static Single getMoreKioskItems(final int serviceId, - final String url, - final String nextStreamsUrl) { + public static Single getMoreKioskItems(final int serviceId, final String url, + final Page nextPage) { return Single.fromCallable(() -> - KioskInfo.getMoreItems(NewPipe.getService(serviceId), - url, nextStreamsUrl)); + KioskInfo.getMoreItems(NewPipe.getService(serviceId), url, nextPage)); } /*////////////////////////////////////////////////////////////////////////// @@ -186,23 +198,31 @@ public final class ExtractorHelper { * Check if we can load it from the cache (forceLoad parameter), if we can't, * load from the network (Single loadFromNetwork) * and put the results in the cache. + * + * @param the item type's class that extends {@link Info} + * @param forceLoad whether to force loading from the network instead of from the cache + * @param serviceId the service to load from + * @param url the URL to load + * @param infoType the {@link InfoItem.InfoType} of the item + * @param loadFromNetwork the {@link Single} to load the item from the network + * @return a {@link Single} that loads the item */ - private static Single checkCache(boolean forceLoad, - int serviceId, - String url, - InfoItem.InfoType infoType, - Single loadFromNetwork) { + private static Single checkCache(final boolean forceLoad, + final int serviceId, final String url, + final InfoItem.InfoType infoType, + final Single loadFromNetwork) { checkServiceId(serviceId); - loadFromNetwork = loadFromNetwork.doOnSuccess(info -> cache.putInfo(serviceId, url, info, infoType)); + Single actualLoadFromNetwork = loadFromNetwork + .doOnSuccess(info -> CACHE.putInfo(serviceId, url, info, infoType)); Single load; if (forceLoad) { - cache.removeInfo(serviceId, url, infoType); - load = loadFromNetwork; + CACHE.removeInfo(serviceId, url, infoType); + load = actualLoadFromNetwork; } else { load = Maybe.concat(ExtractorHelper.loadFromCache(serviceId, url, infoType), - loadFromNetwork.toMaybe()) - .firstElement() //Take the first valid + actualLoadFromNetwork.toMaybe()) + .firstElement() // Take the first valid .toSingle(); } @@ -210,14 +230,23 @@ public final class ExtractorHelper { } /** - * Default implementation uses the {@link InfoCache} to get cached results + * Default implementation uses the {@link InfoCache} to get cached results. + * + * @param the item type's class that extends {@link Info} + * @param serviceId the service to load from + * @param url the URL to load + * @param infoType the {@link InfoItem.InfoType} of the item + * @return a {@link Single} that loads the item */ - public static Maybe loadFromCache(final int serviceId, final String url, InfoItem.InfoType infoType) { + private static Maybe loadFromCache(final int serviceId, final String url, + final InfoItem.InfoType infoType) { checkServiceId(serviceId); return Maybe.defer(() -> { //noinspection unchecked - I info = (I) cache.getFromKey(serviceId, url, infoType); - if (MainActivity.DEBUG) Log.d(TAG, "loadFromCache() called, info > " + info); + I info = (I) CACHE.getFromKey(serviceId, url, infoType); + if (MainActivity.DEBUG) { + Log.d(TAG, "loadFromCache() called, info > " + info); + } // Only return info if it's not null (it is cached) if (info != null) { @@ -228,14 +257,26 @@ public final class ExtractorHelper { }); } - public static boolean isCached(final int serviceId, final String url, InfoItem.InfoType infoType) { + public static boolean isCached(final int serviceId, final String url, + final InfoItem.InfoType infoType) { return null != loadFromCache(serviceId, url, infoType).blockingGet(); } /** - * A simple and general error handler that show a Toast for known exceptions, and for others, opens the report error activity with the (optional) error message. + * A simple and general error handler that show a Toast for known exceptions, + * and for others, opens the report error activity with the (optional) error message. + * + * @param context Android app context + * @param serviceId the service the exception happened in + * @param url the URL where the exception happened + * @param exception the exception to be handled + * @param userAction the action of the user that caused the exception + * @param optionalErrorMessage the optional error message */ - public static void handleGeneralException(Context context, int serviceId, String url, Throwable exception, UserAction userAction, String optionalErrorMessage) { + public static void handleGeneralException(final Context context, final int serviceId, + final String url, final Throwable exception, + final UserAction userAction, + final String optionalErrorMessage) { final Handler handler = new Handler(context.getMainLooper()); handler.post(() -> { @@ -245,82 +286,23 @@ public final class ExtractorHelper { Intent intent = new Intent(context, ReCaptchaActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); - } else if (exception instanceof IOException) { + } else if (ExceptionUtils.isNetworkRelated(exception)) { Toast.makeText(context, R.string.network_error, Toast.LENGTH_LONG).show(); } else if (exception instanceof ContentNotAvailableException) { Toast.makeText(context, R.string.content_not_available, Toast.LENGTH_LONG).show(); + } else if (exception instanceof ContentNotSupportedException) { + Toast.makeText(context, R.string.content_not_supported, Toast.LENGTH_LONG).show(); } else { - int errorId = exception instanceof YoutubeStreamExtractor.DecryptException ? R.string.youtube_signature_decryption_error : - exception instanceof ParsingException ? R.string.parsing_error : R.string.general_error; - ErrorActivity.reportError(handler, context, exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(userAction, - serviceId == -1 ? "none" : NewPipe.getNameOfService(serviceId), url + (optionalErrorMessage == null ? "" : optionalErrorMessage), errorId)); + int errorId = exception instanceof YoutubeStreamExtractor.DecryptException + ? R.string.youtube_signature_decryption_error + : exception instanceof ParsingException + ? R.string.parsing_error : R.string.general_error; + ErrorActivity.reportError(handler, context, exception, MainActivity.class, null, + ErrorActivity.ErrorInfo.make(userAction, serviceId == -1 ? "none" + : NewPipe.getNameOfService(serviceId), + url + (optionalErrorMessage == null ? "" + : optionalErrorMessage), errorId)); } }); } - - /** - * Check if throwable have the cause that can be assignable from the causes to check. - * - * @see Class#isAssignableFrom(Class) - */ - public static boolean hasAssignableCauseThrowable(Throwable throwable, - Class... causesToCheck) { - // Check if getCause is not the same as cause (the getCause is already the root), - // as it will cause a infinite loop if it is - Throwable cause, getCause = throwable; - - // Check if throwable is a subclass of any of the filtered classes - final Class throwableClass = throwable.getClass(); - for (Class causesEl : causesToCheck) { - if (causesEl.isAssignableFrom(throwableClass)) { - return true; - } - } - - // Iteratively checks if the root cause of the throwable is a subclass of the filtered class - while ((cause = throwable.getCause()) != null && getCause != cause) { - getCause = cause; - final Class causeClass = cause.getClass(); - for (Class causesEl : causesToCheck) { - if (causesEl.isAssignableFrom(causeClass)) { - return true; - } - } - } - return false; - } - - /** - * Check if throwable have the exact cause from one of the causes to check. - */ - public static boolean hasExactCauseThrowable(Throwable throwable, Class... causesToCheck) { - // Check if getCause is not the same as cause (the getCause is already the root), - // as it will cause a infinite loop if it is - Throwable cause, getCause = throwable; - - for (Class causesEl : causesToCheck) { - if (throwable.getClass().equals(causesEl)) { - return true; - } - } - - while ((cause = throwable.getCause()) != null && getCause != cause) { - getCause = cause; - for (Class causesEl : causesToCheck) { - if (cause.getClass().equals(causesEl)) { - return true; - } - } - } - return false; - } - - /** - * Check if throwable have Interrupted* exception as one of its causes. - */ - public static boolean isInterruptedCaused(Throwable throwable) { - return ExtractorHelper.hasExactCauseThrowable(throwable, - InterruptedIOException.class, - InterruptedException.class); - } } diff --git a/app/src/main/java/org/schabi/newpipe/util/FallbackViewHolder.java b/app/src/main/java/org/schabi/newpipe/util/FallbackViewHolder.java index bfe0ae5c5..967a54f0a 100644 --- a/app/src/main/java/org/schabi/newpipe/util/FallbackViewHolder.java +++ b/app/src/main/java/org/schabi/newpipe/util/FallbackViewHolder.java @@ -1,10 +1,11 @@ package org.schabi.newpipe.util; -import androidx.recyclerview.widget.RecyclerView; import android.view.View; +import androidx.recyclerview.widget.RecyclerView; + public class FallbackViewHolder extends RecyclerView.ViewHolder { - public FallbackViewHolder(View itemView) { + public FallbackViewHolder(final View itemView) { super(itemView); } } diff --git a/app/src/main/java/org/schabi/newpipe/util/FilePickerActivityHelper.java b/app/src/main/java/org/schabi/newpipe/util/FilePickerActivityHelper.java index 420322c27..6ede163a3 100644 --- a/app/src/main/java/org/schabi/newpipe/util/FilePickerActivityHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/FilePickerActivityHelper.java @@ -5,11 +5,6 @@ import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.os.Environment; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.loader.content.Loader; -import androidx.recyclerview.widget.SortedList; -import androidx.recyclerview.widget.RecyclerView; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; @@ -17,6 +12,12 @@ import android.view.ViewGroup; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.loader.content.Loader; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.SortedList; + import com.nononsenseapps.filepicker.AbstractFilePickerFragment; import com.nononsenseapps.filepicker.FilePickerFragment; @@ -25,11 +26,36 @@ import org.schabi.newpipe.R; import java.io.File; public class FilePickerActivityHelper extends com.nononsenseapps.filepicker.FilePickerActivity { - private CustomFilePickerFragment currentFragment; + public static Intent chooseSingleFile(@NonNull final Context context) { + return new Intent(context, FilePickerActivityHelper.class) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, false) + .putExtra(FilePickerActivityHelper.EXTRA_SINGLE_CLICK, true) + .putExtra(FilePickerActivityHelper.EXTRA_MODE, FilePickerActivityHelper.MODE_FILE); + } + + public static Intent chooseFileToSave(@NonNull final Context context, + @Nullable final String startPath) { + return new Intent(context, FilePickerActivityHelper.class) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_EXISTING_FILE, true) + .putExtra(FilePickerActivityHelper.EXTRA_START_PATH, startPath) + .putExtra(FilePickerActivityHelper.EXTRA_MODE, + FilePickerActivityHelper.MODE_NEW_FILE); + } + + public static boolean isOwnFileUri(@NonNull final Context context, @NonNull final Uri uri) { + if (uri.getAuthority() == null) { + return false; + } + return uri.getAuthority().startsWith(context.getPackageName()); + } + @Override - public void onCreate(Bundle savedInstanceState) { + public void onCreate(final Bundle savedInstanceState) { if (ThemeHelper.isLightThemeSelected(this)) { this.setTheme(R.style.FilePickerThemeLight); } else { @@ -50,33 +76,18 @@ public class FilePickerActivityHelper extends com.nononsenseapps.filepicker.File } @Override - protected AbstractFilePickerFragment getFragment(@Nullable String startPath, int mode, boolean allowMultiple, boolean allowCreateDir, boolean allowExistingFile, boolean singleClick) { + protected AbstractFilePickerFragment getFragment(@Nullable final String startPath, + final int mode, + final boolean allowMultiple, + final boolean allowCreateDir, + final boolean allowExistingFile, + final boolean singleClick) { final CustomFilePickerFragment fragment = new CustomFilePickerFragment(); - fragment.setArgs(startPath != null ? startPath : Environment.getExternalStorageDirectory().getPath(), + fragment.setArgs(startPath != null ? startPath + : Environment.getExternalStorageDirectory().getPath(), mode, allowMultiple, allowCreateDir, allowExistingFile, singleClick); - return currentFragment = fragment; - } - - public static Intent chooseSingleFile(@NonNull Context context) { - return new Intent(context, FilePickerActivityHelper.class) - .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) - .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, false) - .putExtra(FilePickerActivityHelper.EXTRA_SINGLE_CLICK, true) - .putExtra(FilePickerActivityHelper.EXTRA_MODE, FilePickerActivityHelper.MODE_FILE); - } - - public static Intent chooseFileToSave(@NonNull Context context, @Nullable String startPath) { - return new Intent(context, FilePickerActivityHelper.class) - .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) - .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true) - .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_EXISTING_FILE, true) - .putExtra(FilePickerActivityHelper.EXTRA_START_PATH, startPath) - .putExtra(FilePickerActivityHelper.EXTRA_MODE, FilePickerActivityHelper.MODE_NEW_FILE); - } - - public static boolean isOwnFileUri(@NonNull Context context, @NonNull Uri uri) { - if (uri.getAuthority() == null) return false; - return uri.getAuthority().startsWith(context.getPackageName()); + currentFragment = fragment; + return currentFragment; } /*////////////////////////////////////////////////////////////////////////// @@ -84,30 +95,35 @@ public class FilePickerActivityHelper extends com.nononsenseapps.filepicker.File //////////////////////////////////////////////////////////////////////////*/ public static class CustomFilePickerFragment extends FilePickerFragment { - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + public View onCreateView(final LayoutInflater inflater, final ViewGroup container, + final Bundle savedInstanceState) { return super.onCreateView(inflater, container, savedInstanceState); } @NonNull @Override - public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, + final int viewType) { final RecyclerView.ViewHolder viewHolder = super.onCreateViewHolder(parent, viewType); final View view = viewHolder.itemView.findViewById(android.R.id.text1); if (view instanceof TextView) { - ((TextView) view).setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimension(R.dimen.file_picker_items_text_size)); + ((TextView) view).setTextSize(TypedValue.COMPLEX_UNIT_PX, + getResources().getDimension(R.dimen.file_picker_items_text_size)); } return viewHolder; } @Override - public void onClickOk(@NonNull View view) { + public void onClickOk(@NonNull final View view) { if (mode == MODE_NEW_FILE && getNewFileName().isEmpty()) { - if (mToast != null) mToast.cancel(); - mToast = Toast.makeText(getActivity(), R.string.file_name_empty_error, Toast.LENGTH_SHORT); + if (mToast != null) { + mToast.cancel(); + } + mToast = Toast.makeText(getActivity(), R.string.file_name_empty_error, + Toast.LENGTH_SHORT); mToast.show(); return; } @@ -116,13 +132,17 @@ public class FilePickerActivityHelper extends com.nononsenseapps.filepicker.File } @Override - protected boolean isItemVisible(@NonNull File file) { - if (file.isDirectory() && file.isHidden()) return true; + protected boolean isItemVisible(@NonNull final File file) { + if (file.isDirectory() && file.isHidden()) { + return true; + } return super.isItemVisible(file); } public File getBackTop() { - if (getArguments() == null) return Environment.getExternalStorageDirectory(); + if (getArguments() == null) { + return Environment.getExternalStorageDirectory(); + } final String path = getArguments().getString(KEY_START_PATH, "/"); if (path.contains(Environment.getExternalStorageDirectory().getPath())) { @@ -133,11 +153,13 @@ public class FilePickerActivityHelper extends com.nononsenseapps.filepicker.File } public boolean isBackTop() { - return compareFiles(mCurrentPath, getBackTop()) == 0 || compareFiles(mCurrentPath, new File("/")) == 0; + return compareFiles(mCurrentPath, + getBackTop()) == 0 || compareFiles(mCurrentPath, new File("/")) == 0; } @Override - public void onLoadFinished(Loader> loader, SortedList data) { + public void onLoadFinished(final Loader> loader, + final SortedList data) { super.onLoadFinished(loader, data); layoutManager.scrollToPosition(0); } diff --git a/app/src/main/java/org/schabi/newpipe/util/FilenameUtils.java b/app/src/main/java/org/schabi/newpipe/util/FilenameUtils.java index 37d94cd16..3179662ba 100644 --- a/app/src/main/java/org/schabi/newpipe/util/FilenameUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/FilenameUtils.java @@ -8,37 +8,44 @@ import org.schabi.newpipe.R; import java.util.regex.Pattern; -public class FilenameUtils { - +public final class FilenameUtils { private static final String CHARSET_MOST_SPECIAL = "[\\n\\r|?*<\":\\\\>/']+"; private static final String CHARSET_ONLY_LETTERS_AND_DIGITS = "[^\\w\\d]+"; + private FilenameUtils() { } + /** * #143 #44 #42 #22: make sure that the filename does not contain illegal chars. + * * @param context the context to retrieve strings and preferences from - * @param title the title to create a filename from + * @param title the title to create a filename from * @return the filename */ - public static String createFilename(Context context, String title) { - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + public static String createFilename(final Context context, final String title) { + SharedPreferences sharedPreferences = PreferenceManager + .getDefaultSharedPreferences(context); - final String charset_ld = context.getString(R.string.charset_letters_and_digits_value); - final String charset_ms = context.getString(R.string.charset_most_special_value); + final String charsetLd = context.getString(R.string.charset_letters_and_digits_value); + final String charsetMs = context.getString(R.string.charset_most_special_value); final String defaultCharset = context.getString(R.string.default_file_charset_value); - final String replacementChar = sharedPreferences.getString(context.getString(R.string.settings_file_replacement_character_key), "_"); - String selectedCharset = sharedPreferences.getString(context.getString(R.string.settings_file_charset_key), null); + final String replacementChar = sharedPreferences.getString( + context.getString(R.string.settings_file_replacement_character_key), "_"); + String selectedCharset = sharedPreferences.getString( + context.getString(R.string.settings_file_charset_key), null); final String charset; - if (selectedCharset == null || selectedCharset.isEmpty()) selectedCharset = defaultCharset; + if (selectedCharset == null || selectedCharset.isEmpty()) { + selectedCharset = defaultCharset; + } - if (selectedCharset.equals(charset_ld)) { + if (selectedCharset.equals(charsetLd)) { charset = CHARSET_ONLY_LETTERS_AND_DIGITS; - } else if (selectedCharset.equals(charset_ms)) { + } else if (selectedCharset.equals(charsetMs)) { charset = CHARSET_MOST_SPECIAL; } else { - charset = selectedCharset;// ¿is the user using a custom charset? + charset = selectedCharset; // Is the user using a custom charset? } Pattern pattern = Pattern.compile(charset); @@ -47,13 +54,15 @@ public class FilenameUtils { } /** - * Create a valid filename - * @param title the title to create a filename from + * Create a valid filename. + * + * @param title the title to create a filename from * @param invalidCharacters patter matching invalid characters - * @param replacementChar the replacement + * @param replacementChar the replacement * @return the filename */ - private static String createFilename(String title, Pattern invalidCharacters, String replacementChar) { + private static String createFilename(final String title, final Pattern invalidCharacters, + final String replacementChar) { return title.replaceAll(invalidCharacters.pattern(), replacementChar); } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/schabi/newpipe/util/FireTvUtils.java b/app/src/main/java/org/schabi/newpipe/util/FireTvUtils.java deleted file mode 100644 index 69666463e..000000000 --- a/app/src/main/java/org/schabi/newpipe/util/FireTvUtils.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.schabi.newpipe.util; - -import org.schabi.newpipe.App; - -public class FireTvUtils { - public static boolean isFireTv(){ - final String AMAZON_FEATURE_FIRE_TV = "amazon.hardware.fire_tv"; - return App.getApp().getPackageManager().hasSystemFeature(AMAZON_FEATURE_FIRE_TV); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/util/ImageDisplayConstants.java b/app/src/main/java/org/schabi/newpipe/util/ImageDisplayConstants.java index 9ee8a1095..37ebd636a 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ImageDisplayConstants.java +++ b/app/src/main/java/org/schabi/newpipe/util/ImageDisplayConstants.java @@ -8,11 +8,11 @@ import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer; import org.schabi.newpipe.R; -public class ImageDisplayConstants { +public final class ImageDisplayConstants { private static final int BITMAP_FADE_IN_DURATION_MILLIS = 250; /** - * Base display options + * This constant contains the base display options. */ private static final DisplayImageOptions BASE_DISPLAY_IMAGE_OPTIONS = new DisplayImageOptions.Builder() @@ -55,4 +55,6 @@ public class ImageDisplayConstants { .showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist) .showImageOnFail(R.drawable.dummy_thumbnail_playlist) .build(); + + private ImageDisplayConstants() { } } diff --git a/app/src/main/java/org/schabi/newpipe/util/InfoCache.java b/app/src/main/java/org/schabi/newpipe/util/InfoCache.java index afb7604c5..035416dcd 100644 --- a/app/src/main/java/org/schabi/newpipe/util/InfoCache.java +++ b/app/src/main/java/org/schabi/newpipe/util/InfoCache.java @@ -19,10 +19,11 @@ package org.schabi.newpipe.util; +import android.util.Log; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.collection.LruCache; -import android.util.Log; import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.extractor.Info; @@ -30,104 +31,121 @@ import org.schabi.newpipe.extractor.InfoItem; import java.util.Map; - public final class InfoCache { - private static final boolean DEBUG = MainActivity.DEBUG; private final String TAG = getClass().getSimpleName(); + private static final boolean DEBUG = MainActivity.DEBUG; - private static final InfoCache instance = new InfoCache(); + private static final InfoCache INSTANCE = new InfoCache(); private static final int MAX_ITEMS_ON_CACHE = 60; /** - * Trim the cache to this size + * Trim the cache to this size. */ private static final int TRIM_CACHE_TO = 30; - private static final LruCache lruCache = new LruCache<>(MAX_ITEMS_ON_CACHE); + private static final LruCache LRU_CACHE = new LruCache<>(MAX_ITEMS_ON_CACHE); private InfoCache() { - //no instance + // no instance } public static InfoCache getInstance() { - return instance; - } - - @Nullable - public Info getFromKey(int serviceId, @NonNull String url, @NonNull InfoItem.InfoType infoType) { - if (DEBUG) Log.d(TAG, "getFromKey() called with: serviceId = [" + serviceId + "], url = [" + url + "]"); - synchronized (lruCache) { - return getInfo(keyOf(serviceId, url, infoType)); - } - } - - public void putInfo(int serviceId, @NonNull String url, @NonNull Info info, @NonNull InfoItem.InfoType infoType) { - if (DEBUG) Log.d(TAG, "putInfo() called with: info = [" + info + "]"); - - final long expirationMillis = ServiceHelper.getCacheExpirationMillis(info.getServiceId()); - synchronized (lruCache) { - final CacheData data = new CacheData(info, expirationMillis); - lruCache.put(keyOf(serviceId, url, infoType), data); - } - } - - public void removeInfo(int serviceId, @NonNull String url, @NonNull InfoItem.InfoType infoType) { - if (DEBUG) Log.d(TAG, "removeInfo() called with: serviceId = [" + serviceId + "], url = [" + url + "]"); - synchronized (lruCache) { - lruCache.remove(keyOf(serviceId, url, infoType)); - } - } - - public void clearCache() { - if (DEBUG) Log.d(TAG, "clearCache() called"); - synchronized (lruCache) { - lruCache.evictAll(); - } - } - - public void trimCache() { - if (DEBUG) Log.d(TAG, "trimCache() called"); - synchronized (lruCache) { - removeStaleCache(); - lruCache.trimToSize(TRIM_CACHE_TO); - } - } - - public long getSize() { - synchronized (lruCache) { - return lruCache.size(); - } + return INSTANCE; } @NonNull - private static String keyOf(final int serviceId, @NonNull final String url, @NonNull InfoItem.InfoType infoType) { + private static String keyOf(final int serviceId, @NonNull final String url, + @NonNull final InfoItem.InfoType infoType) { return serviceId + url + infoType.toString(); } private static void removeStaleCache() { - for (Map.Entry entry : InfoCache.lruCache.snapshot().entrySet()) { + for (Map.Entry entry : InfoCache.LRU_CACHE.snapshot().entrySet()) { final CacheData data = entry.getValue(); if (data != null && data.isExpired()) { - InfoCache.lruCache.remove(entry.getKey()); + InfoCache.LRU_CACHE.remove(entry.getKey()); } } } @Nullable private static Info getInfo(@NonNull final String key) { - final CacheData data = InfoCache.lruCache.get(key); - if (data == null) return null; + final CacheData data = InfoCache.LRU_CACHE.get(key); + if (data == null) { + return null; + } if (data.isExpired()) { - InfoCache.lruCache.remove(key); + InfoCache.LRU_CACHE.remove(key); return null; } return data.info; } - final private static class CacheData { - final private long expireTimestamp; - final private Info info; + @Nullable + public Info getFromKey(final int serviceId, @NonNull final String url, + @NonNull final InfoItem.InfoType infoType) { + if (DEBUG) { + Log.d(TAG, "getFromKey() called with: " + + "serviceId = [" + serviceId + "], url = [" + url + "]"); + } + synchronized (LRU_CACHE) { + return getInfo(keyOf(serviceId, url, infoType)); + } + } + + public void putInfo(final int serviceId, @NonNull final String url, @NonNull final Info info, + @NonNull final InfoItem.InfoType infoType) { + if (DEBUG) { + Log.d(TAG, "putInfo() called with: info = [" + info + "]"); + } + + final long expirationMillis = ServiceHelper.getCacheExpirationMillis(info.getServiceId()); + synchronized (LRU_CACHE) { + final CacheData data = new CacheData(info, expirationMillis); + LRU_CACHE.put(keyOf(serviceId, url, infoType), data); + } + } + + public void removeInfo(final int serviceId, @NonNull final String url, + @NonNull final InfoItem.InfoType infoType) { + if (DEBUG) { + Log.d(TAG, "removeInfo() called with: " + + "serviceId = [" + serviceId + "], url = [" + url + "]"); + } + synchronized (LRU_CACHE) { + LRU_CACHE.remove(keyOf(serviceId, url, infoType)); + } + } + + public void clearCache() { + if (DEBUG) { + Log.d(TAG, "clearCache() called"); + } + synchronized (LRU_CACHE) { + LRU_CACHE.evictAll(); + } + } + + public void trimCache() { + if (DEBUG) { + Log.d(TAG, "trimCache() called"); + } + synchronized (LRU_CACHE) { + removeStaleCache(); + LRU_CACHE.trimToSize(TRIM_CACHE_TO); + } + } + + public long getSize() { + synchronized (LRU_CACHE) { + return LRU_CACHE.size(); + } + } + + private static final class CacheData { + private final long expireTimestamp; + private final Info info; private CacheData(@NonNull final Info info, final long timeoutMillis) { this.expireTimestamp = System.currentTimeMillis() + timeoutMillis; diff --git a/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java b/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java index 18c95e394..b676a1a88 100644 --- a/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java +++ b/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java @@ -7,23 +7,28 @@ import org.schabi.newpipe.R; /** * Created by Chrsitian Schabesberger on 28.09.17. * KioskTranslator.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 . + *

*/ -public class KioskTranslator { - public static String getTranslatedKioskName(String kioskId, Context c) { +public final class KioskTranslator { + private KioskTranslator() { } + + public static String getTranslatedKioskName(final String kioskId, final Context c) { switch (kioskId) { case "Trending": return c.getString(R.string.trending); @@ -44,22 +49,19 @@ public class KioskTranslator { } } - public static int getKioskIcons(String kioskId, Context c) { - switch(kioskId) { + public static int getKioskIcon(final String kioskId, final Context c) { + switch (kioskId) { case "Trending": - return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot); case "Top 50": - return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot); case "New & hot": - return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot); + case "conferences": + return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_kiosk_hot); case "Local": return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_kiosk_local); case "Recently added": return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_kiosk_recent); case "Most liked": - return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.thumbs_up); - case "conferences": - return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot); + return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_thumb_up); default: return 0; } diff --git a/app/src/main/java/org/schabi/newpipe/util/KoreUtil.java b/app/src/main/java/org/schabi/newpipe/util/KoreUtil.java new file mode 100644 index 000000000..983fe689b --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/KoreUtil.java @@ -0,0 +1,29 @@ +package org.schabi.newpipe.util; + + +import android.content.Context; +import android.content.DialogInterface; + +import androidx.appcompat.app.AlertDialog; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.ServiceList; + +public final class KoreUtil { + private KoreUtil() { } + + public static boolean isServiceSupportedByKore(final int serviceId) { + return (serviceId == ServiceList.YouTube.getServiceId() + || serviceId == ServiceList.SoundCloud.getServiceId()); + } + + public static void showInstallKoreDialog(final Context context) { + final AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setMessage(R.string.kore_not_found) + .setPositiveButton(R.string.install, (DialogInterface dialog, int which) -> + NavigationHelper.installKore(context)) + .setNegativeButton(R.string.cancel, (DialogInterface dialog, int which) -> { + }); + builder.create().show(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/util/LayoutManagerSmoothScroller.java b/app/src/main/java/org/schabi/newpipe/util/LayoutManagerSmoothScroller.java index df7549c47..2ca128409 100644 --- a/app/src/main/java/org/schabi/newpipe/util/LayoutManagerSmoothScroller.java +++ b/app/src/main/java/org/schabi/newpipe/util/LayoutManagerSmoothScroller.java @@ -2,35 +2,38 @@ package org.schabi.newpipe.util; import android.content.Context; import android.graphics.PointF; + import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearSmoothScroller; import androidx.recyclerview.widget.RecyclerView; public class LayoutManagerSmoothScroller extends LinearLayoutManager { - - public LayoutManagerSmoothScroller(Context context) { + public LayoutManagerSmoothScroller(final Context context) { super(context, VERTICAL, false); } - public LayoutManagerSmoothScroller(Context context, int orientation, boolean reverseLayout) { + public LayoutManagerSmoothScroller(final Context context, final int orientation, + final boolean reverseLayout) { super(context, orientation, reverseLayout); } @Override - public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) { - RecyclerView.SmoothScroller smoothScroller = new TopSnappedSmoothScroller(recyclerView.getContext()); + public void smoothScrollToPosition(final RecyclerView recyclerView, + final RecyclerView.State state, final int position) { + RecyclerView.SmoothScroller smoothScroller + = new TopSnappedSmoothScroller(recyclerView.getContext()); smoothScroller.setTargetPosition(position); startSmoothScroll(smoothScroller); } private class TopSnappedSmoothScroller extends LinearSmoothScroller { - public TopSnappedSmoothScroller(Context context) { + TopSnappedSmoothScroller(final Context context) { super(context); } @Override - public PointF computeScrollVectorForPosition(int targetPosition) { + public PointF computeScrollVectorForPosition(final int targetPosition) { return LayoutManagerSmoothScroller.this .computeScrollVectorForPosition(targetPosition); } @@ -40,4 +43,4 @@ public class LayoutManagerSmoothScroller extends LinearLayoutManager { return SNAP_TO_START; } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java index d878a2b87..47486ae49 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java @@ -4,6 +4,8 @@ import android.content.Context; import android.content.SharedPreferences; import android.net.ConnectivityManager; import android.preference.PreferenceManager; + +import androidx.annotation.Nullable; import androidx.annotation.StringRes; import org.schabi.newpipe.R; @@ -17,26 +19,31 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; -@SuppressWarnings("WeakerAccess") public final class ListHelper { - // Video format in order of quality. 0=lowest quality, n=highest quality private static final List VIDEO_FORMAT_QUALITY_RANKING = - Arrays.asList(MediaFormat.v3GPP, MediaFormat.WEBM, MediaFormat.MPEG_4); + Arrays.asList(MediaFormat.v3GPP, MediaFormat.WEBM, MediaFormat.MPEG_4); // Audio format in order of quality. 0=lowest quality, n=highest quality private static final List AUDIO_FORMAT_QUALITY_RANKING = - Arrays.asList(MediaFormat.MP3, MediaFormat.WEBMA, MediaFormat.M4A); + Arrays.asList(MediaFormat.MP3, MediaFormat.WEBMA, MediaFormat.M4A); // Audio format in order of efficiency. 0=most efficient, n=least efficient private static final List AUDIO_FORMAT_EFFICIENCY_RANKING = Arrays.asList(MediaFormat.WEBMA, MediaFormat.M4A, MediaFormat.MP3); - private static final List HIGH_RESOLUTION_LIST = Arrays.asList("1440p", "2160p", "1440p60", "2160p60"); + private static final List HIGH_RESOLUTION_LIST + = Arrays.asList("1440p", "2160p", "1440p60", "2160p60"); + + private ListHelper() { } /** * @see #getDefaultResolutionIndex(String, String, MediaFormat, List) + * @param context Android app context + * @param videoStreams list of the video streams to check + * @return index of the video stream with the default index */ - public static int getDefaultResolutionIndex(Context context, List videoStreams) { + public static int getDefaultResolutionIndex(final Context context, + final List videoStreams) { String defaultResolution = computeDefaultResolution(context, R.string.default_resolution_key, R.string.default_resolution_value); return getDefaultResolutionWithDefaultFormat(context, defaultResolution, videoStreams); @@ -44,15 +51,25 @@ public final class ListHelper { /** * @see #getDefaultResolutionIndex(String, String, MediaFormat, List) + * @param context Android app context + * @param videoStreams list of the video streams to check + * @param defaultResolution the default resolution to look for + * @return index of the video stream with the default index */ - public static int getResolutionIndex(Context context, List videoStreams, String defaultResolution) { + public static int getResolutionIndex(final Context context, + final List videoStreams, + final String defaultResolution) { return getDefaultResolutionWithDefaultFormat(context, defaultResolution, videoStreams); } /** * @see #getDefaultResolutionIndex(String, String, MediaFormat, List) + * @param context Android app context + * @param videoStreams list of the video streams to check + * @return index of the video stream with the default index */ - public static int getPopupDefaultResolutionIndex(Context context, List videoStreams) { + public static int getPopupDefaultResolutionIndex(final Context context, + final List videoStreams) { String defaultResolution = computeDefaultResolution(context, R.string.default_popup_resolution_key, R.string.default_popup_resolution_value); return getDefaultResolutionWithDefaultFormat(context, defaultResolution, videoStreams); @@ -60,12 +77,19 @@ public final class ListHelper { /** * @see #getDefaultResolutionIndex(String, String, MediaFormat, List) + * @param context Android app context + * @param videoStreams list of the video streams to check + * @param defaultResolution the default resolution to look for + * @return index of the video stream with the default index */ - public static int getPopupResolutionIndex(Context context, List videoStreams, String defaultResolution) { + public static int getPopupResolutionIndex(final Context context, + final List videoStreams, + final String defaultResolution) { return getDefaultResolutionWithDefaultFormat(context, defaultResolution, videoStreams); } - public static int getDefaultAudioFormat(Context context, List audioStreams) { + public static int getDefaultAudioFormat(final Context context, + final List audioStreams) { MediaFormat defaultFormat = getDefaultFormat(context, R.string.default_audio_format_key, R.string.default_audio_format_value); @@ -79,8 +103,8 @@ public final class ListHelper { } /** - * Join the two lists of video streams (video_only and normal videos), and sort them according with default format - * chosen by the user + * Join the two lists of video streams (video_only and normal videos), + * and sort them according with default format chosen by the user. * * @param context context to search for the format to give preference * @param videoStreams normal videos list @@ -88,20 +112,28 @@ public final class ListHelper { * @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest * @return the sorted list */ - public static List getSortedStreamVideosList(Context context, List videoStreams, List videoOnlyStreams, boolean ascendingOrder) { + public static List getSortedStreamVideosList(final Context context, + final List videoStreams, + final List + videoOnlyStreams, + final boolean ascendingOrder) { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); - boolean showHigherResolutions = preferences.getBoolean(context.getString(R.string.show_higher_resolutions_key), false); - MediaFormat defaultFormat = getDefaultFormat(context, R.string.default_video_format_key, R.string.default_video_format_value); + boolean showHigherResolutions = preferences.getBoolean( + context.getString(R.string.show_higher_resolutions_key), false); + MediaFormat defaultFormat = getDefaultFormat(context, R.string.default_video_format_key, + R.string.default_video_format_value); - return getSortedStreamVideosList(defaultFormat, showHigherResolutions, videoStreams, videoOnlyStreams, ascendingOrder); + return getSortedStreamVideosList(defaultFormat, showHigherResolutions, videoStreams, + videoOnlyStreams, ascendingOrder); } /*////////////////////////////////////////////////////////////////////////// // Utils //////////////////////////////////////////////////////////////////////////*/ - private static String computeDefaultResolution(Context context, int key, int value) { + private static String computeDefaultResolution(final Context context, final int key, + final int value) { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); // Load the prefered resolution otherwise the best available @@ -110,7 +142,8 @@ public final class ListHelper { : context.getString(R.string.best_resolution_key); String maxResolution = getResolutionLimit(context); - if (maxResolution != null && (resolution.equals(context.getString(R.string.best_resolution_key)) + if (maxResolution != null + && (resolution.equals(context.getString(R.string.best_resolution_key)) || compareVideoStreamResolution(maxResolution, resolution) < 1)) { resolution = maxResolution; } @@ -119,20 +152,29 @@ public final class ListHelper { /** * Return the index of the default stream in the list, based on the parameters - * defaultResolution and defaultFormat + * defaultResolution and defaultFormat. * + * @param defaultResolution the default resolution to look for + * @param bestResolutionKey key of the best resolution + * @param defaultFormat the default fomat to look for + * @param videoStreams list of the video streams to check * @return index of the default resolution&format */ - static int getDefaultResolutionIndex(String defaultResolution, String bestResolutionKey, - MediaFormat defaultFormat, List videoStreams) { - if (videoStreams == null || videoStreams.isEmpty()) return -1; + static int getDefaultResolutionIndex(final String defaultResolution, + final String bestResolutionKey, + final MediaFormat defaultFormat, + final List videoStreams) { + if (videoStreams == null || videoStreams.isEmpty()) { + return -1; + } sortStreamList(videoStreams, false); if (defaultResolution.equals(bestResolutionKey)) { return 0; } - int defaultStreamIndex = getVideoStreamIndex(defaultResolution, defaultFormat, videoStreams); + int defaultStreamIndex + = getVideoStreamIndex(defaultResolution, defaultFormat, videoStreams); // this is actually an error, // but maybe there is really no stream fitting to the default value. @@ -143,39 +185,53 @@ public final class ListHelper { } /** - * Join the two lists of video streams (video_only and normal videos), and sort them according with default format - * chosen by the user + * Join the two lists of video streams (video_only and normal videos), + * and sort them according with default format chosen by the user. * - * @param defaultFormat format to give preference + * @param defaultFormat format to give preference * @param showHigherResolutions show >1080p resolutions * @param videoStreams normal videos list * @param videoOnlyStreams video only stream list - * @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest @return the sorted list + * @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest * @return the sorted list */ - static List getSortedStreamVideosList(MediaFormat defaultFormat, boolean showHigherResolutions, List videoStreams, List videoOnlyStreams, boolean ascendingOrder) { + static List getSortedStreamVideosList(final MediaFormat defaultFormat, + final boolean showHigherResolutions, + final List videoStreams, + final List videoOnlyStreams, + final boolean ascendingOrder) { ArrayList retList = new ArrayList<>(); HashMap hashMap = new HashMap<>(); if (videoOnlyStreams != null) { for (VideoStream stream : videoOnlyStreams) { - if (!showHigherResolutions && HIGH_RESOLUTION_LIST.contains(stream.getResolution())) continue; + if (!showHigherResolutions + && HIGH_RESOLUTION_LIST.contains(stream.getResolution())) { + continue; + } retList.add(stream); } } if (videoStreams != null) { for (VideoStream stream : videoStreams) { - if (!showHigherResolutions && HIGH_RESOLUTION_LIST.contains(stream.getResolution())) continue; + if (!showHigherResolutions + && HIGH_RESOLUTION_LIST.contains(stream.getResolution())) { + continue; + } retList.add(stream); } } // Add all to the hashmap - for (VideoStream videoStream : retList) hashMap.put(videoStream.getResolution(), videoStream); + for (VideoStream videoStream : retList) { + hashMap.put(videoStream.getResolution(), videoStream); + } // Override the values when the key == resolution, with the defaultFormat for (VideoStream videoStream : retList) { - if (videoStream.getFormat() == defaultFormat) hashMap.put(videoStream.getResolution(), videoStream); + if (videoStream.getFormat() == defaultFormat) { + hashMap.put(videoStream.getResolution(), videoStream); + } } retList.clear(); @@ -203,7 +259,8 @@ public final class ListHelper { * @param videoStreams list that the sorting will be applied * @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest */ - private static void sortStreamList(List videoStreams, final boolean ascendingOrder) { + private static void sortStreamList(final List videoStreams, + final boolean ascendingOrder) { Collections.sort(videoStreams, (o1, o2) -> { int result = compareVideoStreamResolution(o1, o2); return result == 0 ? 0 : (ascendingOrder ? result : -result); @@ -211,21 +268,23 @@ public final class ListHelper { } /** - * Get the audio from the list with the highest quality. Format will be ignored if it yields - * no results. + * Get the audio from the list with the highest quality. + * Format will be ignored if it yields no results. * - * @param audioStreams list the audio streams - * @return index of the audio with the highest average bitrate of the default format + * @param format The target format type or null if it doesn't matter + * @param audioStreams List of audio streams + * @return Index of audio stream that produces the most compact results or -1 if not found */ - static int getHighestQualityAudioIndex(MediaFormat format, List audioStreams) { + static int getHighestQualityAudioIndex(@Nullable MediaFormat format, + final List audioStreams) { int result = -1; if (audioStreams != null) { - while(result == -1) { + while (result == -1) { AudioStream prevStream = null; for (int idx = 0; idx < audioStreams.size(); idx++) { AudioStream stream = audioStreams.get(idx); - if ((format == null || stream.getFormat() == format) && - (prevStream == null || compareAudioStreamBitrate(prevStream, stream, + if ((format == null || stream.getFormat() == format) + && (prevStream == null || compareAudioStreamBitrate(prevStream, stream, AUDIO_FORMAT_QUALITY_RANKING) < 0)) { prevStream = stream; result = idx; @@ -241,22 +300,23 @@ public final class ListHelper { } /** - * Get the audio from the list with the lowest bitrate and efficient format. Format will be - * ignored if it yields no results. + * Get the audio from the list with the lowest bitrate and most efficient format. + * Format will be ignored if it yields no results. * - * @param format The target format type or null if it doesn't matter - * @param audioStreams list the audio streams - * @return index of the audio stream that can produce the most compact results or -1 if not found. + * @param format The target format type or null if it doesn't matter + * @param audioStreams List of audio streams + * @return Index of audio stream that produces the most compact results or -1 if not found */ - static int getMostCompactAudioIndex(MediaFormat format, List audioStreams) { + static int getMostCompactAudioIndex(@Nullable MediaFormat format, + final List audioStreams) { int result = -1; if (audioStreams != null) { - while(result == -1) { + while (result == -1) { AudioStream prevStream = null; for (int idx = 0; idx < audioStreams.size(); idx++) { AudioStream stream = audioStreams.get(idx); - if ((format == null || stream.getFormat() == format) && - (prevStream == null || compareAudioStreamBitrate(prevStream, stream, + if ((format == null || stream.getFormat() == format) + && (prevStream == null || compareAudioStreamBitrate(prevStream, stream, AUDIO_FORMAT_EFFICIENCY_RANKING) > 0)) { prevStream = stream; result = idx; @@ -273,16 +333,25 @@ public final class ListHelper { /** * Locates a possible match for the given resolution and format in the provided list. - * In this order: - * 1. Find a format and resolution match - * 2. Find a format and resolution match and ignore the refresh - * 3. Find a resolution match - * 4. Find a resolution match and ignore the refresh - * 5. Find a resolution just below the requested resolution and ignore the refresh - * 6. Give up + * + *

In this order:

+ * + *
    + *
  1. Find a format and resolution match
  2. + *
  3. Find a format and resolution match and ignore the refresh
  4. + *
  5. Find a resolution match
  6. + *
  7. Find a resolution match and ignore the refresh
  8. + *
  9. Find a resolution just below the requested resolution and ignore the refresh
  10. + *
  11. Give up
  12. + *
+ * + * @param targetResolution the resolution to look for + * @param targetFormat the format to look for + * @param videoStreams the available video streams + * @return the index of the prefered video stream */ - static int getVideoStreamIndex(String targetResolution, MediaFormat targetFormat, - List videoStreams) { + static int getVideoStreamIndex(final String targetResolution, final MediaFormat targetFormat, + final List videoStreams) { int fullMatchIndex = -1; int fullMatchNoRefreshIndex = -1; int resMatchOnlyIndex = -1; @@ -307,11 +376,13 @@ public final class ListHelper { resMatchOnlyIndex = idx; } - if (resMatchOnlyNoRefreshIndex == -1 && resolutionNoRefresh.equals(targetResolutionNoRefresh)) { + if (resMatchOnlyNoRefreshIndex == -1 + && resolutionNoRefresh.equals(targetResolutionNoRefresh)) { resMatchOnlyNoRefreshIndex = idx; } - if (lowerResMatchNoRefreshIndex == -1 && compareVideoStreamResolution(resolutionNoRefresh, targetResolutionNoRefresh) < 0) { + if (lowerResMatchNoRefreshIndex == -1 && compareVideoStreamResolution( + resolutionNoRefresh, targetResolutionNoRefresh) < 0) { lowerResMatchNoRefreshIndex = idx; } } @@ -332,30 +403,44 @@ public final class ListHelper { } /** - * Fetches the desired resolution or returns the default if it is not found. The resolution - * will be reduced if video chocking is active. + * Fetches the desired resolution or returns the default if it is not found. + * The resolution will be reduced if video chocking is active. + * + * @param context Android app context + * @param defaultResolution the default resolution + * @param videoStreams the list of video streams to check + * @return the index of the prefered video stream */ - private static int getDefaultResolutionWithDefaultFormat(Context context, String defaultResolution, List videoStreams) { - MediaFormat defaultFormat = getDefaultFormat(context, R.string.default_video_format_key, R.string.default_video_format_value); - return getDefaultResolutionIndex(defaultResolution, context.getString(R.string.best_resolution_key), defaultFormat, videoStreams); + private static int getDefaultResolutionWithDefaultFormat(final Context context, + final String defaultResolution, + final List videoStreams) { + MediaFormat defaultFormat = getDefaultFormat(context, R.string.default_video_format_key, + R.string.default_video_format_value); + return getDefaultResolutionIndex(defaultResolution, + context.getString(R.string.best_resolution_key), defaultFormat, videoStreams); } - private static MediaFormat getDefaultFormat(Context context, @StringRes int defaultFormatKey, @StringRes int defaultFormatValueKey) { + private static MediaFormat getDefaultFormat(final Context context, + @StringRes final int defaultFormatKey, + @StringRes final int defaultFormatValueKey) { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); String defaultFormat = context.getString(defaultFormatValueKey); - String defaultFormatString = preferences.getString(context.getString(defaultFormatKey), defaultFormat); + String defaultFormatString = preferences.getString( + context.getString(defaultFormatKey), defaultFormat); MediaFormat defaultMediaFormat = getMediaFormatFromKey(context, defaultFormatString); if (defaultMediaFormat == null) { - preferences.edit().putString(context.getString(defaultFormatKey), defaultFormat).apply(); + preferences.edit().putString(context.getString(defaultFormatKey), defaultFormat) + .apply(); defaultMediaFormat = getMediaFormatFromKey(context, defaultFormat); } return defaultMediaFormat; } - private static MediaFormat getMediaFormatFromKey(Context context, String formatKey) { + private static MediaFormat getMediaFormatFromKey(final Context context, + final String formatKey) { MediaFormat format = null; if (formatKey.equals(context.getString(R.string.video_webm_key))) { format = MediaFormat.WEBM; @@ -372,8 +457,9 @@ public final class ListHelper { } // Compares the quality of two audio streams - private static int compareAudioStreamBitrate(AudioStream streamA, AudioStream streamB, - List formatRanking) { + private static int compareAudioStreamBitrate(final AudioStream streamA, + final AudioStream streamB, + final List formatRanking) { if (streamA == null) { return -1; } @@ -388,10 +474,11 @@ public final class ListHelper { } // Same bitrate and format - return formatRanking.indexOf(streamA.getFormat()) - formatRanking.indexOf(streamB.getFormat()); + return formatRanking.indexOf(streamA.getFormat()) + - formatRanking.indexOf(streamB.getFormat()); } - private static int compareVideoStreamResolution(String r1, String r2) { + private static int compareVideoStreamResolution(final String r1, final String r2) { int res1 = Integer.parseInt(r1.replaceAll("0p\\d+$", "1") .replaceAll("[^\\d.]", "")); int res2 = Integer.parseInt(r2.replaceAll("0p\\d+$", "1") @@ -400,7 +487,8 @@ public final class ListHelper { } // Compares the quality of two video streams. - private static int compareVideoStreamResolution(VideoStream streamA, VideoStream streamB) { + private static int compareVideoStreamResolution(final VideoStream streamA, + final VideoStream streamB) { if (streamA == null) { return -1; } @@ -408,27 +496,29 @@ public final class ListHelper { return 1; } - int resComp = compareVideoStreamResolution(streamA.getResolution(), streamB.getResolution()); + int resComp = compareVideoStreamResolution(streamA.getResolution(), + streamB.getResolution()); if (resComp != 0) { return resComp; } // Same bitrate and format - return ListHelper.VIDEO_FORMAT_QUALITY_RANKING.indexOf(streamA.getFormat()) - ListHelper.VIDEO_FORMAT_QUALITY_RANKING.indexOf(streamB.getFormat()); + return ListHelper.VIDEO_FORMAT_QUALITY_RANKING.indexOf(streamA.getFormat()) + - ListHelper.VIDEO_FORMAT_QUALITY_RANKING.indexOf(streamB.getFormat()); } - - private static boolean isLimitingDataUsage(Context context) { + private static boolean isLimitingDataUsage(final Context context) { return getResolutionLimit(context) != null; } /** - * The maximum resolution allowed + * The maximum resolution allowed. + * * @param context App context * @return maximum resolution allowed or null if there is no maximum */ - private static String getResolutionLimit(Context context) { + private static String getResolutionLimit(final Context context) { String resolutionLimit = null; if (isMeteredNetwork(context)) { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); @@ -442,13 +532,16 @@ public final class ListHelper { /** * The current network is metered (like mobile data)? + * * @param context App context * @return {@code true} if connected to a metered network */ - public static boolean isMeteredNetwork(Context context) - { - ConnectivityManager manager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); - if (manager == null || manager.getActiveNetworkInfo() == null) return false; + public static boolean isMeteredNetwork(final Context context) { + ConnectivityManager manager + = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + if (manager == null || manager.getActiveNetworkInfo() == null) { + return false; + } return manager.isActiveNetworkMetered(); } diff --git a/app/src/main/java/org/schabi/newpipe/util/Localization.java b/app/src/main/java/org/schabi/newpipe/util/Localization.java index 9274df848..7e336f02d 100644 --- a/app/src/main/java/org/schabi/newpipe/util/Localization.java +++ b/app/src/main/java/org/schabi/newpipe/util/Localization.java @@ -1,15 +1,26 @@ package org.schabi.newpipe.util; +import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.content.res.Resources; import android.preference.PreferenceManager; import android.text.TextUtils; +import android.util.DisplayMetrics; + +import androidx.annotation.NonNull; +import androidx.annotation.PluralsRes; +import androidx.annotation.StringRes; import org.ocpsoft.prettytime.PrettyTime; import org.ocpsoft.prettytime.units.Decade; import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.localization.ContentCountry; +import java.math.BigDecimal; +import java.math.RoundingMode; import java.text.DateFormat; import java.text.NumberFormat; import java.util.Arrays; @@ -18,9 +29,6 @@ import java.util.Date; import java.util.List; import java.util.Locale; -import androidx.annotation.NonNull; -import androidx.annotation.PluralsRes; -import androidx.annotation.StringRes; /* * Created by chschtsch on 12/29/15. @@ -42,16 +50,15 @@ import androidx.annotation.StringRes; * along with NewPipe. If not, see . */ -public class Localization { +public final class Localization { - private static PrettyTime prettyTime; private static final String DOT_SEPARATOR = " • "; + private static PrettyTime prettyTime; - private Localization() { - } + private Localization() { } - public static void init() { - initPrettyTime(); + public static void init(final Context context) { + initPrettyTime(context); } @NonNull @@ -61,7 +68,9 @@ public class Localization { @NonNull public static String concatenateStrings(final List strings) { - if (strings.isEmpty()) return ""; + if (strings.isEmpty()) { + return ""; + } final StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(strings.get(0)); @@ -76,31 +85,41 @@ public class Localization { return stringBuilder.toString(); } - public static org.schabi.newpipe.extractor.localization.Localization getPreferredLocalization(final Context context) { + public static org.schabi.newpipe.extractor.localization.Localization getPreferredLocalization( + final Context context) { final String contentLanguage = PreferenceManager .getDefaultSharedPreferences(context) - .getString(context.getString(R.string.content_language_key), context.getString(R.string.default_language_value)); - return org.schabi.newpipe.extractor.localization.Localization.fromLocalizationCode(contentLanguage); + .getString(context.getString(R.string.content_language_key), + context.getString(R.string.default_localization_key)); + if (contentLanguage.equals(context.getString(R.string.default_localization_key))) { + return org.schabi.newpipe.extractor.localization.Localization + .fromLocale(Locale.getDefault()); + } + return org.schabi.newpipe.extractor.localization.Localization + .fromLocalizationCode(contentLanguage); } public static ContentCountry getPreferredContentCountry(final Context context) { - final String contentCountry = PreferenceManager - .getDefaultSharedPreferences(context) - .getString(context.getString(R.string.content_country_key), context.getString(R.string.default_country_value)); + final String contentCountry = PreferenceManager.getDefaultSharedPreferences(context) + .getString(context.getString(R.string.content_country_key), + context.getString(R.string.default_localization_key)); + if (contentCountry.equals(context.getString(R.string.default_localization_key))) { + return new ContentCountry(Locale.getDefault().getCountry()); + } return new ContentCountry(contentCountry); } - public static Locale getPreferredLocale(Context context) { + public static Locale getPreferredLocale(final 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)); + context.getString(R.string.default_localization_key)); try { if (languageCode.length() == 2) { return new Locale(languageCode); } else if (languageCode.contains("_")) { - String country = languageCode.substring(languageCode.indexOf("_"), languageCode.length()); + String country = languageCode.substring(languageCode.indexOf("_")); return new Locale(languageCode.substring(0, 2), country); } } catch (Exception ignored) { @@ -109,83 +128,125 @@ public class Localization { return Locale.getDefault(); } - public static String localizeNumber(Context context, long number) { - Locale locale = getPreferredLocale(context); - NumberFormat nf = NumberFormat.getInstance(locale); + public static String localizeNumber(final Context context, final long number) { + return localizeNumber(context, (double) number); + } + + public static String localizeNumber(final Context context, final double number) { + NumberFormat nf = NumberFormat.getInstance(getAppLocale(context)); return nf.format(number); } - public static String formatDate(Date date) { - return DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.getDefault()).format(date); + public static String formatDate(final Date date, final Context context) { + return DateFormat.getDateInstance(DateFormat.MEDIUM, getAppLocale(context)).format(date); } - public static String localizeUploadDate(Context context, Date date) { - return context.getString(R.string.upload_date_text, formatDate(date)); + @SuppressLint("StringFormatInvalid") + public static String localizeUploadDate(final Context context, final Date date) { + return context.getString(R.string.upload_date_text, formatDate(date, context)); } - public static String localizeViewCount(Context context, long viewCount) { - return getQuantity(context, R.plurals.views, R.string.no_views, viewCount, localizeNumber(context, viewCount)); + public static String localizeViewCount(final Context context, final long viewCount) { + return getQuantity(context, R.plurals.views, R.string.no_views, viewCount, + localizeNumber(context, viewCount)); } - public static String localizeSubscribersCount(Context context, long subscriberCount) { - return getQuantity(context, R.plurals.subscribers, R.string.no_subscribers, subscriberCount, localizeNumber(context, subscriberCount)); - } - - public static String localizeStreamCount(Context context, long streamCount) { - return getQuantity(context, R.plurals.videos, R.string.no_videos, streamCount, localizeNumber(context, streamCount)); - } - - public static String shortCount(Context context, long count) { - if (count >= 1000000000) { - return Long.toString(count / 1000000000) + context.getString(R.string.short_billion); - } else if (count >= 1000000) { - return Long.toString(count / 1000000) + context.getString(R.string.short_million); - } else if (count >= 1000) { - return Long.toString(count / 1000) + context.getString(R.string.short_thousand); - } else { - return Long.toString(count); + public static String localizeStreamCount(final Context context, final long streamCount) { + switch ((int) streamCount) { + case (int) ListExtractor.ITEM_COUNT_UNKNOWN: + return ""; + case (int) ListExtractor.ITEM_COUNT_INFINITE: + return context.getResources().getString(R.string.infinite_videos); + case (int) ListExtractor.ITEM_COUNT_MORE_THAN_100: + return context.getResources().getString(R.string.more_than_100_videos); + default: + return getQuantity(context, R.plurals.videos, R.string.no_videos, streamCount, + localizeNumber(context, streamCount)); } } - public static String listeningCount(Context context, long listeningCount) { - return getQuantity(context, R.plurals.listening, R.string.no_one_listening, listeningCount, shortCount(context, listeningCount)); + public static String localizeStreamCountMini(final Context context, final long streamCount) { + switch ((int) streamCount) { + case (int) ListExtractor.ITEM_COUNT_UNKNOWN: + return ""; + case (int) ListExtractor.ITEM_COUNT_INFINITE: + return context.getResources().getString(R.string.infinite_videos_mini); + case (int) ListExtractor.ITEM_COUNT_MORE_THAN_100: + return context.getResources().getString(R.string.more_than_100_videos_mini); + default: + return String.valueOf(streamCount); + } } - public static String watchingCount(Context context, long watchingCount) { - return getQuantity(context, R.plurals.watching, R.string.no_one_watching, watchingCount, shortCount(context, watchingCount)); + public static String localizeWatchingCount(final Context context, final long watchingCount) { + return getQuantity(context, R.plurals.watching, R.string.no_one_watching, watchingCount, + localizeNumber(context, watchingCount)); } - public static String shortViewCount(Context context, long viewCount) { - return getQuantity(context, R.plurals.views, R.string.no_views, viewCount, shortCount(context, viewCount)); + public static String shortCount(final Context context, final long count) { + double value = (double) count; + if (count >= 1000000000) { + return localizeNumber(context, round(value / 1000000000, 1)) + + context.getString(R.string.short_billion); + } else if (count >= 1000000) { + return localizeNumber(context, round(value / 1000000, 1)) + + context.getString(R.string.short_million); + } else if (count >= 1000) { + return localizeNumber(context, round(value / 1000, 1)) + + context.getString(R.string.short_thousand); + } else { + return localizeNumber(context, value); + } } - public static String shortSubscriberCount(Context context, long subscriberCount) { - return getQuantity(context, R.plurals.subscribers, R.string.no_subscribers, subscriberCount, shortCount(context, subscriberCount)); + public static String listeningCount(final Context context, final long listeningCount) { + return getQuantity(context, R.plurals.listening, R.string.no_one_listening, listeningCount, + shortCount(context, listeningCount)); } - private static String getQuantity(Context context, @PluralsRes int pluralId, @StringRes int zeroCaseStringId, long count, String formattedCount) { - if (count == 0) return context.getString(zeroCaseStringId); + public static String shortWatchingCount(final Context context, final long watchingCount) { + return getQuantity(context, R.plurals.watching, R.string.no_one_watching, watchingCount, + shortCount(context, watchingCount)); + } - // As we use the already formatted count, is not the responsibility of this method handle long numbers - // (it probably will fall in the "other" category, or some language have some specific rule... then we have to change it) - int safeCount = count > Integer.MAX_VALUE ? Integer.MAX_VALUE : count < Integer.MIN_VALUE ? Integer.MIN_VALUE : (int) count; + public static String shortViewCount(final Context context, final long viewCount) { + return getQuantity(context, R.plurals.views, R.string.no_views, viewCount, + shortCount(context, viewCount)); + } + + public static String shortSubscriberCount(final Context context, final long subscriberCount) { + return getQuantity(context, R.plurals.subscribers, R.string.no_subscribers, subscriberCount, + shortCount(context, subscriberCount)); + } + + private static String getQuantity(final Context context, @PluralsRes final int pluralId, + @StringRes final int zeroCaseStringId, final long count, + final String formattedCount) { + if (count == 0) { + return context.getString(zeroCaseStringId); + } + + // As we use the already formatted count + // is not the responsibility of this method handle long numbers + // (it probably will fall in the "other" category, + // or some language have some specific rule... then we have to change it) + int safeCount = count > Integer.MAX_VALUE ? Integer.MAX_VALUE : count < Integer.MIN_VALUE + ? Integer.MIN_VALUE : (int) count; return context.getResources().getQuantityString(pluralId, safeCount, formattedCount); } - public static String getDurationString(long duration) { - if (duration < 0) { - duration = 0; - } - String output; - long days = duration / (24 * 60 * 60L); /* greater than a day */ - duration %= (24 * 60 * 60L); - long hours = duration / (60 * 60L); /* greater than an hour */ - duration %= (60 * 60L); - long minutes = duration / 60L; - long seconds = duration % 60L; + public static String getDurationString(final long duration) { + final String output; - //handle days - if (days > 0) { + final long days = duration / (24 * 60 * 60L); /* greater than a day */ + final long hours = duration % (24 * 60 * 60L) / (60 * 60L); /* greater than an hour */ + final long minutes = duration % (24 * 60 * 60L) % (60 * 60L) / 60L; + final long seconds = duration % 60L; + + if (duration < 0) { + output = "0:00"; + } else if (days > 0) { + //handle days output = String.format(Locale.US, "%d:%02d:%02d:%02d", days, hours, minutes, seconds); } else if (hours > 0) { output = String.format(Locale.US, "%d:%02d:%02d", hours, minutes, seconds); @@ -195,25 +256,91 @@ public class Localization { return output; } + /** + * Localize an amount of seconds into a human readable string. + * + *

The seconds will be converted to the closest whole time unit. + *

For example, 60 seconds would give "1 minute", 119 would also give "1 minute". + * + * @param context used to get plurals resources. + * @param durationInSecs an amount of seconds. + * @return duration in a human readable string. + */ + @NonNull + public static String localizeDuration(final Context context, final int durationInSecs) { + if (durationInSecs < 0) { + throw new IllegalArgumentException("duration can not be negative"); + } + + final int days = (int) (durationInSecs / (24 * 60 * 60L)); + final int hours = (int) (durationInSecs % (24 * 60 * 60L) / (60 * 60L)); + final int minutes = (int) (durationInSecs % (24 * 60 * 60L) % (60 * 60L) / 60L); + final int seconds = (int) (durationInSecs % (24 * 60 * 60L) % (60 * 60L) % 60L); + + final Resources resources = context.getResources(); + + if (days > 0) { + return resources.getQuantityString(R.plurals.days, days, days); + } else if (hours > 0) { + return resources.getQuantityString(R.plurals.hours, hours, hours); + } else if (minutes > 0) { + return resources.getQuantityString(R.plurals.minutes, minutes, minutes); + } else { + return resources.getQuantityString(R.plurals.seconds, seconds, seconds); + } + } + /*////////////////////////////////////////////////////////////////////////// // Pretty Time //////////////////////////////////////////////////////////////////////////*/ - private static void initPrettyTime() { - prettyTime = new PrettyTime(Locale.getDefault()); + private static void initPrettyTime(final Context context) { + prettyTime = new PrettyTime(getAppLocale(context)); // Do not use decades as YouTube doesn't either. prettyTime.removeUnit(Decade.class); } private static PrettyTime getPrettyTime() { - // If pretty time's Locale is different, init again with the new one. - if (!prettyTime.getLocale().equals(Locale.getDefault())) { - initPrettyTime(); - } return prettyTime; } - public static String relativeTime(Calendar calendarTime) { - return getPrettyTime().formatUnrounded(calendarTime); + public static String relativeTime(final Calendar calendarTime) { + String time = getPrettyTime().formatUnrounded(calendarTime); + return time.startsWith("-") ? time.substring(1) : time; + //workaround fix for russian showing -1 day ago, -19hrs ago… + } + + private static void changeAppLanguage(final Locale loc, final Resources res) { + DisplayMetrics dm = res.getDisplayMetrics(); + Configuration conf = res.getConfiguration(); + conf.setLocale(loc); + res.updateConfiguration(conf, dm); + } + + public static Locale getAppLocale(final Context context) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + String lang = prefs.getString(context.getString(R.string.app_language_key), "en"); + Locale loc; + if (lang.equals(context.getString(R.string.default_localization_key))) { + loc = Locale.getDefault(); + } else if (lang.matches(".*-.*")) { + //to differentiate different versions of the language + //for example, pt (portuguese in Portugal) and pt-br (portuguese in Brazil) + String[] localisation = lang.split("-"); + lang = localisation[0]; + String country = localisation[1]; + loc = new Locale(lang, country); + } else { + loc = new Locale(lang); + } + return loc; + } + + public static void assureCorrectAppLanguage(final Context c) { + changeAppLanguage(getAppLocale(c), c.getResources()); + } + + private static double round(final double value, final int places) { + return new BigDecimal(value).setScale(places, RoundingMode.HALF_UP).doubleValue(); } } diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index e897e827f..7136453e4 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -8,15 +8,16 @@ import android.content.Intent; import android.net.Uri; import android.os.Build; import android.preference.PreferenceManager; +import android.util.Log; +import android.widget.Toast; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; -import androidx.appcompat.app.AlertDialog; -import android.util.Log; -import android.widget.Toast; import com.nostra13.universalimageloader.core.ImageLoader; @@ -24,6 +25,7 @@ import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; import org.schabi.newpipe.RouterActivity; import org.schabi.newpipe.about.AboutActivity; +import org.schabi.newpipe.database.feed.model.FeedGroupEntity; import org.schabi.newpipe.download.DownloadActivity; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; @@ -53,10 +55,12 @@ import org.schabi.newpipe.settings.SettingsActivity; import java.util.ArrayList; @SuppressWarnings({"unused", "WeakerAccess"}) -public class NavigationHelper { +public final class NavigationHelper { public static final String MAIN_FRAGMENT_TAG = "main_fragment_tag"; public static final String SEARCH_FRAGMENT_TAG = "search_fragment_tag"; + private NavigationHelper() { } + /*////////////////////////////////////////////////////////////////////////// // Players //////////////////////////////////////////////////////////////////////////*/ @@ -70,8 +74,12 @@ public class NavigationHelper { Intent intent = new Intent(context, targetClazz); final String cacheKey = SerializedCache.getInstance().put(playQueue, PlayQueue.class); - if (cacheKey != null) intent.putExtra(VideoPlayer.PLAY_QUEUE_KEY, cacheKey); - if (quality != null) intent.putExtra(VideoPlayer.PLAYBACK_QUALITY, quality); + if (cacheKey != null) { + intent.putExtra(VideoPlayer.PLAY_QUEUE_KEY, cacheKey); + } + if (quality != null) { + intent.putExtra(VideoPlayer.PLAYBACK_QUALITY, quality); + } intent.putExtra(VideoPlayer.RESUME_PLAYBACK, resumePlayback); intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_VIDEO); @@ -106,12 +114,13 @@ public class NavigationHelper { final float playbackPitch, final boolean playbackSkipSilence, @Nullable final String playbackQuality, - final boolean resumePlayback) { + final boolean resumePlayback, + final boolean startPaused, + final boolean isMuted) { return getPlayerIntent(context, targetClazz, playQueue, playbackQuality, resumePlayback) .putExtra(BasePlayer.REPEAT_MODE, repeatMode) - .putExtra(BasePlayer.PLAYBACK_SPEED, playbackSpeed) - .putExtra(BasePlayer.PLAYBACK_PITCH, playbackPitch) - .putExtra(BasePlayer.PLAYBACK_SKIP_SILENCE, playbackSkipSilence); + .putExtra(BasePlayer.START_PAUSED, startPaused) + .putExtra(BasePlayer.IS_MUTED, isMuted); } public static void playOnMainPlayer( @@ -152,7 +161,8 @@ public class NavigationHelper { context.startActivity(intent); } - public static void playOnPopupPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) { + public static void playOnPopupPlayer(final Context context, final PlayQueue queue, + final boolean resumePlayback) { if (!PermissionHelper.isPopupEnabled(context)) { PermissionHelper.showPopupEnablementToast(context); return; @@ -171,11 +181,14 @@ public class NavigationHelper { startService(context, intent); } - public static void enqueueOnPopupPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) { + public static void enqueueOnPopupPlayer(final Context context, final PlayQueue queue, + final boolean resumePlayback) { enqueueOnPopupPlayer(context, queue, false, resumePlayback); } - public static void enqueueOnPopupPlayer(final Context context, final PlayQueue queue, boolean selectOnAppend, final boolean resumePlayback) { + public static void enqueueOnPopupPlayer(final Context context, final PlayQueue queue, + final boolean selectOnAppend, + final boolean resumePlayback) { if (!PermissionHelper.isPopupEnabled(context)) { PermissionHelper.showPopupEnablementToast(context); return; @@ -187,11 +200,13 @@ public class NavigationHelper { startService(context, intent); } - public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) { + public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue, + final boolean resumePlayback) { enqueueOnBackgroundPlayer(context, queue, false, resumePlayback); } - public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue, final boolean selectOnAppend, final boolean resumePlayback) { + public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue, final boolean selectOnAppend, + final boolean resumePlayback) { Toast.makeText(context, R.string.background_player_append, Toast.LENGTH_SHORT).show(); final Intent intent = getPlayerEnqueueIntent(context, MainPlayer.class, queue, selectOnAppend, resumePlayback); intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_AUDIO); @@ -210,7 +225,7 @@ public class NavigationHelper { // External Players //////////////////////////////////////////////////////////////////////////*/ - public static void playOnExternalAudioPlayer(Context context, StreamInfo info) { + public static void playOnExternalAudioPlayer(final Context context, final StreamInfo info) { final int index = ListHelper.getDefaultAudioFormat(context, info.getAudioStreams()); if (index == -1) { @@ -222,8 +237,9 @@ public class NavigationHelper { playOnExternalPlayer(context, info.getName(), info.getUploaderName(), audioStream); } - public static void playOnExternalVideoPlayer(Context context, StreamInfo info) { - ArrayList videoStreamsList = new ArrayList<>(ListHelper.getSortedStreamVideosList(context, info.getVideoStreams(), null, false)); + public static void playOnExternalVideoPlayer(final Context context, final StreamInfo info) { + ArrayList videoStreamsList = new ArrayList<>( + ListHelper.getSortedStreamVideosList(context, info.getVideoStreams(), null, false)); int index = ListHelper.getDefaultResolutionIndex(context, videoStreamsList); if (index == -1) { @@ -235,7 +251,8 @@ public class NavigationHelper { playOnExternalPlayer(context, info.getName(), info.getUploaderName(), videoStream); } - public static void playOnExternalPlayer(Context context, String name, String artist, Stream stream) { + public static void playOnExternalPlayer(final Context context, final String name, + final String artist, final Stream stream) { Intent intent = new Intent(); intent.setAction(Intent.ACTION_VIEW); intent.setDataAndType(Uri.parse(stream.getUrl()), stream.getFormat().getMimeType()); @@ -247,7 +264,7 @@ public class NavigationHelper { resolveActivityOrAskToInstall(context, intent); } - public static void resolveActivityOrAskToInstall(Context context, Intent intent) { + public static void resolveActivityOrAskToInstall(final Context context, final Intent intent) { if (intent.resolveActivity(context.getPackageManager()) != null) { context.startActivity(intent); } else { @@ -260,9 +277,12 @@ public class NavigationHelper { i.setData(Uri.parse(context.getString(R.string.fdroid_vlc_url))); context.startActivity(i); }) - .setNegativeButton(R.string.cancel, (dialog, which) -> Log.i("NavigationHelper", "You unlocked a secret unicorn.")) + .setNegativeButton(R.string.cancel, (dialog, which) + -> Log.i("NavigationHelper", "You unlocked a secret unicorn.")) .show(); - //Log.e("NavigationHelper", "Either no Streaming player for audio was installed, or something important crashed:"); +// Log.e("NavigationHelper", +// "Either no Streaming player for audio was installed, " +// + "or something important crashed:"); } else { Toast.makeText(context, R.string.no_player_found_toast, Toast.LENGTH_LONG).show(); } @@ -274,19 +294,22 @@ public class NavigationHelper { //////////////////////////////////////////////////////////////////////////*/ @SuppressLint("CommitTransaction") - private static FragmentTransaction defaultTransaction(FragmentManager fragmentManager) { + private static FragmentTransaction defaultTransaction(final FragmentManager fragmentManager) { return fragmentManager.beginTransaction() - .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out); + .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, + R.animator.custom_fade_in, R.animator.custom_fade_out); } - public static void gotoMainFragment(FragmentManager fragmentManager) { + public static void gotoMainFragment(final FragmentManager fragmentManager) { ImageLoader.getInstance().clearMemoryCache(); boolean popped = fragmentManager.popBackStackImmediate(MAIN_FRAGMENT_TAG, 0); - if (!popped) openMainFragment(fragmentManager); + if (!popped) { + openMainFragment(fragmentManager); + } } - public static void openMainFragment(FragmentManager fragmentManager) { + public static void openMainFragment(final FragmentManager fragmentManager) { InfoCache.getInstance().trimCache(); fragmentManager.popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE); @@ -296,49 +319,50 @@ public class NavigationHelper { .commit(); } - public static boolean tryGotoSearchFragment(FragmentManager fragmentManager) { + public static boolean tryGotoSearchFragment(final FragmentManager fragmentManager) { if (MainActivity.DEBUG) { for (int i = 0; i < fragmentManager.getBackStackEntryCount(); i++) { - Log.d("NavigationHelper", "tryGoToSearchFragment() [" + i + "] = [" + fragmentManager.getBackStackEntryAt(i) + "]"); + Log.d("NavigationHelper", "tryGoToSearchFragment() [" + i + "]" + + " = [" + fragmentManager.getBackStackEntryAt(i) + "]"); } } return fragmentManager.popBackStackImmediate(SEARCH_FRAGMENT_TAG, 0); } - public static void openSearchFragment(FragmentManager fragmentManager, - int serviceId, - String searchString) { + public static void openSearchFragment(final FragmentManager fragmentManager, + final int serviceId, final String searchString) { defaultTransaction(fragmentManager) .replace(R.id.fragment_holder, SearchFragment.getInstance(serviceId, searchString)) .addToBackStack(SEARCH_FRAGMENT_TAG) .commit(); } - public static void openVideoDetailFragment(FragmentManager fragmentManager, int serviceId, String url, String title) { + public static void openVideoDetailFragment(final FragmentManager fragmentManager, + final int serviceId, final String url, + final String title) { openVideoDetailFragment(fragmentManager, serviceId, url, title, true, null); } public static void openVideoDetailFragment( - FragmentManager fragmentManager, - int serviceId, - String url, - String title, - boolean autoPlay, - PlayQueue playQueue) { + final FragmentManager fragmentManager, + final int serviceId, + final String url, + final String title, + final boolean autoPlay, + final PlayQueue playQueue) { final Fragment fragment = fragmentManager.findFragmentById(R.id.fragment_player_holder); - if (title == null) title = ""; if (fragment instanceof VideoDetailFragment && fragment.isVisible()) { expandMainPlayer(fragment.requireActivity()); final VideoDetailFragment detailFragment = (VideoDetailFragment) fragment; detailFragment.setAutoplay(autoPlay); - detailFragment.selectAndLoadVideo(serviceId, url, title, playQueue); + detailFragment.selectAndLoadVideo(serviceId, url, title == null ? "" : title, playQueue); detailFragment.scrollToTop(); return; } - final VideoDetailFragment instance = VideoDetailFragment.getInstance(serviceId, url, title, playQueue); + final VideoDetailFragment instance = VideoDetailFragment.getInstance(serviceId, url, title == null ? "" : title, playQueue); instance.setAutoplay(autoPlay); defaultTransaction(fragmentManager) @@ -347,90 +371,94 @@ public class NavigationHelper { .commit(); } - public static void expandMainPlayer(Context context) { + public static void expandMainPlayer(final Context context) { final Intent intent = new Intent(VideoDetailFragment.ACTION_SHOW_MAIN_PLAYER); context.sendBroadcast(intent); } - public static void openChannelFragment( - final FragmentManager fragmentManager, - final int serviceId, - final String url, - String name) { - if (name == null) name = ""; + public static void openChannelFragment(final FragmentManager fragmentManager, + final int serviceId, final String url, + final String name) { defaultTransaction(fragmentManager) - .replace(R.id.fragment_holder, ChannelFragment.getInstance(serviceId, url, name)) + .replace(R.id.fragment_holder, ChannelFragment.getInstance(serviceId, url, + name == null ? "" : name)) .addToBackStack(null) .commit(); } - public static void openCommentsFragment( - FragmentManager fragmentManager, - int serviceId, - String url, - String name) { - if (name == null) name = ""; - fragmentManager.beginTransaction().setCustomAnimations(R.anim.switch_service_in, R.anim.switch_service_out) - .replace(R.id.fragment_holder, CommentsFragment.getInstance(serviceId, url, name)) + public static void openCommentsFragment(final FragmentManager fragmentManager, + final int serviceId, final String url, + final String name) { + fragmentManager.beginTransaction() + .setCustomAnimations(R.anim.switch_service_in, R.anim.switch_service_out) + .replace(R.id.fragment_holder, CommentsFragment.getInstance(serviceId, url, + name == null ? "" : name)) .addToBackStack(null) .commit(); } - public static void openPlaylistFragment(FragmentManager fragmentManager, - int serviceId, - String url, - String name) { - if (name == null) name = ""; + public static void openPlaylistFragment(final FragmentManager fragmentManager, + final int serviceId, final String url, + final String name) { defaultTransaction(fragmentManager) - .replace(R.id.fragment_holder, PlaylistFragment.getInstance(serviceId, url, name)) + .replace(R.id.fragment_holder, PlaylistFragment.getInstance(serviceId, url, + name == null ? "" : name)) .addToBackStack(null) .commit(); } - public static void openWhatsNewFragment(FragmentManager fragmentManager) { + public static void openFeedFragment(final FragmentManager fragmentManager) { + openFeedFragment(fragmentManager, FeedGroupEntity.GROUP_ALL_ID, null); + } + + public static void openFeedFragment(final FragmentManager fragmentManager, final long groupId, + @Nullable final String groupName) { defaultTransaction(fragmentManager) - .replace(R.id.fragment_holder, new FeedFragment()) + .replace(R.id.fragment_holder, FeedFragment.newInstance(groupId, groupName)) .addToBackStack(null) .commit(); } - public static void openBookmarksFragment(FragmentManager fragmentManager) { + public static void openBookmarksFragment(final FragmentManager fragmentManager) { defaultTransaction(fragmentManager) .replace(R.id.fragment_holder, new BookmarkFragment()) .addToBackStack(null) .commit(); } - public static void openSubscriptionFragment(FragmentManager fragmentManager) { + public static void openSubscriptionFragment(final FragmentManager fragmentManager) { defaultTransaction(fragmentManager) .replace(R.id.fragment_holder, new SubscriptionFragment()) .addToBackStack(null) .commit(); } - public static void openKioskFragment(FragmentManager fragmentManager, int serviceId, String kioskId) throws ExtractionException { + public static void openKioskFragment(final FragmentManager fragmentManager, final int serviceId, + final String kioskId) throws ExtractionException { defaultTransaction(fragmentManager) .replace(R.id.fragment_holder, KioskFragment.getInstance(serviceId, kioskId)) .addToBackStack(null) .commit(); } - public static void openLocalPlaylistFragment(FragmentManager fragmentManager, long playlistId, String name) { - if (name == null) name = ""; + public static void openLocalPlaylistFragment(final FragmentManager fragmentManager, + final long playlistId, final String name) { defaultTransaction(fragmentManager) - .replace(R.id.fragment_holder, LocalPlaylistFragment.getInstance(playlistId, name)) + .replace(R.id.fragment_holder, LocalPlaylistFragment.getInstance(playlistId, + name == null ? "" : name)) .addToBackStack(null) .commit(); } - public static void openStatisticFragment(FragmentManager fragmentManager) { + public static void openStatisticFragment(final FragmentManager fragmentManager) { defaultTransaction(fragmentManager) .replace(R.id.fragment_holder, new StatisticsPlaylistFragment()) .addToBackStack(null) .commit(); } - public static void openSubscriptionsImportFragment(FragmentManager fragmentManager, int serviceId) { + public static void openSubscriptionsImportFragment(final FragmentManager fragmentManager, + final int serviceId) { defaultTransaction(fragmentManager) .replace(R.id.fragment_holder, SubscriptionsImportFragment.getInstance(serviceId)) .addToBackStack(null) @@ -441,7 +469,8 @@ public class NavigationHelper { // Through Intents //////////////////////////////////////////////////////////////////////////*/ - public static void openSearch(Context context, int serviceId, String searchString) { + public static void openSearch(final Context context, final int serviceId, + final String searchString) { Intent mIntent = new Intent(context, MainActivity.class); mIntent.putExtra(Constants.KEY_SERVICE_ID, serviceId); mIntent.putExtra(Constants.KEY_SEARCH_STRING, searchString); @@ -449,52 +478,62 @@ public class NavigationHelper { context.startActivity(mIntent); } - public static void openChannel(Context context, int serviceId, String url) { + public static void openChannel(final Context context, final int serviceId, final String url) { openChannel(context, serviceId, url, null); } - public static void openChannel(Context context, int serviceId, String url, String name) { - Intent openIntent = getOpenIntent(context, url, serviceId, StreamingService.LinkType.CHANNEL); - if (name != null && !name.isEmpty()) openIntent.putExtra(Constants.KEY_TITLE, name); + public static void openChannel(final Context context, final int serviceId, + final String url, final String name) { + Intent openIntent = getOpenIntent(context, url, serviceId, + StreamingService.LinkType.CHANNEL); + if (name != null && !name.isEmpty()) { + openIntent.putExtra(Constants.KEY_TITLE, name); + } context.startActivity(openIntent); } - public static void openVideoDetail(Context context, int serviceId, String url) { + public static void openVideoDetail(final Context context, final int serviceId, + final String url) { openVideoDetail(context, serviceId, url, null); } - public static void openVideoDetail(Context context, int serviceId, String url, String title) { - Intent openIntent = getOpenIntent(context, url, serviceId, StreamingService.LinkType.STREAM); - if (title != null && !title.isEmpty()) openIntent.putExtra(Constants.KEY_TITLE, title); + public static void openVideoDetail(final Context context, final int serviceId, + final String url, final String title) { + Intent openIntent = getOpenIntent(context, url, serviceId, + StreamingService.LinkType.STREAM); + if (title != null && !title.isEmpty()) { + openIntent.putExtra(Constants.KEY_TITLE, title); + } context.startActivity(openIntent); } - public static void openMainActivity(Context context) { + public static void openMainActivity(final Context context) { Intent mIntent = new Intent(context, MainActivity.class); mIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); mIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); context.startActivity(mIntent); } - public static void openRouterActivity(Context context, String url) { + public static void openRouterActivity(final Context context, final String url) { Intent mIntent = new Intent(context, RouterActivity.class); mIntent.setData(Uri.parse(url)); - mIntent.putExtra(RouterActivity.internalRouteKey, true); + mIntent.putExtra(RouterActivity.INTERNAL_ROUTE_KEY, true); context.startActivity(mIntent); } - public static void openAbout(Context context) { + public static void openAbout(final Context context) { Intent intent = new Intent(context, AboutActivity.class); context.startActivity(intent); } - public static void openSettings(Context context) { + public static void openSettings(final Context context) { Intent intent = new Intent(context, SettingsActivity.class); context.startActivity(intent); } - public static boolean openDownloads(Activity activity) { - if (!PermissionHelper.checkStoragePermissions(activity, PermissionHelper.DOWNLOADS_REQUEST_CODE)) { + public static boolean openDownloads(final Activity activity) { + if (!PermissionHelper.checkStoragePermissions( + activity, PermissionHelper.DOWNLOADS_REQUEST_CODE)) { return false; } Intent intent = new Intent(activity, DownloadActivity.class); @@ -518,7 +557,8 @@ public class NavigationHelper { // Link handling //////////////////////////////////////////////////////////////////////////*/ - private static Intent getOpenIntent(Context context, String url, int serviceId, StreamingService.LinkType type) { + private static Intent getOpenIntent(final Context context, final String url, + final int serviceId, final StreamingService.LinkType type) { Intent mIntent = new Intent(context, MainActivity.class); mIntent.putExtra(Constants.KEY_SERVICE_ID, serviceId); mIntent.putExtra(Constants.KEY_URL, url); @@ -526,45 +566,46 @@ public class NavigationHelper { return mIntent; } - public static Intent getIntentByLink(Context context, String url) throws ExtractionException { + public static Intent getIntentByLink(final Context context, final String url) + throws ExtractionException { return getIntentByLink(context, NewPipe.getServiceByUrl(url), url); } - public static Intent getIntentByLink(Context context, StreamingService service, String url) throws ExtractionException { + public static Intent getIntentByLink(final Context context, final StreamingService service, + final String url) throws ExtractionException { StreamingService.LinkType linkType = service.getLinkTypeByUrl(url); if (linkType == StreamingService.LinkType.NONE) { - throw new ExtractionException("Url not known to service. service=" + service + " url=" + url); + throw new ExtractionException("Url not known to service. service=" + service + + " url=" + url); } Intent rIntent = getOpenIntent(context, url, service.getServiceId(), linkType); - switch (linkType) { - case STREAM: - rIntent.putExtra(VideoDetailFragment.AUTO_PLAY, - PreferenceManager.getDefaultSharedPreferences(context) - .getBoolean(context.getString(R.string.autoplay_through_intent_key), false)); - break; + if (linkType == StreamingService.LinkType.STREAM) { + rIntent.putExtra(VideoDetailFragment.AUTO_PLAY, + PreferenceManager.getDefaultSharedPreferences(context).getBoolean( + context.getString(R.string.autoplay_through_intent_key), false)); } return rIntent; } - private static Uri openMarketUrl(String packageName) { + private static Uri openMarketUrl(final String packageName) { return Uri.parse("market://details") .buildUpon() .appendQueryParameter("id", packageName) .build(); } - private static Uri getGooglePlayUrl(String packageName) { + private static Uri getGooglePlayUrl(final String packageName) { return Uri.parse("https://play.google.com/store/apps/details") .buildUpon() .appendQueryParameter("id", packageName) .build(); } - private static void installApp(Context context, String packageName) { + private static void installApp(final Context context, final String packageName) { try { // Try market:// scheme context.startActivity(new Intent(Intent.ACTION_VIEW, openMarketUrl(packageName))); @@ -575,16 +616,16 @@ public class NavigationHelper { } /** - * Start an activity to install Kore + * Start an activity to install Kore. * * @param context the context */ - public static void installKore(Context context) { + public static void installKore(final Context context) { installApp(context, context.getString(R.string.kore_package)); } /** - * Start Kore app to show a video on Kodi + * Start Kore app to show a video on Kodi. *

* For a list of supported urls see the * @@ -594,7 +635,7 @@ public class NavigationHelper { * @param context the context to use * @param videoURL the url to the video */ - public static void playWithKore(Context context, Uri videoURL) { + public static void playWithKore(final Context context, final Uri videoURL) { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setPackage(context.getString(R.string.kore_package)); intent.setData(videoURL); diff --git a/app/src/main/java/org/schabi/newpipe/util/OnClickGesture.java b/app/src/main/java/org/schabi/newpipe/util/OnClickGesture.java index 18f4f67f4..5f44cab8b 100644 --- a/app/src/main/java/org/schabi/newpipe/util/OnClickGesture.java +++ b/app/src/main/java/org/schabi/newpipe/util/OnClickGesture.java @@ -6,11 +6,11 @@ public abstract class OnClickGesture { public abstract void selected(T selectedItem); - public void held(T selectedItem) { + public void held(final T selectedItem) { // Optional gesture } - public void drag(T selectedItem, RecyclerView.ViewHolder viewHolder) { + public void drag(final T selectedItem, final RecyclerView.ViewHolder viewHolder) { // Optional gesture } } diff --git a/app/src/main/java/org/schabi/newpipe/util/PeertubeHelper.java b/app/src/main/java/org/schabi/newpipe/util/PeertubeHelper.java index 0d695e275..e89cbf5db 100644 --- a/app/src/main/java/org/schabi/newpipe/util/PeertubeHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/PeertubeHelper.java @@ -19,10 +19,12 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -public class PeertubeHelper { +public final class PeertubeHelper { + private PeertubeHelper() { } - public static List getInstanceList(Context context) { - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + public static List getInstanceList(final Context context) { + SharedPreferences sharedPreferences = PreferenceManager + .getDefaultSharedPreferences(context); String savedInstanceListKey = context.getString(R.string.peertube_instance_list_key); final String savedJson = sharedPreferences.getString(savedInstanceListKey, null); if (null == savedJson) { @@ -47,8 +49,10 @@ public class PeertubeHelper { } - public static PeertubeInstance selectInstance(PeertubeInstance instance, Context context) { - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + public static PeertubeInstance selectInstance(final PeertubeInstance instance, + final Context context) { + SharedPreferences sharedPreferences = PreferenceManager + .getDefaultSharedPreferences(context); String selectedInstanceKey = context.getString(R.string.peertube_selected_instance_key); JsonStringWriter jsonWriter = JsonWriter.string().object(); jsonWriter.value("name", instance.getName()); @@ -59,7 +63,7 @@ public class PeertubeHelper { return instance; } - public static PeertubeInstance getCurrentInstance(){ + public static PeertubeInstance getCurrentInstance() { return ServiceList.PeerTube.getInstance(); } } diff --git a/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java b/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java index f32bb6587..9ba6ed36c 100644 --- a/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java @@ -2,34 +2,41 @@ package org.schabi.newpipe.util; import android.Manifest; import android.app.Activity; +import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import android.provider.Settings; -import androidx.annotation.RequiresApi; -import androidx.core.app.ActivityCompat; -import androidx.core.content.ContextCompat; import android.view.Gravity; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.RequiresApi; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + import org.schabi.newpipe.R; -public class PermissionHelper { +public final class PermissionHelper { public static final int DOWNLOAD_DIALOG_REQUEST_CODE = 778; public static final int DOWNLOADS_REQUEST_CODE = 777; - public static boolean checkStoragePermissions(Activity activity, int requestCode) { + private PermissionHelper() { } + + public static boolean checkStoragePermissions(final Activity activity, final int requestCode) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - if (!checkReadStoragePermissions(activity, requestCode)) return false; + if (!checkReadStoragePermissions(activity, requestCode)) { + return false; + } } return checkWriteStoragePermissions(activity, requestCode); } @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) - public static boolean checkReadStoragePermissions(Activity activity, int requestCode) { + public static boolean checkReadStoragePermissions(final Activity activity, + final int requestCode) { if (ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(activity, @@ -44,7 +51,8 @@ public class PermissionHelper { } - public static boolean checkWriteStoragePermissions(Activity activity, int requestCode) { + public static boolean checkWriteStoragePermissions(final Activity activity, + final int requestCode) { // Here, thisActivity is the current activity if (ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) @@ -75,34 +83,48 @@ public class PermissionHelper { /** - * In order to be able to draw over other apps, the permission android.permission.SYSTEM_ALERT_WINDOW have to be granted. + * In order to be able to draw over other apps, + * the permission android.permission.SYSTEM_ALERT_WINDOW have to be granted. *

- * On < API 23 (MarshMallow) the permission was granted when the user installed the application (via AndroidManifest), + * On < API 23 (MarshMallow) the permission was granted + * when the user installed the application (via AndroidManifest), * on > 23, however, it have to start a activity asking the user if he agrees. + *

*

- * This method just return if the app has permission to draw over other apps, and if it doesn't, it will try to get the permission. + * This method just return if the app has permission to draw over other apps, + * and if it doesn't, it will try to get the permission. + *

* - * @return returns {@link Settings#canDrawOverlays(Context)} + * @param context {@link Context} + * @return {@link Settings#canDrawOverlays(Context)} **/ @RequiresApi(api = Build.VERSION_CODES.M) - public static boolean checkSystemAlertWindowPermission(Context context) { + public static boolean checkSystemAlertWindowPermission(final Context context) { if (!Settings.canDrawOverlays(context)) { - Intent i = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + context.getPackageName())); + Intent i = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, + Uri.parse("package:" + context.getPackageName())); i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(i); + try { + context.startActivity(i); + } catch (ActivityNotFoundException ignored) { + } return false; - } else return true; + } else { + return true; + } } - public static boolean isPopupEnabled(Context context) { - return Build.VERSION.SDK_INT < Build.VERSION_CODES.M || - PermissionHelper.checkSystemAlertWindowPermission(context); + public static boolean isPopupEnabled(final Context context) { + return Build.VERSION.SDK_INT < Build.VERSION_CODES.M + || PermissionHelper.checkSystemAlertWindowPermission(context); } - public static void showPopupEnablementToast(Context context) { + public static void showPopupEnablementToast(final Context context) { Toast toast = Toast.makeText(context, R.string.msg_popup_permission, Toast.LENGTH_LONG); TextView messageView = toast.getView().findViewById(android.R.id.message); - if (messageView != null) messageView.setGravity(Gravity.CENTER); + if (messageView != null) { + messageView.setGravity(Gravity.CENTER); + } toast.show(); } } diff --git a/app/src/main/java/org/schabi/newpipe/util/RelatedStreamInfo.java b/app/src/main/java/org/schabi/newpipe/util/RelatedStreamInfo.java index 6de663c13..ce642da5e 100644 --- a/app/src/main/java/org/schabi/newpipe/util/RelatedStreamInfo.java +++ b/app/src/main/java/org/schabi/newpipe/util/RelatedStreamInfo.java @@ -14,15 +14,18 @@ public class RelatedStreamInfo extends ListInfo { private StreamInfoItem nextStream; - public RelatedStreamInfo(int serviceId, ListLinkHandler listUrlIdHandler, String name) { + public RelatedStreamInfo(final int serviceId, final ListLinkHandler listUrlIdHandler, + final String name) { super(serviceId, listUrlIdHandler, name); } - 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()); + public static RelatedStreamInfo getInfo(final StreamInfo info) { + ListLinkHandler handler = new ListLinkHandler( + info.getOriginalUrl(), info.getUrl(), info.getId(), Collections.emptyList(), null); + RelatedStreamInfo relatedStreamInfo = new RelatedStreamInfo( + info.getServiceId(), handler, info.getName()); List streams = new ArrayList<>(); - if(info.getNextVideo() != null){ + if (info.getNextVideo() != null) { streams.add(info.getNextVideo()); } streams.addAll(info.getRelatedStreams()); @@ -35,7 +38,7 @@ public class RelatedStreamInfo extends ListInfo { return nextStream; } - public void setNextStream(StreamInfoItem nextStream) { + public void setNextStream(final StreamInfoItem nextStream) { this.nextStream = nextStream; } } diff --git a/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java b/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java index ab58bc917..081d981a1 100644 --- a/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java @@ -14,28 +14,23 @@ public class SecondaryStreamHelper { private final int position; private final StreamSizeWrapper streams; - public SecondaryStreamHelper(StreamSizeWrapper streams, T selectedStream) { + public SecondaryStreamHelper(final StreamSizeWrapper streams, final 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); + if (this.position < 0) { + throw new RuntimeException("selected stream not found"); + } } /** - * find the correct audio stream for the desired video stream + * 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 audioStreams, @NonNull VideoStream videoStream) { + public static AudioStream getAudioStreamFor(@NonNull final List audioStreams, + @NonNull final VideoStream videoStream) { switch (videoStream.getFormat()) { case WEBM: case MPEG_4:// ¿is mpeg-4 DASH? @@ -52,7 +47,9 @@ public class SecondaryStreamHelper { } } - if (m4v) return null; + if (m4v) { + return null; + } // retry, but this time in reverse order for (int i = audioStreams.size() - 1; i >= 0; i--) { @@ -64,4 +61,12 @@ public class SecondaryStreamHelper { return null; } + + public T getStream() { + return streams.getStreamsList().get(position); + } + + public long getSizeInBytes() { + return streams.getSizeInBytes(position); + } } diff --git a/app/src/main/java/org/schabi/newpipe/util/SerializedCache.java b/app/src/main/java/org/schabi/newpipe/util/SerializedCache.java index 7680daf48..9d97e013a 100644 --- a/app/src/main/java/org/schabi/newpipe/util/SerializedCache.java +++ b/app/src/main/java/org/schabi/newpipe/util/SerializedCache.java @@ -1,9 +1,10 @@ package org.schabi.newpipe.util; +import android.util.Log; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.collection.LruCache; -import android.util.Log; import org.schabi.newpipe.MainActivity; @@ -14,53 +15,58 @@ import java.io.ObjectOutputStream; import java.io.Serializable; import java.util.UUID; -public class SerializedCache { +public final class SerializedCache { private static final boolean DEBUG = MainActivity.DEBUG; - private final String TAG = getClass().getSimpleName(); - - private static final SerializedCache instance = new SerializedCache(); + private static final SerializedCache INSTANCE = new SerializedCache(); private static final int MAX_ITEMS_ON_CACHE = 5; - - private static final LruCache lruCache = + private static final LruCache LRU_CACHE = new LruCache<>(MAX_ITEMS_ON_CACHE); + private static final String TAG = "SerializedCache"; private SerializedCache() { //no instance } public static SerializedCache getInstance() { - return instance; + return INSTANCE; } @Nullable public T take(@NonNull final String key, @NonNull final Class type) { - if (DEBUG) Log.d(TAG, "take() called with: key = [" + key + "]"); - synchronized (lruCache) { - return lruCache.get(key) != null ? getItem(lruCache.remove(key), type) : null; + if (DEBUG) { + Log.d(TAG, "take() called with: key = [" + key + "]"); + } + synchronized (LRU_CACHE) { + return LRU_CACHE.get(key) != null ? getItem(LRU_CACHE.remove(key), type) : null; } } @Nullable public T get(@NonNull final String key, @NonNull final Class type) { - if (DEBUG) Log.d(TAG, "get() called with: key = [" + key + "]"); - synchronized (lruCache) { - final CacheData data = lruCache.get(key); + if (DEBUG) { + Log.d(TAG, "get() called with: key = [" + key + "]"); + } + synchronized (LRU_CACHE) { + final CacheData data = LRU_CACHE.get(key); return data != null ? getItem(data, type) : null; } } @Nullable - public String put(@NonNull T item, @NonNull final Class type) { + public String put(@NonNull final T item, + @NonNull final Class type) { final String key = UUID.randomUUID().toString(); return put(key, item, type) ? key : null; } - public boolean put(@NonNull final String key, @NonNull T item, + public boolean put(@NonNull final String key, @NonNull final T item, @NonNull final Class type) { - if (DEBUG) Log.d(TAG, "put() called with: key = [" + key + "], item = [" + item + "]"); - synchronized (lruCache) { + if (DEBUG) { + Log.d(TAG, "put() called with: key = [" + key + "], item = [" + item + "]"); + } + synchronized (LRU_CACHE) { try { - lruCache.put(key, new CacheData<>(clone(item, type), type)); + LRU_CACHE.put(key, new CacheData<>(clone(item, type), type)); return true; } catch (final Exception error) { Log.e(TAG, "Serialization failed for: ", error); @@ -70,15 +76,17 @@ public class SerializedCache { } public void clear() { - if (DEBUG) Log.d(TAG, "clear() called"); - synchronized (lruCache) { - lruCache.evictAll(); + if (DEBUG) { + Log.d(TAG, "clear() called"); + } + synchronized (LRU_CACHE) { + LRU_CACHE.evictAll(); } } public long size() { - synchronized (lruCache) { - return lruCache.size(); + synchronized (LRU_CACHE) { + return LRU_CACHE.size(); } } @@ -88,10 +96,10 @@ public class SerializedCache { } @NonNull - private T clone(@NonNull T item, + private T clone(@NonNull final T item, @NonNull final Class type) throws Exception { final ByteArrayOutputStream bytesOutput = new ByteArrayOutputStream(); - try (final ObjectOutputStream objectOutput = new ObjectOutputStream(bytesOutput)) { + try (ObjectOutputStream objectOutput = new ObjectOutputStream(bytesOutput)) { objectOutput.writeObject(item); objectOutput.flush(); } @@ -100,11 +108,11 @@ public class SerializedCache { return type.cast(clone); } - final private static class CacheData { + private static final class CacheData { private final T item; private final Class type; - private CacheData(@NonNull final T item, @NonNull Class type) { + private CacheData(@NonNull final T item, @NonNull final Class type) { this.item = item; this.type = type; } diff --git a/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java index 8929cc654..dacf7d844 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java @@ -22,11 +22,13 @@ import java.util.concurrent.TimeUnit; import static org.schabi.newpipe.extractor.ServiceList.SoundCloud; -public class ServiceHelper { +public final class ServiceHelper { private static final StreamingService DEFAULT_FALLBACK_SERVICE = ServiceList.YouTube; + private ServiceHelper() { } + @DrawableRes - public static int getIcon(int serviceId) { + public static int getIcon(final int serviceId) { switch (serviceId) { case 0: return R.drawable.place_holder_youtube; @@ -41,27 +43,45 @@ public class ServiceHelper { } } - public static String getTranslatedFilterString(String filter, Context c) { + public static String getTranslatedFilterString(final String filter, final Context c) { switch (filter) { - case "all": return c.getString(R.string.all); - case "videos": return c.getString(R.string.videos); - case "channels": return c.getString(R.string.channels); - case "playlists": return c.getString(R.string.playlists); - case "tracks": return c.getString(R.string.tracks); - case "users": return c.getString(R.string.users); - case "conferences" : return c.getString(R.string.conferences); - case "events" : return c.getString(R.string.events); - default: return filter; + case "all": + return c.getString(R.string.all); + case "videos": + case "music_videos": + return c.getString(R.string.videos_string); + case "channels": + return c.getString(R.string.channels); + case "playlists": + case "music_playlists": + return c.getString(R.string.playlists); + case "tracks": + return c.getString(R.string.tracks); + case "users": + return c.getString(R.string.users); + case "conferences": + return c.getString(R.string.conferences); + case "events": + return c.getString(R.string.events); + case "music_songs": + return c.getString(R.string.songs); + case "music_albums": + return c.getString(R.string.albums); + case "music_artists": + return c.getString(R.string.artists); + default: + return filter; } } /** * Get a resource string with instructions for importing subscriptions for each service. * + * @param serviceId service to get the instructions for * @return the string resource containing the instructions or -1 if the service don't support it */ @StringRes - public static int getImportInstructions(int serviceId) { + public static int getImportInstructions(final int serviceId) { switch (serviceId) { case 0: return R.string.import_youtube_instructions; @@ -76,10 +96,11 @@ public class ServiceHelper { * For services that support importing from a channel url, return a hint that will * be used in the EditText that the user will type in his channel url. * + * @param serviceId service to get the hint for * @return the hint's string resource or -1 if the service don't support it */ @StringRes - public static int getImportInstructionsHint(int serviceId) { + public static int getImportInstructionsHint(final int serviceId) { switch (serviceId) { case 1: return R.string.import_soundcloud_instructions_hint; @@ -88,10 +109,10 @@ public class ServiceHelper { } } - public static int getSelectedServiceId(Context context) { - + public static int getSelectedServiceId(final Context context) { final String serviceName = PreferenceManager.getDefaultSharedPreferences(context) - .getString(context.getString(R.string.current_service_key), context.getString(R.string.default_service_value)); + .getString(context.getString(R.string.current_service_key), + context.getString(R.string.default_service_value)); int serviceId; try { @@ -103,7 +124,7 @@ public class ServiceHelper { return serviceId; } - public static void setSelectedServiceId(Context context, int serviceId) { + public static void setSelectedServiceId(final Context context, final int serviceId) { String serviceName; try { serviceName = NewPipe.getService(serviceId).getServiceInfo().getName(); @@ -114,14 +135,18 @@ public class ServiceHelper { setSelectedServicePreferences(context, serviceName); } - public static void setSelectedServiceId(Context context, String serviceName) { + public static void setSelectedServiceId(final Context context, final String serviceName) { int serviceId = NewPipe.getIdOfService(serviceName); - if (serviceId == -1) serviceName = DEFAULT_FALLBACK_SERVICE.getServiceInfo().getName(); - - setSelectedServicePreferences(context, serviceName); + if (serviceId == -1) { + setSelectedServicePreferences(context, + DEFAULT_FALLBACK_SERVICE.getServiceInfo().getName()); + } else { + setSelectedServicePreferences(context, serviceName); + } } - private static void setSelectedServicePreferences(Context context, String serviceName) { + private static void setSelectedServicePreferences(final Context context, + final String serviceName) { PreferenceManager.getDefaultSharedPreferences(context).edit(). putString(context.getString(R.string.current_service_key), serviceName).apply(); } @@ -136,15 +161,19 @@ public class ServiceHelper { public static boolean isBeta(final StreamingService s) { switch (s.getServiceInfo().getName()) { - case "YouTube": return false; - default: return true; + case "YouTube": + return false; + default: + return true; } } - public static void initService(Context context, int serviceId) { + public static void initService(final Context context, final int serviceId) { if (serviceId == ServiceList.PeerTube.getServiceId()) { - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); - String json = sharedPreferences.getString(context.getString(R.string.peertube_selected_instance_key), null); + SharedPreferences sharedPreferences = PreferenceManager + .getDefaultSharedPreferences(context); + String json = sharedPreferences.getString(context.getString( + R.string.peertube_selected_instance_key), null); if (null == json) { return; } @@ -162,7 +191,7 @@ public class ServiceHelper { } } - public static void initServices(Context context) { + public static void initServices(final Context context) { for (StreamingService s : ServiceList.all()) { initService(context, s.getServiceId()); } diff --git a/app/src/main/java/org/schabi/newpipe/util/ShareUtils.java b/app/src/main/java/org/schabi/newpipe/util/ShareUtils.java index 17768cd08..a3571b96f 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ShareUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/ShareUtils.java @@ -1,22 +1,111 @@ package org.schabi.newpipe.util; +import android.content.ClipData; +import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.net.Uri; +import android.widget.Toast; import org.schabi.newpipe.R; -public class ShareUtils { - public static void openUrlInBrowser(Context context, String url) { - Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - context.startActivity(Intent.createChooser(intent, context.getString(R.string.share_dialog_title))); +public final class ShareUtils { + private ShareUtils() { } - public static void shareUrl(Context context, String subject, String url) { + /** + * Open the url with the system default browser. + *

+ * If no browser is set as default, fallbacks to + * {@link ShareUtils#openInDefaultApp(Context, String)} + * + * @param context the context to use + * @param url the url to browse + */ + public static void openUrlInBrowser(final Context context, final String url) { + final String defaultBrowserPackageName = getDefaultBrowserPackageName(context); + + if (defaultBrowserPackageName.equals("android")) { + // no browser set as default + openInDefaultApp(context, url); + } else { + final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)) + .setPackage(defaultBrowserPackageName) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } + } + + /** + * Open the url in the default app set to open this type of link. + *

+ * If no app is set as default, it will open a chooser + * + * @param context the context to use + * @param url the url to browse + */ + private static void openInDefaultApp(final Context context, final String url) { + final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + context.startActivity(Intent.createChooser( + intent, context.getString(R.string.share_dialog_title)) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + } + + /** + * Get the default browser package name. + *

+ * If no browser is set as default, it will return "android" + * + * @param context the context to use + * @return the package name of the default browser, or "android" if there's no default + */ + private static String getDefaultBrowserPackageName(final Context context) { + final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://")) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + final ResolveInfo resolveInfo = context.getPackageManager().resolveActivity( + intent, PackageManager.MATCH_DEFAULT_ONLY); + return resolveInfo.activityInfo.packageName; + } + + /** + * Open the android share menu to share the current url. + * + * @param context the context to use + * @param subject the url subject, typically the title + * @param url the url to share + */ + public static void shareUrl(final Context context, final String subject, final String url) { Intent intent = new Intent(Intent.ACTION_SEND); intent.setType("text/plain"); intent.putExtra(Intent.EXTRA_SUBJECT, subject); intent.putExtra(Intent.EXTRA_TEXT, url); - context.startActivity(Intent.createChooser(intent, context.getString(R.string.share_dialog_title)).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + context.startActivity(Intent.createChooser( + intent, context.getString(R.string.share_dialog_title)) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + } + + /** + * Copy the text to clipboard, and indicate to the user whether the operation was completed + * successfully using a Toast. + * + * @param context the context to use + * @param text the text to copy + */ + public static void copyToClipboard(final Context context, final String text) { + final ClipboardManager clipboardManager = + (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + + if (clipboardManager == null) { + Toast.makeText(context, + R.string.permission_denied, + Toast.LENGTH_LONG).show(); + return; + } + + clipboardManager.setPrimaryClip(ClipData.newPlainText(null, text)); + Toast.makeText(context, R.string.msg_copied, Toast.LENGTH_SHORT) + .show(); } } diff --git a/app/src/main/java/org/schabi/newpipe/util/SliderStrategy.java b/app/src/main/java/org/schabi/newpipe/util/SliderStrategy.java index efec1abb0..c6191fcc2 100644 --- a/app/src/main/java/org/schabi/newpipe/util/SliderStrategy.java +++ b/app/src/main/java/org/schabi/newpipe/util/SliderStrategy.java @@ -3,15 +3,21 @@ package org.schabi.newpipe.util; public interface SliderStrategy { /** * Converts from zeroed double with a minimum offset to the nearest rounded slider - * equivalent integer - * */ - int progressOf(final double value); + * equivalent integer. + * + * @param value the value to convert + * @return the converted value + */ + int progressOf(double value); /** * Converts from slider integer value to an equivalent double value with a given - * minimum offset - * */ - double valueOf(final int progress); + * minimum offset. + * + * @param progress the value to convert + * @return the converted value + */ + double valueOf(int progress); // TODO: also implement linear strategy when needed @@ -27,18 +33,19 @@ public interface SliderStrategy { * progress is from the center of the slider. The further away from the center, * the faster the interpreted value changes, and vice versa. * - * @param minimum the minimum value of the interpreted value of the slider. - * @param maximum the maximum value of the interpreted value of the slider. - * @param center center of the interpreted value between the minimum and maximum, which - * will be used as the center value on the slider progress. Doesn't need - * to be the average of the minimum and maximum values, but must be in - * between the two. + * @param minimum the minimum value of the interpreted value of the slider. + * @param maximum the maximum value of the interpreted value of the slider. + * @param center center of the interpreted value between the minimum and maximum, which + * will be used as the center value on the slider progress. Doesn't need + * to be the average of the minimum and maximum values, but must be in + * between the two. * @param maxProgress the maximum possible progress of the slider, this is the * value that is shown for the UI and controls the granularity of * the slider. Should be as large as possible to avoid floating * point round-off error. Using odd number is recommended. - * */ - public Quadratic(double minimum, double maximum, double center, int maxProgress) { + */ + public Quadratic(final double minimum, final double maximum, final double center, + final int maxProgress) { if (center < minimum || center > maximum) { throw new IllegalArgumentException("Center must be in between minimum and maximum"); } @@ -51,18 +58,17 @@ public interface SliderStrategy { } @Override - public int progressOf(double value) { + public int progressOf(final double value) { final double difference = value - center; - final double root = difference >= 0 ? - Math.sqrt(difference / rightGap) : - -Math.sqrt(Math.abs(difference / leftGap)); + final double root = difference >= 0 ? Math.sqrt(difference / rightGap) + : -Math.sqrt(Math.abs(difference / leftGap)); final double offset = Math.round(root * centerProgress); return (int) (centerProgress + offset); } @Override - public double valueOf(int progress) { + public double valueOf(final int progress) { final int offset = progress - centerProgress; final double square = Math.pow(((double) offset) / ((double) centerProgress), 2); final double difference = square * (offset >= 0 ? rightGap : leftGap); diff --git a/app/src/main/java/org/schabi/newpipe/util/SparseArrayUtils.java b/app/src/main/java/org/schabi/newpipe/util/SparseArrayUtils.java deleted file mode 100644 index d17c9aa42..000000000 --- a/app/src/main/java/org/schabi/newpipe/util/SparseArrayUtils.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.schabi.newpipe.util; - -import android.util.SparseArray; - -public abstract class SparseArrayUtils { - - public static void shiftItemsDown(SparseArray sparseArray, int lower, int upper) { - for (int i = lower + 1; i <= upper; i++) { - final T o = sparseArray.get(i); - sparseArray.put(i - 1, o); - sparseArray.remove(i); - } - } - - public static void shiftItemsUp(SparseArray sparseArray, int lower, int upper) { - for (int i = upper - 1; i >= lower; i--) { - final T o = sparseArray.get(i); - sparseArray.put(i + 1, o); - sparseArray.remove(i); - } - } - - public static int[] getKeys(SparseArray sparseArray) { - final int[] result = new int[sparseArray.size()]; - for (int i = 0; i < result.length; i++) { - result[i] = sparseArray.keyAt(i); - } - return result; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/util/StateSaver.java b/app/src/main/java/org/schabi/newpipe/util/StateSaver.java index fffa9e99f..2a1dff5c9 100644 --- a/app/src/main/java/org/schabi/newpipe/util/StateSaver.java +++ b/app/src/main/java/org/schabi/newpipe/util/StateSaver.java @@ -24,11 +24,12 @@ import android.content.Context; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import android.text.TextUtils; import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.MainActivity; @@ -44,14 +45,15 @@ import java.util.Queue; import java.util.concurrent.ConcurrentHashMap; /** - * A way to save state to disk or in a in-memory map if it's just changing configurations (i.e. rotating the phone). + * A way to save state to disk or in a in-memory map + * if it's just changing configurations (i.e. rotating the phone). */ -public class StateSaver { - private static final ConcurrentHashMap> stateObjectsHolder = new ConcurrentHashMap<>(); +public final class StateSaver { + public static final String KEY_SAVED_STATE = "key_saved_state"; + private static final ConcurrentHashMap> STATE_OBJECTS_HOLDER + = new ConcurrentHashMap<>(); private static final String TAG = "StateSaver"; private static final String CACHE_DIR_NAME = "state_cache"; - - public static final String KEY_SAVED_STATE = "key_saved_state"; private static String cacheDirPath; private StateSaver() { @@ -59,78 +61,70 @@ public class StateSaver { } /** - * Initialize the StateSaver, usually you want to call this in the Application class + * Initialize the StateSaver, usually you want to call this in the Application class. * * @param context used to get the available cache dir */ - public static void init(Context context) { + public static void init(final Context context) { File externalCacheDir = context.getExternalCacheDir(); - if (externalCacheDir != null) cacheDirPath = externalCacheDir.getAbsolutePath(); - if (TextUtils.isEmpty(cacheDirPath)) cacheDirPath = context.getCacheDir().getAbsolutePath(); - } - - /** - * Used for describe how to save/read the objects. - *

- * Queue was chosen by its FIFO property. - */ - public interface WriteRead { - /** - * Generate a changing suffix that will name the cache file, - * and be used to identify if it changed (thus reducing useless reading/saving). - * - * @return a unique value - */ - String generateSuffix(); - - /** - * Add to this queue objects that you want to save. - */ - void writeTo(Queue objectsToSave); - - /** - * Poll saved objects from the queue in the order they were written. - * - * @param savedObjects queue of objects returned by {@link #writeTo(Queue)} - */ - void readFrom(@NonNull Queue savedObjects) throws Exception; + if (externalCacheDir != null) { + cacheDirPath = externalCacheDir.getAbsolutePath(); + } + if (TextUtils.isEmpty(cacheDirPath)) { + cacheDirPath = context.getCacheDir().getAbsolutePath(); + } } /** * @see #tryToRestore(SavedState, WriteRead) + * @param outState + * @param writeRead + * @return the saved state */ - public static SavedState tryToRestore(Bundle outState, WriteRead writeRead) { - if (outState == null || writeRead == null) return null; + public static SavedState tryToRestore(final Bundle outState, final WriteRead writeRead) { + if (outState == null || writeRead == null) { + return null; + } SavedState savedState = outState.getParcelable(KEY_SAVED_STATE); - if (savedState == null) return null; + if (savedState == null) { + return null; + } return tryToRestore(savedState, writeRead); } /** - * Try to restore the state from memory and disk, using the {@link StateSaver.WriteRead#readFrom(Queue)} from the writeRead. + * Try to restore the state from memory and disk, + * using the {@link StateSaver.WriteRead#readFrom(Queue)} from the writeRead. + * @param savedState + * @param writeRead + * @return the saved state */ @Nullable - private static SavedState tryToRestore(@NonNull SavedState savedState, @NonNull WriteRead writeRead) { + private static SavedState tryToRestore(@NonNull final SavedState savedState, + @NonNull final WriteRead writeRead) { if (MainActivity.DEBUG) { - Log.d(TAG, "tryToRestore() called with: savedState = [" + savedState + "], writeRead = [" + writeRead + "]"); + Log.d(TAG, "tryToRestore() called with: savedState = [" + savedState + "], " + + "writeRead = [" + writeRead + "]"); } FileInputStream fileInputStream = null; try { - Queue savedObjects = stateObjectsHolder.remove(savedState.getPrefixFileSaved()); + Queue savedObjects + = STATE_OBJECTS_HOLDER.remove(savedState.getPrefixFileSaved()); if (savedObjects != null) { writeRead.readFrom(savedObjects); if (MainActivity.DEBUG) { - Log.d(TAG, "tryToSave: reading objects from holder > " + savedObjects + ", stateObjectsHolder > " + stateObjectsHolder); + Log.d(TAG, "tryToSave: reading objects from holder > " + savedObjects + + ", stateObjectsHolder > " + STATE_OBJECTS_HOLDER); } return savedState; } File file = new File(savedState.getPathFileSaved()); if (!file.exists()) { - if(MainActivity.DEBUG) { + if (MainActivity.DEBUG) { Log.d(TAG, "Cache file doesn't exist: " + file.getAbsolutePath()); } return null; @@ -160,9 +154,16 @@ public class StateSaver { /** * @see #tryToSave(boolean, String, String, WriteRead) + * @param isChangingConfig + * @param savedState + * @param outState + * @param writeRead + * @return the saved state or {@code null} */ @Nullable - public static SavedState tryToSave(boolean isChangingConfig, @Nullable SavedState savedState, Bundle outState, WriteRead writeRead) { + public static SavedState tryToSave(final boolean isChangingConfig, + @Nullable final SavedState savedState, final Bundle outState, + final WriteRead writeRead) { @NonNull String currentSavedPrefix; if (savedState == null || TextUtils.isEmpty(savedState.getPrefixFileSaved())) { @@ -173,34 +174,45 @@ public class StateSaver { currentSavedPrefix = savedState.getPrefixFileSaved(); } - savedState = tryToSave(isChangingConfig, currentSavedPrefix, writeRead.generateSuffix(), writeRead); - if (savedState != null) { - outState.putParcelable(StateSaver.KEY_SAVED_STATE, savedState); - return savedState; + final SavedState newSavedState = tryToSave(isChangingConfig, currentSavedPrefix, + writeRead.generateSuffix(), writeRead); + if (newSavedState != null) { + outState.putParcelable(StateSaver.KEY_SAVED_STATE, newSavedState); + return newSavedState; } return null; } /** - * If it's not changing configuration (i.e. rotating screen), try to write the state from {@link StateSaver.WriteRead#writeTo(Queue)} - * to the file with the name of prefixFileName + suffixFileName, in a cache folder got from the {@link #init(Context)}. + * If it's not changing configuration (i.e. rotating screen), + * try to write the state from {@link StateSaver.WriteRead#writeTo(Queue)} + * to the file with the name of prefixFileName + suffixFileName, + * in a cache folder got from the {@link #init(Context)}. *

- * It checks if the file already exists and if it does, just return the path, so a good way to save is: + * It checks if the file already exists and if it does, just return the path, + * so a good way to save is: + *

*
    - *
  • A fixed prefix for the file
  • - *
  • A changing suffix
  • + *
  • A fixed prefix for the file
  • + *
  • A changing suffix
  • *
* * @param isChangingConfig * @param prefixFileName * @param suffixFileName * @param writeRead + * @return the saved state or {@code null} */ @Nullable - private static SavedState tryToSave(boolean isChangingConfig, final String prefixFileName, String suffixFileName, WriteRead writeRead) { + private static SavedState tryToSave(final boolean isChangingConfig, final String prefixFileName, + final String suffixFileName, final WriteRead writeRead) { if (MainActivity.DEBUG) { - Log.d(TAG, "tryToSave() called with: isChangingConfig = [" + isChangingConfig + "], prefixFileName = [" + prefixFileName + "], suffixFileName = [" + suffixFileName + "], writeRead = [" + writeRead + "]"); + Log.d(TAG, "tryToSave() called with: " + + "isChangingConfig = [" + isChangingConfig + "], " + + "prefixFileName = [" + prefixFileName + "], " + + "suffixFileName = [" + suffixFileName + "], " + + "writeRead = [" + writeRead + "]"); } LinkedList savedObjects = new LinkedList<>(); @@ -208,10 +220,12 @@ public class StateSaver { if (isChangingConfig) { if (savedObjects.size() > 0) { - stateObjectsHolder.put(prefixFileName, savedObjects); + STATE_OBJECTS_HOLDER.put(prefixFileName, savedObjects); return new SavedState(prefixFileName, ""); } else { - if(MainActivity.DEBUG) Log.d(TAG, "Nothing to save"); + if (MainActivity.DEBUG) { + Log.d(TAG, "Nothing to save"); + } return null; } } @@ -219,19 +233,22 @@ public class StateSaver { FileOutputStream fileOutputStream = null; try { File cacheDir = new File(cacheDirPath); - if (!cacheDir.exists()) throw new RuntimeException("Cache dir does not exist > " + cacheDirPath); + if (!cacheDir.exists()) { + throw new RuntimeException("Cache dir does not exist > " + cacheDirPath); + } cacheDir = new File(cacheDir, CACHE_DIR_NAME); if (!cacheDir.exists()) { - if(!cacheDir.mkdir()) { - if(BuildConfig.DEBUG) { - Log.e(TAG, "Failed to create cache directory " + cacheDir.getAbsolutePath()); + if (!cacheDir.mkdir()) { + if (BuildConfig.DEBUG) { + Log.e(TAG, + "Failed to create cache directory " + cacheDir.getAbsolutePath()); } return null; } } - if (TextUtils.isEmpty(suffixFileName)) suffixFileName = ".cache"; - File file = new File(cacheDir, prefixFileName + suffixFileName); + File file = new File(cacheDir, prefixFileName + + (TextUtils.isEmpty(suffixFileName) ? ".cache" : suffixFileName)); if (file.exists() && file.length() > 0) { // If the file already exists, just return it return new SavedState(prefixFileName, file.getAbsolutePath()); @@ -239,7 +256,7 @@ public class StateSaver { // Delete any file that contains the prefix File[] files = cacheDir.listFiles(new FilenameFilter() { @Override - public boolean accept(File dir, String name) { + public boolean accept(final File dir, final String name) { return name.contains(prefixFileName); } }); @@ -259,21 +276,25 @@ public class StateSaver { if (fileOutputStream != null) { try { fileOutputStream.close(); - } catch (IOException ignored) { - } + } catch (IOException ignored) { } } } return null; } /** - * Delete the cache file contained in the savedState and remove any possible-existing value in the memory-cache. + * Delete the cache file contained in the savedState. + * Also remove any possible-existing value in the memory-cache. + * + * @param savedState the saved state to delete */ - public static void onDestroy(SavedState savedState) { - if (MainActivity.DEBUG) Log.d(TAG, "onDestroy() called with: savedState = [" + savedState + "]"); + public static void onDestroy(final SavedState savedState) { + if (MainActivity.DEBUG) { + Log.d(TAG, "onDestroy() called with: savedState = [" + savedState + "]"); + } if (savedState != null && !TextUtils.isEmpty(savedState.getPathFileSaved())) { - stateObjectsHolder.remove(savedState.getPrefixFileSaved()); + STATE_OBJECTS_HOLDER.remove(savedState.getPrefixFileSaved()); try { //noinspection ResultOfMethodCallIgnored new File(savedState.getPathFileSaved()).delete(); @@ -286,35 +307,83 @@ public class StateSaver { * Clear all the files in cache (in memory and disk). */ public static void clearStateFiles() { - if (MainActivity.DEBUG) Log.d(TAG, "clearStateFiles() called"); + if (MainActivity.DEBUG) { + Log.d(TAG, "clearStateFiles() called"); + } - stateObjectsHolder.clear(); + STATE_OBJECTS_HOLDER.clear(); File cacheDir = new File(cacheDirPath); - if (!cacheDir.exists()) return; + if (!cacheDir.exists()) { + return; + } cacheDir = new File(cacheDir, CACHE_DIR_NAME); if (cacheDir.exists()) { - for (File file : cacheDir.listFiles()) file.delete(); + for (File file : cacheDir.listFiles()) { + file.delete(); + } } } + /** + * Used for describe how to save/read the objects. + *

+ * Queue was chosen by its FIFO property. + */ + public interface WriteRead { + /** + * Generate a changing suffix that will name the cache file, + * and be used to identify if it changed (thus reducing useless reading/saving). + * + * @return a unique value + */ + String generateSuffix(); + + /** + * Add to this queue objects that you want to save. + * + * @param objectsToSave the objects to save + */ + void writeTo(Queue objectsToSave); + + /** + * Poll saved objects from the queue in the order they were written. + * + * @param savedObjects queue of objects returned by {@link #writeTo(Queue)} + */ + void readFrom(@NonNull Queue savedObjects) throws Exception; + } + /*////////////////////////////////////////////////////////////////////////// // Inner //////////////////////////////////////////////////////////////////////////*/ /** - * Information about the saved state on the disk + * Information about the saved state on the disk. */ public static class SavedState implements Parcelable { + @SuppressWarnings("unused") + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + @Override + public SavedState createFromParcel(final Parcel in) { + return new SavedState(in); + } + + @Override + public SavedState[] newArray(final int size) { + return new SavedState[size]; + } + }; private final String prefixFileSaved; private final String pathFileSaved; - public SavedState(String prefixFileSaved, String pathFileSaved) { + public SavedState(final String prefixFileSaved, final String pathFileSaved) { this.prefixFileSaved = prefixFileSaved; this.pathFileSaved = pathFileSaved; } - protected SavedState(Parcel in) { + protected SavedState(final Parcel in) { prefixFileSaved = in.readString(); pathFileSaved = in.readString(); } @@ -330,26 +399,14 @@ public class StateSaver { } @Override - public void writeToParcel(Parcel dest, int flags) { + public void writeToParcel(final Parcel dest, final int flags) { dest.writeString(prefixFileSaved); dest.writeString(pathFileSaved); } - @SuppressWarnings("unused") - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { - @Override - public SavedState createFromParcel(Parcel in) { - return new SavedState(in); - } - - @Override - public SavedState[] newArray(int size) { - return new SavedState[size]; - } - }; - /** - * Get the prefix of the saved file + * Get the prefix of the saved file. + * * @return the file prefix */ public String getPrefixFileSaved() { @@ -357,7 +414,8 @@ public class StateSaver { } /** - * Get the path to the saved file + * Get the path to the saved file. + * * @return the path to the saved file */ public String getPathFileSaved() { diff --git a/app/src/main/java/org/schabi/newpipe/util/StreamDialogEntry.java b/app/src/main/java/org/schabi/newpipe/util/StreamDialogEntry.java index b3ec4d14e..92aee8ba7 100644 --- a/app/src/main/java/org/schabi/newpipe/util/StreamDialogEntry.java +++ b/app/src/main/java/org/schabi/newpipe/util/StreamDialogEntry.java @@ -1,6 +1,7 @@ package org.schabi.newpipe.util; import android.content.Context; + import androidx.fragment.app.Fragment; import org.schabi.newpipe.R; @@ -16,26 +17,33 @@ public enum StreamDialogEntry { ////////////////////////////////////// enqueue_on_background(R.string.enqueue_on_background, (fragment, item) -> - NavigationHelper.enqueueOnBackgroundPlayer(fragment.getContext(), new SinglePlayQueue(item), false)), + NavigationHelper.enqueueOnBackgroundPlayer(fragment.getContext(), + new SinglePlayQueue(item), false)), enqueue_on_popup(R.string.enqueue_on_popup, (fragment, item) -> - NavigationHelper.enqueueOnPopupPlayer(fragment.getContext(), new SinglePlayQueue(item), false)), + NavigationHelper.enqueueOnPopupPlayer(fragment.getContext(), + new SinglePlayQueue(item), false)), start_here_on_background(R.string.start_here_on_background, (fragment, item) -> - NavigationHelper.playOnBackgroundPlayer(fragment.getContext(), new SinglePlayQueue(item), true)), + NavigationHelper.playOnBackgroundPlayer(fragment.getContext(), + new SinglePlayQueue(item), true)), start_here_on_popup(R.string.start_here_on_popup, (fragment, item) -> - NavigationHelper.playOnPopupPlayer(fragment.getContext(), new SinglePlayQueue(item), true)), + NavigationHelper.playOnPopupPlayer(fragment.getContext(), + new SinglePlayQueue(item), true)), - set_as_playlist_thumbnail(R.string.set_as_playlist_thumbnail, (fragment, item) -> {}), // has to be set manually + set_as_playlist_thumbnail(R.string.set_as_playlist_thumbnail, (fragment, item) -> { + }), // has to be set manually - delete(R.string.delete, (fragment, item) -> {}), // has to be set manually + delete(R.string.delete, (fragment, item) -> { + }), // has to be set manually append_playlist(R.string.append_playlist, (fragment, item) -> { if (fragment.getFragmentManager() != null) { PlaylistAppendDialog.fromStreamInfoItems(Collections.singletonList(item)) .show(fragment.getFragmentManager(), "StreamDialogEntry@append_playlist"); - }}), + } + }), share(R.string.share, (fragment, item) -> ShareUtils.shareUrl(fragment.getContext(), item.getName(), item.getUrl())); @@ -45,43 +53,28 @@ public enum StreamDialogEntry { // variables // /////////////// - public interface StreamDialogEntryAction { - void onClick(Fragment fragment, final StreamInfoItem infoItem); - } - + private static StreamDialogEntry[] enabledEntries; private final int resource; private final StreamDialogEntryAction defaultAction; private StreamDialogEntryAction customAction; - private static StreamDialogEntry[] enabledEntries; + StreamDialogEntry(final int resource, final StreamDialogEntryAction defaultAction) { + this.resource = resource; + this.defaultAction = defaultAction; + this.customAction = null; + } /////////////////////////////////////////////////////// // non-static methods to initialize and edit entries // /////////////////////////////////////////////////////// - StreamDialogEntry(final int resource, StreamDialogEntryAction defaultAction) { - this.resource = resource; - this.defaultAction = defaultAction; - this.customAction = null; - } - /** - * Can be used after {@link #setEnabledEntries(StreamDialogEntry...)} has been called + * To be called before using {@link #setCustomAction(StreamDialogEntryAction)}. + * + * @param entries the entries to be enabled */ - public void setCustomAction(StreamDialogEntryAction action) { - this.customAction = action; - } - - - //////////////////////////////////////////////// - // static methods that act on enabled entries // - //////////////////////////////////////////////// - - /** - * To be called before using {@link #setCustomAction(StreamDialogEntryAction)} - */ - public static void setEnabledEntries(StreamDialogEntry... entries) { + public static void setEnabledEntries(final StreamDialogEntry... entries) { // cleanup from last time StreamDialogEntry was used for (StreamDialogEntry streamDialogEntry : values()) { streamDialogEntry.customAction = null; @@ -90,7 +83,7 @@ public enum StreamDialogEntry { enabledEntries = entries; } - public static String[] getCommands(Context context) { + public static String[] getCommands(final Context context) { String[] commands = new String[enabledEntries.length]; for (int i = 0; i != enabledEntries.length; ++i) { commands[i] = context.getResources().getString(enabledEntries[i].resource); @@ -99,11 +92,30 @@ public enum StreamDialogEntry { return commands; } - public static void clickOn(int which, Fragment fragment, StreamInfoItem infoItem) { + + //////////////////////////////////////////////// + // static methods that act on enabled entries // + //////////////////////////////////////////////// + + public static void clickOn(final int which, final Fragment fragment, + final StreamInfoItem infoItem) { if (enabledEntries[which].customAction == null) { enabledEntries[which].defaultAction.onClick(fragment, infoItem); } else { enabledEntries[which].customAction.onClick(fragment, infoItem); } } + + /** + * Can be used after {@link #setEnabledEntries(StreamDialogEntry...)} has been called. + * + * @param action the action to be set + */ + public void setCustomAction(final StreamDialogEntryAction action) { + this.customAction = action; + } + + public interface StreamDialogEntryAction { + void onClick(Fragment fragment, StreamInfoItem infoItem); + } } diff --git a/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java b/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java index 312c47263..6a244a69b 100644 --- a/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java @@ -18,6 +18,7 @@ import org.schabi.newpipe.extractor.stream.SubtitlesStream; import org.schabi.newpipe.extractor.stream.VideoStream; import java.io.Serializable; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.concurrent.Callable; @@ -28,8 +29,11 @@ import io.reactivex.schedulers.Schedulers; import us.shandian.giga.util.Utility; /** - * A list adapter for a list of {@link Stream streams}, - * currently supporting {@link VideoStream}, {@link AudioStream} and {@link SubtitlesStream} + * A list adapter for a list of {@link Stream streams}. + * It currently supports {@link VideoStream}, {@link AudioStream} and {@link SubtitlesStream}. + * + * @param the primary stream type's class extending {@link Stream} + * @param the secondary stream type's class extending {@link Stream} */ public class StreamItemAdapter extends BaseAdapter { private final Context context; @@ -37,17 +41,19 @@ public class StreamItemAdapter extends BaseA private final StreamSizeWrapper streamsWrapper; private final SparseArray> secondaryStreams; - public StreamItemAdapter(Context context, StreamSizeWrapper streamsWrapper, SparseArray> secondaryStreams) { + public StreamItemAdapter(final Context context, final StreamSizeWrapper streamsWrapper, + final SparseArray> secondaryStreams) { this.context = context; this.streamsWrapper = streamsWrapper; this.secondaryStreams = secondaryStreams; } - public StreamItemAdapter(Context context, StreamSizeWrapper streamsWrapper, boolean showIconNoAudio) { + public StreamItemAdapter(final Context context, final StreamSizeWrapper streamsWrapper, + final boolean showIconNoAudio) { this(context, streamsWrapper, showIconNoAudio ? new SparseArray<>() : null); } - public StreamItemAdapter(Context context, StreamSizeWrapper streamsWrapper) { + public StreamItemAdapter(final Context context, final StreamSizeWrapper streamsWrapper) { this(context, streamsWrapper, null); } @@ -65,28 +71,33 @@ public class StreamItemAdapter extends BaseA } @Override - public T getItem(int position) { + public T getItem(final int position) { return streamsWrapper.getStreamsList().get(position); } @Override - public long getItemId(int position) { + public long getItemId(final int position) { return position; } @Override - public View getDropDownView(int position, View convertView, ViewGroup parent) { + public View getDropDownView(final int position, final View convertView, + final ViewGroup parent) { return getCustomView(position, convertView, parent, true); } @Override - public View getView(int position, View convertView, ViewGroup parent) { - return getCustomView(((Spinner) parent).getSelectedItemPosition(), convertView, parent, false); + public View getView(final int position, final View convertView, final ViewGroup parent) { + return getCustomView(((Spinner) parent).getSelectedItemPosition(), + convertView, parent, false); } - private View getCustomView(int position, View convertView, ViewGroup parent, boolean isDropdownItem) { + private View getCustomView(final int position, final View view, final ViewGroup parent, + final boolean isDropdownItem) { + View convertView = view; if (convertView == null) { - convertView = LayoutInflater.from(context).inflate(R.layout.stream_quality_item, parent, false); + convertView = LayoutInflater.from(context).inflate( + R.layout.stream_quality_item, parent, false); } final ImageView woSoundIconView = convertView.findViewById(R.id.wo_sound_icon); @@ -105,7 +116,8 @@ public class StreamItemAdapter extends BaseA if (secondaryStreams != null) { if (videoStream.isVideoOnly()) { - woSoundIconVisibility = secondaryStreams.get(position) == null ? View.VISIBLE : View.INVISIBLE; + woSoundIconVisibility = secondaryStreams.get(position) == null ? View.VISIBLE + : View.INVISIBLE; } else if (isDropdownItem) { woSoundIconVisibility = View.INVISIBLE; } @@ -125,7 +137,8 @@ public class StreamItemAdapter extends BaseA } if (streamsWrapper.getSizeInBytes(position) > 0) { - SecondaryStreamHelper secondary = secondaryStreams == null ? null : secondaryStreams.get(position); + SecondaryStreamHelper secondary = secondaryStreams == null ? null + : secondaryStreams.get(position); if (secondary != null) { long size = secondary.getSizeInBytes() + streamsWrapper.getSizeInBytes(position); sizeView.setText(Utility.formatBytes(size)); @@ -140,7 +153,15 @@ public class StreamItemAdapter extends BaseA if (stream instanceof SubtitlesStream) { formatNameView.setText(((SubtitlesStream) stream).getLanguageTag()); } else { - formatNameView.setText(stream.getFormat().getName()); + switch (stream.getFormat()) { + case WEBMA_OPUS: + // noinspection AndroidLintSetTextI18n + formatNameView.setText("opus"); + break; + default: + formatNameView.setText(stream.getFormat().getName()); + break; + } } qualityView.setText(qualityString); @@ -151,30 +172,36 @@ public class StreamItemAdapter extends BaseA /** * A wrapper class that includes a way of storing the stream sizes. + * + * @param the stream type's class extending {@link Stream} */ public static class StreamSizeWrapper implements Serializable { - private static final StreamSizeWrapper EMPTY = new StreamSizeWrapper<>(Collections.emptyList(), null); + private static final StreamSizeWrapper EMPTY = new StreamSizeWrapper<>( + Collections.emptyList(), null); private final List streamsList; private final long[] streamSizes; private final String unknownSize; - public StreamSizeWrapper(List sL, Context context) { + public StreamSizeWrapper(final List sL, final Context context) { this.streamsList = sL != null ? sL : Collections.emptyList(); this.streamSizes = new long[streamsList.size()]; - this.unknownSize = context == null ? "--.-" : context.getString(R.string.unknown_content); + this.unknownSize = context == null + ? "--.-" : context.getString(R.string.unknown_content); - for (int i = 0; i < streamSizes.length; i++) streamSizes[i] = -2; + Arrays.fill(streamSizes, -2); } /** * Helper method to fetch the sizes of all the streams in a wrapper. * + * @param the stream type's class extending {@link Stream} * @param streamsWrapper the wrapper * @return a {@link Single} that returns a boolean indicating if any elements were changed */ - public static Single fetchSizeForWrapper(StreamSizeWrapper streamsWrapper) { + public static Single fetchSizeForWrapper( + final StreamSizeWrapper streamsWrapper) { final Callable fetchAndSet = () -> { boolean hasChanged = false; for (X stream : streamsWrapper.getStreamsList()) { @@ -182,7 +209,8 @@ public class StreamItemAdapter extends BaseA continue; } - final long contentLength = DownloaderImpl.getInstance().getContentLength(stream.getUrl()); + final long contentLength = DownloaderImpl.getInstance().getContentLength( + stream.getUrl()); streamsWrapper.setSize(stream, contentLength); hasChanged = true; } @@ -195,44 +223,44 @@ public class StreamItemAdapter extends BaseA .onErrorReturnItem(true); } + public static StreamSizeWrapper empty() { + //noinspection unchecked + return (StreamSizeWrapper) EMPTY; + } + public List getStreamsList() { return streamsList; } - public long getSizeInBytes(int streamIndex) { + public long getSizeInBytes(final int streamIndex) { return streamSizes[streamIndex]; } - public long getSizeInBytes(T stream) { + public long getSizeInBytes(final T stream) { return streamSizes[streamsList.indexOf(stream)]; } - public String getFormattedSize(int streamIndex) { + public String getFormattedSize(final int streamIndex) { return formatSize(getSizeInBytes(streamIndex)); } - public String getFormattedSize(T stream) { + public String getFormattedSize(final T stream) { return formatSize(getSizeInBytes(stream)); } - private String formatSize(long size) { + private String formatSize(final long size) { if (size > -1) { return Utility.formatBytes(size); } return unknownSize; } - public void setSize(int streamIndex, long sizeInBytes) { + public void setSize(final int streamIndex, final long sizeInBytes) { streamSizes[streamIndex] = sizeInBytes; } - public void setSize(T stream, long sizeInBytes) { + public void setSize(final T stream, final long sizeInBytes) { streamSizes[streamsList.indexOf(stream)] = sizeInBytes; } - - public static StreamSizeWrapper empty() { - //noinspection unchecked - return (StreamSizeWrapper) EMPTY; - } } } diff --git a/app/src/main/java/org/schabi/newpipe/util/TLSSocketFactoryCompat.java b/app/src/main/java/org/schabi/newpipe/util/TLSSocketFactoryCompat.java index d8b6f78f5..105af5086 100644 --- a/app/src/main/java/org/schabi/newpipe/util/TLSSocketFactoryCompat.java +++ b/app/src/main/java/org/schabi/newpipe/util/TLSSocketFactoryCompat.java @@ -3,7 +3,6 @@ package org.schabi.newpipe.util; import java.io.IOException; import java.net.InetAddress; import java.net.Socket; -import java.net.UnknownHostException; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; @@ -27,31 +26,36 @@ public class TLSSocketFactoryCompat extends SSLSocketFactory { private SSLSocketFactory internalSSLSocketFactory; - public static TLSSocketFactoryCompat getInstance() throws NoSuchAlgorithmException, KeyManagementException { - if (instance != null) { - return instance; - } - return instance = new TLSSocketFactoryCompat(); - } - - public TLSSocketFactoryCompat() throws KeyManagementException, NoSuchAlgorithmException { SSLContext context = SSLContext.getInstance("TLS"); context.init(null, null, null); internalSSLSocketFactory = context.getSocketFactory(); } - public TLSSocketFactoryCompat(TrustManager[] tm) throws KeyManagementException, NoSuchAlgorithmException { + + public TLSSocketFactoryCompat(final TrustManager[] tm) + throws KeyManagementException, NoSuchAlgorithmException { SSLContext context = SSLContext.getInstance("TLS"); context.init(null, tm, new java.security.SecureRandom()); internalSSLSocketFactory = context.getSocketFactory(); } + public static TLSSocketFactoryCompat getInstance() + throws NoSuchAlgorithmException, KeyManagementException { + if (instance != null) { + return instance; + } + instance = new TLSSocketFactoryCompat(); + return instance; + } + public static void setAsDefault() { try { HttpsURLConnection.setDefaultSSLSocketFactory(getInstance()); } catch (NoSuchAlgorithmException | KeyManagementException e) { - if (DEBUG) e.printStackTrace(); + if (DEBUG) { + e.printStackTrace(); + } } } @@ -71,34 +75,40 @@ public class TLSSocketFactoryCompat extends SSLSocketFactory { } @Override - public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { + public Socket createSocket(final Socket s, final String host, final int port, + final boolean autoClose) throws IOException { return enableTLSOnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose)); } @Override - public Socket createSocket(String host, int port) throws IOException, UnknownHostException { + public Socket createSocket(final String host, final int port) throws IOException { return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port)); } @Override - public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException { - return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort)); + public Socket createSocket(final String host, final int port, final InetAddress localHost, + final int localPort) throws IOException { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket( + host, port, localHost, localPort)); } @Override - public Socket createSocket(InetAddress host, int port) throws IOException { + public Socket createSocket(final InetAddress host, final int port) throws IOException { return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port)); } @Override - public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { - return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort)); + public Socket createSocket(final InetAddress address, final int port, + final InetAddress localAddress, final int localPort) + throws IOException { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket( + address, port, localAddress, localPort)); } - private Socket enableTLSOnSocket(Socket socket) { + private Socket enableTLSOnSocket(final Socket socket) { if (socket != null && (socket instanceof SSLSocket)) { ((SSLSocket) socket).setEnabledProtocols(new String[]{"TLSv1.1", "TLSv1.2"}); } return socket; } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java b/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java index 661aa47c1..74ea34fcc 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java @@ -22,18 +22,20 @@ package org.schabi.newpipe.util; import android.content.Context; import android.content.res.TypedArray; import android.preference.PreferenceManager; +import android.util.TypedValue; +import android.view.ContextThemeWrapper; + import androidx.annotation.AttrRes; import androidx.annotation.StyleRes; import androidx.core.content.ContextCompat; -import android.util.TypedValue; -import android.view.ContextThemeWrapper; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.exceptions.ExtractionException; -public class ThemeHelper { +public final class ThemeHelper { + private ThemeHelper() { } /** * Apply the selected theme (on NewPipe settings) in the context @@ -41,7 +43,7 @@ public class ThemeHelper { * * @param context context that the theme will be applied */ - public static void setTheme(Context context) { + public static void setTheme(final Context context) { setTheme(context, -1); } @@ -53,17 +55,19 @@ public class ThemeHelper { * @param serviceId the theme will be styled to the service with this id, * pass -1 to get the default style */ - public static void setTheme(Context context, int serviceId) { + public static void setTheme(final Context context, final int serviceId) { context.setTheme(getThemeForService(context, serviceId)); } /** - * Return true if the selected theme (on NewPipe settings) is the Light theme + * Return true if the selected theme (on NewPipe settings) is the Light theme. * * @param context context to get the preference + * @return whether the light theme is selected */ - public static boolean isLightThemeSelected(Context context) { - return getSelectedThemeString(context).equals(context.getResources().getString(R.string.light_theme_key)); + public static boolean isLightThemeSelected(final Context context) { + return getSelectedThemeString(context).equals(context.getResources() + .getString(R.string.light_theme_key)); } @@ -73,18 +77,19 @@ public class ThemeHelper { * @param baseContext the base context for the wrapper * @return a wrapped-styled context */ - public static Context getThemedContext(Context baseContext) { + public static Context getThemedContext(final Context baseContext) { return new ContextThemeWrapper(baseContext, getThemeForService(baseContext, -1)); } /** - * Return the selected theme without being styled to any service (see {@link #getThemeForService(Context, int)}). + * Return the selected theme without being styled to any service. + * See {@link #getThemeForService(Context, int)}. * * @param context context to get the selected theme * @return the selected style (the default one) */ @StyleRes - public static int getDefaultTheme(Context context) { + public static int getDefaultTheme(final Context context) { return getThemeForService(context, -1); } @@ -95,10 +100,22 @@ public class ThemeHelper { * @return the dialog style (the default one) */ @StyleRes - public static int getDialogTheme(Context context) { + public static int getDialogTheme(final Context context) { return isLightThemeSelected(context) ? R.style.LightDialogTheme : R.style.DarkDialogTheme; } + /** + * Return a min-width dialog theme styled according to the (default) selected theme. + * + * @param context context to get the selected theme + * @return the dialog style (the default one) + */ + @StyleRes + public static int getMinWidthDialogTheme(final Context context) { + return isLightThemeSelected(context) ? R.style.LightDialogMinWidthTheme + : R.style.DarkDialogMinWidthTheme; + } + /** * Return the selected theme styled according to the serviceId. * @@ -108,7 +125,7 @@ public class ThemeHelper { * @return the selected style (styled) */ @StyleRes - public static int getThemeForService(Context context, int serviceId) { + public static int getThemeForService(final Context context, final int serviceId) { String lightTheme = context.getResources().getString(R.string.light_theme_key); String darkTheme = context.getResources().getString(R.string.dark_theme_key); String blackTheme = context.getResources().getString(R.string.black_theme_key); @@ -116,9 +133,13 @@ public class ThemeHelper { String selectedTheme = getSelectedThemeString(context); int defaultTheme = R.style.DarkTheme; - if (selectedTheme.equals(lightTheme)) defaultTheme = R.style.LightTheme; - else if (selectedTheme.equals(blackTheme)) defaultTheme = R.style.BlackTheme; - else if (selectedTheme.equals(darkTheme)) defaultTheme = R.style.DarkTheme; + if (selectedTheme.equals(lightTheme)) { + defaultTheme = R.style.LightTheme; + } else if (selectedTheme.equals(blackTheme)) { + defaultTheme = R.style.BlackTheme; + } else if (selectedTheme.equals(darkTheme)) { + defaultTheme = R.style.DarkTheme; + } if (serviceId <= -1) { return defaultTheme; @@ -132,9 +153,13 @@ public class ThemeHelper { } String themeName = "DarkTheme"; - if (selectedTheme.equals(lightTheme)) themeName = "LightTheme"; - else if (selectedTheme.equals(blackTheme)) themeName = "BlackTheme"; - else if (selectedTheme.equals(darkTheme)) themeName = "DarkTheme"; + if (selectedTheme.equals(lightTheme)) { + themeName = "LightTheme"; + } else if (selectedTheme.equals(blackTheme)) { + themeName = "BlackTheme"; + } else if (selectedTheme.equals(darkTheme)) { + themeName = "DarkTheme"; + } themeName += "." + service.getServiceInfo().getName(); int resourceId = context @@ -149,24 +174,33 @@ public class ThemeHelper { } @StyleRes - public static int getSettingsThemeStyle(Context context) { + public static int getSettingsThemeStyle(final Context context) { String lightTheme = context.getResources().getString(R.string.light_theme_key); String darkTheme = context.getResources().getString(R.string.dark_theme_key); String blackTheme = context.getResources().getString(R.string.black_theme_key); String selectedTheme = getSelectedThemeString(context); - if (selectedTheme.equals(lightTheme)) return R.style.LightSettingsTheme; - else if (selectedTheme.equals(blackTheme)) return R.style.BlackSettingsTheme; - else if (selectedTheme.equals(darkTheme)) return R.style.DarkSettingsTheme; + if (selectedTheme.equals(lightTheme)) { + return R.style.LightSettingsTheme; + } else if (selectedTheme.equals(blackTheme)) { + return R.style.BlackSettingsTheme; + } else if (selectedTheme.equals(darkTheme)) { + return R.style.DarkSettingsTheme; + } else { // Fallback - else return R.style.DarkSettingsTheme; + return R.style.DarkSettingsTheme; + } } /** - * Get a resource id from a resource styled according to the the context's theme. + * Get a resource id from a resource styled according to the context's theme. + * + * @param context Android app context + * @param attr attribute reference of the resource + * @return resource ID */ - public static int resolveResourceIdFromAttr(Context context, @AttrRes int attr) { + public static int resolveResourceIdFromAttr(final Context context, @AttrRes final int attr) { TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{attr}); int attributeResourceId = a.getResourceId(0, 0); a.recycle(); @@ -174,9 +208,13 @@ public class ThemeHelper { } /** - * Get a color from an attr styled according to the the context's theme. + * Get a color from an attr styled according to the context's theme. + * + * @param context Android app context + * @param attrColor attribute reference of the resource + * @return the color */ - public static int resolveColorFromAttr(Context context, @AttrRes int attrColor) { + public static int resolveColorFromAttr(final Context context, @AttrRes final int attrColor) { final TypedValue value = new TypedValue(); context.getTheme().resolveAttribute(attrColor, value, true); @@ -187,21 +225,10 @@ public class ThemeHelper { return value.data; } - private static String getSelectedThemeString(Context context) { + private static String getSelectedThemeString(final Context context) { String themeKey = context.getString(R.string.theme_key); String defaultTheme = context.getResources().getString(R.string.default_theme_value); - return PreferenceManager.getDefaultSharedPreferences(context).getString(themeKey, defaultTheme); - } - - /** - * This will get the R.drawable.* resource to which attr is currently pointing to. - * - * @param attr a R.attribute.* resource value - * @param context the context to use - * @return a R.drawable.* resource value - */ - public static int getIconByAttr(final int attr, final Context context) { - return context.obtainStyledAttributes(new int[] {attr}) - .getResourceId(0, -1); + return PreferenceManager.getDefaultSharedPreferences(context) + .getString(themeKey, defaultTheme); } } diff --git a/app/src/main/java/org/schabi/newpipe/util/ZipHelper.java b/app/src/main/java/org/schabi/newpipe/util/ZipHelper.java index 3142ad8dc..31f5fd222 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ZipHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ZipHelper.java @@ -12,42 +12,45 @@ import java.util.zip.ZipOutputStream; * Created by Christian Schabesberger on 28.01.18. * Copyright 2018 Christian Schabesberger * ZipHelper.java is part of NewPipe - * + *

* License: GPL-3.0+ * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + *

* This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + *

* You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -public class ZipHelper { +public final class ZipHelper { + private ZipHelper() { } private static final int BUFFER_SIZE = 2048; /** * This function helps to create zip files. * Caution this will override the original file. + * * @param outZip The ZipOutputStream where the data should be stored in - * @param file The path of the file that should be added to zip. - * @param name The path of the file inside the zip. + * @param file The path of the file that should be added to zip. + * @param name The path of the file inside the zip. * @throws Exception */ - public static void addFileToZip(ZipOutputStream outZip, String file, String name) throws Exception { - byte data[] = new byte[BUFFER_SIZE]; + public static void addFileToZip(final ZipOutputStream outZip, final String file, + final String name) throws Exception { + byte[] data = new byte[BUFFER_SIZE]; FileInputStream fi = new FileInputStream(file); BufferedInputStream inputStream = new BufferedInputStream(fi, BUFFER_SIZE); ZipEntry entry = new ZipEntry(name); outZip.putNextEntry(entry); int count; - while((count = inputStream.read(data, 0, BUFFER_SIZE)) != -1) { + while ((count = inputStream.read(data, 0, BUFFER_SIZE)) != -1) { outZip.write(data, 0, count); } inputStream.close(); @@ -56,36 +59,39 @@ public class ZipHelper { /** * This will extract data from Zipfiles. * Caution this will override the original file. + * + * @param filePath The path of the zip * @param file The path of the file on the disk where the data should be extracted to. * @param name The path of the file inside the zip. * @return will return true if the file was found within the zip file * @throws Exception */ - public static boolean extractFileFromZip(String filePath, String file, String name) throws Exception { + public static boolean extractFileFromZip(final String filePath, final String file, + final String name) throws Exception { ZipInputStream inZip = new ZipInputStream( new BufferedInputStream( new FileInputStream(filePath))); - byte data[] = new byte[BUFFER_SIZE]; + byte[] data = new byte[BUFFER_SIZE]; boolean found = false; ZipEntry ze; - while((ze = inZip.getNextEntry()) != null) { - if(ze.getName().equals(name)) { + while ((ze = inZip.getNextEntry()) != null) { + if (ze.getName().equals(name)) { found = true; // delete old file first File oldFile = new File(file); - if(oldFile.exists()) { - if(!oldFile.delete()) { + if (oldFile.exists()) { + if (!oldFile.delete()) { throw new Exception("Could not delete " + file); } } FileOutputStream outFile = new FileOutputStream(file); int count = 0; - while((count = inZip.read(data)) != -1) { + while ((count = inZip.read(data)) != -1) { outFile.write(data, 0, count); } diff --git a/app/src/main/java/org/schabi/newpipe/util/urlfinder/PatternsCompat.java b/app/src/main/java/org/schabi/newpipe/util/urlfinder/PatternsCompat.java new file mode 100644 index 000000000..5a0dbb003 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/urlfinder/PatternsCompat.java @@ -0,0 +1,377 @@ +/* THIS FILE WAS MODIFIED, CHANGES ARE DOCUMENTED. */ + +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.schabi.newpipe.util.urlfinder; + +import androidx.annotation.RestrictTo; + +import java.util.regex.Pattern; + +import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX; + +/** + * Commonly used regular expression patterns. + */ +public final class PatternsCompat { + /** + * Regular expression to match all IANA top-level domains. + * + * List accurate as of 2015/11/24. List taken from: + * http://data.iana.org/TLD/tlds-alpha-by-domain.txt + * This pattern is auto-generated by frameworks/ex/common/tools/make-iana-tld-pattern.py + */ + static final String IANA_TOP_LEVEL_DOMAINS = "(?:" + + "(?:aaa|aarp|abb|abbott|abogado|academy|accenture|accountant|accountants|aco|active" + + "|actor|ads|adult|aeg|aero|afl|agency|aig|airforce|airtel|allfinanz|alsace|amica" + + "|amsterdam|android|apartments|app|apple|aquarelle|aramco|archi|army|arpa|arte|asia" + + "|associates|attorney|auction|audio|auto|autos|axa|azure|a[cdefgilmoqrstuwxz])" + + "|(?:band|bank|bar|barcelona|barclaycard|barclays|bargains|bauhaus|bayern|bbc|bbva" + + "|bcn|beats|beer|bentley|berlin|best|bet|bharti|bible|bid|bike|bing|bingo|bio|biz" + + "|black|blackfriday|bloomberg|blue|bms|bmw|bnl|bnpparibas|boats|bom|bond|boo|boots" + + "|boutique|bradesco|bridgestone|broadway|broker|brother|brussels|budapest|build" + + "|builders|business|buzz|bzh|b[abdefghijmnorstvwyz])" + + "|(?:cab|cafe|cal|camera|camp|cancerresearch|canon|capetown|capital|car|caravan|cards" + + "|care|career|careers|cars|cartier|casa|cash|casino|cat|catering|cba|cbn|ceb|center" + + "|ceo|cern|cfa|cfd|chanel|channel|chat|cheap|chloe|christmas|chrome|church|cipriani" + + "|cisco|citic|city|cityeats|claims|cleaning|click|clinic|clothing|cloud|club|clubmed" + + "|coach|codes|coffee|college|cologne|com|commbank|community|company|computer|comsec" + + "|condos|construction|consulting|contractors|cooking|cool|coop|corsica|country" + + "|coupons|courses|credit|creditcard|creditunion|cricket|crown|crs|cruises|csc" + + "|cuisinella|cymru|cyou|c[acdfghiklmnoruvwxyz])" + + "|(?:dabur|dad|dance|date|dating|datsun|day|dclk|deals|degree|delivery|dell|delta" + + "|democrat|dental|dentist|desi|design|dev|diamonds|diet|digital|direct|directory" + + "|discount|dnp|docs|dog|doha|domains|doosan|download|drive|durban|dvag|d[ejkmoz])" + + "|(?:earth|eat|edu|education|email|emerck|energy|engineer|engineering|enterprises" + + "|epson|equipment|erni|esq|estate|eurovision|eus|events|everbank|exchange|expert" + + "|exposed|express|e[cegrstu])" + + "|(?:fage|fail|fairwinds|faith|family|fan|fans|farm" + + "|fashion|feedback|ferrero|film|final|finance|financial|firmdale|fish|fishing|fit" + + "|fitness|flights|florist|flowers|flsmidth|fly|foo|football|forex|forsale|forum" + + "|foundation|frl|frogans|fund|furniture|futbol|fyi|f[ijkmor])" + + "|(?:gal|gallery|game|garden|gbiz|gdn|gea|gent|genting|ggee|gift|gifts|gives|giving" + + "|glass|gle|global|globo|gmail|gmo|gmx|gold|goldpoint|golf|goo|goog|google|gop|gov" + + "|grainger|graphics|gratis|green|gripe|group|gucci|guge|guide|guitars|guru" + + "|g[abdefghilmnpqrstuwy])" + + "|(?:hamburg|hangout|haus|healthcare|help|here|hermes|hiphop|hitachi|hiv|hockey" + + "|holdings|holiday|homedepot|homes|honda|horse|host|hosting|hoteles|hotmail|house" + + "|how|hsbc|hyundai|h[kmnrtu])" + + "|(?:ibm|icbc|ice|icu|ifm|iinet|immo|immobilien|industries|infiniti|info|ing|ink" + + "|institute|insure|int|international|investments|ipiranga|irish|ist|istanbul|itau" + + "|iwc|i[delmnoqrst])" + + "|(?:jaguar|java|jcb|jetzt|jewelry|jlc|jll|jobs|joburg|jprs|juegos|j[emop])" + + "|(?:kaufen|kddi|kia|kim|kinder|kitchen|kiwi|koeln|komatsu|krd|kred|kyoto" + + "|k[eghimnprwyz])" + + "|(?:lacaixa|lancaster|land|landrover|lasalle|lat|latrobe|law|lawyer|lds|lease" + + "|leclerc|legal|lexus|lgbt|liaison|lidl|life|lifestyle|lighting|limited|limo|linde" + + "|link|live|lixil|loan|loans|lol|london|lotte|lotto|love|ltd|ltda|lupin|luxe|luxury" + + "|l[abcikrstuvy])" + + "|(?:madrid|maif|maison|man|management|mango|market|marketing|markets|marriott|mba" + + "|media|meet|melbourne|meme|memorial|men|menu|meo|miami|microsoft|mil|mini|mma|mobi" + + "|moda|moe|moi|mom|monash|money|montblanc|mormon|mortgage|moscow|motorcycles|mov" + + "|movie|movistar|mtn|mtpc|mtr|museum|mutuelle|m[acdeghklmnopqrstuvwxyz])" + + "|(?:nadex|nagoya|name|navy|nec|net|netbank|network|neustar|new|news|nexus|ngo|nhk" + + "|nico|ninja|nissan|nokia|nra|nrw|ntt|nyc|n[acefgilopruz])" + + "|(?:obi|office|okinawa|omega|one|ong|onl|online|ooo|oracle|orange|org|organic|osaka" + + "|otsuka|ovh|om)" + + "|(?:page|panerai|paris|partners|parts|party|pet|pharmacy|philips|photo|photography" + + "|photos|physio|piaget|pics|pictet|pictures|ping|pink|pizza|place|play|playstation" + + "|plumbing|plus|pohl|poker|porn|post|praxi|press|pro|prod|productions|prof|properties" + + "|property|protection|pub|p[aefghklmnrstwy])" + + "|(?:qpon|quebec|qa)" + + "|(?:racing|realtor|realty|recipes|red|redstone|rehab|reise|reisen|reit|ren|rent" + + "|rentals|repair|report|republican|rest|restaurant|review|reviews|rich|ricoh|rio|rip" + + "|rocher|rocks|rodeo|rsvp|ruhr|run|rwe|ryukyu|r[eosuw])" + + "|(?:saarland|sakura|sale|samsung|sandvik|sandvikcoromant|sanofi|sap|sapo|sarl|saxo" + + "|sbs|sca|scb|schmidt|scholarships|school|schule|schwarz|science|scor|scot|seat" + + "|security|seek|sener|services|seven|sew|sex|sexy|shiksha|shoes|show|shriram|singles" + + "|site|ski|sky|skype|sncf|soccer|social|software|sohu|solar|solutions|sony|soy|space" + + "|spiegel|spreadbetting|srl|stada|starhub|statoil|stc|stcgroup|stockholm|studio|study" + + "|style|sucks|supplies|supply|support|surf|surgery|suzuki|swatch|swiss|sydney|systems" + + "|s[abcdeghijklmnortuvxyz])" + + "|(?:tab|taipei|tatamotors|tatar|tattoo|tax|taxi|team|tech|technology|tel|telefonica" + + "|temasek|tennis|thd|theater|theatre|tickets|tienda|tips|tires|tirol|today|tokyo" + + "|tools|top|toray|toshiba|tours|town|toyota|toys|trade|trading|training|travel|trust" + + "|tui|t[cdfghjklmnortvwz])" + + "|(?:ubs|university|uno|uol|u[agksyz])" + + "|(?:vacations|vana|vegas|ventures|versicherung|vet|viajes|video|villas|vin|virgin" + + "|vision|vista|vistaprint|viva|vlaanderen|vodka|vote|voting|voto|voyage|v[aceginu])" + + "|(?:wales|walter|wang|watch|webcam|website|wed|wedding|weir|whoswho|wien|wiki" + + "|williamhill|win|windows|wine|wme|work|works|world|wtc|wtf|w[fs])" + + "|(?:\u03b5\u03bb|\u0431\u0435\u043b|\u0434\u0435\u0442\u0438|\u043a\u043e\u043c" + + "|\u043c\u043a\u0434|\u043c\u043e\u043d|\u043c\u043e\u0441\u043a\u0432\u0430" + + "|\u043e\u043d\u043b\u0430\u0439\u043d|\u043e\u0440\u0433|\u0440\u0443\u0441" + + "|\u0440\u0444|\u0441\u0430\u0439\u0442|\u0441\u0440\u0431|\u0443\u043a\u0440" + + "|\u049b\u0430\u0437|\u0570\u0561\u0575|\u05e7\u05d5\u05dd" + + "|\u0627\u0631\u0627\u0645\u0643\u0648|\u0627\u0644\u0627\u0631\u062f\u0646" + + "|\u0627\u0644\u062c\u0632\u0627\u0626\u0631" + + "|\u0627\u0644\u0633\u0639\u0648\u062f\u064a\u0629" + + "|\u0627\u0644\u0645\u063a\u0631\u0628|\u0627\u0645\u0627\u0631\u0627\u062a" + + "|\u0627\u06cc\u0631\u0627\u0646|\u0628\u0627\u0632\u0627\u0631" + + "|\u0628\u06be\u0627\u0631\u062a|\u062a\u0648\u0646\u0633" + + "|\u0633\u0648\u062f\u0627\u0646|\u0633\u0648\u0631\u064a\u0629" + + "|\u0634\u0628\u0643\u0629|\u0639\u0631\u0627\u0642|\u0639\u0645\u0627\u0646" + + "|\u0641\u0644\u0633\u0637\u064a\u0646|\u0642\u0637\u0631|\u0643\u0648\u0645" + + "|\u0645\u0635\u0631|\u0645\u0644\u064a\u0633\u064a\u0627|\u0645\u0648\u0642\u0639" + + "|\u0915\u0949\u092e|\u0928\u0947\u091f|\u092d\u093e\u0930\u0924" + + "|\u0938\u0902\u0917\u0920\u0928|\u09ad\u09be\u09b0\u09a4|\u0a2d\u0a3e\u0a30\u0a24" + + "|\u0aad\u0abe\u0ab0\u0aa4|\u0b87\u0ba8\u0bcd\u0ba4\u0bbf\u0baf\u0bbe" + + "|\u0b87\u0bb2\u0b99\u0bcd\u0b95\u0bc8" + + "|\u0b9a\u0bbf\u0b99\u0bcd\u0b95\u0baa\u0bcd\u0baa\u0bc2\u0bb0\u0bcd" + + "|\u0c2d\u0c3e\u0c30\u0c24\u0c4d|\u0dbd\u0d82\u0d9a\u0dcf|\u0e04\u0e2d\u0e21" + + "|\u0e44\u0e17\u0e22|\u10d2\u10d4|\u307f\u3093\u306a|\u30b0\u30fc\u30b0\u30eb" + + "|\u30b3\u30e0|\u4e16\u754c|\u4e2d\u4fe1|\u4e2d\u56fd|\u4e2d\u570b|\u4e2d\u6587\u7f51" + + "|\u4f01\u4e1a|\u4f5b\u5c71|\u4fe1\u606f|\u5065\u5eb7|\u516b\u5366|\u516c\u53f8" + + "|\u516c\u76ca|\u53f0\u6e7e|\u53f0\u7063|\u5546\u57ce|\u5546\u5e97|\u5546\u6807" + + "|\u5728\u7ebf|\u5927\u62ff|\u5a31\u4e50|\u5de5\u884c|\u5e7f\u4e1c|\u6148\u5584" + + "|\u6211\u7231\u4f60|\u624b\u673a|\u653f\u52a1|\u653f\u5e9c|\u65b0\u52a0\u5761" + + "|\u65b0\u95fb|\u65f6\u5c1a|\u673a\u6784|\u6de1\u9a6c\u9521|\u6e38\u620f|\u70b9\u770b" + + "|\u79fb\u52a8|\u7ec4\u7ec7\u673a\u6784|\u7f51\u5740|\u7f51\u5e97|\u7f51\u7edc" + + "|\u8c37\u6b4c|\u96c6\u56e2|\u98de\u5229\u6d66|\u9910\u5385|\u9999\u6e2f|\ub2f7\ub137" + + "|\ub2f7\ucef4|\uc0bc\uc131|\ud55c\uad6d|xbox|xerox|xin|xn\\-\\-11b4c3d" + + "|xn\\-\\-1qqw23a|xn\\-\\-30rr7y|xn\\-\\-3bst00m|xn\\-\\-3ds443g|xn\\-\\-3e0b707e" + + "|xn\\-\\-3pxu8k|xn\\-\\-42c2d9a|xn\\-\\-45brj9c|xn\\-\\-45q11c|xn\\-\\-4gbrim" + + "|xn\\-\\-55qw42g|xn\\-\\-55qx5d|xn\\-\\-6frz82g|xn\\-\\-6qq986b3xl|xn\\-\\-80adxhks" + + "|xn\\-\\-80ao21a|xn\\-\\-80asehdb|xn\\-\\-80aswg|xn\\-\\-90a3ac|xn\\-\\-90ais" + + "|xn\\-\\-9dbq2a|xn\\-\\-9et52u|xn\\-\\-b4w605ferd|xn\\-\\-c1avg|xn\\-\\-c2br7g" + + "|xn\\-\\-cg4bki|xn\\-\\-clchc0ea0b2g2a9gcd|xn\\-\\-czr694b|xn\\-\\-czrs0t" + + "|xn\\-\\-czru2d|xn\\-\\-d1acj3b|xn\\-\\-d1alf|xn\\-\\-efvy88h|xn\\-\\-estv75g" + + "|xn\\-\\-fhbei|xn\\-\\-fiq228c5hs|xn\\-\\-fiq64b|xn\\-\\-fiqs8s|xn\\-\\-fiqz9s" + + "|xn\\-\\-fjq720a|xn\\-\\-flw351e|xn\\-\\-fpcrj9c3d|xn\\-\\-fzc2c9e2c|xn\\-\\-gecrj9c" + + "|xn\\-\\-h2brj9c|xn\\-\\-hxt814e|xn\\-\\-i1b6b1a6a2e|xn\\-\\-imr513n|xn\\-\\-io0a7i" + + "|xn\\-\\-j1aef|xn\\-\\-j1amh|xn\\-\\-j6w193g|xn\\-\\-kcrx77d1x4a|xn\\-\\-kprw13d" + + "|xn\\-\\-kpry57d|xn\\-\\-kput3i|xn\\-\\-l1acc|xn\\-\\-lgbbat1ad8j|xn\\-\\-mgb9awbf" + + "|xn\\-\\-mgba3a3ejt|xn\\-\\-mgba3a4f16a|xn\\-\\-mgbaam7a8h|xn\\-\\-mgbab2bd" + + "|xn\\-\\-mgbayh7gpa|xn\\-\\-mgbbh1a71e|xn\\-\\-mgbc0a9azcg|xn\\-\\-mgberp4a5d4ar" + + "|xn\\-\\-mgbpl2fh|xn\\-\\-mgbtx2b|xn\\-\\-mgbx4cd0ab|xn\\-\\-mk1bu44c|xn\\-\\-mxtq1m" + + "|xn\\-\\-ngbc5azd|xn\\-\\-node|xn\\-\\-nqv7f|xn\\-\\-nqv7fs00ema|xn\\-\\-nyqy26a" + + "|xn\\-\\-o3cw4h|xn\\-\\-ogbpf8fl|xn\\-\\-p1acf|xn\\-\\-p1ai|xn\\-\\-pgbs0dh" + + "|xn\\-\\-pssy2u|xn\\-\\-q9jyb4c|xn\\-\\-qcka1pmc|xn\\-\\-qxam|xn\\-\\-rhqv96g" + + "|xn\\-\\-s9brj9c|xn\\-\\-ses554g|xn\\-\\-t60b56a|xn\\-\\-tckwe|xn\\-\\-unup4y" + + "|xn\\-\\-vermgensberater\\-ctb|xn\\-\\-vermgensberatung\\-pwb|xn\\-\\-vhquv" + + "|xn\\-\\-vuq861b|xn\\-\\-wgbh1c|xn\\-\\-wgbl6a|xn\\-\\-xhq521b|xn\\-\\-xkc2al3hye2a" + + "|xn\\-\\-xkc2dl3a5ee0h|xn\\-\\-y9a3aq|xn\\-\\-yfro4i67o|xn\\-\\-ygbi2ammx" + + "|xn\\-\\-zfr164b|xperia|xxx|xyz)" + + "|(?:yachts|yamaxun|yandex|yodobashi|yoga|yokohama|youtube|y[et])" + + "|(?:zara|zip|zone|zuerich|z[amw]))"; + + public static final Pattern IP_ADDRESS + = Pattern.compile( + "((25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(25[0-5]|2[0-4]" + + "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]" + + "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}" + + "|[1-9][0-9]|[0-9]))"); + + /** + * Valid UCS characters defined in RFC 3987. Excludes space characters. + */ + private static final String UCS_CHAR = "[" + + "\u00A0-\uD7FF" + + "\uF900-\uFDCF" + + "\uFDF0-\uFFEF" + + "\uD800\uDC00-\uD83F\uDFFD" + + "\uD840\uDC00-\uD87F\uDFFD" + + "\uD880\uDC00-\uD8BF\uDFFD" + + "\uD8C0\uDC00-\uD8FF\uDFFD" + + "\uD900\uDC00-\uD93F\uDFFD" + + "\uD940\uDC00-\uD97F\uDFFD" + + "\uD980\uDC00-\uD9BF\uDFFD" + + "\uD9C0\uDC00-\uD9FF\uDFFD" + + "\uDA00\uDC00-\uDA3F\uDFFD" + + "\uDA40\uDC00-\uDA7F\uDFFD" + + "\uDA80\uDC00-\uDABF\uDFFD" + + "\uDAC0\uDC00-\uDAFF\uDFFD" + + "\uDB00\uDC00-\uDB3F\uDFFD" + + "\uDB44\uDC00-\uDB7F\uDFFD" + + "&&[^\u00A0[\u2000-\u200A]\u2028\u2029\u202F\u3000]]"; + + /** + * Valid characters for IRI label defined in RFC 3987. + */ + private static final String LABEL_CHAR = "a-zA-Z0-9" + UCS_CHAR; + + /** + * Valid characters for IRI TLD defined in RFC 3987. + */ + private static final String TLD_CHAR = "a-zA-Z" + UCS_CHAR; + + /** + * RFC 1035 Section 2.3.4 limits the labels to a maximum 63 octets. + */ + private static final String IRI_LABEL + = "[" + LABEL_CHAR + "](?:[" + LABEL_CHAR + "_\\-]{0,61}[" + LABEL_CHAR + "]){0,1}"; + + /** + * RFC 3492 references RFC 1034 and limits Punycode algorithm output to 63 characters. + */ + private static final String PUNYCODE_TLD = "xn\\-\\-[\\w\\-]{0,58}\\w"; + + private static final String TLD = "(" + PUNYCODE_TLD + "|" + "[" + TLD_CHAR + "]{2,63}" + ")"; + + private static final String HOST_NAME = "(" + IRI_LABEL + "\\.)+" + TLD; + + public static final Pattern DOMAIN_NAME + = Pattern.compile("(" + HOST_NAME + "|" + IP_ADDRESS + ")"); + + //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // CHANGED: Removed rtsp from supported protocols // + //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + private static final String PROTOCOL = "(?i:http|https)://"; + + /* A word boundary or end of input. This is to stop foo.sure from matching as foo.su */ + private static final String WORD_BOUNDARY = "(?:\\b|$|^)"; + + private static final String USER_INFO = "(?:[a-zA-Z0-9\\$\\-\\_\\.\\+\\!\\*\\'\\(\\)" + + "\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,64}(?:\\:(?:[a-zA-Z0-9\\$\\-\\_" + + "\\.\\+\\!\\*\\'\\(\\)\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,25})?\\@"; + + private static final String PORT_NUMBER = "\\:\\d{1,5}"; + + private static final String PATH_AND_QUERY = "[/\\?](?:(?:[" + LABEL_CHAR + + ";/\\?:@&=#~" // plus optional query params + + "\\-\\.\\+!\\*'\\(\\),_\\$])|(?:%[a-fA-F0-9]{2}))*"; + + /** + * Regular expression pattern to match most part of RFC 3987 + * Internationalized URLs, aka IRIs. + */ + public static final Pattern WEB_URL = Pattern.compile("(" + + "(" + + "(?:" + PROTOCOL + "(?:" + USER_INFO + ")?" + ")?" + + "(?:" + DOMAIN_NAME + ")" + + "(?:" + PORT_NUMBER + ")?" + + ")" + + "(" + PATH_AND_QUERY + ")?" + + WORD_BOUNDARY + + ")"); + + /** + * Regular expression that matches known TLDs and punycode TLDs. + */ + private static final String STRICT_TLD = "(?:" + + IANA_TOP_LEVEL_DOMAINS + "|" + PUNYCODE_TLD + ")"; + + /** + * Regular expression that matches host names using {@link #STRICT_TLD}. + */ + private static final String STRICT_HOST_NAME = "(?:(?:" + IRI_LABEL + "\\.)+" + + STRICT_TLD + ")"; + + /** + * Regular expression that matches domain names using either {@link #STRICT_HOST_NAME} or + * {@link #IP_ADDRESS}. + */ + private static final Pattern STRICT_DOMAIN_NAME + = Pattern.compile("(?:" + STRICT_HOST_NAME + "|" + IP_ADDRESS + ")"); + + /** + * Regular expression that matches domain names without a TLD. + */ + private static final String RELAXED_DOMAIN_NAME + = "(?:" + "(?:" + IRI_LABEL + "(?:\\.(?=\\S))" + "?)+" + "|" + IP_ADDRESS + ")"; + + /** + * Regular expression to match strings that do not start with a supported protocol. The TLDs + * are expected to be one of the known TLDs. + */ + private static final String WEB_URL_WITHOUT_PROTOCOL = "(" + + WORD_BOUNDARY + + "(? listeners = new ArrayList<>(); + + public CollapsibleView(final Context context) { super(context); } - public CollapsibleView(Context context, @Nullable AttributeSet attrs) { + public CollapsibleView(final Context context, @Nullable final AttributeSet attrs) { super(context, attrs); } - public CollapsibleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + public CollapsibleView(final Context context, @Nullable final AttributeSet attrs, + final int defStyleAttr) { super(context, attrs, defStyleAttr); } @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - public CollapsibleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + public CollapsibleView(final Context context, final AttributeSet attrs, final int defStyleAttr, + final int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } @@ -69,20 +86,6 @@ public class CollapsibleView extends LinearLayout { // Collapse/expand logic //////////////////////////////////////////////////////////////////////////*/ - private static final int ANIMATION_DURATION = 420; - public static final int COLLAPSED = 0, EXPANDED = 1; - - @Retention(SOURCE) - @IntDef({COLLAPSED, EXPANDED}) - public @interface ViewMode {} - - @State @ViewMode int currentState = COLLAPSED; - private boolean readyToChangeState; - - private int targetHeight = -1; - private ValueAnimator currentAnimator; - private final List listeners = new ArrayList<>(); - /** * This method recalculates the height of this view so it must be called when * some child changes (e.g. add new views, change text). @@ -92,7 +95,8 @@ public class CollapsibleView extends LinearLayout { Log.d(TAG, getDebugLogString("ready() called")); } - measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST), MeasureSpec.UNSPECIFIED); + measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST), + MeasureSpec.UNSPECIFIED); targetHeight = getMeasuredHeight(); getLayoutParams().height = currentState == COLLAPSED ? 0 : targetHeight; @@ -111,7 +115,9 @@ public class CollapsibleView extends LinearLayout { Log.d(TAG, getDebugLogString("collapse() called")); } - if (!readyToChangeState) return; + if (!readyToChangeState) { + return; + } final int height = getHeight(); if (height == 0) { @@ -119,7 +125,9 @@ public class CollapsibleView extends LinearLayout { return; } - if (currentAnimator != null && currentAnimator.isRunning()) currentAnimator.cancel(); + if (currentAnimator != null && currentAnimator.isRunning()) { + currentAnimator.cancel(); + } currentAnimator = AnimationUtils.animateHeight(this, ANIMATION_DURATION, 0); setCurrentState(COLLAPSED); @@ -130,7 +138,9 @@ public class CollapsibleView extends LinearLayout { Log.d(TAG, getDebugLogString("expand() called")); } - if (!readyToChangeState) return; + if (!readyToChangeState) { + return; + } final int height = getHeight(); if (height == this.targetHeight) { @@ -138,13 +148,17 @@ public class CollapsibleView extends LinearLayout { return; } - if (currentAnimator != null && currentAnimator.isRunning()) currentAnimator.cancel(); + if (currentAnimator != null && currentAnimator.isRunning()) { + currentAnimator.cancel(); + } currentAnimator = AnimationUtils.animateHeight(this, ANIMATION_DURATION, this.targetHeight); setCurrentState(EXPANDED); } public void switchState() { - if (!readyToChangeState) return; + if (!readyToChangeState) { + return; + } if (currentState == COLLAPSED) { expand(); @@ -158,7 +172,7 @@ public class CollapsibleView extends LinearLayout { return currentState; } - public void setCurrentState(@ViewMode int currentState) { + public void setCurrentState(@ViewMode final int currentState) { this.currentState = currentState; broadcastState(); } @@ -171,6 +185,7 @@ public class CollapsibleView extends LinearLayout { /** * Add a listener which will be listening for changes in this view (i.e. collapsed or expanded). + * @param listener {@link StateListener} to be added */ public void addListener(final StateListener listener) { if (listeners.contains(listener)) { @@ -182,24 +197,12 @@ public class CollapsibleView extends LinearLayout { /** * Remove a listener so it doesn't receive more state changes. + * @param listener {@link StateListener} to be removed */ public void removeListener(final StateListener listener) { listeners.remove(listener); } - /** - * Simple interface used for listening state changes of the {@link CollapsibleView}. - */ - public interface StateListener { - /** - * Called when the state changes. - * - * @param newState the state that the {@link CollapsibleView} transitioned to,
- * it's an integer being either {@link #COLLAPSED} or {@link #EXPANDED} - */ - void onStateChanged(@ViewMode int newState); - } - /*////////////////////////////////////////////////////////////////////////// // State Saving //////////////////////////////////////////////////////////////////////////*/ @@ -211,7 +214,7 @@ public class CollapsibleView extends LinearLayout { } @Override - public void onRestoreInstanceState(Parcelable state) { + public void onRestoreInstanceState(final Parcelable state) { super.onRestoreInstanceState(Icepick.restoreInstanceState(this, state)); ready(); @@ -221,10 +224,29 @@ public class CollapsibleView extends LinearLayout { // Internal //////////////////////////////////////////////////////////////////////////*/ - public String getDebugLogString(String description) { + public String getDebugLogString(final String description) { return String.format("%-100s → %s", - description, "readyToChangeState = [" + readyToChangeState + "], currentState = [" + currentState + "], targetHeight = [" + targetHeight + "]," + - " mW x mH = [" + getMeasuredWidth() + "x" + getMeasuredHeight() + "]" + - " W x H = [" + getWidth() + "x" + getHeight() + "]"); + description, "readyToChangeState = [" + readyToChangeState + "], " + + "currentState = [" + currentState + "], " + + "targetHeight = [" + targetHeight + "], " + + "mW x mH = [" + getMeasuredWidth() + "x" + getMeasuredHeight() + "], " + + "W x H = [" + getWidth() + "x" + getHeight() + "]"); + } + + @Retention(SOURCE) + @IntDef({COLLAPSED, EXPANDED}) + public @interface ViewMode { } + + /** + * Simple interface used for listening state changes of the {@link CollapsibleView}. + */ + public interface StateListener { + /** + * Called when the state changes. + * + * @param newState the state that the {@link CollapsibleView} transitioned to,
+ * it's an integer being either {@link #COLLAPSED} or {@link #EXPANDED} + */ + void onStateChanged(@ViewMode int newState); } } diff --git a/app/src/main/java/org/schabi/newpipe/views/FocusAwareCoordinator.java b/app/src/main/java/org/schabi/newpipe/views/FocusAwareCoordinator.java new file mode 100644 index 000000000..1ffb7d069 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/views/FocusAwareCoordinator.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) Eltex ltd 2019 + * FocusAwareCoordinator.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 . + */ +package org.schabi.newpipe.views; + +import android.content.Context; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.coordinatorlayout.widget.CoordinatorLayout; + +public final class FocusAwareCoordinator extends CoordinatorLayout { + private final Rect childFocus = new Rect(); + + public FocusAwareCoordinator(@NonNull final Context context) { + super(context); + } + + public FocusAwareCoordinator(@NonNull final Context context, + @Nullable final AttributeSet attrs) { + super(context, attrs); + } + + public FocusAwareCoordinator(@NonNull final Context context, + @Nullable final AttributeSet attrs, final int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + public void requestChildFocus(final View child, final View focused) { + super.requestChildFocus(child, focused); + + if (!isInTouchMode()) { + if (focused.getHeight() >= getHeight()) { + focused.getFocusedRect(childFocus); + + ((ViewGroup) child).offsetDescendantRectToMyCoords(focused, childFocus); + } else { + focused.getHitRect(childFocus); + + ((ViewGroup) child).offsetDescendantRectToMyCoords((View) focused.getParent(), + childFocus); + } + + requestChildRectangleOnScreen(child, childFocus, false); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/views/FocusAwareDrawerLayout.java b/app/src/main/java/org/schabi/newpipe/views/FocusAwareDrawerLayout.java new file mode 100644 index 000000000..0da42fab6 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/views/FocusAwareDrawerLayout.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) Eltex ltd 2019 + * FocusAwareDrawerLayout.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 . + */ +package org.schabi.newpipe.views; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.drawerlayout.widget.DrawerLayout; + +import java.util.ArrayList; + +public final class FocusAwareDrawerLayout extends DrawerLayout { + public FocusAwareDrawerLayout(@NonNull final Context context) { + super(context); + } + + public FocusAwareDrawerLayout(@NonNull final Context context, + @Nullable final AttributeSet attrs) { + super(context, attrs); + } + + public FocusAwareDrawerLayout(@NonNull final Context context, + @Nullable final AttributeSet attrs, + final int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected boolean onRequestFocusInDescendants(final int direction, + final Rect previouslyFocusedRect) { + // SDK implementation of this method picks whatever visible View takes the focus first + // without regard to addFocusables. If the open drawer is temporarily empty, the focus + // escapes outside of it, which can be confusing + + boolean hasOpenPanels = false; + + for (int i = 0; i < getChildCount(); ++i) { + View child = getChildAt(i); + + DrawerLayout.LayoutParams lp = (DrawerLayout.LayoutParams) child.getLayoutParams(); + + if (lp.gravity != 0 && isDrawerVisible(child)) { + hasOpenPanels = true; + + if (child.requestFocus(direction, previouslyFocusedRect)) { + return true; + } + } + } + + if (hasOpenPanels) { + return false; + } + + return super.onRequestFocusInDescendants(direction, previouslyFocusedRect); + } + + @Override + public void addFocusables(final ArrayList views, final int direction, + final int focusableMode) { + boolean hasOpenPanels = false; + View content = null; + + for (int i = 0; i < getChildCount(); ++i) { + View child = getChildAt(i); + + DrawerLayout.LayoutParams lp = (DrawerLayout.LayoutParams) child.getLayoutParams(); + + if (lp.gravity == 0) { + content = child; + } else { + if (isDrawerVisible(child)) { + hasOpenPanels = true; + child.addFocusables(views, direction, focusableMode); + } + } + } + + if (content != null && !hasOpenPanels) { + content.addFocusables(views, direction, focusableMode); + } + } + + // this override isn't strictly necessary, but it is helpful when DrawerLayout isn't + // the topmost view in hierarchy (such as when system or builtin appcompat ActionBar is used) + @Override + @SuppressLint("RtlHardcoded") + public void openDrawer(@NonNull final View drawerView, final boolean animate) { + super.openDrawer(drawerView, animate); + + drawerView.requestFocus(FOCUS_FORWARD); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/views/FocusAwareSeekBar.java b/app/src/main/java/org/schabi/newpipe/views/FocusAwareSeekBar.java new file mode 100644 index 000000000..6dbcded48 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/views/FocusAwareSeekBar.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) Eltex ltd 2019 + * FocusAwareDrawerLayout.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 . + */ +package org.schabi.newpipe.views; + +import android.content.Context; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.ViewTreeObserver; +import android.widget.SeekBar; + +import androidx.appcompat.widget.AppCompatSeekBar; + +import org.schabi.newpipe.util.AndroidTvUtils; + +/** + * SeekBar, adapted for directional navigation. It emulates touch-related callbacks + * (onStartTrackingTouch/onStopTrackingTouch), so existing code does not need to be changed to + * work with it. + */ +public final class FocusAwareSeekBar extends AppCompatSeekBar { + private NestedListener listener; + + private ViewTreeObserver treeObserver; + + public FocusAwareSeekBar(final Context context) { + super(context); + } + + public FocusAwareSeekBar(final Context context, final AttributeSet attrs) { + super(context, attrs); + } + + public FocusAwareSeekBar(final Context context, final AttributeSet attrs, + final int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + public void setOnSeekBarChangeListener(final OnSeekBarChangeListener l) { + this.listener = l == null ? null : new NestedListener(l); + + super.setOnSeekBarChangeListener(listener); + } + + @Override + public boolean onKeyDown(final int keyCode, final KeyEvent event) { + if (!isInTouchMode() && AndroidTvUtils.isConfirmKey(keyCode)) { + releaseTrack(); + } + + return super.onKeyDown(keyCode, event); + } + + @Override + protected void onFocusChanged(final boolean gainFocus, final int direction, + final Rect previouslyFocusedRect) { + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); + + if (!isInTouchMode() && !gainFocus) { + releaseTrack(); + } + } + + private final ViewTreeObserver.OnTouchModeChangeListener touchModeListener = isInTouchMode -> { + if (isInTouchMode) { + releaseTrack(); + } + }; + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + treeObserver = getViewTreeObserver(); + treeObserver.addOnTouchModeChangeListener(touchModeListener); + } + + @Override + protected void onDetachedFromWindow() { + if (treeObserver == null || !treeObserver.isAlive()) { + treeObserver = getViewTreeObserver(); + } + + treeObserver.removeOnTouchModeChangeListener(touchModeListener); + treeObserver = null; + + super.onDetachedFromWindow(); + } + + private void releaseTrack() { + if (listener != null && listener.isSeeking) { + listener.onStopTrackingTouch(this); + } + } + + private final class NestedListener implements OnSeekBarChangeListener { + private final OnSeekBarChangeListener delegate; + + boolean isSeeking; + + private NestedListener(final OnSeekBarChangeListener delegate) { + this.delegate = delegate; + } + + @Override + public void onProgressChanged(final SeekBar seekBar, final int progress, + final boolean fromUser) { + if (!seekBar.isInTouchMode() && !isSeeking && fromUser) { + isSeeking = true; + + onStartTrackingTouch(seekBar); + } + + delegate.onProgressChanged(seekBar, progress, fromUser); + } + + @Override + public void onStartTrackingTouch(final SeekBar seekBar) { + isSeeking = true; + + delegate.onStartTrackingTouch(seekBar); + } + + @Override + public void onStopTrackingTouch(final SeekBar seekBar) { + isSeeking = false; + + delegate.onStopTrackingTouch(seekBar); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/views/FocusOverlayView.java b/app/src/main/java/org/schabi/newpipe/views/FocusOverlayView.java new file mode 100644 index 000000000..1c868f66a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/views/FocusOverlayView.java @@ -0,0 +1,293 @@ +/* + * Copyright 2019 Alexander Rvachev + * FocusOverlayView.java is part of NewPipe + * + * License: GPL-3.0+ + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.schabi.newpipe.views; + +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.Window; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import androidx.appcompat.view.WindowCallbackWrapper; + +import org.schabi.newpipe.R; + +import java.lang.ref.WeakReference; + +public final class FocusOverlayView extends Drawable implements + ViewTreeObserver.OnGlobalFocusChangeListener, + ViewTreeObserver.OnDrawListener, + ViewTreeObserver.OnGlobalLayoutListener, + ViewTreeObserver.OnScrollChangedListener, ViewTreeObserver.OnTouchModeChangeListener { + + private boolean isInTouchMode; + + private final Rect focusRect = new Rect(); + + private final Paint rectPaint = new Paint(); + + private final Handler animator = new Handler(Looper.getMainLooper()) { + @Override + public void handleMessage(final Message msg) { + updateRect(); + } + }; + + private WeakReference focused; + + public FocusOverlayView(final Context context) { + rectPaint.setStyle(Paint.Style.STROKE); + rectPaint.setStrokeWidth(2); + rectPaint.setColor(context.getResources().getColor(R.color.white)); + } + + @Override + public void onGlobalFocusChanged(final View oldFocus, final View newFocus) { + int l = focusRect.left; + int r = focusRect.right; + int t = focusRect.top; + int b = focusRect.bottom; + + if (newFocus != null && newFocus.getWidth() > 0 && newFocus.getHeight() > 0) { + newFocus.getGlobalVisibleRect(focusRect); + + focused = new WeakReference<>(newFocus); + } else { + focusRect.setEmpty(); + + focused = null; + } + + if (l != focusRect.left || r != focusRect.right + || t != focusRect.top || b != focusRect.bottom) { + invalidateSelf(); + } + + focused = new WeakReference<>(newFocus); + + animator.sendEmptyMessageDelayed(0, 1000); + } + + private void updateRect() { + if (focused == null) { + return; + } + + View focusedView = this.focused.get(); + + int l = focusRect.left; + int r = focusRect.right; + int t = focusRect.top; + int b = focusRect.bottom; + + if (focusedView != null) { + focusedView.getGlobalVisibleRect(focusRect); + } else { + focusRect.setEmpty(); + } + + if (l != focusRect.left || r != focusRect.right + || t != focusRect.top || b != focusRect.bottom) { + invalidateSelf(); + } + } + + @Override + public void onDraw() { + updateRect(); + } + + @Override + public void onScrollChanged() { + updateRect(); + + animator.removeMessages(0); + animator.sendEmptyMessageDelayed(0, 1000); + } + + @Override + public void onGlobalLayout() { + updateRect(); + + animator.sendEmptyMessageDelayed(0, 1000); + } + + @Override + public void onTouchModeChanged(final boolean inTouchMode) { + this.isInTouchMode = inTouchMode; + + if (inTouchMode) { + updateRect(); + } else { + invalidateSelf(); + } + } + + public void setCurrentFocus(final View newFocus) { + if (newFocus == null) { + return; + } + + this.isInTouchMode = newFocus.isInTouchMode(); + + onGlobalFocusChanged(null, newFocus); + } + + @Override + public void draw(@NonNull final Canvas canvas) { + if (!isInTouchMode && focusRect.width() != 0) { + canvas.drawRect(focusRect, rectPaint); + } + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSPARENT; + } + + @Override + public void setAlpha(final int alpha) { + } + + @Override + public void setColorFilter(final ColorFilter colorFilter) { + } + + public static void setupFocusObserver(final Dialog dialog) { + Rect displayRect = new Rect(); + + Window window = dialog.getWindow(); + assert window != null; + + View decor = window.getDecorView(); + decor.getWindowVisibleDisplayFrame(displayRect); + + FocusOverlayView overlay = new FocusOverlayView(dialog.getContext()); + overlay.setBounds(0, 0, displayRect.width(), displayRect.height()); + + setupOverlay(window, overlay); + } + + public static void setupFocusObserver(final Activity activity) { + Rect displayRect = new Rect(); + + Window window = activity.getWindow(); + View decor = window.getDecorView(); + decor.getWindowVisibleDisplayFrame(displayRect); + + FocusOverlayView overlay = new FocusOverlayView(activity); + overlay.setBounds(0, 0, displayRect.width(), displayRect.height()); + + setupOverlay(window, overlay); + } + + private static void setupOverlay(final Window window, final FocusOverlayView overlay) { + ViewGroup decor = (ViewGroup) window.getDecorView(); + decor.getOverlay().add(overlay); + + fixFocusHierarchy(decor); + + ViewTreeObserver observer = decor.getViewTreeObserver(); + observer.addOnScrollChangedListener(overlay); + observer.addOnGlobalFocusChangeListener(overlay); + observer.addOnGlobalLayoutListener(overlay); + observer.addOnTouchModeChangeListener(overlay); + + overlay.setCurrentFocus(decor.getFocusedChild()); + + // Some key presses don't actually move focus, but still result in movement on screen. + // For example, MovementMethod of TextView may cause requestRectangleOnScreen() due to + // some "focusable" spans, which in turn causes CoordinatorLayout to "scroll" it's children. + // Unfortunately many such forms of "scrolling" do not count as scrolling for purpose + // of dispatching ViewTreeObserver callbacks, so we have to intercept them by directly + // receiving keys from Window. + window.setCallback(new WindowCallbackWrapper(window.getCallback()) { + @Override + public boolean dispatchKeyEvent(final KeyEvent event) { + boolean res = super.dispatchKeyEvent(event); + overlay.onKey(event); + return res; + } + }); + } + + private void onKey(final KeyEvent event) { + if (event.getAction() != KeyEvent.ACTION_DOWN) { + return; + } + + updateRect(); + + animator.sendEmptyMessageDelayed(0, 100); + } + + private static void fixFocusHierarchy(final View decor) { + // During Android 8 development some dumb ass decided, that action bar has to be + // a keyboard focus cluster. Unfortunately, keyboard clusters do not work for primary + // auditory of key navigation — Android TV users (Android TV remotes do not have + // keyboard META key for moving between clusters). We have to fix this unfortunate accident + // While we are at it, let's deal with touchscreenBlocksFocus too. + + if (Build.VERSION.SDK_INT < 26) { + return; + } + + if (!(decor instanceof ViewGroup)) { + return; + } + + clearFocusObstacles((ViewGroup) decor); + } + + @RequiresApi(api = 26) + private static void clearFocusObstacles(final ViewGroup viewGroup) { + viewGroup.setTouchscreenBlocksFocus(false); + + if (viewGroup.isKeyboardNavigationCluster()) { + viewGroup.setKeyboardNavigationCluster(false); + + return; // clusters aren't supposed to nest + } + + int childCount = viewGroup.getChildCount(); + + for (int i = 0; i < childCount; ++i) { + View view = viewGroup.getChildAt(i); + + if (view instanceof ViewGroup) { + clearFocusObstacles((ViewGroup) view); + } + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/views/LargeTextMovementMethod.java b/app/src/main/java/org/schabi/newpipe/views/LargeTextMovementMethod.java new file mode 100644 index 000000000..3a3384b51 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/views/LargeTextMovementMethod.java @@ -0,0 +1,303 @@ +/* + * Copyright 2019 Alexander Rvachev + * FocusOverlayView.java is part of NewPipe + * + * License: GPL-3.0+ + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.schabi.newpipe.views; + +import android.graphics.Rect; +import android.text.Layout; +import android.text.Selection; +import android.text.Spannable; +import android.text.method.LinkMovementMethod; +import android.text.style.ClickableSpan; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.widget.TextView; + +public class LargeTextMovementMethod extends LinkMovementMethod { + private final Rect visibleRect = new Rect(); + + private int direction; + + @Override + public void onTakeFocus(final TextView view, final Spannable text, final int dir) { + Selection.removeSelection(text); + + super.onTakeFocus(view, text, dir); + + this.direction = dirToRelative(dir); + } + + @Override + protected boolean handleMovementKey(final TextView widget, + final Spannable buffer, + final int keyCode, + final int movementMetaState, + final KeyEvent event) { + if (!doHandleMovement(widget, buffer, keyCode, movementMetaState, event)) { + // clear selection to make sure, that it does not confuse focus handling code + Selection.removeSelection(buffer); + return false; + } + + return true; + } + + private boolean doHandleMovement(final TextView widget, + final Spannable buffer, + final int keyCode, + final int movementMetaState, + final KeyEvent event) { + int newDir = keyToDir(keyCode); + + if (direction != 0 && newDir != direction) { + return false; + } + + this.direction = 0; + + ViewGroup root = findScrollableParent(widget); + + widget.getHitRect(visibleRect); + + root.offsetDescendantRectToMyCoords((View) widget.getParent(), visibleRect); + + return super.handleMovementKey(widget, buffer, keyCode, movementMetaState, event); + } + + @Override + protected boolean up(final TextView widget, final Spannable buffer) { + if (gotoPrev(widget, buffer)) { + return true; + } + + return super.up(widget, buffer); + } + + @Override + protected boolean left(final TextView widget, final Spannable buffer) { + if (gotoPrev(widget, buffer)) { + return true; + } + + return super.left(widget, buffer); + } + + @Override + protected boolean right(final TextView widget, final Spannable buffer) { + if (gotoNext(widget, buffer)) { + return true; + } + + return super.right(widget, buffer); + } + + @Override + protected boolean down(final TextView widget, final Spannable buffer) { + if (gotoNext(widget, buffer)) { + return true; + } + + return super.down(widget, buffer); + } + + private boolean gotoPrev(final TextView view, final Spannable buffer) { + Layout layout = view.getLayout(); + if (layout == null) { + return false; + } + + View root = findScrollableParent(view); + + int rootHeight = root.getHeight(); + + if (visibleRect.top >= 0) { + // we fit entirely into the viewport, no need for fancy footwork + return false; + } + + int topExtra = -visibleRect.top; + + int firstVisibleLineNumber = layout.getLineForVertical(topExtra); + + // when deciding whether to pass "focus" to span, account for one more line + // this ensures, that focus is never passed to spans partially outside scroll window + int visibleStart = firstVisibleLineNumber == 0 + ? 0 + : layout.getLineStart(firstVisibleLineNumber - 1); + + ClickableSpan[] candidates = buffer.getSpans( + visibleStart, buffer.length(), ClickableSpan.class); + + if (candidates.length != 0) { + int a = Selection.getSelectionStart(buffer); + int b = Selection.getSelectionEnd(buffer); + + int selStart = Math.min(a, b); + int selEnd = Math.max(a, b); + + int bestStart = -1; + int bestEnd = -1; + + for (int i = 0; i < candidates.length; i++) { + int start = buffer.getSpanStart(candidates[i]); + int end = buffer.getSpanEnd(candidates[i]); + + if ((end < selEnd || selStart == selEnd) && start >= visibleStart) { + if (end > bestEnd) { + bestStart = buffer.getSpanStart(candidates[i]); + bestEnd = end; + } + } + } + + if (bestStart >= 0) { + Selection.setSelection(buffer, bestEnd, bestStart); + return true; + } + } + + float fourLines = view.getTextSize() * 4; + + visibleRect.left = 0; + visibleRect.right = view.getWidth(); + visibleRect.top = Math.max(0, (int) (topExtra - fourLines)); + visibleRect.bottom = visibleRect.top + rootHeight; + + return view.requestRectangleOnScreen(visibleRect); + } + + private boolean gotoNext(final TextView view, final Spannable buffer) { + Layout layout = view.getLayout(); + if (layout == null) { + return false; + } + + View root = findScrollableParent(view); + + int rootHeight = root.getHeight(); + + if (visibleRect.bottom <= rootHeight) { + // we fit entirely into the viewport, no need for fancy footwork + return false; + } + + int bottomExtra = visibleRect.bottom - rootHeight; + + int visibleBottomBorder = view.getHeight() - bottomExtra; + + int lineCount = layout.getLineCount(); + + int lastVisibleLineNumber = layout.getLineForVertical(visibleBottomBorder); + + // when deciding whether to pass "focus" to span, account for one more line + // this ensures, that focus is never passed to spans partially outside scroll window + int visibleEnd = lastVisibleLineNumber == lineCount - 1 + ? buffer.length() + : layout.getLineEnd(lastVisibleLineNumber - 1); + + ClickableSpan[] candidates = buffer.getSpans(0, visibleEnd, ClickableSpan.class); + + if (candidates.length != 0) { + int a = Selection.getSelectionStart(buffer); + int b = Selection.getSelectionEnd(buffer); + + int selStart = Math.min(a, b); + int selEnd = Math.max(a, b); + + int bestStart = Integer.MAX_VALUE; + int bestEnd = Integer.MAX_VALUE; + + for (int i = 0; i < candidates.length; i++) { + int start = buffer.getSpanStart(candidates[i]); + int end = buffer.getSpanEnd(candidates[i]); + + if ((start > selStart || selStart == selEnd) && end <= visibleEnd) { + if (start < bestStart) { + bestStart = start; + bestEnd = buffer.getSpanEnd(candidates[i]); + } + } + } + + if (bestEnd < Integer.MAX_VALUE) { + // cool, we have managed to find next link without having to adjust self within view + Selection.setSelection(buffer, bestStart, bestEnd); + return true; + } + } + + // there are no links within visible area, but still some text past visible area + // scroll visible area further in required direction + float fourLines = view.getTextSize() * 4; + + visibleRect.left = 0; + visibleRect.right = view.getWidth(); + visibleRect.bottom = Math.min((int) (visibleBottomBorder + fourLines), view.getHeight()); + visibleRect.top = visibleRect.bottom - rootHeight; + + return view.requestRectangleOnScreen(visibleRect); + } + + private ViewGroup findScrollableParent(final View view) { + View current = view; + + ViewParent parent; + do { + parent = current.getParent(); + + if (parent == current || !(parent instanceof View)) { + return (ViewGroup) view.getRootView(); + } + + current = (View) parent; + + if (current.isScrollContainer()) { + return (ViewGroup) current; + } + } + while (true); + } + + private static int dirToRelative(final int dir) { + switch (dir) { + case View.FOCUS_DOWN: + case View.FOCUS_RIGHT: + return View.FOCUS_FORWARD; + case View.FOCUS_UP: + case View.FOCUS_LEFT: + return View.FOCUS_BACKWARD; + } + + return dir; + } + + private int keyToDir(final int keyCode) { + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_DPAD_LEFT: + return View.FOCUS_BACKWARD; + case KeyEvent.KEYCODE_DPAD_DOWN: + case KeyEvent.KEYCODE_DPAD_RIGHT: + return View.FOCUS_FORWARD; + } + + return View.FOCUS_FORWARD; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/views/NewPipeRecyclerView.java b/app/src/main/java/org/schabi/newpipe/views/NewPipeRecyclerView.java new file mode 100644 index 000000000..655b86818 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/views/NewPipeRecyclerView.java @@ -0,0 +1,222 @@ +/* + * Copyright (C) Eltex ltd 2019 + * NewPipeRecyclerView.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 . + */ +package org.schabi.newpipe.views; + +import android.content.Context; +import android.graphics.Rect; +import android.os.Build; +import android.util.AttributeSet; +import android.util.Log; +import android.view.FocusFinder; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +public class NewPipeRecyclerView extends RecyclerView { + private static final String TAG = "NewPipeRecyclerView"; + + private Rect focusRect = new Rect(); + private Rect tempFocus = new Rect(); + + private boolean allowDpadScroll = true; + + public NewPipeRecyclerView(@NonNull final Context context) { + super(context); + + init(); + } + + public NewPipeRecyclerView(@NonNull final Context context, + @Nullable final AttributeSet attrs) { + super(context, attrs); + + init(); + } + + public NewPipeRecyclerView(@NonNull final Context context, + @Nullable final AttributeSet attrs, final int defStyle) { + super(context, attrs, defStyle); + + init(); + } + + private void init() { + setFocusable(true); + + setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); + } + + public void setFocusScrollAllowed(final boolean allowed) { + this.allowDpadScroll = allowed; + } + + @Override + public View focusSearch(final View focused, final int direction) { + // RecyclerView has buggy focusSearch(), that calls into Adapter several times, + // but ultimately fails to produce correct results in many cases. To add insult to injury, + // it's focusSearch() hard-codes several behaviors, incompatible with widely accepted focus + // handling practices: RecyclerView does not allow Adapter to give focus to itself (!!) and + // always checks, that returned View is located in "correct" direction (which prevents us + // from temporarily giving focus to special hidden View). + return null; + } + + @Override + protected void removeDetachedView(final View child, final boolean animate) { + if (child.hasFocus()) { + // If the focused child is being removed (can happen during very fast scrolling), + // temporarily give focus to ourselves. This will usually result in another child + // gaining focus (which one does not really matter, because at that point scrolling + // is FAST, and that child will soon be off-screen too) + requestFocus(); + } + + super.removeDetachedView(child, animate); + } + + // we override focusSearch to always return null, so all moves moves lead to + // dispatchUnhandledMove(). As added advantage, we can fully swallow some kinds of moves + // (such as downward movement, that happens when loading additional contents is in progress + + @Override + public boolean dispatchUnhandledMove(final View focused, final int direction) { + tempFocus.setEmpty(); + + // save focus rect before further manipulation (both focusSearch() and scrollBy() + // can mess with focused View by moving it off-screen and detaching) + + if (focused != null) { + View focusedItem = findContainingItemView(focused); + if (focusedItem != null) { + focusedItem.getHitRect(focusRect); + } + } + + // call focusSearch() to initiate layout, but disregard returned View for now + View adapterResult = super.focusSearch(focused, direction); + if (adapterResult != null && !isOutside(adapterResult)) { + adapterResult.requestFocus(direction); + return true; + } + + if (arrowScroll(direction)) { + // if RecyclerView can not yield focus, but there is still some scrolling space in + // indicated, direction, scroll some fixed amount in that direction + // (the same logic in ScrollView) + return true; + } + + if (focused != this && direction == FOCUS_DOWN && !allowDpadScroll) { + Log.i(TAG, "Consuming downward scroll: content load in progress"); + return true; + } + + if (tryFocusFinder(direction)) { + return true; + } + + if (adapterResult != null) { + adapterResult.requestFocus(direction); + return true; + } + + return super.dispatchUnhandledMove(focused, direction); + } + + private boolean tryFocusFinder(final int direction) { + if (Build.VERSION.SDK_INT >= 28) { + // Android 9 implemented bunch of handy changes to focus, that render code below less + // useful, and also broke findNextFocusFromRect in way, that render this hack useless + return false; + } + + FocusFinder finder = FocusFinder.getInstance(); + + // try to use FocusFinder instead of adapter + ViewGroup root = (ViewGroup) getRootView(); + + tempFocus.set(focusRect); + + root.offsetDescendantRectToMyCoords(this, tempFocus); + + View focusFinderResult = finder.findNextFocusFromRect(root, tempFocus, direction); + if (focusFinderResult != null && !isOutside(focusFinderResult)) { + focusFinderResult.requestFocus(direction); + return true; + } + + // look for focus in our ancestors, increasing search scope with each failure + // this provides much better locality than using FocusFinder with root + ViewGroup parent = (ViewGroup) getParent(); + + while (parent != root) { + tempFocus.set(focusRect); + + parent.offsetDescendantRectToMyCoords(this, tempFocus); + + View candidate = finder.findNextFocusFromRect(parent, tempFocus, direction); + if (candidate != null && candidate.requestFocus(direction)) { + return true; + } + + parent = (ViewGroup) parent.getParent(); + } + + return false; + } + + private boolean arrowScroll(final int direction) { + switch (direction) { + case FOCUS_DOWN: + if (!canScrollVertically(1)) { + return false; + } + scrollBy(0, 100); + break; + case FOCUS_UP: + if (!canScrollVertically(-1)) { + return false; + } + scrollBy(0, -100); + break; + case FOCUS_LEFT: + if (!canScrollHorizontally(-1)) { + return false; + } + scrollBy(-100, 0); + break; + case FOCUS_RIGHT: + if (!canScrollHorizontally(-1)) { + return false; + } + scrollBy(100, 0); + break; + default: + return false; + } + + return true; + } + + private boolean isOutside(final View view) { + return findContainingItemView(view) == null; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/views/ScrollableTabLayout.java b/app/src/main/java/org/schabi/newpipe/views/ScrollableTabLayout.java new file mode 100644 index 000000000..48e8ef81c --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/views/ScrollableTabLayout.java @@ -0,0 +1,134 @@ +package org.schabi.newpipe.views; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; + +import androidx.annotation.NonNull; + +import com.google.android.material.tabs.TabLayout; + +/** + * A TabLayout that is scrollable when tabs exceed its width. + * Hides when there are less than 2 tabs. + */ +public class ScrollableTabLayout extends TabLayout { + private static final String TAG = ScrollableTabLayout.class.getSimpleName(); + + private int layoutWidth = 0; + private int prevVisibility = View.GONE; + + public ScrollableTabLayout(final Context context) { + super(context); + } + + public ScrollableTabLayout(final Context context, final AttributeSet attrs) { + super(context, attrs); + } + + public ScrollableTabLayout(final Context context, final AttributeSet attrs, + final int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + protected void onLayout(final boolean changed, final int l, final int t, final int r, + final int b) { + super.onLayout(changed, l, t, r, b); + + remeasureTabs(); + } + + @Override + protected void onSizeChanged(final int w, final int h, final int oldw, final int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + + layoutWidth = w; + } + + @Override + public void addTab(@NonNull final Tab tab, final int position, final boolean setSelected) { + super.addTab(tab, position, setSelected); + + hasMultipleTabs(); + + // Adding a tab won't decrease total tabs' width so tabMode won't have to change to FIXED + if (getTabMode() != MODE_SCROLLABLE) { + remeasureTabs(); + } + } + + @Override + public void removeTabAt(final int position) { + super.removeTabAt(position); + + hasMultipleTabs(); + + // Removing a tab won't increase total tabs' width + // so tabMode won't have to change to SCROLLABLE + if (getTabMode() != MODE_FIXED) { + remeasureTabs(); + } + } + + @Override + protected void onVisibilityChanged(final View changedView, final int visibility) { + super.onVisibilityChanged(changedView, visibility); + + // Check width if some tabs have been added/removed while ScrollableTabLayout was invisible + // We don't have to check if it was GONE because then requestLayout() will be called + if (changedView == this) { + if (prevVisibility == View.INVISIBLE) { + remeasureTabs(); + } + prevVisibility = visibility; + } + } + + private void setMode(final int mode) { + if (mode == getTabMode()) { + return; + } + + setTabMode(mode); + } + + /** + * Make ScrollableTabLayout not visible if there are less than two tabs. + */ + private void hasMultipleTabs() { + if (getTabCount() > 1) { + setVisibility(View.VISIBLE); + } else { + setVisibility(View.GONE); + } + } + + /** + * Calculate minimal width required by tabs and set tabMode accordingly. + */ + private void remeasureTabs() { + if (prevVisibility != View.VISIBLE) { + return; + } + if (layoutWidth == 0) { + return; + } + + final int count = getTabCount(); + int contentWidth = 0; + for (int i = 0; i < count; i++) { + View child = getTabAt(i).view; + if (child.getVisibility() == View.VISIBLE) { + // Use tab's minimum requested width should actual content be too small + contentWidth += Math.max(child.getMinimumWidth(), child.getMeasuredWidth()); + } + } + + if (contentWidth > layoutWidth) { + setMode(TabLayout.MODE_SCROLLABLE); + } else { + setMode(TabLayout.MODE_FIXED); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/views/SuperScrollLayoutManager.java b/app/src/main/java/org/schabi/newpipe/views/SuperScrollLayoutManager.java new file mode 100644 index 000000000..6c4d20603 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/views/SuperScrollLayoutManager.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) Eltex ltd 2019 + * SuperScrollLayoutManager.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 . + */ +package org.schabi.newpipe.views; + +import android.content.Context; +import android.graphics.Rect; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; + +public final class SuperScrollLayoutManager extends LinearLayoutManager { + private final Rect handy = new Rect(); + + private final ArrayList focusables = new ArrayList<>(); + + public SuperScrollLayoutManager(final Context context) { + super(context); + } + + @Override + public boolean requestChildRectangleOnScreen(@NonNull final RecyclerView parent, + @NonNull final View child, + @NonNull final Rect rect, + final boolean immediate, + final boolean focusedChildVisible) { + if (!parent.isInTouchMode()) { + // only activate when in directional navigation mode (Android TV etc) — fine grained + // touch scrolling is better served by nested scroll system + + if (!focusedChildVisible || getFocusedChild() == child) { + handy.set(rect); + + parent.offsetDescendantRectToMyCoords(child, handy); + + parent.requestRectangleOnScreen(handy, immediate); + } + } + + return super.requestChildRectangleOnScreen(parent, child, rect, immediate, + focusedChildVisible); + } + + @Nullable + @Override + public View onInterceptFocusSearch(@NonNull final View focused, final int direction) { + View focusedItem = findContainingItemView(focused); + if (focusedItem == null) { + return super.onInterceptFocusSearch(focused, direction); + } + + int listDirection = getAbsoluteDirection(direction); + if (listDirection == 0) { + return super.onInterceptFocusSearch(focused, direction); + } + + // FocusFinder has an oddity: it considers size of Views more important + // than closeness to source View. This means, that big Views far away from current item + // are preferred to smaller sub-View of closer item. Setting focusability of closer item + // to FOCUS_AFTER_DESCENDANTS does not solve this, because ViewGroup#addFocusables omits + // such parent itself from list, if any of children are focusable. + // Fortunately we can intercept focus search and implement our own logic, based purely + // on position along the LinearLayoutManager axis + + ViewGroup recycler = (ViewGroup) focusedItem.getParent(); + + int sourcePosition = getPosition(focusedItem); + if (sourcePosition == 0 && listDirection < 0) { + return super.onInterceptFocusSearch(focused, direction); + } + + View preferred = null; + + int distance = Integer.MAX_VALUE; + + focusables.clear(); + + recycler.addFocusables(focusables, direction, recycler.isInTouchMode() + ? View.FOCUSABLES_TOUCH_MODE + : View.FOCUSABLES_ALL); + + try { + for (View view : focusables) { + if (view == focused || view == recycler) { + continue; + } + + if (view == focusedItem) { + // do not pass focus back to the item View itself - it makes no sense + // (we can still pass focus to it's children however) + continue; + } + + int candidate = getDistance(sourcePosition, view, listDirection); + if (candidate < 0) { + continue; + } + + if (candidate < distance) { + distance = candidate; + preferred = view; + } + } + } finally { + focusables.clear(); + } + + return preferred; + } + + private int getAbsoluteDirection(final int direction) { + switch (direction) { + default: + break; + case View.FOCUS_FORWARD: + return 1; + case View.FOCUS_BACKWARD: + return -1; + } + + if (getOrientation() == RecyclerView.HORIZONTAL) { + switch (direction) { + default: + break; + case View.FOCUS_LEFT: + return getReverseLayout() ? 1 : -1; + case View.FOCUS_RIGHT: + return getReverseLayout() ? -1 : 1; + } + } else { + switch (direction) { + default: + break; + case View.FOCUS_UP: + return getReverseLayout() ? 1 : -1; + case View.FOCUS_DOWN: + return getReverseLayout() ? -1 : 1; + } + } + + return 0; + } + + private int getDistance(final int sourcePosition, final View candidate, final int direction) { + View itemView = findContainingItemView(candidate); + if (itemView == null) { + return -1; + } + + int position = getPosition(itemView); + + return direction * (position - sourcePosition); + } +} diff --git a/app/src/main/java/us/shandian/giga/get/DownloadMission.java b/app/src/main/java/us/shandian/giga/get/DownloadMission.java index c0f85b321..3f651d2ee 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadMission.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadMission.java @@ -6,8 +6,8 @@ import android.system.ErrnoException; import android.system.OsConstants; import android.util.Log; -import androidx.annotation.Nullable; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import org.schabi.newpipe.DownloaderImpl; @@ -223,6 +223,7 @@ public class DownloadMission extends Mission { conn.setInstanceFollowRedirects(true); conn.setRequestProperty("User-Agent", DownloaderImpl.USER_AGENT); conn.setRequestProperty("Accept", "*/*"); + conn.setRequestProperty("Accept-Encoding", "*"); if (headRequest) conn.setRequestMethod("HEAD"); diff --git a/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java b/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java index 4aa6e912e..7fb12d088 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java @@ -70,7 +70,7 @@ public class DownloadRunnable extends Thread { Log.d(TAG, mId + ":acquired block at position=" + block.position + " done=" + block.done); } - long start = block.position * DownloadMission.BLOCK_SIZE; + long start = (long)block.position * DownloadMission.BLOCK_SIZE; long end = start + DownloadMission.BLOCK_SIZE - 1; start += block.done; diff --git a/app/src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java b/app/src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java index 1fa987c88..bf9460b3d 100644 --- a/app/src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java +++ b/app/src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java @@ -6,9 +6,10 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.net.Uri; -import androidx.annotation.NonNull; import android.util.Log; +import androidx.annotation.NonNull; + import java.io.File; import java.util.ArrayList; diff --git a/app/src/main/java/us/shandian/giga/io/ChunkFileInputStream.java b/app/src/main/java/us/shandian/giga/io/ChunkFileInputStream.java index 98015e37e..f7edf3975 100644 --- a/app/src/main/java/us/shandian/giga/io/ChunkFileInputStream.java +++ b/app/src/main/java/us/shandian/giga/io/ChunkFileInputStream.java @@ -104,7 +104,7 @@ public class ChunkFileInputStream extends SharpStream { @Override public long available() { - return (int) (length - position); + return length - position; } @SuppressWarnings("EmptyCatchBlock") diff --git a/app/src/main/java/us/shandian/giga/io/CircularFileWriter.java b/app/src/main/java/us/shandian/giga/io/CircularFileWriter.java index 102580570..d3dde7835 100644 --- a/app/src/main/java/us/shandian/giga/io/CircularFileWriter.java +++ b/app/src/main/java/us/shandian/giga/io/CircularFileWriter.java @@ -221,7 +221,7 @@ public class CircularFileWriter extends SharpStream { available = out.length - offsetOut; } - int length = Math.min(len, (int) available); + int length = Math.min(len, (int) Math.min(Integer.MAX_VALUE, available)); out.write(b, off, length); len -= length; diff --git a/app/src/main/java/us/shandian/giga/io/FileStreamSAF.java b/app/src/main/java/us/shandian/giga/io/FileStreamSAF.java index fc716b4f9..000900918 100644 --- a/app/src/main/java/us/shandian/giga/io/FileStreamSAF.java +++ b/app/src/main/java/us/shandian/giga/io/FileStreamSAF.java @@ -3,9 +3,10 @@ package us.shandian.giga.io; import android.content.ContentResolver; import android.net.Uri; import android.os.ParcelFileDescriptor; -import androidx.annotation.NonNull; import android.util.Log; +import androidx.annotation.NonNull; + import org.schabi.newpipe.streams.io.SharpStream; import java.io.FileInputStream; diff --git a/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java b/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java index c1a63bb6a..8f6070ff4 100644 --- a/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java +++ b/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java @@ -8,6 +8,7 @@ import android.database.Cursor; import android.net.Uri; import android.os.Build; import android.provider.DocumentsContract; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.documentfile.provider.DocumentFile; diff --git a/app/src/main/java/us/shandian/giga/io/StoredFileHelper.java b/app/src/main/java/us/shandian/giga/io/StoredFileHelper.java index 463c3ce5f..ad3ceec3d 100644 --- a/app/src/main/java/us/shandian/giga/io/StoredFileHelper.java +++ b/app/src/main/java/us/shandian/giga/io/StoredFileHelper.java @@ -7,10 +7,11 @@ import android.content.Intent; import android.net.Uri; import android.os.Build; import android.provider.DocumentsContract; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; import androidx.documentfile.provider.DocumentFile; +import androidx.fragment.app.Fragment; import org.schabi.newpipe.streams.io.SharpStream; diff --git a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java index bf9202a75..680f484e6 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java @@ -80,7 +80,7 @@ public abstract class Postprocessing implements Serializable { private transient DownloadMission mission; - private File tempFile; + private transient File tempFile; Postprocessing(boolean reserveSpace, boolean worksOnSameFile, String algorithmName) { this.reserveSpace = reserveSpace; @@ -95,8 +95,12 @@ public abstract class Postprocessing implements Serializable { public void cleanupTemporalDir() { if (tempFile != null && tempFile.exists()) { - //noinspection ResultOfMethodCallIgnored - tempFile.delete(); + try { + //noinspection ResultOfMethodCallIgnored + tempFile.delete(); + } catch (Exception e) { + // nothing to do + } } } diff --git a/app/src/main/java/us/shandian/giga/postprocessing/TtmlConverter.java b/app/src/main/java/us/shandian/giga/postprocessing/TtmlConverter.java index 5a5b687f7..8ed0dfae5 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/TtmlConverter.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/TtmlConverter.java @@ -2,15 +2,10 @@ package us.shandian.giga.postprocessing; import android.util.Log; -import org.schabi.newpipe.streams.SubtitleConverter; +import org.schabi.newpipe.streams.SrtFromTtmlWriter; import org.schabi.newpipe.streams.io.SharpStream; -import org.xml.sax.SAXException; import java.io.IOException; -import java.text.ParseException; - -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.xpath.XPathExpressionException; /** * @author kapodamy @@ -27,33 +22,16 @@ class TtmlConverter extends Postprocessing { 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); + boolean ignoreEmptyFrames = getArgumentAt(1, "true").equals("true"); if (format == null || format.equals("ttml")) { - SubtitleConverter ttmlDumper = new SubtitleConverter(); + SrtFromTtmlWriter writer = new SrtFromTtmlWriter(out, ignoreEmptyFrames); try { - ttmlDumper.dumpTTML( - sources[0], - out, - getArgumentAt(1, "true").equals("true"), - getArgumentAt(2, "true").equals("true") - ); + writer.build(sources[0]); } 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 err instanceof IOException ? 1 : 8; } return OK_RESULT; diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManager.java b/app/src/main/java/us/shandian/giga/service/DownloadManager.java index e8bc468e9..994c6ee63 100644 --- a/app/src/main/java/us/shandian/giga/service/DownloadManager.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManager.java @@ -139,6 +139,9 @@ public class DownloadManager { Log.d(TAG, "Loading pending downloads from directory: " + mPendingMissionsDir.getAbsolutePath()); } + File tempDir = pickAvailableTemporalDir(ctx); + Log.i(TAG, "using '" + tempDir + "' as temporal directory"); + for (File sub : subs) { if (!sub.isFile()) continue; if (sub.getName().equals(".tmp")) continue; @@ -184,7 +187,7 @@ public class DownloadManager { if (mis.psAlgorithm != null) { mis.psAlgorithm.cleanupTemporalDir(); - mis.psAlgorithm.setTemporalDir(pickAvailableTemporalDir(ctx)); + mis.psAlgorithm.setTemporalDir(tempDir); } mis.metadata = sub; @@ -513,13 +516,21 @@ public class DownloadManager { } static File pickAvailableTemporalDir(@NonNull Context ctx) { - if (isDirectoryAvailable(ctx.getExternalFilesDir(null))) - return ctx.getExternalFilesDir(null); - else if (isDirectoryAvailable(ctx.getFilesDir())) - return ctx.getFilesDir(); + File dir = ctx.getExternalFilesDir(null); + if (isDirectoryAvailable(dir)) return dir; + + dir = ctx.getFilesDir(); + if (isDirectoryAvailable(dir)) return dir; // this never should happen - return ctx.getDir("tmp", Context.MODE_PRIVATE); + dir = ctx.getDir("muxing_tmp", Context.MODE_PRIVATE); + if (isDirectoryAvailable(dir)) return dir; + + // fallback to cache dir + dir = ctx.getCacheDir(); + if (isDirectoryAvailable(dir)) return dir; + + throw new RuntimeException("Not temporal directories are available"); } @Nullable diff --git a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java index aa7e42abc..d490bcb0f 100644 --- a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java +++ b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java @@ -5,6 +5,7 @@ import android.app.Activity; import android.app.ProgressDialog; import android.content.Context; import android.content.Intent; +import android.graphics.Color; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; @@ -35,6 +36,8 @@ import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView.Adapter; import androidx.recyclerview.widget.RecyclerView.ViewHolder; +import com.google.android.material.snackbar.Snackbar; + import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.NewPipe; @@ -46,6 +49,7 @@ import java.io.File; import java.lang.ref.WeakReference; import java.net.URI; import java.util.ArrayList; +import java.util.Iterator; import us.shandian.giga.get.DownloadMission; import us.shandian.giga.get.FinishedMission; @@ -104,8 +108,12 @@ public class MissionAdapter extends Adapter implements Handler.Callb private MenuItem mPauseButton; private View mEmptyMessage; private RecoverHelper mRecover; + private View mView; + private ArrayList mHidden; + private Snackbar mSnackbar; private final Runnable rUpdater = this::updater; + private final Runnable rDelete = this::deleteFinishedDownloads; public MissionAdapter(Context context, @NonNull DownloadManager downloadManager, View emptyMessage, View root) { mContext = context; @@ -122,6 +130,10 @@ public class MissionAdapter extends Adapter implements Handler.Callb mDeleter = new Deleter(root, mContext, this, mDownloadManager, mIterator, mHandler); + mView = root; + + mHidden = new ArrayList<>(); + checkEmptyMessageVisibility(); onResume(); } @@ -329,17 +341,7 @@ public class MissionAdapter extends Adapter implements Handler.Callb if (BuildConfig.DEBUG) Log.v(TAG, "Mime: " + mimeType + " package: " + BuildConfig.APPLICATION_ID + ".provider"); - Uri uri; - - if (mission.storage.isDirect()) { - uri = FileProvider.getUriForFile( - mContext, - BuildConfig.APPLICATION_ID + ".provider", - new File(URI.create(mission.storage.getUri().toString())) - ); - } else { - uri = mission.storage.getUri(); - } + Uri uri = resolveShareableUri(mission); Intent intent = new Intent(); intent.setAction(Intent.ACTION_VIEW); @@ -367,11 +369,30 @@ public class MissionAdapter extends Adapter implements Handler.Callb Intent intent = new Intent(Intent.ACTION_SEND); intent.setType(resolveMimeType(mission)); - intent.putExtra(Intent.EXTRA_STREAM, mission.storage.getUri()); + intent.putExtra(Intent.EXTRA_STREAM, resolveShareableUri(mission)); + intent.addFlags(FLAG_GRANT_READ_URI_PERMISSION); mContext.startActivity(Intent.createChooser(intent, null)); } + /** + * Returns an Uri which can be shared to other applications. + * + * @see + * https://stackoverflow.com/questions/38200282/android-os-fileuriexposedexception-file-storage-emulated-0-test-txt-exposed + */ + private Uri resolveShareableUri(Mission mission) { + if (mission.storage.isDirect()) { + return FileProvider.getUriForFile( + mContext, + BuildConfig.APPLICATION_ID + ".provider", + new File(URI.create(mission.storage.getUri().toString())) + ); + } else { + return mission.storage.getUri(); + } + } + private static String resolveMimeType(@NonNull Mission mission) { String mimeType; @@ -522,7 +543,7 @@ public class MissionAdapter extends Adapter implements Handler.Callb ); } - builder.setNegativeButton(android.R.string.ok, (dialog, which) -> dialog.cancel()) + builder.setNegativeButton(R.string.finish, (dialog, which) -> dialog.cancel()) .setTitle(mission.storage.getName()) .create() .show(); @@ -557,9 +578,50 @@ public class MissionAdapter extends Adapter implements Handler.Callb ); } - public void clearFinishedDownloads() { - mDownloadManager.forgetFinishedDownloads(); - applyChanges(); + public void clearFinishedDownloads(boolean delete) { + if (delete && mIterator.hasFinishedMissions() && mHidden.isEmpty()) { + for (int i = 0; i < mIterator.getOldListSize(); i++) { + FinishedMission mission = mIterator.getItem(i).mission instanceof FinishedMission ? (FinishedMission) mIterator.getItem(i).mission : null; + if (mission != null) { + mIterator.hide(mission); + mHidden.add(mission); + } + } + applyChanges(); + + String msg = String.format(mContext.getString(R.string.deleted_downloads), mHidden.size()); + mSnackbar = Snackbar.make(mView, msg, Snackbar.LENGTH_INDEFINITE); + mSnackbar.setAction(R.string.undo, s -> { + Iterator i = mHidden.iterator(); + while (i.hasNext()) { + mIterator.unHide(i.next()); + i.remove(); + } + applyChanges(); + mHandler.removeCallbacks(rDelete); + }); + mSnackbar.setActionTextColor(Color.YELLOW); + mSnackbar.show(); + + mHandler.postDelayed(rDelete, 5000); + } else if (!delete) { + mDownloadManager.forgetFinishedDownloads(); + applyChanges(); + } + } + + private void deleteFinishedDownloads() { + if (mSnackbar != null) mSnackbar.dismiss(); + + Iterator i = mHidden.iterator(); + while (i.hasNext()) { + Mission mission = i.next(); + if (mission != null) { + mDownloadManager.deleteMission(mission); + mContext.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, mission.storage.getUri())); + } + i.remove(); + } } private boolean handlePopupItem(@NonNull ViewHolderItem h, @NonNull MenuItem option) { diff --git a/app/src/main/java/us/shandian/giga/ui/common/ToolbarActivity.java b/app/src/main/java/us/shandian/giga/ui/common/ToolbarActivity.java index 2ba091573..1542d3ff0 100644 --- a/app/src/main/java/us/shandian/giga/ui/common/ToolbarActivity.java +++ b/app/src/main/java/us/shandian/giga/ui/common/ToolbarActivity.java @@ -1,6 +1,7 @@ package us.shandian.giga.ui.common; import android.os.Bundle; + import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; diff --git a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java index 921eaff5c..09f4d0c79 100644 --- a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java +++ b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java @@ -189,10 +189,12 @@ public class MissionsFragment extends Fragment { return true; case R.id.clear_list: AlertDialog.Builder prompt = new AlertDialog.Builder(mContext); - prompt.setTitle(R.string.clear_finished_download); + prompt.setTitle(R.string.clear_download_history); prompt.setMessage(R.string.confirm_prompt); - prompt.setPositiveButton(android.R.string.ok, (dialog, which) -> mAdapter.clearFinishedDownloads()); - prompt.setNegativeButton(R.string.cancel, null); + // Intentionally misusing button's purpose in order to achieve good order + prompt.setNegativeButton(R.string.clear_download_history, (dialog, which) -> mAdapter.clearFinishedDownloads(false)); + prompt.setPositiveButton(R.string.delete_downloaded_files, (dialog, which) -> mAdapter.clearFinishedDownloads(true)); + prompt.setNeutralButton(R.string.cancel, null); prompt.create().show(); return true; case R.id.start_downloads: @@ -222,15 +224,9 @@ public class MissionsFragment extends Fragment { mList.setAdapter(mAdapter); if (mSwitch != null) { - boolean isLight = ThemeHelper.isLightThemeSelected(mContext); - int icon; - - if (mLinear) - icon = isLight ? R.drawable.ic_grid_black_24dp : R.drawable.ic_grid_white_24dp; - else - icon = isLight ? R.drawable.ic_list_black_24dp : R.drawable.ic_list_white_24dp; - - mSwitch.setIcon(icon); + mSwitch.setIcon(mLinear + ? ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_grid) + : ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_list)); mSwitch.setTitle(mLinear ? R.string.grid : R.string.list); mPrefs.edit().putBoolean("linear", mLinear).apply(); } diff --git a/app/src/main/java/us/shandian/giga/util/Utility.java b/app/src/main/java/us/shandian/giga/util/Utility.java index 46207777a..551e80a3e 100644 --- a/app/src/main/java/us/shandian/giga/util/Utility.java +++ b/app/src/main/java/us/shandian/giga/util/Utility.java @@ -191,12 +191,12 @@ public class Utility { public static int getIconForFileType(FileType type) { switch (type) { case MUSIC: - return R.drawable.music; + return R.drawable.ic_headset_white_24dp; default: case VIDEO: - return R.drawable.video; + return R.drawable.ic_movie_white_24dp; case SUBTITLE: - return R.drawable.subtitle; + return R.drawable.ic_subtitles_white_24dp; } } diff --git a/app/src/main/res/drawable-hdpi/ic_action_av_fast_forward.png b/app/src/main/res/drawable-hdpi/ic_action_av_fast_forward.png deleted file mode 100644 index 672ee37bc..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_action_av_fast_forward.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_av_fast_rewind.png b/app/src/main/res/drawable-hdpi/ic_action_av_fast_rewind.png deleted file mode 100644 index 8d49992eb..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_action_av_fast_rewind.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_add.png b/app/src/main/res/drawable-hdpi/ic_add.png deleted file mode 100644 index 1ae5b2dc4..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_add.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_arrow_back_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_arrow_back_white_24dp.png deleted file mode 100644 index cd1972677..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_arrow_back_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_arrow_down_white.png b/app/src/main/res/drawable-hdpi/ic_arrow_down_white.png deleted file mode 100644 index 33939600d..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_arrow_down_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_arrow_top_left_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_arrow_top_left_black_24dp.png deleted file mode 100644 index da5605741..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_arrow_top_left_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_arrow_top_left_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_arrow_top_left_white_24dp.png deleted file mode 100644 index 24376b637..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_arrow_top_left_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_arrow_up_white.png b/app/src/main/res/drawable-hdpi/ic_arrow_up_white.png deleted file mode 100644 index 0972a9bca..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_arrow_up_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_backup_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_backup_black_24dp.png deleted file mode 100644 index e0938f1dc..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_backup_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_backup_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_backup_white_24dp.png deleted file mode 100644 index 5e0b464cf..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_backup_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_bookmark_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_bookmark_black_24dp.png deleted file mode 100644 index 7ad39da3a..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_bookmark_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_bookmark_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_bookmark_white_24dp.png deleted file mode 100644 index 9de15c51a..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_bookmark_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_bug_report_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_bug_report_black_24dp.png deleted file mode 100644 index 1bccb1d11..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_bug_report_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_bug_report_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_bug_report_white_24dp.png deleted file mode 100644 index 0c963e1ca..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_bug_report_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_cast_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_cast_black_24dp.png deleted file mode 100644 index a35e1c672..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_cast_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_cast_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_cast_white_24dp.png deleted file mode 100644 index 60d3915ed..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_cast_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_channel_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_channel_black_24dp.png deleted file mode 100644 index ac66a3b86..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_channel_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_channel_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_channel_white_24dp.png deleted file mode 100644 index e0ef2a1a8..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_channel_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_close_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_close_black_24dp.png deleted file mode 100644 index 1a9cd75a0..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_close_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_delete_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_delete_black_24dp.png deleted file mode 100644 index ba8820363..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_delete_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_delete_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_delete_white_24dp.png deleted file mode 100644 index be2850b3d..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_delete_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_drag_handle_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_drag_handle_black_24dp.png deleted file mode 100644 index 7ebc39358..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_drag_handle_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_drag_handle_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_drag_handle_white_24dp.png deleted file mode 100644 index 8747b9ecb..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_drag_handle_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_expand_less_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_expand_less_black_24dp.png deleted file mode 100644 index 57139a78a..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_expand_less_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_expand_less_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_expand_less_white_24dp.png deleted file mode 100644 index dea898838..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_expand_less_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_expand_more_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_expand_more_black_24dp.png deleted file mode 100644 index 9625f148f..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_expand_more_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_expand_more_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_expand_more_white_24dp.png deleted file mode 100644 index 022e05799..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_expand_more_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_fiber_manual_record_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_fiber_manual_record_black_24dp.png deleted file mode 100644 index 459eec3fe..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_fiber_manual_record_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_fiber_manual_record_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_fiber_manual_record_white_24dp.png deleted file mode 100644 index 2c476010b..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_fiber_manual_record_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_file_download_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_file_download_black_24dp.png deleted file mode 100644 index d9aacea4c..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_file_download_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_file_download_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_file_download_white_24dp.png deleted file mode 100644 index c8a2039c5..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_file_download_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_filter_list_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_filter_list_black_24dp.png deleted file mode 100644 index a966cb9bd..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_filter_list_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_filter_list_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_filter_list_white_24dp.png deleted file mode 100644 index 7e8a6b536..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_filter_list_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_fullscreen_exit_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_fullscreen_exit_black_24dp.png deleted file mode 100644 index 8328e2efe..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_fullscreen_exit_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_fullscreen_exit_white.png b/app/src/main/res/drawable-hdpi/ic_fullscreen_exit_white.png deleted file mode 100644 index 159bea7fd..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_fullscreen_exit_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_fullscreen_white.png b/app/src/main/res/drawable-hdpi/ic_fullscreen_white.png deleted file mode 100644 index 9b8131124..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_fullscreen_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_grid_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_grid_black_24dp.png deleted file mode 100644 index 2db18582c..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_grid_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_grid_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_grid_white_24dp.png deleted file mode 100644 index 5cc4722f6..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_grid_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_headset_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_headset_black_24dp.png deleted file mode 100644 index 38eb219ef..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_headset_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_headset_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_headset_white_24dp.png deleted file mode 100644 index d25d3888e..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_headset_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_history_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_history_black_24dp.png deleted file mode 100644 index 9abddaa50..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_history_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_history_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_history_white_24dp.png deleted file mode 100644 index 485c826fd..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_history_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_import_export_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_import_export_black_24dp.png deleted file mode 100644 index b4466c849..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_import_export_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_import_export_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_import_export_white_24dp.png deleted file mode 100644 index 5b6c02010..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_import_export_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_info_outline_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_info_outline_black_24dp.png deleted file mode 100644 index 4b5ab06e1..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_info_outline_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_info_outline_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_info_outline_white_24dp.png deleted file mode 100644 index c7b1113cf..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_info_outline_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_kiosk_local_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_kiosk_local_black_24dp.png deleted file mode 100755 index a9e2993eb..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_kiosk_local_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_kiosk_local_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_kiosk_local_white_24dp.png deleted file mode 100755 index a9af000b4..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_kiosk_local_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_kiosk_recent_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_kiosk_recent_black_24dp.png deleted file mode 100755 index 13813ff82..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_kiosk_recent_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_kiosk_recent_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_kiosk_recent_white_24dp.png deleted file mode 100755 index 9054e0042..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_kiosk_recent_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_language_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_language_black_24dp.png deleted file mode 100644 index 36125569b..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_language_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_language_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_language_white_24dp.png deleted file mode 100644 index b7c8248fb..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_language_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_list_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_list_black_24dp.png deleted file mode 100644 index cc7b7a091..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_list_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_list_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_list_white_24dp.png deleted file mode 100644 index 5e6e304e3..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_list_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_more.png b/app/src/main/res/drawable-hdpi/ic_menu_more.png deleted file mode 100644 index 928fcab8f..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_menu_more.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_more_vert_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_more_vert_black_24dp.png deleted file mode 100644 index 22acc5500..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_more_vert_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_more_vert_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_more_vert_white_24dp.png deleted file mode 100644 index 67f07e473..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_more_vert_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_palette_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_palette_black_24dp.png deleted file mode 100644 index 8362e21a0..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_palette_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_palette_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_palette_white_24dp.png deleted file mode 100644 index 9470e79e1..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_palette_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_pause_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_pause_black_24dp.png deleted file mode 100644 index 3770b9124..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_pause_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_pause_white.png b/app/src/main/res/drawable-hdpi/ic_pause_white.png deleted file mode 100644 index 7192ad487..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_pause_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_pause_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_pause_white_24dp.png deleted file mode 100644 index 3e3de2dce..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_pause_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_picture_in_picture_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_picture_in_picture_black_24dp.png deleted file mode 100644 index 54f824410..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_picture_in_picture_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_picture_in_picture_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_picture_in_picture_white_24dp.png deleted file mode 100644 index b4ec6bb70..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_picture_in_picture_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_play_arrow_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_play_arrow_black_24dp.png deleted file mode 100644 index e9c288c99..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_play_arrow_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_play_arrow_white.png b/app/src/main/res/drawable-hdpi/ic_play_arrow_white.png deleted file mode 100644 index 547ef30aa..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_play_arrow_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_play_arrow_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_play_arrow_white_24dp.png deleted file mode 100644 index 57c9fa546..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_play_arrow_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_play_circle_filled_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_play_circle_filled_white_24dp.png deleted file mode 100644 index f8c7bc9f8..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_play_circle_filled_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_play_circle_transparent.png b/app/src/main/res/drawable-hdpi/ic_play_circle_transparent.png deleted file mode 100644 index 4290e2346..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_play_circle_transparent.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_playlist_add_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_playlist_add_black_24dp.png deleted file mode 100644 index 731b42590..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_playlist_add_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_playlist_add_check_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_playlist_add_check_black_24dp.png deleted file mode 100644 index 92448842b..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_playlist_add_check_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_playlist_add_check_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_playlist_add_check_white_24dp.png deleted file mode 100644 index bd23b9c48..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_playlist_add_check_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_playlist_add_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_playlist_add_white_24dp.png deleted file mode 100644 index 4fb76e178..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_playlist_add_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_playlist_play_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_playlist_play_white_24dp.png deleted file mode 100644 index 34dc90f11..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_playlist_play_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_remove.png b/app/src/main/res/drawable-hdpi/ic_remove.png deleted file mode 100644 index 75e65bc9c..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_remove.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_repeat_white.png b/app/src/main/res/drawable-hdpi/ic_repeat_white.png deleted file mode 100644 index 5de7a2951..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_repeat_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_replay_white.png b/app/src/main/res/drawable-hdpi/ic_replay_white_24dp.png similarity index 100% rename from app/src/main/res/drawable-hdpi/ic_replay_white.png rename to app/src/main/res/drawable-hdpi/ic_replay_white_24dp.png diff --git a/app/src/main/res/drawable-hdpi/ic_rss_feed_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_rss_feed_black_24dp.png deleted file mode 100644 index b0adc2912..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_rss_feed_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_rss_feed_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_rss_feed_white_24dp.png deleted file mode 100644 index c966a6e0a..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_rss_feed_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_save_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_save_black_24dp.png deleted file mode 100644 index b959dc4a8..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_save_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_save_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_save_white_24dp.png deleted file mode 100644 index dd3f10664..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_save_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_screen_rotation_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_screen_rotation_black_24dp.png deleted file mode 100644 index 9a55a65b7..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_screen_rotation_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_screen_rotation_white.png b/app/src/main/res/drawable-hdpi/ic_screen_rotation_white.png deleted file mode 100644 index b81f22246..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_screen_rotation_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_search_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_search_black_24dp.png deleted file mode 100644 index c593e7ad8..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_search_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_search_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_search_white_24dp.png deleted file mode 100644 index bbfbc96cb..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_search_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_settings_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_settings_black_24dp.png deleted file mode 100644 index acf1ddf85..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_settings_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_settings_update_black.png b/app/src/main/res/drawable-hdpi/ic_settings_update_black.png deleted file mode 100755 index cdd51d35f..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_settings_update_black.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_settings_update_white.png b/app/src/main/res/drawable-hdpi/ic_settings_update_white.png deleted file mode 100755 index 544a85c9d..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_settings_update_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_settings_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_settings_white_24dp.png deleted file mode 100644 index 97ded33b5..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_settings_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_share_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_share_black_24dp.png deleted file mode 100644 index 20ba48063..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_share_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_share_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_share_white_24dp.png deleted file mode 100644 index b09a6926d..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_share_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_shuffle_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_shuffle_white_24dp.png deleted file mode 100644 index ab55a83f4..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_shuffle_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_thumb_down_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_thumb_down_black_24dp.png deleted file mode 100644 index c7807e4d6..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_thumb_down_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_thumb_down_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_thumb_down_white_24dp.png deleted file mode 100644 index 3be775a52..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_thumb_down_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_thumb_up_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_thumb_up_black_24dp.png deleted file mode 100644 index 7ffe4fa78..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_thumb_up_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_thumb_up_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_thumb_up_white_24dp.png deleted file mode 100644 index c21a4643d..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_thumb_up_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_volume_off_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_volume_off_black_24dp.png deleted file mode 100644 index f1326ba7c..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_volume_off_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_volume_off_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_volume_off_white_24dp.png deleted file mode 100644 index ce0c21427..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_volume_off_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_whatshot_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_whatshot_black_24dp.png deleted file mode 100644 index b2db5994c..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_whatshot_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_whatshot_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_whatshot_white_24dp.png deleted file mode 100644 index 46ed1f8b6..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_whatshot_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/new_play_arrow.png b/app/src/main/res/drawable-hdpi/new_play_arrow.png deleted file mode 100644 index 1e2bafcc0..000000000 Binary files a/app/src/main/res/drawable-hdpi/new_play_arrow.png and /dev/null differ diff --git a/app/src/main/res/drawable-ldrtl-hdpi/ic_arrow_back_white_24dp.png b/app/src/main/res/drawable-ldrtl-hdpi/ic_arrow_back_white_24dp.png deleted file mode 100644 index f51755762..000000000 Binary files a/app/src/main/res/drawable-ldrtl-hdpi/ic_arrow_back_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-ldrtl-mdpi/ic_arrow_back_white_24dp.png b/app/src/main/res/drawable-ldrtl-mdpi/ic_arrow_back_white_24dp.png deleted file mode 100644 index 22a1140ae..000000000 Binary files a/app/src/main/res/drawable-ldrtl-mdpi/ic_arrow_back_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-ldrtl-mdpi/ic_settings_update_black.png b/app/src/main/res/drawable-ldrtl-mdpi/ic_settings_update_black.png deleted file mode 100755 index d85ec5975..000000000 Binary files a/app/src/main/res/drawable-ldrtl-mdpi/ic_settings_update_black.png and /dev/null differ diff --git a/app/src/main/res/drawable-ldrtl-xhdpi/ic_arrow_back_white_24dp.png b/app/src/main/res/drawable-ldrtl-xhdpi/ic_arrow_back_white_24dp.png deleted file mode 100644 index d858f18e6..000000000 Binary files a/app/src/main/res/drawable-ldrtl-xhdpi/ic_arrow_back_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-ldrtl-xxhdpi/ic_arrow_back_white_24dp.png b/app/src/main/res/drawable-ldrtl-xxhdpi/ic_arrow_back_white_24dp.png deleted file mode 100644 index 614ad49a3..000000000 Binary files a/app/src/main/res/drawable-ldrtl-xxhdpi/ic_arrow_back_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-ldrtl-xxxhdpi/ic_arrow_back_white_24dp.png b/app/src/main/res/drawable-ldrtl-xxxhdpi/ic_arrow_back_white_24dp.png deleted file mode 100644 index d409b544b..000000000 Binary files a/app/src/main/res/drawable-ldrtl-xxxhdpi/ic_arrow_back_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_av_fast_forward.png b/app/src/main/res/drawable-mdpi/ic_action_av_fast_forward.png deleted file mode 100644 index f266f3efd..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_action_av_fast_forward.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_av_fast_rewind.png b/app/src/main/res/drawable-mdpi/ic_action_av_fast_rewind.png deleted file mode 100644 index e280bd470..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_action_av_fast_rewind.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_add.png b/app/src/main/res/drawable-mdpi/ic_add.png deleted file mode 100644 index d51f0ddad..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_add.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_arrow_back_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_arrow_back_white_24dp.png deleted file mode 100644 index 4ef72eec9..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_arrow_back_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_arrow_down_white.png b/app/src/main/res/drawable-mdpi/ic_arrow_down_white.png deleted file mode 100644 index 40a0f499e..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_arrow_down_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_arrow_top_left_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_arrow_top_left_black_24dp.png deleted file mode 100644 index 056a0ff28..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_arrow_top_left_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_arrow_top_left_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_arrow_top_left_white_24dp.png deleted file mode 100644 index a2e73369c..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_arrow_top_left_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_arrow_up_white.png b/app/src/main/res/drawable-mdpi/ic_arrow_up_white.png deleted file mode 100644 index fe67b4673..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_arrow_up_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_backup_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_backup_black_24dp.png deleted file mode 100644 index 4cd6741c0..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_backup_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_backup_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_backup_white_24dp.png deleted file mode 100644 index aa640629a..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_backup_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_bookmark_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_bookmark_black_24dp.png deleted file mode 100644 index 0a10c2494..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_bookmark_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_bookmark_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_bookmark_white_24dp.png deleted file mode 100644 index 84f16627d..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_bookmark_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_bug_report_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_bug_report_black_24dp.png deleted file mode 100644 index 58aef662d..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_bug_report_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_bug_report_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_bug_report_white_24dp.png deleted file mode 100644 index 86e15f0d7..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_bug_report_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_cast_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_cast_black_24dp.png deleted file mode 100644 index aa5d6cd2a..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_cast_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_cast_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_cast_white_24dp.png deleted file mode 100644 index d62923f16..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_cast_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_channel_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_channel_black_24dp.png deleted file mode 100644 index 984ff498e..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_channel_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_channel_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_channel_white_24dp.png deleted file mode 100644 index 68f6ffd7f..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_channel_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_close_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_close_black_24dp.png deleted file mode 100644 index 40a1a84e3..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_close_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_delete_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_delete_black_24dp.png deleted file mode 100644 index 65bc6817d..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_delete_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_delete_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_delete_white_24dp.png deleted file mode 100644 index 8f10392ca..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_delete_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_drag_handle_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_drag_handle_black_24dp.png deleted file mode 100644 index e09d492fc..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_drag_handle_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_drag_handle_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_drag_handle_white_24dp.png deleted file mode 100644 index e509264d3..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_drag_handle_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_expand_less_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_expand_less_black_24dp.png deleted file mode 100644 index 08c16a328..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_expand_less_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_expand_less_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_expand_less_white_24dp.png deleted file mode 100644 index a2e4baad0..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_expand_less_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_expand_more_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_expand_more_black_24dp.png deleted file mode 100644 index feb85a775..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_expand_more_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_expand_more_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_expand_more_white_24dp.png deleted file mode 100644 index 910bb2a0a..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_expand_more_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_fiber_manual_record_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_fiber_manual_record_black_24dp.png deleted file mode 100644 index cfc8b4e60..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_fiber_manual_record_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_fiber_manual_record_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_fiber_manual_record_white_24dp.png deleted file mode 100644 index f6f53a154..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_fiber_manual_record_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_file_download_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_file_download_black_24dp.png deleted file mode 100644 index c2c845e84..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_file_download_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_file_download_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_file_download_white_24dp.png deleted file mode 100644 index d400472fd..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_file_download_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_filter_list_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_filter_list_black_24dp.png deleted file mode 100644 index d86492b42..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_filter_list_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_filter_list_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_filter_list_white_24dp.png deleted file mode 100644 index 59a2ec755..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_filter_list_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_fullscreen_exit_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_fullscreen_exit_black_24dp.png deleted file mode 100644 index c8394487c..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_fullscreen_exit_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_fullscreen_exit_white.png b/app/src/main/res/drawable-mdpi/ic_fullscreen_exit_white.png deleted file mode 100644 index 364bad0b8..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_fullscreen_exit_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_fullscreen_white.png b/app/src/main/res/drawable-mdpi/ic_fullscreen_white.png deleted file mode 100644 index 4423c7ce9..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_fullscreen_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_grid_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_grid_black_24dp.png deleted file mode 100644 index 0f878e4ed..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_grid_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_grid_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_grid_white_24dp.png deleted file mode 100644 index 0096c9f11..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_grid_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_headset_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_headset_black_24dp.png deleted file mode 100644 index d872b05d5..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_headset_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_headset_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_headset_white_24dp.png deleted file mode 100644 index df063799d..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_headset_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_history_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_history_black_24dp.png deleted file mode 100644 index 9fade8bb5..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_history_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_history_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_history_white_24dp.png deleted file mode 100644 index d67647c56..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_history_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_import_export_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_import_export_black_24dp.png deleted file mode 100644 index 90f8c4567..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_import_export_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_import_export_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_import_export_white_24dp.png deleted file mode 100644 index 151188cf8..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_import_export_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_info_outline_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_info_outline_black_24dp.png deleted file mode 100644 index e0c9fe0eb..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_info_outline_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_info_outline_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_info_outline_white_24dp.png deleted file mode 100644 index 353e06495..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_info_outline_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_kiosk_local_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_kiosk_local_black_24dp.png deleted file mode 100755 index 1eba63792..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_kiosk_local_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_kiosk_local_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_kiosk_local_white_24dp.png deleted file mode 100755 index 23d8145f5..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_kiosk_local_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_kiosk_recent_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_kiosk_recent_black_24dp.png deleted file mode 100755 index adc36b227..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_kiosk_recent_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_kiosk_recent_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_kiosk_recent_white_24dp.png deleted file mode 100755 index c19bfe964..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_kiosk_recent_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_language_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_language_black_24dp.png deleted file mode 100644 index 62ef88c67..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_language_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_language_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_language_white_24dp.png deleted file mode 100644 index 0bc7dfd48..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_language_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_list_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_list_black_24dp.png deleted file mode 100644 index 6fa4e5034..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_list_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_list_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_list_white_24dp.png deleted file mode 100644 index f0d3f5f7c..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_list_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_more_vert_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_more_vert_black_24dp.png deleted file mode 100644 index 0e4f2f6ea..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_more_vert_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_more_vert_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_more_vert_white_24dp.png deleted file mode 100644 index 017e45ede..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_more_vert_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_palette_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_palette_black_24dp.png deleted file mode 100644 index f7491eb79..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_palette_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_palette_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_palette_white_24dp.png deleted file mode 100644 index 6ce59523d..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_palette_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_pause_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_pause_black_24dp.png deleted file mode 100644 index 6e81d3ad4..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_pause_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_pause_white.png b/app/src/main/res/drawable-mdpi/ic_pause_white.png deleted file mode 100644 index f49aed757..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_pause_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_pause_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_pause_white_24dp.png deleted file mode 100644 index f5236e8aa..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_pause_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_picture_in_picture_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_picture_in_picture_black_24dp.png deleted file mode 100644 index e7a9be944..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_picture_in_picture_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_picture_in_picture_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_picture_in_picture_white_24dp.png deleted file mode 100644 index 96b5ed3f4..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_picture_in_picture_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_play_arrow_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_play_arrow_black_24dp.png deleted file mode 100644 index d78c57bad..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_play_arrow_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_play_arrow_white.png b/app/src/main/res/drawable-mdpi/ic_play_arrow_white.png deleted file mode 100644 index a3c80e73d..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_play_arrow_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_play_arrow_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_play_arrow_white_24dp.png deleted file mode 100644 index c61e948bb..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_play_arrow_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_play_circle_filled_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_play_circle_filled_white_24dp.png deleted file mode 100644 index 9681bf5a5..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_play_circle_filled_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_play_circle_transparent.png b/app/src/main/res/drawable-mdpi/ic_play_circle_transparent.png deleted file mode 100644 index 743e4e810..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_play_circle_transparent.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_playlist_add_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_playlist_add_black_24dp.png deleted file mode 100644 index d7a7514a8..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_playlist_add_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_playlist_add_check_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_playlist_add_check_black_24dp.png deleted file mode 100644 index 416490774..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_playlist_add_check_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_playlist_add_check_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_playlist_add_check_white_24dp.png deleted file mode 100644 index 0e35fe739..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_playlist_add_check_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_playlist_add_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_playlist_add_white_24dp.png deleted file mode 100644 index 73c981285..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_playlist_add_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_playlist_play_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_playlist_play_white_24dp.png deleted file mode 100644 index 1be3cdbd6..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_playlist_play_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_remove.png b/app/src/main/res/drawable-mdpi/ic_remove.png deleted file mode 100644 index a1816d4c6..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_remove.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_repeat_white.png b/app/src/main/res/drawable-mdpi/ic_repeat_white.png deleted file mode 100644 index ad8b8c0df..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_repeat_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_replay_white.png b/app/src/main/res/drawable-mdpi/ic_replay_white_24dp.png similarity index 100% rename from app/src/main/res/drawable-mdpi/ic_replay_white.png rename to app/src/main/res/drawable-mdpi/ic_replay_white_24dp.png diff --git a/app/src/main/res/drawable-mdpi/ic_rss_feed_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_rss_feed_black_24dp.png deleted file mode 100644 index 7fb28d5c5..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_rss_feed_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_rss_feed_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_rss_feed_white_24dp.png deleted file mode 100644 index 067b10752..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_rss_feed_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_save_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_save_black_24dp.png deleted file mode 100644 index 663479b73..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_save_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_save_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_save_white_24dp.png deleted file mode 100644 index 015062ed3..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_save_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_screen_rotation_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_screen_rotation_black_24dp.png deleted file mode 100644 index 012a32d4f..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_screen_rotation_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_screen_rotation_white.png b/app/src/main/res/drawable-mdpi/ic_screen_rotation_white.png deleted file mode 100644 index bb36eef34..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_screen_rotation_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_search_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_search_black_24dp.png deleted file mode 100644 index 6b1634323..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_search_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_search_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_search_white_24dp.png deleted file mode 100644 index faefc59c8..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_search_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_settings_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_settings_black_24dp.png deleted file mode 100644 index c59419c02..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_settings_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_settings_update_black.png b/app/src/main/res/drawable-mdpi/ic_settings_update_black.png deleted file mode 100755 index 964553137..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_settings_update_black.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_settings_update_white.png b/app/src/main/res/drawable-mdpi/ic_settings_update_white.png deleted file mode 100755 index cf4642f97..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_settings_update_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_settings_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_settings_white_24dp.png deleted file mode 100644 index 8909c3553..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_settings_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_share_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_share_black_24dp.png deleted file mode 100644 index f02d360aa..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_share_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_share_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_share_white_24dp.png deleted file mode 100644 index e944fd70c..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_share_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_shuffle_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_shuffle_white_24dp.png deleted file mode 100644 index d13a258a3..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_shuffle_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_thumb_down_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_thumb_down_black_24dp.png deleted file mode 100644 index fa4d6ff7e..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_thumb_down_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_thumb_down_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_thumb_down_white_24dp.png deleted file mode 100644 index ccb7bb76b..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_thumb_down_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_thumb_up_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_thumb_up_black_24dp.png deleted file mode 100644 index 04162adc8..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_thumb_up_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_thumb_up_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_thumb_up_white_24dp.png deleted file mode 100644 index 10a6de71f..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_thumb_up_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_volume_off_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_volume_off_black_24dp.png deleted file mode 100644 index a629cfff5..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_volume_off_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_volume_off_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_volume_off_white_24dp.png deleted file mode 100644 index 4681ec141..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_volume_off_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_whatshot_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_whatshot_black_24dp.png deleted file mode 100644 index 31b1981f0..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_whatshot_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_whatshot_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_whatshot_white_24dp.png deleted file mode 100644 index 4cf6f85f8..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_whatshot_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/new_play_arrow.png b/app/src/main/res/drawable-mdpi/new_play_arrow.png deleted file mode 100644 index 96103be9d..000000000 Binary files a/app/src/main/res/drawable-mdpi/new_play_arrow.png and /dev/null differ diff --git a/app/src/main/res/drawable-nodpi/arrow_down.png b/app/src/main/res/drawable-nodpi/arrow_down.png deleted file mode 100644 index f968ab32b..000000000 Binary files a/app/src/main/res/drawable-nodpi/arrow_down.png and /dev/null differ diff --git a/app/src/main/res/drawable-nodpi/arrow_up.png b/app/src/main/res/drawable-nodpi/arrow_up.png deleted file mode 100644 index e5081691a..000000000 Binary files a/app/src/main/res/drawable-nodpi/arrow_up.png and /dev/null differ diff --git a/app/src/main/res/drawable-nodpi/gruese_die_gema.png b/app/src/main/res/drawable-nodpi/gruese_die_gema.png deleted file mode 100644 index d6e2af3d5..000000000 Binary files a/app/src/main/res/drawable-nodpi/gruese_die_gema.png and /dev/null differ diff --git a/app/src/main/res/drawable-nodpi/np_logo_nude_shadow.png b/app/src/main/res/drawable-nodpi/newpipe_logo_nude_shadow.png similarity index 100% rename from app/src/main/res/drawable-nodpi/np_logo_nude_shadow.png rename to app/src/main/res/drawable-nodpi/newpipe_logo_nude_shadow.png diff --git a/app/src/main/res/drawable-nodpi/service.png b/app/src/main/res/drawable-nodpi/service.png deleted file mode 100644 index cfaff19e2..000000000 Binary files a/app/src/main/res/drawable-nodpi/service.png and /dev/null differ diff --git a/app/src/main/res/drawable-nodpi/soundcloud.png b/app/src/main/res/drawable-nodpi/soundcloud.png deleted file mode 100644 index 0fa6045d5..000000000 Binary files a/app/src/main/res/drawable-nodpi/soundcloud.png and /dev/null differ diff --git a/app/src/main/res/drawable-nodpi/youtube.png b/app/src/main/res/drawable-nodpi/youtube.png deleted file mode 100644 index 82aa58ff1..000000000 Binary files a/app/src/main/res/drawable-nodpi/youtube.png and /dev/null differ diff --git a/app/src/main/res/drawable-v23/splash_background.xml b/app/src/main/res/drawable-v23/splash_background.xml index a67fbc4a6..a11787c8a 100644 --- a/app/src/main/res/drawable-v23/splash_background.xml +++ b/app/src/main/res/drawable-v23/splash_background.xml @@ -8,5 +8,5 @@ android:width="80dp" android:height="80dp" android:gravity="center" - android:drawable="@drawable/splash_forground"/> + android:drawable="@drawable/splash_foreground"/> \ No newline at end of file diff --git a/app/src/main/res/drawable-xhdpi/ic_action_av_fast_forward.png b/app/src/main/res/drawable-xhdpi/ic_action_av_fast_forward.png deleted file mode 100644 index 2e6f43cae..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_action_av_fast_forward.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_av_fast_rewind.png b/app/src/main/res/drawable-xhdpi/ic_action_av_fast_rewind.png deleted file mode 100644 index 1e804aafc..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_action_av_fast_rewind.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_add.png b/app/src/main/res/drawable-xhdpi/ic_add.png deleted file mode 100644 index 9ea0eeb7e..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_add.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_arrow_back_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_arrow_back_white_24dp.png deleted file mode 100644 index 832f5a361..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_arrow_back_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_arrow_down_white.png b/app/src/main/res/drawable-xhdpi/ic_arrow_down_white.png deleted file mode 100644 index 86bc5db3b..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_arrow_down_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_arrow_top_left_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_arrow_top_left_black_24dp.png deleted file mode 100644 index e4255a18a..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_arrow_top_left_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_arrow_top_left_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_arrow_top_left_white_24dp.png deleted file mode 100644 index db578cab9..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_arrow_top_left_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_arrow_up_white.png b/app/src/main/res/drawable-xhdpi/ic_arrow_up_white.png deleted file mode 100644 index dda36882e..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_arrow_up_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_backup_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_backup_black_24dp.png deleted file mode 100644 index 81155da52..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_backup_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_backup_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_backup_white_24dp.png deleted file mode 100644 index a9602d11b..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_backup_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_bookmark_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_bookmark_black_24dp.png deleted file mode 100644 index 5d71bf213..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_bookmark_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_bookmark_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_bookmark_white_24dp.png deleted file mode 100644 index 872349cca..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_bookmark_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_bug_report_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_bug_report_black_24dp.png deleted file mode 100644 index 107f74a20..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_bug_report_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_bug_report_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_bug_report_white_24dp.png deleted file mode 100644 index 36b826bb8..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_bug_report_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_cast_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_cast_black_24dp.png deleted file mode 100644 index 1fe4879df..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_cast_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_cast_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_cast_white_24dp.png deleted file mode 100644 index f5f7c14b3..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_cast_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_channel_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_channel_black_24dp.png deleted file mode 100644 index 0851f1738..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_channel_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_channel_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_channel_white_24dp.png deleted file mode 100644 index 2f0f6c5fd..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_channel_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_close_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_close_black_24dp.png deleted file mode 100644 index 6bc437298..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_close_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_delete_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_delete_black_24dp.png deleted file mode 100644 index f080aa9e8..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_delete_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_delete_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_delete_white_24dp.png deleted file mode 100644 index a1b828bf9..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_delete_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_drag_handle_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_drag_handle_black_24dp.png deleted file mode 100644 index 906f5eee0..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_drag_handle_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_drag_handle_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_drag_handle_white_24dp.png deleted file mode 100644 index aa1547b04..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_drag_handle_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_expand_less_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_expand_less_black_24dp.png deleted file mode 100644 index 323360ead..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_expand_less_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_expand_less_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_expand_less_white_24dp.png deleted file mode 100644 index ae36d91e1..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_expand_less_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_expand_more_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_expand_more_black_24dp.png deleted file mode 100644 index d3ee65e9a..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_expand_more_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_expand_more_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_expand_more_white_24dp.png deleted file mode 100644 index c42e2a049..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_expand_more_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_fiber_manual_record_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_fiber_manual_record_black_24dp.png deleted file mode 100644 index 3eb79e4c1..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_fiber_manual_record_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_fiber_manual_record_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_fiber_manual_record_white_24dp.png deleted file mode 100644 index 0fa16b016..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_fiber_manual_record_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_file_download_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_file_download_black_24dp.png deleted file mode 100644 index f5afb24dc..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_file_download_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_file_download_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_file_download_white_24dp.png deleted file mode 100644 index f53cc0c62..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_file_download_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_filter_list_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_filter_list_black_24dp.png deleted file mode 100644 index b64df3612..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_filter_list_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_filter_list_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_filter_list_white_24dp.png deleted file mode 100644 index 9416c70ec..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_filter_list_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_fullscreen_exit_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_fullscreen_exit_black_24dp.png deleted file mode 100644 index 5fc3166ac..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_fullscreen_exit_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_fullscreen_exit_white.png b/app/src/main/res/drawable-xhdpi/ic_fullscreen_exit_white.png deleted file mode 100644 index ef360fe40..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_fullscreen_exit_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_fullscreen_white.png b/app/src/main/res/drawable-xhdpi/ic_fullscreen_white.png deleted file mode 100644 index c1dcfb290..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_fullscreen_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_grid_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_grid_black_24dp.png deleted file mode 100644 index bcefc5221..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_grid_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_grid_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_grid_white_24dp.png deleted file mode 100644 index 745cb6cb8..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_grid_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_headset_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_headset_black_24dp.png deleted file mode 100644 index f2664dcde..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_headset_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_headset_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_headset_white_24dp.png deleted file mode 100644 index d7a741b61..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_headset_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_history_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_history_black_24dp.png deleted file mode 100644 index f6096cab3..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_history_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_history_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_history_white_24dp.png deleted file mode 100644 index 3e73b49ee..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_history_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_import_export_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_import_export_black_24dp.png deleted file mode 100644 index 9b643bd3b..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_import_export_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_import_export_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_import_export_white_24dp.png deleted file mode 100644 index e22e18866..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_import_export_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_info_outline_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_info_outline_black_24dp.png deleted file mode 100644 index b706f0d06..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_info_outline_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_info_outline_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_info_outline_white_24dp.png deleted file mode 100644 index c571b2e3e..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_info_outline_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_kiosk_local_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_kiosk_local_black_24dp.png deleted file mode 100755 index e20865ab0..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_kiosk_local_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_kiosk_local_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_kiosk_local_white_24dp.png deleted file mode 100755 index 2d3474832..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_kiosk_local_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_kiosk_recent_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_kiosk_recent_black_24dp.png deleted file mode 100755 index 54e815980..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_kiosk_recent_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_kiosk_recent_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_kiosk_recent_white_24dp.png deleted file mode 100755 index 3141a790d..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_kiosk_recent_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_language_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_language_black_24dp.png deleted file mode 100644 index 5ed4a9252..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_language_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_language_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_language_white_24dp.png deleted file mode 100644 index eeaab46c0..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_language_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_list_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_list_black_24dp.png deleted file mode 100644 index 0f7327fad..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_list_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_list_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_list_white_24dp.png deleted file mode 100644 index 1de314a57..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_list_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_more.png b/app/src/main/res/drawable-xhdpi/ic_menu_more.png deleted file mode 100644 index 13596f594..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_menu_more.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_more_vert_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_more_vert_black_24dp.png deleted file mode 100644 index 9f10aa275..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_more_vert_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_more_vert_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_more_vert_white_24dp.png deleted file mode 100644 index efab8a74f..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_more_vert_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_palette_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_palette_black_24dp.png deleted file mode 100644 index ce3a94c3c..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_palette_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_palette_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_palette_white_24dp.png deleted file mode 100644 index 4af10a4d0..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_palette_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_pause_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_pause_black_24dp.png deleted file mode 100644 index e6de3973a..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_pause_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_pause_white.png b/app/src/main/res/drawable-xhdpi/ic_pause_white.png deleted file mode 100644 index 660ac6585..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_pause_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_pause_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_pause_white_24dp.png deleted file mode 100644 index b94b2ae40..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_pause_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_picture_in_picture_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_picture_in_picture_black_24dp.png deleted file mode 100644 index d85b80f84..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_picture_in_picture_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_picture_in_picture_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_picture_in_picture_white_24dp.png deleted file mode 100644 index 8039bebcf..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_picture_in_picture_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_play_arrow_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_play_arrow_black_24dp.png deleted file mode 100644 index f208795fc..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_play_arrow_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_play_arrow_white.png b/app/src/main/res/drawable-xhdpi/ic_play_arrow_white.png deleted file mode 100644 index be5c062b5..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_play_arrow_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_play_arrow_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_play_arrow_white_24dp.png deleted file mode 100644 index a3c80e73d..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_play_arrow_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_play_circle_filled_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_play_circle_filled_white_24dp.png deleted file mode 100644 index 5dcdf0d7a..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_play_circle_filled_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_play_circle_transparent.png b/app/src/main/res/drawable-xhdpi/ic_play_circle_transparent.png deleted file mode 100644 index afb9a7bf6..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_play_circle_transparent.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_playlist_add_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_playlist_add_black_24dp.png deleted file mode 100644 index dc4ebe9f3..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_playlist_add_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_playlist_add_check_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_playlist_add_check_black_24dp.png deleted file mode 100644 index 24855e94f..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_playlist_add_check_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_playlist_add_check_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_playlist_add_check_white_24dp.png deleted file mode 100644 index a94c5d035..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_playlist_add_check_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_playlist_add_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_playlist_add_white_24dp.png deleted file mode 100644 index 52ccba0b2..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_playlist_add_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_playlist_play_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_playlist_play_white_24dp.png deleted file mode 100644 index e7d2159c5..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_playlist_play_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_remove.png b/app/src/main/res/drawable-xhdpi/ic_remove.png deleted file mode 100644 index ffbdaa6ed..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_remove.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_repeat_white.png b/app/src/main/res/drawable-xhdpi/ic_repeat_white.png deleted file mode 100644 index c13d00242..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_repeat_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_replay_white.png b/app/src/main/res/drawable-xhdpi/ic_replay_white_24dp.png similarity index 100% rename from app/src/main/res/drawable-xhdpi/ic_replay_white.png rename to app/src/main/res/drawable-xhdpi/ic_replay_white_24dp.png diff --git a/app/src/main/res/drawable-xhdpi/ic_rss_feed_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_rss_feed_black_24dp.png deleted file mode 100644 index 7eb9897d8..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_rss_feed_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_rss_feed_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_rss_feed_white_24dp.png deleted file mode 100644 index 650627286..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_rss_feed_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_save_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_save_black_24dp.png deleted file mode 100644 index eca2d92ec..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_save_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_save_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_save_white_24dp.png deleted file mode 100644 index adda09575..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_save_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_screen_rotation_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_screen_rotation_black_24dp.png deleted file mode 100644 index ae2be1fa8..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_screen_rotation_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_screen_rotation_white.png b/app/src/main/res/drawable-xhdpi/ic_screen_rotation_white.png deleted file mode 100644 index 449b6725f..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_screen_rotation_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_search_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_search_black_24dp.png deleted file mode 100644 index 638190268..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_search_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_search_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_search_white_24dp.png deleted file mode 100644 index bfc3e3939..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_search_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_settings_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_settings_black_24dp.png deleted file mode 100644 index e84e188a1..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_settings_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_settings_update_black.png b/app/src/main/res/drawable-xhdpi/ic_settings_update_black.png deleted file mode 100755 index 0304e6fd1..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_settings_update_black.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_settings_update_white.png b/app/src/main/res/drawable-xhdpi/ic_settings_update_white.png deleted file mode 100755 index 9c71b13f7..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_settings_update_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_settings_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_settings_white_24dp.png deleted file mode 100644 index 5caedc8e5..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_settings_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_share_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_share_black_24dp.png deleted file mode 100644 index 81c80b700..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_share_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_share_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_share_white_24dp.png deleted file mode 100644 index 22a8783e7..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_share_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_shuffle_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_shuffle_white_24dp.png deleted file mode 100644 index 66c15ce62..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_shuffle_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_thumb_down_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_thumb_down_black_24dp.png deleted file mode 100644 index a5679f360..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_thumb_down_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_thumb_down_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_thumb_down_white_24dp.png deleted file mode 100644 index 709b89291..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_thumb_down_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_thumb_up_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_thumb_up_black_24dp.png deleted file mode 100644 index 76ccf695a..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_thumb_up_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_thumb_up_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_thumb_up_white_24dp.png deleted file mode 100644 index 75ae1f5f1..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_thumb_up_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_volume_off_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_volume_off_black_24dp.png deleted file mode 100644 index 52c9cc1de..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_volume_off_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_volume_off_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_volume_off_white_24dp.png deleted file mode 100644 index 732a1c0f4..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_volume_off_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_whatshot_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_whatshot_black_24dp.png deleted file mode 100644 index e9ae82670..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_whatshot_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_whatshot_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_whatshot_white_24dp.png deleted file mode 100644 index 3651d061a..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_whatshot_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/music.png b/app/src/main/res/drawable-xhdpi/music.png deleted file mode 100644 index 130f5da30..000000000 Binary files a/app/src/main/res/drawable-xhdpi/music.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/new_play_arrow.png b/app/src/main/res/drawable-xhdpi/new_play_arrow.png deleted file mode 100644 index 1a6b8d568..000000000 Binary files a/app/src/main/res/drawable-xhdpi/new_play_arrow.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/subtitle.png b/app/src/main/res/drawable-xhdpi/subtitle.png deleted file mode 100644 index 7f535288e..000000000 Binary files a/app/src/main/res/drawable-xhdpi/subtitle.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/video.png b/app/src/main/res/drawable-xhdpi/video.png deleted file mode 100644 index fefb72d9c..000000000 Binary files a/app/src/main/res/drawable-xhdpi/video.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_av_fast_forward.png b/app/src/main/res/drawable-xxhdpi/ic_action_av_fast_forward.png deleted file mode 100644 index 3012145f0..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_action_av_fast_forward.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_av_fast_rewind.png b/app/src/main/res/drawable-xxhdpi/ic_action_av_fast_rewind.png deleted file mode 100644 index a04b8433a..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_action_av_fast_rewind.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_add.png b/app/src/main/res/drawable-xxhdpi/ic_add.png deleted file mode 100644 index 75f192aab..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_add.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_arrow_back_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_arrow_back_white_24dp.png deleted file mode 100644 index 32a6d91ce..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_arrow_back_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_arrow_down_white.png b/app/src/main/res/drawable-xxhdpi/ic_arrow_down_white.png deleted file mode 100644 index 7e901e098..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_arrow_down_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_arrow_top_left_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_arrow_top_left_black_24dp.png deleted file mode 100644 index 566b5c5d3..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_arrow_top_left_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_arrow_top_left_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_arrow_top_left_white_24dp.png deleted file mode 100644 index 89d726f6c..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_arrow_top_left_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_arrow_up_white.png b/app/src/main/res/drawable-xxhdpi/ic_arrow_up_white.png deleted file mode 100644 index bc71e23de..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_arrow_up_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_backup_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_backup_black_24dp.png deleted file mode 100644 index 6506c7236..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_backup_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_backup_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_backup_white_24dp.png deleted file mode 100644 index 3ff57ad3e..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_backup_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_bookmark_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_bookmark_black_24dp.png deleted file mode 100644 index 2189be346..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_bookmark_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_bookmark_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_bookmark_white_24dp.png deleted file mode 100644 index 3faff90bb..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_bookmark_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_bug_report_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_bug_report_black_24dp.png deleted file mode 100644 index af8c82e6e..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_bug_report_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_bug_report_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_bug_report_white_24dp.png deleted file mode 100644 index 766bac447..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_bug_report_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_cast_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_cast_black_24dp.png deleted file mode 100644 index 454b66b62..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_cast_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_cast_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_cast_white_24dp.png deleted file mode 100644 index 7a7673fb9..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_cast_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_channel_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_channel_black_24dp.png deleted file mode 100644 index 4861728c4..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_channel_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_channel_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_channel_white_24dp.png deleted file mode 100644 index 2fd740ee8..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_channel_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_close_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_close_black_24dp.png deleted file mode 100644 index 51b4401ca..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_close_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_delete_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_delete_black_24dp.png deleted file mode 100644 index 4cb4c08e2..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_delete_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_delete_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_delete_white_24dp.png deleted file mode 100644 index bd11b4c66..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_delete_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_drag_handle_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_drag_handle_black_24dp.png deleted file mode 100644 index 71da19a59..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_drag_handle_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_drag_handle_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_drag_handle_white_24dp.png deleted file mode 100644 index e91ef07e9..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_drag_handle_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_expand_less_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_expand_less_black_24dp.png deleted file mode 100644 index ee92f4ecd..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_expand_less_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_expand_less_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_expand_less_white_24dp.png deleted file mode 100644 index 62fc386c1..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_expand_less_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_expand_more_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_expand_more_black_24dp.png deleted file mode 100644 index 5cd142c1d..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_expand_more_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_expand_more_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_expand_more_white_24dp.png deleted file mode 100644 index dbc0b2032..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_expand_more_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_fiber_manual_record_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_fiber_manual_record_black_24dp.png deleted file mode 100644 index b53beb106..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_fiber_manual_record_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_fiber_manual_record_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_fiber_manual_record_white_24dp.png deleted file mode 100644 index 422487473..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_fiber_manual_record_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_file_download_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_file_download_black_24dp.png deleted file mode 100644 index ce97c85df..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_file_download_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_file_download_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_file_download_white_24dp.png deleted file mode 100644 index 78aa59166..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_file_download_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_filter_list_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_filter_list_black_24dp.png deleted file mode 100644 index 2314642f9..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_filter_list_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_filter_list_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_filter_list_white_24dp.png deleted file mode 100644 index 1263ae82e..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_filter_list_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_fullscreen_exit_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_fullscreen_exit_black_24dp.png deleted file mode 100644 index 5691b5541..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_fullscreen_exit_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_fullscreen_exit_white.png b/app/src/main/res/drawable-xxhdpi/ic_fullscreen_exit_white.png deleted file mode 100644 index b7f4133fd..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_fullscreen_exit_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_fullscreen_white.png b/app/src/main/res/drawable-xxhdpi/ic_fullscreen_white.png deleted file mode 100644 index a0a1b4d4f..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_fullscreen_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_grid_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_grid_black_24dp.png deleted file mode 100644 index 31cd3dc83..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_grid_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_grid_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_grid_white_24dp.png deleted file mode 100644 index ebe059481..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_grid_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_headset_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_headset_black_24dp.png deleted file mode 100644 index baf3ee295..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_headset_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_headset_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_headset_white_24dp.png deleted file mode 100644 index 82db5427b..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_headset_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_history_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_history_black_24dp.png deleted file mode 100644 index f837fda0e..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_history_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_history_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_history_white_24dp.png deleted file mode 100644 index 1358a129c..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_history_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_import_export_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_import_export_black_24dp.png deleted file mode 100644 index 78e865dfa..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_import_export_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_import_export_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_import_export_white_24dp.png deleted file mode 100644 index 33c21c5c4..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_import_export_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_info_outline_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_info_outline_black_24dp.png deleted file mode 100644 index 3847a9fe7..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_info_outline_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_info_outline_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_info_outline_white_24dp.png deleted file mode 100644 index c41a5fcff..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_info_outline_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_kiosk_local_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_kiosk_local_black_24dp.png deleted file mode 100755 index bcbeb199c..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_kiosk_local_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_kiosk_local_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_kiosk_local_white_24dp.png deleted file mode 100755 index 6b27fb23c..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_kiosk_local_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_kiosk_recent_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_kiosk_recent_black_24dp.png deleted file mode 100755 index 92fc748ec..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_kiosk_recent_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_kiosk_recent_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_kiosk_recent_white_24dp.png deleted file mode 100755 index 5b0aa6ae2..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_kiosk_recent_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_language_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_language_black_24dp.png deleted file mode 100644 index 68608c70c..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_language_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_language_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_language_white_24dp.png deleted file mode 100644 index d4b55183c..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_language_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_list_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_list_black_24dp.png deleted file mode 100644 index 52d0c00a1..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_list_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_list_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_list_white_24dp.png deleted file mode 100644 index e5f698298..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_list_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_more_vert_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_more_vert_black_24dp.png deleted file mode 100644 index 94d5ab98c..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_more_vert_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_more_vert_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_more_vert_white_24dp.png deleted file mode 100644 index d32281307..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_more_vert_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_palette_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_palette_black_24dp.png deleted file mode 100644 index d93fe2e0c..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_palette_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_palette_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_palette_white_24dp.png deleted file mode 100644 index 119b1fd8c..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_palette_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_pause_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_pause_black_24dp.png deleted file mode 100644 index 2691adeb3..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_pause_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_pause_white.png b/app/src/main/res/drawable-xxhdpi/ic_pause_white.png deleted file mode 100644 index 3ea7e03e5..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_pause_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_pause_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_pause_white_24dp.png deleted file mode 100644 index 15cb0b51c..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_pause_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_picture_in_picture_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_picture_in_picture_black_24dp.png deleted file mode 100644 index c3892ff23..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_picture_in_picture_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_picture_in_picture_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_picture_in_picture_white_24dp.png deleted file mode 100644 index 719a3fef8..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_picture_in_picture_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_play_arrow_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_play_arrow_black_24dp.png deleted file mode 100644 index 5345ee3c4..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_play_arrow_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_play_arrow_white.png b/app/src/main/res/drawable-xxhdpi/ic_play_arrow_white.png deleted file mode 100644 index 2745c3ab9..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_play_arrow_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_play_arrow_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_play_arrow_white_24dp.png deleted file mode 100644 index 547ef30aa..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_play_arrow_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_play_circle_filled_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_play_circle_filled_white_24dp.png deleted file mode 100644 index 30330cfad..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_play_circle_filled_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_play_circle_transparent.png b/app/src/main/res/drawable-xxhdpi/ic_play_circle_transparent.png deleted file mode 100644 index 5d7afaef4..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_play_circle_transparent.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_playlist_add_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_playlist_add_black_24dp.png deleted file mode 100644 index af0bae3f0..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_playlist_add_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_playlist_add_check_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_playlist_add_check_black_24dp.png deleted file mode 100644 index ac03e19ab..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_playlist_add_check_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_playlist_add_check_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_playlist_add_check_white_24dp.png deleted file mode 100644 index 290088718..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_playlist_add_check_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_playlist_add_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_playlist_add_white_24dp.png deleted file mode 100644 index 3f652366d..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_playlist_add_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_playlist_play_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_playlist_play_white_24dp.png deleted file mode 100644 index 7f3b00168..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_playlist_play_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_remove.png b/app/src/main/res/drawable-xxhdpi/ic_remove.png deleted file mode 100644 index d35469d3c..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_remove.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_repeat_white.png b/app/src/main/res/drawable-xxhdpi/ic_repeat_white.png deleted file mode 100644 index bf7607966..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_repeat_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_replay_white.png b/app/src/main/res/drawable-xxhdpi/ic_replay_white_24dp.png similarity index 100% rename from app/src/main/res/drawable-xxhdpi/ic_replay_white.png rename to app/src/main/res/drawable-xxhdpi/ic_replay_white_24dp.png diff --git a/app/src/main/res/drawable-xxhdpi/ic_rss_feed_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_rss_feed_black_24dp.png deleted file mode 100644 index 3e3d4d97a..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_rss_feed_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_rss_feed_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_rss_feed_white_24dp.png deleted file mode 100644 index 2267aad8f..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_rss_feed_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_save_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_save_black_24dp.png deleted file mode 100644 index 871291b4e..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_save_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_save_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_save_white_24dp.png deleted file mode 100644 index 3e0ce1a5f..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_save_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_screen_rotation_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_screen_rotation_black_24dp.png deleted file mode 100644 index bfd31c55a..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_screen_rotation_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_screen_rotation_white.png b/app/src/main/res/drawable-xxhdpi/ic_screen_rotation_white.png deleted file mode 100644 index a160572a4..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_screen_rotation_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_search_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_search_black_24dp.png deleted file mode 100644 index 3ae490ef9..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_search_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_search_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_search_white_24dp.png deleted file mode 100644 index abbb98951..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_search_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_settings_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_settings_black_24dp.png deleted file mode 100644 index 3023ff8da..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_settings_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_settings_update_black.png b/app/src/main/res/drawable-xxhdpi/ic_settings_update_black.png deleted file mode 100755 index 7316dbc88..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_settings_update_black.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_settings_update_white.png b/app/src/main/res/drawable-xxhdpi/ic_settings_update_white.png deleted file mode 100755 index 07b1f712d..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_settings_update_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_settings_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_settings_white_24dp.png deleted file mode 100644 index eabb0a2ba..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_settings_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_share_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_share_black_24dp.png deleted file mode 100644 index 784933ad5..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_share_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_share_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_share_white_24dp.png deleted file mode 100644 index a35b3cd14..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_share_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_shuffle_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_shuffle_white_24dp.png deleted file mode 100644 index dc8e5341b..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_shuffle_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_thumb_down_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_thumb_down_black_24dp.png deleted file mode 100644 index 37ce6c909..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_thumb_down_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_thumb_down_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_thumb_down_white_24dp.png deleted file mode 100644 index a6e0b10c6..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_thumb_down_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_thumb_up_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_thumb_up_black_24dp.png deleted file mode 100644 index d7004fe5b..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_thumb_up_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_thumb_up_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_thumb_up_white_24dp.png deleted file mode 100644 index cedf001de..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_thumb_up_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_volume_off_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_volume_off_black_24dp.png deleted file mode 100644 index 2d57c8674..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_volume_off_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_volume_off_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_volume_off_white_24dp.png deleted file mode 100644 index 474aae51e..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_volume_off_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_whatshot_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_whatshot_black_24dp.png deleted file mode 100644 index a14dcd695..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_whatshot_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_whatshot_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_whatshot_white_24dp.png deleted file mode 100644 index 8eaf3755d..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_whatshot_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/new_play_arrow.png b/app/src/main/res/drawable-xxhdpi/new_play_arrow.png deleted file mode 100644 index c32a818c5..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/new_play_arrow.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_av_fast_forward.png b/app/src/main/res/drawable-xxxhdpi/ic_action_av_fast_forward.png deleted file mode 100644 index 599c66f70..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_action_av_fast_forward.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_av_fast_rewind.png b/app/src/main/res/drawable-xxxhdpi/ic_action_av_fast_rewind.png deleted file mode 100644 index 5a9fa3de0..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_action_av_fast_rewind.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_arrow_back_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_arrow_back_white_24dp.png deleted file mode 100644 index e27034d67..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_arrow_back_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_arrow_top_left_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_arrow_top_left_black_24dp.png deleted file mode 100644 index d536127b5..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_arrow_top_left_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_arrow_top_left_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_arrow_top_left_white_24dp.png deleted file mode 100644 index 0ddd5a8fa..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_arrow_top_left_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_backup_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_backup_black_24dp.png deleted file mode 100644 index 248289e97..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_backup_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_backup_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_backup_white_24dp.png deleted file mode 100644 index 2180f73e8..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_backup_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_bookmark_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_bookmark_black_24dp.png deleted file mode 100644 index 2b90acd74..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_bookmark_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_bookmark_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_bookmark_white_24dp.png deleted file mode 100644 index 370cf8af5..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_bookmark_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_bug_report_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_bug_report_black_24dp.png deleted file mode 100644 index 6eb1474e3..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_bug_report_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_bug_report_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_bug_report_white_24dp.png deleted file mode 100644 index e0b5b1964..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_bug_report_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_cast_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_cast_black_24dp.png deleted file mode 100644 index 9dbfcd941..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_cast_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_cast_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_cast_white_24dp.png deleted file mode 100644 index bb3539c64..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_cast_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_channel_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_channel_black_24dp.png deleted file mode 100644 index 2ff64b449..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_channel_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_channel_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_channel_white_24dp.png deleted file mode 100644 index 9384592d6..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_channel_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_close_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_close_black_24dp.png deleted file mode 100644 index df42feecb..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_close_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_delete_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_delete_black_24dp.png deleted file mode 100644 index ab07ea2ae..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_delete_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_delete_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_delete_white_24dp.png deleted file mode 100644 index a8358eb71..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_delete_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_drag_handle_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_drag_handle_black_24dp.png deleted file mode 100644 index d102adeb2..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_drag_handle_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_drag_handle_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_drag_handle_white_24dp.png deleted file mode 100644 index 122690738..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_drag_handle_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_expand_less_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_expand_less_black_24dp.png deleted file mode 100644 index 99c6e3e1c..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_expand_less_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_expand_less_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_expand_less_white_24dp.png deleted file mode 100644 index 42615516b..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_expand_less_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_expand_more_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_expand_more_black_24dp.png deleted file mode 100644 index ad852e3e6..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_expand_more_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_expand_more_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_expand_more_white_24dp.png deleted file mode 100644 index 2859a6fec..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_expand_more_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_fiber_manual_record_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_fiber_manual_record_black_24dp.png deleted file mode 100644 index eff1e3594..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_fiber_manual_record_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_fiber_manual_record_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_fiber_manual_record_white_24dp.png deleted file mode 100644 index 591b54a57..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_fiber_manual_record_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_file_download_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_file_download_black_24dp.png deleted file mode 100644 index 8c83bffa7..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_file_download_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_file_download_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_file_download_white_24dp.png deleted file mode 100644 index ded5652e4..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_file_download_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_filter_list_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_filter_list_black_24dp.png deleted file mode 100644 index 9319c4bb4..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_filter_list_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_filter_list_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_filter_list_white_24dp.png deleted file mode 100644 index cb2207f11..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_filter_list_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_fullscreen_exit_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_fullscreen_exit_black_24dp.png deleted file mode 100644 index 2221235df..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_fullscreen_exit_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_fullscreen_exit_white.png b/app/src/main/res/drawable-xxxhdpi/ic_fullscreen_exit_white.png deleted file mode 100644 index b47b3f8bd..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_fullscreen_exit_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_fullscreen_white.png b/app/src/main/res/drawable-xxxhdpi/ic_fullscreen_white.png deleted file mode 100644 index ea9f18ae6..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_fullscreen_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_grid_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_grid_black_24dp.png deleted file mode 100644 index fe78d853e..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_grid_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_grid_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_grid_white_24dp.png deleted file mode 100644 index d52610ec8..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_grid_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_headset_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_headset_black_24dp.png deleted file mode 100644 index 974457ee1..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_headset_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_headset_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_headset_white_24dp.png deleted file mode 100644 index 0f0b2e154..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_headset_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_history_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_history_black_24dp.png deleted file mode 100644 index c7153092e..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_history_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_history_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_history_white_24dp.png deleted file mode 100644 index 5b99ef655..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_history_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_import_export_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_import_export_black_24dp.png deleted file mode 100644 index 36aa872e5..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_import_export_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_import_export_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_import_export_white_24dp.png deleted file mode 100644 index a5e55a470..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_import_export_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_info_outline_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_info_outline_black_24dp.png deleted file mode 100644 index c1e2a03a4..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_info_outline_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_info_outline_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_info_outline_white_24dp.png deleted file mode 100644 index 3a82cab3b..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_info_outline_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_kiosk_local_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_kiosk_local_black_24dp.png deleted file mode 100755 index e208010a2..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_kiosk_local_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_kiosk_local_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_kiosk_local_white_24dp.png deleted file mode 100755 index b04fd7a88..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_kiosk_local_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_kiosk_recent_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_kiosk_recent_black_24dp.png deleted file mode 100755 index 152259fab..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_kiosk_recent_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_kiosk_recent_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_kiosk_recent_white_24dp.png deleted file mode 100755 index 1aac3b986..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_kiosk_recent_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_language_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_language_black_24dp.png deleted file mode 100644 index 48997bab8..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_language_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_language_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_language_white_24dp.png deleted file mode 100644 index a576ac7a5..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_language_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_list_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_list_black_24dp.png deleted file mode 100644 index b165df44d..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_list_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_list_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_list_white_24dp.png deleted file mode 100644 index 053a1a0da..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_list_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_more_vert_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_more_vert_black_24dp.png deleted file mode 100644 index 4642a3b66..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_more_vert_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_more_vert_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_more_vert_white_24dp.png deleted file mode 100644 index 2f2cb3d00..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_more_vert_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_palette_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_palette_black_24dp.png deleted file mode 100644 index 79360b16d..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_palette_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_palette_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_palette_white_24dp.png deleted file mode 100644 index dba40d4eb..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_palette_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_pause_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_pause_black_24dp.png deleted file mode 100644 index 3fbcd0326..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_pause_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_pause_white.png b/app/src/main/res/drawable-xxxhdpi/ic_pause_white.png deleted file mode 100644 index 76482b1fd..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_pause_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_pause_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_pause_white_24dp.png deleted file mode 100644 index 12a49bc12..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_pause_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_picture_in_picture_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_picture_in_picture_black_24dp.png deleted file mode 100644 index bb43aa64a..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_picture_in_picture_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_picture_in_picture_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_picture_in_picture_white_24dp.png deleted file mode 100644 index b9d101119..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_picture_in_picture_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_play_arrow_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_play_arrow_black_24dp.png deleted file mode 100644 index d12d49562..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_play_arrow_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_play_arrow_white.png b/app/src/main/res/drawable-xxxhdpi/ic_play_arrow_white.png deleted file mode 100644 index 8dbc4ea7c..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_play_arrow_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_play_arrow_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_play_arrow_white_24dp.png deleted file mode 100644 index be5c062b5..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_play_arrow_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_play_circle_filled_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_play_circle_filled_white_24dp.png deleted file mode 100644 index 9dc082586..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_play_circle_filled_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_play_circle_transparent.png b/app/src/main/res/drawable-xxxhdpi/ic_play_circle_transparent.png deleted file mode 100644 index 5bc515bf2..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_play_circle_transparent.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_playlist_add_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_playlist_add_black_24dp.png deleted file mode 100644 index 46020a7e0..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_playlist_add_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_playlist_add_check_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_playlist_add_check_black_24dp.png deleted file mode 100644 index 068c596a3..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_playlist_add_check_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_playlist_add_check_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_playlist_add_check_white_24dp.png deleted file mode 100644 index 767d066de..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_playlist_add_check_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_playlist_add_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_playlist_add_white_24dp.png deleted file mode 100644 index 70e74e4a2..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_playlist_add_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_playlist_play_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_playlist_play_white_24dp.png deleted file mode 100644 index c88d9c8e3..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_playlist_play_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_repeat_white.png b/app/src/main/res/drawable-xxxhdpi/ic_repeat_white.png deleted file mode 100644 index a59db47ee..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_repeat_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_replay_white.png b/app/src/main/res/drawable-xxxhdpi/ic_replay_white_24dp.png similarity index 100% rename from app/src/main/res/drawable-xxxhdpi/ic_replay_white.png rename to app/src/main/res/drawable-xxxhdpi/ic_replay_white_24dp.png diff --git a/app/src/main/res/drawable-xxxhdpi/ic_rss_feed_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_rss_feed_black_24dp.png deleted file mode 100644 index d2cc96ac7..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_rss_feed_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_rss_feed_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_rss_feed_white_24dp.png deleted file mode 100644 index 209688dc5..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_rss_feed_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_save_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_save_black_24dp.png deleted file mode 100644 index ba001835a..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_save_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_save_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_save_white_24dp.png deleted file mode 100644 index bd80bf1f7..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_save_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_screen_rotation_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_screen_rotation_black_24dp.png deleted file mode 100644 index 29fef9a47..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_screen_rotation_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_screen_rotation_white.png b/app/src/main/res/drawable-xxxhdpi/ic_screen_rotation_white.png deleted file mode 100644 index 3cde2bfef..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_screen_rotation_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_search_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_search_black_24dp.png deleted file mode 100644 index 21be57299..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_search_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_search_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_search_white_24dp.png deleted file mode 100644 index dd5adfc7f..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_search_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_settings_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_settings_black_24dp.png deleted file mode 100644 index 476d5c978..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_settings_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_settings_update_black.png b/app/src/main/res/drawable-xxxhdpi/ic_settings_update_black.png deleted file mode 100755 index 8186c6f5f..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_settings_update_black.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_settings_update_white.png b/app/src/main/res/drawable-xxxhdpi/ic_settings_update_white.png deleted file mode 100755 index 8b5e6fa38..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_settings_update_white.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_settings_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_settings_white_24dp.png deleted file mode 100644 index 507c5edd4..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_settings_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_share_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_share_black_24dp.png deleted file mode 100644 index 5a8544ce5..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_share_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_share_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_share_white_24dp.png deleted file mode 100644 index e351c7beb..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_share_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_shuffle_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_shuffle_white_24dp.png deleted file mode 100644 index e24dfa3b0..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_shuffle_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_thumb_down_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_thumb_down_black_24dp.png deleted file mode 100644 index cea7381b5..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_thumb_down_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_thumb_down_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_thumb_down_white_24dp.png deleted file mode 100644 index afecc66b7..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_thumb_down_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_thumb_up_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_thumb_up_black_24dp.png deleted file mode 100644 index 8d9682036..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_thumb_up_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_thumb_up_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_thumb_up_white_24dp.png deleted file mode 100644 index f43cef8ec..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_thumb_up_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_volume_off_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_volume_off_black_24dp.png deleted file mode 100644 index 6d9b35584..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_volume_off_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_volume_off_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_volume_off_white_24dp.png deleted file mode 100644 index df06b06b5..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_volume_off_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_whatshot_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_whatshot_black_24dp.png deleted file mode 100644 index 8f03a95c7..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_whatshot_black_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_whatshot_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_whatshot_white_24dp.png deleted file mode 100644 index 5c5d86873..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_whatshot_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/new_play_arrow.png b/app/src/main/res/drawable-xxxhdpi/new_play_arrow.png deleted file mode 100644 index 39559b221..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/new_play_arrow.png and /dev/null differ diff --git a/app/src/main/res/drawable/dashed_border_black.xml b/app/src/main/res/drawable/dashed_border_black.xml new file mode 100644 index 000000000..b6bac6252 --- /dev/null +++ b/app/src/main/res/drawable/dashed_border_black.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/dashed_border_dark.xml b/app/src/main/res/drawable/dashed_border_dark.xml new file mode 100644 index 000000000..5af152ecc --- /dev/null +++ b/app/src/main/res/drawable/dashed_border_dark.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/dashed_border_light.xml b/app/src/main/res/drawable/dashed_border_light.xml new file mode 100644 index 000000000..5d29112bd --- /dev/null +++ b/app/src/main/res/drawable/dashed_border_light.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/default_dot.xml b/app/src/main/res/drawable/dot_default.xml similarity index 100% rename from app/src/main/res/drawable/default_dot.xml rename to app/src/main/res/drawable/dot_default.xml diff --git a/app/src/main/res/drawable/selected_dot.xml b/app/src/main/res/drawable/dot_selected.xml similarity index 100% rename from app/src/main/res/drawable/selected_dot.xml rename to app/src/main/res/drawable/dot_selected.xml diff --git a/app/src/main/res/drawable/drawer_header_bottom_background.xml b/app/src/main/res/drawable/drawer_header_bottom_background.xml new file mode 100644 index 000000000..913522274 --- /dev/null +++ b/app/src/main/res/drawable/drawer_header_bottom_background.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_add_circle_outline_black_24dp.xml b/app/src/main/res/drawable/ic_add_circle_outline_black_24dp.xml new file mode 100644 index 000000000..900f2275e --- /dev/null +++ b/app/src/main/res/drawable/ic_add_circle_outline_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_add_circle_outline_white_24dp.xml b/app/src/main/res/drawable/ic_add_circle_outline_white_24dp.xml new file mode 100644 index 000000000..66d3247ae --- /dev/null +++ b/app/src/main/res/drawable/ic_add_circle_outline_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_apps_black_24dp.xml b/app/src/main/res/drawable/ic_apps_black_24dp.xml new file mode 100644 index 000000000..ff485cf1a --- /dev/null +++ b/app/src/main/res/drawable/ic_apps_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_apps_white_24dp.xml b/app/src/main/res/drawable/ic_apps_white_24dp.xml new file mode 100644 index 000000000..373f7752b --- /dev/null +++ b/app/src/main/res/drawable/ic_apps_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_back_black_24dp.xml b/app/src/main/res/drawable/ic_arrow_back_black_24dp.xml index 8d8acb883..beafea395 100644 --- a/app/src/main/res/drawable/ic_arrow_back_black_24dp.xml +++ b/app/src/main/res/drawable/ic_arrow_back_black_24dp.xml @@ -1,10 +1,9 @@ - + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> - \ No newline at end of file + diff --git a/app/src/main/res/drawable/ic_arrow_back_white_24dp.xml b/app/src/main/res/drawable/ic_arrow_back_white_24dp.xml new file mode 100644 index 000000000..71d5bbd29 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_back_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_drop_down_white_24dp.xml b/app/src/main/res/drawable/ic_arrow_drop_down_white_24dp.xml new file mode 100644 index 000000000..65e1e4228 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_drop_down_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_drop_up_white_24dp.xml b/app/src/main/res/drawable/ic_arrow_drop_up_white_24dp.xml new file mode 100644 index 000000000..1d266cecc --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_drop_up_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_asterisk_black_24dp.xml b/app/src/main/res/drawable/ic_asterisk_black_24dp.xml new file mode 100644 index 000000000..fa16cd5e8 --- /dev/null +++ b/app/src/main/res/drawable/ic_asterisk_black_24dp.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_asterisk_white_24dp.xml b/app/src/main/res/drawable/ic_asterisk_white_24dp.xml new file mode 100644 index 000000000..bd487cb55 --- /dev/null +++ b/app/src/main/res/drawable/ic_asterisk_white_24dp.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_attach_money_black_24dp.xml b/app/src/main/res/drawable/ic_attach_money_black_24dp.xml new file mode 100644 index 000000000..b520fc98d --- /dev/null +++ b/app/src/main/res/drawable/ic_attach_money_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_attach_money_white_24dp.xml b/app/src/main/res/drawable/ic_attach_money_white_24dp.xml new file mode 100644 index 000000000..d198dd14d --- /dev/null +++ b/app/src/main/res/drawable/ic_attach_money_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_backup_black_24dp.xml b/app/src/main/res/drawable/ic_backup_black_24dp.xml new file mode 100644 index 000000000..086281669 --- /dev/null +++ b/app/src/main/res/drawable/ic_backup_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_backup_white_24dp.xml b/app/src/main/res/drawable/ic_backup_white_24dp.xml new file mode 100644 index 000000000..55dbbae85 --- /dev/null +++ b/app/src/main/res/drawable/ic_backup_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_delete_sweep_black_24dp.xml b/app/src/main/res/drawable/ic_bookmark_black_24dp.xml similarity index 62% rename from app/src/main/res/drawable/ic_delete_sweep_black_24dp.xml rename to app/src/main/res/drawable/ic_bookmark_black_24dp.xml index bfa31fc9d..6a6a1b39d 100644 --- a/app/src/main/res/drawable/ic_delete_sweep_black_24dp.xml +++ b/app/src/main/res/drawable/ic_bookmark_black_24dp.xml @@ -5,5 +5,5 @@ android:viewportHeight="24.0"> + android:pathData="M17,3H7c-1.1,0 -1.99,0.9 -1.99,2L5,21l7,-3 7,3V5c0,-1.1 -0.9,-2 -2,-2z"/> diff --git a/app/src/main/res/drawable/ic_bookmark_white_24dp.xml b/app/src/main/res/drawable/ic_bookmark_white_24dp.xml new file mode 100644 index 000000000..feb16ed63 --- /dev/null +++ b/app/src/main/res/drawable/ic_bookmark_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_brightness_high_white_24dp.xml b/app/src/main/res/drawable/ic_brightness_high_white_24dp.xml new file mode 100644 index 000000000..9ed0b086c --- /dev/null +++ b/app/src/main/res/drawable/ic_brightness_high_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_brightness_high_white_72dp.xml b/app/src/main/res/drawable/ic_brightness_high_white_72dp.xml deleted file mode 100644 index 12d0084a8..000000000 --- a/app/src/main/res/drawable/ic_brightness_high_white_72dp.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_brightness_low_white_24dp.xml b/app/src/main/res/drawable/ic_brightness_low_white_24dp.xml new file mode 100644 index 000000000..da4e0ca30 --- /dev/null +++ b/app/src/main/res/drawable/ic_brightness_low_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_brightness_low_white_72dp.xml b/app/src/main/res/drawable/ic_brightness_low_white_72dp.xml deleted file mode 100644 index 9c4f2f71e..000000000 --- a/app/src/main/res/drawable/ic_brightness_low_white_72dp.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_brightness_medium_white_24dp.xml b/app/src/main/res/drawable/ic_brightness_medium_white_24dp.xml new file mode 100644 index 000000000..c522453f1 --- /dev/null +++ b/app/src/main/res/drawable/ic_brightness_medium_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_brightness_medium_white_72dp.xml b/app/src/main/res/drawable/ic_brightness_medium_white_72dp.xml deleted file mode 100644 index fc100086f..000000000 --- a/app/src/main/res/drawable/ic_brightness_medium_white_72dp.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_bug_report_black_24dp.xml b/app/src/main/res/drawable/ic_bug_report_black_24dp.xml new file mode 100644 index 000000000..4d83902b8 --- /dev/null +++ b/app/src/main/res/drawable/ic_bug_report_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_bug_report_white_24dp.xml b/app/src/main/res/drawable/ic_bug_report_white_24dp.xml new file mode 100644 index 000000000..5c8f5bc16 --- /dev/null +++ b/app/src/main/res/drawable/ic_bug_report_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_cast_black_24dp.xml b/app/src/main/res/drawable/ic_cast_black_24dp.xml new file mode 100644 index 000000000..7b143de9f --- /dev/null +++ b/app/src/main/res/drawable/ic_cast_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_cast_white_24dp.xml b/app/src/main/res/drawable/ic_cast_white_24dp.xml new file mode 100644 index 000000000..434c64416 --- /dev/null +++ b/app/src/main/res/drawable/ic_cast_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_child_care_black_24dp.xml b/app/src/main/res/drawable/ic_child_care_black_24dp.xml new file mode 100644 index 000000000..5af39255e --- /dev/null +++ b/app/src/main/res/drawable/ic_child_care_black_24dp.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_child_care_white_24dp.xml b/app/src/main/res/drawable/ic_child_care_white_24dp.xml new file mode 100644 index 000000000..81fa2ddc1 --- /dev/null +++ b/app/src/main/res/drawable/ic_child_care_white_24dp.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_delete_sweep_white_24dp.xml b/app/src/main/res/drawable/ic_close_black_24dp.xml similarity index 53% rename from app/src/main/res/drawable/ic_delete_sweep_white_24dp.xml rename to app/src/main/res/drawable/ic_close_black_24dp.xml index 121b7ed8d..ede4b7108 100644 --- a/app/src/main/res/drawable/ic_delete_sweep_white_24dp.xml +++ b/app/src/main/res/drawable/ic_close_black_24dp.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> + android:fillColor="#FF000000" + android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/> diff --git a/app/src/main/res/drawable/ic_cloud_download_black_24dp.xml b/app/src/main/res/drawable/ic_cloud_download_black_24dp.xml new file mode 100644 index 000000000..261c31217 --- /dev/null +++ b/app/src/main/res/drawable/ic_cloud_download_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_cloud_download_white_24dp.xml b/app/src/main/res/drawable/ic_cloud_download_white_24dp.xml new file mode 100644 index 000000000..0feb270af --- /dev/null +++ b/app/src/main/res/drawable/ic_cloud_download_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_computer_black_24dp.xml b/app/src/main/res/drawable/ic_computer_black_24dp.xml new file mode 100644 index 000000000..4599f98cd --- /dev/null +++ b/app/src/main/res/drawable/ic_computer_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_computer_white_24dp.xml b/app/src/main/res/drawable/ic_computer_white_24dp.xml new file mode 100644 index 000000000..9569b7747 --- /dev/null +++ b/app/src/main/res/drawable/ic_computer_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_blank_page_black_24dp.xml b/app/src/main/res/drawable/ic_crop_portrait_black_24dp.xml similarity index 100% rename from app/src/main/res/drawable/ic_blank_page_black_24dp.xml rename to app/src/main/res/drawable/ic_crop_portrait_black_24dp.xml diff --git a/app/src/main/res/drawable/ic_crop_portrait_white_24dp.xml b/app/src/main/res/drawable/ic_crop_portrait_white_24dp.xml new file mode 100644 index 000000000..caba925a1 --- /dev/null +++ b/app/src/main/res/drawable/ic_crop_portrait_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_delete_black_24dp.xml b/app/src/main/res/drawable/ic_delete_black_24dp.xml new file mode 100644 index 000000000..39e64d698 --- /dev/null +++ b/app/src/main/res/drawable/ic_delete_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_delete_white_24dp.xml b/app/src/main/res/drawable/ic_delete_white_24dp.xml new file mode 100644 index 000000000..8bed121aa --- /dev/null +++ b/app/src/main/res/drawable/ic_delete_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_directions_bike_black_24dp.xml b/app/src/main/res/drawable/ic_directions_bike_black_24dp.xml new file mode 100644 index 000000000..ded5e3359 --- /dev/null +++ b/app/src/main/res/drawable/ic_directions_bike_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_directions_bike_white_24dp.xml b/app/src/main/res/drawable/ic_directions_bike_white_24dp.xml new file mode 100644 index 000000000..f165cea9c --- /dev/null +++ b/app/src/main/res/drawable/ic_directions_bike_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_directions_car_black_24dp.xml b/app/src/main/res/drawable/ic_directions_car_black_24dp.xml new file mode 100644 index 000000000..6d6337c3a --- /dev/null +++ b/app/src/main/res/drawable/ic_directions_car_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_directions_car_white_24dp.xml b/app/src/main/res/drawable/ic_directions_car_white_24dp.xml new file mode 100644 index 000000000..981334c17 --- /dev/null +++ b/app/src/main/res/drawable/ic_directions_car_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_done_black_24dp.xml b/app/src/main/res/drawable/ic_done_black_24dp.xml new file mode 100644 index 000000000..7affe9ba9 --- /dev/null +++ b/app/src/main/res/drawable/ic_done_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_done_white_24dp.xml b/app/src/main/res/drawable/ic_done_white_24dp.xml new file mode 100644 index 000000000..cab2aed1a --- /dev/null +++ b/app/src/main/res/drawable/ic_done_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_drag_handle_black_24dp.xml b/app/src/main/res/drawable/ic_drag_handle_black_24dp.xml new file mode 100644 index 000000000..68a719052 --- /dev/null +++ b/app/src/main/res/drawable/ic_drag_handle_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_drag_handle_white_24dp.xml b/app/src/main/res/drawable/ic_drag_handle_white_24dp.xml new file mode 100644 index 000000000..50f9e6c29 --- /dev/null +++ b/app/src/main/res/drawable/ic_drag_handle_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_edit_black_24dp.xml b/app/src/main/res/drawable/ic_edit_black_24dp.xml new file mode 100644 index 000000000..43489826e --- /dev/null +++ b/app/src/main/res/drawable/ic_edit_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_edit_white_24dp.xml b/app/src/main/res/drawable/ic_edit_white_24dp.xml new file mode 100644 index 000000000..88f94780f --- /dev/null +++ b/app/src/main/res/drawable/ic_edit_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_expand_less_black_24dp.xml b/app/src/main/res/drawable/ic_expand_less_black_24dp.xml new file mode 100644 index 000000000..3afdf9682 --- /dev/null +++ b/app/src/main/res/drawable/ic_expand_less_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_expand_less_white_24dp.xml b/app/src/main/res/drawable/ic_expand_less_white_24dp.xml new file mode 100644 index 000000000..5042d801a --- /dev/null +++ b/app/src/main/res/drawable/ic_expand_less_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_expand_more_black_24dp.xml b/app/src/main/res/drawable/ic_expand_more_black_24dp.xml new file mode 100644 index 000000000..8d57dbc10 --- /dev/null +++ b/app/src/main/res/drawable/ic_expand_more_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_expand_more_white_24dp.xml b/app/src/main/res/drawable/ic_expand_more_white_24dp.xml new file mode 100644 index 000000000..bc72bdce0 --- /dev/null +++ b/app/src/main/res/drawable/ic_expand_more_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_explore_black_24dp.xml b/app/src/main/res/drawable/ic_explore_black_24dp.xml new file mode 100644 index 000000000..c898ed9a5 --- /dev/null +++ b/app/src/main/res/drawable/ic_explore_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_explore_white_24dp.xml b/app/src/main/res/drawable/ic_explore_white_24dp.xml new file mode 100644 index 000000000..65f2818a6 --- /dev/null +++ b/app/src/main/res/drawable/ic_explore_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_fast_forward_white_24dp.xml b/app/src/main/res/drawable/ic_fast_forward_white_24dp.xml new file mode 100644 index 000000000..da7c3fb1e --- /dev/null +++ b/app/src/main/res/drawable/ic_fast_forward_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_fast_rewind_white_24dp.xml b/app/src/main/res/drawable/ic_fast_rewind_white_24dp.xml new file mode 100644 index 000000000..4bab93ecb --- /dev/null +++ b/app/src/main/res/drawable/ic_fast_rewind_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_fastfood_black_24dp.xml b/app/src/main/res/drawable/ic_fastfood_black_24dp.xml new file mode 100644 index 000000000..4de2eb9af --- /dev/null +++ b/app/src/main/res/drawable/ic_fastfood_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_fastfood_white_24dp.xml b/app/src/main/res/drawable/ic_fastfood_white_24dp.xml new file mode 100644 index 000000000..517b92573 --- /dev/null +++ b/app/src/main/res/drawable/ic_fastfood_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_favorite_black_24dp.xml b/app/src/main/res/drawable/ic_favorite_black_24dp.xml new file mode 100644 index 000000000..cfba5d846 --- /dev/null +++ b/app/src/main/res/drawable/ic_favorite_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_favorite_white_24dp.xml b/app/src/main/res/drawable/ic_favorite_white_24dp.xml new file mode 100644 index 000000000..67a25e713 --- /dev/null +++ b/app/src/main/res/drawable/ic_favorite_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_file_download_black_24dp.xml b/app/src/main/res/drawable/ic_file_download_black_24dp.xml new file mode 100644 index 000000000..492b41d34 --- /dev/null +++ b/app/src/main/res/drawable/ic_file_download_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_file_download_white_24dp.xml b/app/src/main/res/drawable/ic_file_download_white_24dp.xml new file mode 100644 index 000000000..b8e836142 --- /dev/null +++ b/app/src/main/res/drawable/ic_file_download_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_filter_list_black_24dp.xml b/app/src/main/res/drawable/ic_filter_list_black_24dp.xml new file mode 100644 index 000000000..b99b672f4 --- /dev/null +++ b/app/src/main/res/drawable/ic_filter_list_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_filter_list_white_24dp.xml b/app/src/main/res/drawable/ic_filter_list_white_24dp.xml new file mode 100644 index 000000000..5d4ec18ee --- /dev/null +++ b/app/src/main/res/drawable/ic_filter_list_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_fitness_center_black_24dp.xml b/app/src/main/res/drawable/ic_fitness_center_black_24dp.xml new file mode 100644 index 000000000..846deb431 --- /dev/null +++ b/app/src/main/res/drawable/ic_fitness_center_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_fitness_center_white_24dp.xml b/app/src/main/res/drawable/ic_fitness_center_white_24dp.xml new file mode 100644 index 000000000..fec3c955c --- /dev/null +++ b/app/src/main/res/drawable/ic_fitness_center_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_fullscreen_exit_white_24dp.xml b/app/src/main/res/drawable/ic_fullscreen_exit_white_24dp.xml new file mode 100644 index 000000000..bb7140f29 --- /dev/null +++ b/app/src/main/res/drawable/ic_fullscreen_exit_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_fullscreen_white_24dp.xml b/app/src/main/res/drawable/ic_fullscreen_white_24dp.xml new file mode 100644 index 000000000..86b7649b6 --- /dev/null +++ b/app/src/main/res/drawable/ic_fullscreen_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_headset_black_24dp.xml b/app/src/main/res/drawable/ic_headset_black_24dp.xml new file mode 100644 index 000000000..d4503ce60 --- /dev/null +++ b/app/src/main/res/drawable/ic_headset_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_headset_shadow.xml b/app/src/main/res/drawable/ic_headset_shadow.xml new file mode 100644 index 000000000..53a3ec31a --- /dev/null +++ b/app/src/main/res/drawable/ic_headset_shadow.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/ic_headset_white_24dp.xml b/app/src/main/res/drawable/ic_headset_white_24dp.xml new file mode 100644 index 000000000..2027245b3 --- /dev/null +++ b/app/src/main/res/drawable/ic_headset_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_help_black_24dp.xml b/app/src/main/res/drawable/ic_help_black_24dp.xml new file mode 100644 index 000000000..1517747d0 --- /dev/null +++ b/app/src/main/res/drawable/ic_help_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_help_white_24dp.xml b/app/src/main/res/drawable/ic_help_white_24dp.xml new file mode 100644 index 000000000..d813b72b8 --- /dev/null +++ b/app/src/main/res/drawable/ic_help_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_history_black_24dp.xml b/app/src/main/res/drawable/ic_history_black_24dp.xml new file mode 100644 index 000000000..a61de1bc9 --- /dev/null +++ b/app/src/main/res/drawable/ic_history_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_history_white_24dp.xml b/app/src/main/res/drawable/ic_history_white_24dp.xml new file mode 100644 index 000000000..de25eb445 --- /dev/null +++ b/app/src/main/res/drawable/ic_history_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_home_black_24dp.xml b/app/src/main/res/drawable/ic_home_black_24dp.xml new file mode 100644 index 000000000..70fb2910c --- /dev/null +++ b/app/src/main/res/drawable/ic_home_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_home_white_24dp.xml b/app/src/main/res/drawable/ic_home_white_24dp.xml new file mode 100644 index 000000000..30296ba99 --- /dev/null +++ b/app/src/main/res/drawable/ic_home_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_import_export_black_24dp.xml b/app/src/main/res/drawable/ic_import_export_black_24dp.xml new file mode 100644 index 000000000..a2d1fa99f --- /dev/null +++ b/app/src/main/res/drawable/ic_import_export_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_import_export_white_24dp.xml b/app/src/main/res/drawable/ic_import_export_white_24dp.xml new file mode 100644 index 000000000..4c6fc6ef6 --- /dev/null +++ b/app/src/main/res/drawable/ic_import_export_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_info_outline_black_24dp.xml b/app/src/main/res/drawable/ic_info_outline_black_24dp.xml new file mode 100644 index 000000000..cf53e145c --- /dev/null +++ b/app/src/main/res/drawable/ic_info_outline_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_info_outline_white_24dp.xml b/app/src/main/res/drawable/ic_info_outline_white_24dp.xml new file mode 100644 index 000000000..af0d4d067 --- /dev/null +++ b/app/src/main/res/drawable/ic_info_outline_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_insert_emoticon_black_24dp.xml b/app/src/main/res/drawable/ic_insert_emoticon_black_24dp.xml new file mode 100644 index 000000000..43d5552cd --- /dev/null +++ b/app/src/main/res/drawable/ic_insert_emoticon_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_insert_emoticon_white_24dp.xml b/app/src/main/res/drawable/ic_insert_emoticon_white_24dp.xml new file mode 100644 index 000000000..a438c34ef --- /dev/null +++ b/app/src/main/res/drawable/ic_insert_emoticon_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_language_black_24dp.xml b/app/src/main/res/drawable/ic_language_black_24dp.xml new file mode 100644 index 000000000..d07324c87 --- /dev/null +++ b/app/src/main/res/drawable/ic_language_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_language_white_24dp.xml b/app/src/main/res/drawable/ic_language_white_24dp.xml new file mode 100644 index 000000000..74bc27903 --- /dev/null +++ b/app/src/main/res/drawable/ic_language_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_list_black_24dp.xml b/app/src/main/res/drawable/ic_list_black_24dp.xml new file mode 100644 index 000000000..4c2fb8834 --- /dev/null +++ b/app/src/main/res/drawable/ic_list_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_list_white_24dp.xml b/app/src/main/res/drawable/ic_list_white_24dp.xml new file mode 100644 index 000000000..f47037629 --- /dev/null +++ b/app/src/main/res/drawable/ic_list_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_megaphone_black_24dp.xml b/app/src/main/res/drawable/ic_megaphone_black_24dp.xml new file mode 100644 index 000000000..21622c162 --- /dev/null +++ b/app/src/main/res/drawable/ic_megaphone_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_megaphone_white_24dp.xml b/app/src/main/res/drawable/ic_megaphone_white_24dp.xml new file mode 100644 index 000000000..90e6ff215 --- /dev/null +++ b/app/src/main/res/drawable/ic_megaphone_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_mic_black_24dp.xml b/app/src/main/res/drawable/ic_mic_black_24dp.xml new file mode 100644 index 000000000..25d8951a7 --- /dev/null +++ b/app/src/main/res/drawable/ic_mic_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_mic_white_24dp.xml b/app/src/main/res/drawable/ic_mic_white_24dp.xml new file mode 100644 index 000000000..36ee9ff81 --- /dev/null +++ b/app/src/main/res/drawable/ic_mic_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_more_vert_black_24dp.xml b/app/src/main/res/drawable/ic_more_vert_black_24dp.xml new file mode 100644 index 000000000..5176d8a4b --- /dev/null +++ b/app/src/main/res/drawable/ic_more_vert_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_more_vert_white_24dp.xml b/app/src/main/res/drawable/ic_more_vert_white_24dp.xml new file mode 100644 index 000000000..c097d3e40 --- /dev/null +++ b/app/src/main/res/drawable/ic_more_vert_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_motorcycle_black_24dp.xml b/app/src/main/res/drawable/ic_motorcycle_black_24dp.xml new file mode 100644 index 000000000..539182f83 --- /dev/null +++ b/app/src/main/res/drawable/ic_motorcycle_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_motorcycle_white_24dp.xml b/app/src/main/res/drawable/ic_motorcycle_white_24dp.xml new file mode 100644 index 000000000..d5f2519d7 --- /dev/null +++ b/app/src/main/res/drawable/ic_motorcycle_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_movie_black_24dp.xml b/app/src/main/res/drawable/ic_movie_black_24dp.xml new file mode 100644 index 000000000..d70c00f00 --- /dev/null +++ b/app/src/main/res/drawable/ic_movie_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_movie_white_24dp.xml b/app/src/main/res/drawable/ic_movie_white_24dp.xml new file mode 100644 index 000000000..a1d539a3f --- /dev/null +++ b/app/src/main/res/drawable/ic_movie_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_music_note_black_24dp.xml b/app/src/main/res/drawable/ic_music_note_black_24dp.xml new file mode 100644 index 000000000..736c004ef --- /dev/null +++ b/app/src/main/res/drawable/ic_music_note_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_music_note_white_24dp.xml b/app/src/main/res/drawable/ic_music_note_white_24dp.xml new file mode 100644 index 000000000..69f0a3a4d --- /dev/null +++ b/app/src/main/res/drawable/ic_music_note_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_palette_black_24dp.xml b/app/src/main/res/drawable/ic_palette_black_24dp.xml new file mode 100644 index 000000000..f75e2fbe3 --- /dev/null +++ b/app/src/main/res/drawable/ic_palette_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_palette_white_24dp.xml b/app/src/main/res/drawable/ic_palette_white_24dp.xml new file mode 100644 index 000000000..4abeea58f --- /dev/null +++ b/app/src/main/res/drawable/ic_palette_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_pause_black_24dp.xml b/app/src/main/res/drawable/ic_pause_black_24dp.xml new file mode 100644 index 000000000..bb28a6c41 --- /dev/null +++ b/app/src/main/res/drawable/ic_pause_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_pause_white_24dp.xml b/app/src/main/res/drawable/ic_pause_white_24dp.xml new file mode 100644 index 000000000..08b34c2da --- /dev/null +++ b/app/src/main/res/drawable/ic_pause_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_people_black_24dp.xml b/app/src/main/res/drawable/ic_people_black_24dp.xml new file mode 100644 index 000000000..4cfd86960 --- /dev/null +++ b/app/src/main/res/drawable/ic_people_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_people_white_24dp.xml b/app/src/main/res/drawable/ic_people_white_24dp.xml new file mode 100644 index 000000000..23afe2270 --- /dev/null +++ b/app/src/main/res/drawable/ic_people_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_person_black_24dp.xml b/app/src/main/res/drawable/ic_person_black_24dp.xml new file mode 100644 index 000000000..b2cb337b0 --- /dev/null +++ b/app/src/main/res/drawable/ic_person_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_person_white_24dp.xml b/app/src/main/res/drawable/ic_person_white_24dp.xml new file mode 100644 index 000000000..d7366bda0 --- /dev/null +++ b/app/src/main/res/drawable/ic_person_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_pets_black_24dp.xml b/app/src/main/res/drawable/ic_pets_black_24dp.xml new file mode 100644 index 000000000..b6247bd87 --- /dev/null +++ b/app/src/main/res/drawable/ic_pets_black_24dp.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_pets_white_24dp.xml b/app/src/main/res/drawable/ic_pets_white_24dp.xml new file mode 100644 index 000000000..46724a33d --- /dev/null +++ b/app/src/main/res/drawable/ic_pets_white_24dp.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_picture_in_picture_black_24dp.xml b/app/src/main/res/drawable/ic_picture_in_picture_black_24dp.xml new file mode 100644 index 000000000..b61c5218b --- /dev/null +++ b/app/src/main/res/drawable/ic_picture_in_picture_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_picture_in_picture_white_24dp.xml b/app/src/main/res/drawable/ic_picture_in_picture_white_24dp.xml new file mode 100644 index 000000000..db1b46f81 --- /dev/null +++ b/app/src/main/res/drawable/ic_picture_in_picture_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_play_arrow_black_24dp.xml b/app/src/main/res/drawable/ic_play_arrow_black_24dp.xml new file mode 100644 index 000000000..bf9b895ac --- /dev/null +++ b/app/src/main/res/drawable/ic_play_arrow_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_play_arrow_shadow.xml b/app/src/main/res/drawable/ic_play_arrow_shadow.xml new file mode 100644 index 000000000..8d5871fad --- /dev/null +++ b/app/src/main/res/drawable/ic_play_arrow_shadow.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_play_arrow_white_24dp.xml b/app/src/main/res/drawable/ic_play_arrow_white_24dp.xml new file mode 100644 index 000000000..e135a55b7 --- /dev/null +++ b/app/src/main/res/drawable/ic_play_arrow_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_playlist_add_black_24dp.xml b/app/src/main/res/drawable/ic_playlist_add_black_24dp.xml new file mode 100644 index 000000000..905d86e64 --- /dev/null +++ b/app/src/main/res/drawable/ic_playlist_add_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_playlist_add_check_black_24dp.xml b/app/src/main/res/drawable/ic_playlist_add_check_black_24dp.xml new file mode 100644 index 000000000..4f7a1c13f --- /dev/null +++ b/app/src/main/res/drawable/ic_playlist_add_check_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_playlist_add_check_white_24dp.xml b/app/src/main/res/drawable/ic_playlist_add_check_white_24dp.xml new file mode 100644 index 000000000..04b4b7855 --- /dev/null +++ b/app/src/main/res/drawable/ic_playlist_add_check_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_playlist_add_white_24dp.xml b/app/src/main/res/drawable/ic_playlist_add_white_24dp.xml new file mode 100644 index 000000000..ed27c167e --- /dev/null +++ b/app/src/main/res/drawable/ic_playlist_add_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_playlist_play_white_24dp.xml b/app/src/main/res/drawable/ic_playlist_play_white_24dp.xml new file mode 100644 index 000000000..06ccbb8eb --- /dev/null +++ b/app/src/main/res/drawable/ic_playlist_play_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_public_black_24dp.xml b/app/src/main/res/drawable/ic_public_black_24dp.xml new file mode 100644 index 000000000..d976b4244 --- /dev/null +++ b/app/src/main/res/drawable/ic_public_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_public_white_24dp.xml b/app/src/main/res/drawable/ic_public_white_24dp.xml new file mode 100644 index 000000000..880e42770 --- /dev/null +++ b/app/src/main/res/drawable/ic_public_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_radio_black_24dp.xml b/app/src/main/res/drawable/ic_radio_black_24dp.xml new file mode 100644 index 000000000..00da9101f --- /dev/null +++ b/app/src/main/res/drawable/ic_radio_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_radio_white_24dp.xml b/app/src/main/res/drawable/ic_radio_white_24dp.xml new file mode 100644 index 000000000..df563ec1d --- /dev/null +++ b/app/src/main/res/drawable/ic_radio_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_refresh_black_24dp.xml b/app/src/main/res/drawable/ic_refresh_black_24dp.xml new file mode 100644 index 000000000..8229a9a64 --- /dev/null +++ b/app/src/main/res/drawable/ic_refresh_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_refresh_white_24dp.xml b/app/src/main/res/drawable/ic_refresh_white_24dp.xml new file mode 100644 index 000000000..cc2d1e04f --- /dev/null +++ b/app/src/main/res/drawable/ic_refresh_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_repeat_white_24dp.xml b/app/src/main/res/drawable/ic_repeat_white_24dp.xml new file mode 100644 index 000000000..f4e1a4f39 --- /dev/null +++ b/app/src/main/res/drawable/ic_repeat_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_restaurant_black_24dp.xml b/app/src/main/res/drawable/ic_restaurant_black_24dp.xml new file mode 100644 index 000000000..e14429d09 --- /dev/null +++ b/app/src/main/res/drawable/ic_restaurant_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_restaurant_white_24dp.xml b/app/src/main/res/drawable/ic_restaurant_white_24dp.xml new file mode 100644 index 000000000..1e2d89c0f --- /dev/null +++ b/app/src/main/res/drawable/ic_restaurant_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_rss_feed_black_24dp.xml b/app/src/main/res/drawable/ic_rss_feed_black_24dp.xml new file mode 100644 index 000000000..4da9b623b --- /dev/null +++ b/app/src/main/res/drawable/ic_rss_feed_black_24dp.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_rss_feed_white_24dp.xml b/app/src/main/res/drawable/ic_rss_feed_white_24dp.xml new file mode 100644 index 000000000..42a802c7e --- /dev/null +++ b/app/src/main/res/drawable/ic_rss_feed_white_24dp.xml @@ -0,0 +1,6 @@ + + + + diff --git a/app/src/main/res/drawable/ic_save_black_24dp.xml b/app/src/main/res/drawable/ic_save_black_24dp.xml new file mode 100644 index 000000000..a561d632a --- /dev/null +++ b/app/src/main/res/drawable/ic_save_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_save_white_24dp.xml b/app/src/main/res/drawable/ic_save_white_24dp.xml new file mode 100644 index 000000000..74ca299c3 --- /dev/null +++ b/app/src/main/res/drawable/ic_save_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_school_black_24dp.xml b/app/src/main/res/drawable/ic_school_black_24dp.xml new file mode 100644 index 000000000..30d83f840 --- /dev/null +++ b/app/src/main/res/drawable/ic_school_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_school_white_24dp.xml b/app/src/main/res/drawable/ic_school_white_24dp.xml new file mode 100644 index 000000000..e9fbe5931 --- /dev/null +++ b/app/src/main/res/drawable/ic_school_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_screen_rotation_white_24dp.xml b/app/src/main/res/drawable/ic_screen_rotation_white_24dp.xml new file mode 100644 index 000000000..1372f04a0 --- /dev/null +++ b/app/src/main/res/drawable/ic_screen_rotation_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_search_add_black_24dp.xml b/app/src/main/res/drawable/ic_search_add_black_24dp.xml new file mode 100644 index 000000000..a5264a6a3 --- /dev/null +++ b/app/src/main/res/drawable/ic_search_add_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_search_add_white_24dp.xml b/app/src/main/res/drawable/ic_search_add_white_24dp.xml new file mode 100644 index 000000000..9341522df --- /dev/null +++ b/app/src/main/res/drawable/ic_search_add_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_search_black_24dp.xml b/app/src/main/res/drawable/ic_search_black_24dp.xml new file mode 100644 index 000000000..affc7ba26 --- /dev/null +++ b/app/src/main/res/drawable/ic_search_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_search_white_24dp.xml b/app/src/main/res/drawable/ic_search_white_24dp.xml new file mode 100644 index 000000000..be5ad99c6 --- /dev/null +++ b/app/src/main/res/drawable/ic_search_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings_backup_restore_white_24dp.xml b/app/src/main/res/drawable/ic_settings_backup_restore_white_24dp.xml index 03a26f550..e3e6530bf 100644 --- a/app/src/main/res/drawable/ic_settings_backup_restore_white_24dp.xml +++ b/app/src/main/res/drawable/ic_settings_backup_restore_white_24dp.xml @@ -1,9 +1,5 @@ - - + + diff --git a/app/src/main/res/drawable/ic_settings_black_24dp.xml b/app/src/main/res/drawable/ic_settings_black_24dp.xml new file mode 100644 index 000000000..24a5623cd --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings_white_24dp.xml b/app/src/main/res/drawable/ic_settings_white_24dp.xml new file mode 100644 index 000000000..1397d370e --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_share_black_24dp.xml b/app/src/main/res/drawable/ic_share_black_24dp.xml new file mode 100644 index 000000000..e3fe874d6 --- /dev/null +++ b/app/src/main/res/drawable/ic_share_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_share_white_24dp.xml b/app/src/main/res/drawable/ic_share_white_24dp.xml new file mode 100644 index 000000000..045bbc0c0 --- /dev/null +++ b/app/src/main/res/drawable/ic_share_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_shopping_cart_black_24dp.xml b/app/src/main/res/drawable/ic_shopping_cart_black_24dp.xml new file mode 100644 index 000000000..452332095 --- /dev/null +++ b/app/src/main/res/drawable/ic_shopping_cart_black_24dp.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_shopping_cart_white_24dp.xml b/app/src/main/res/drawable/ic_shopping_cart_white_24dp.xml new file mode 100644 index 000000000..a55bf8a88 --- /dev/null +++ b/app/src/main/res/drawable/ic_shopping_cart_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_shuffle_white_24dp.xml b/app/src/main/res/drawable/ic_shuffle_white_24dp.xml new file mode 100644 index 000000000..9ab22017b --- /dev/null +++ b/app/src/main/res/drawable/ic_shuffle_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_sort_black_24dp.xml b/app/src/main/res/drawable/ic_sort_black_24dp.xml new file mode 100644 index 000000000..fd4c56f0e --- /dev/null +++ b/app/src/main/res/drawable/ic_sort_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_blank_page_white_24dp.xml b/app/src/main/res/drawable/ic_sort_white_24dp.xml similarity index 63% rename from app/src/main/res/drawable/ic_blank_page_white_24dp.xml rename to app/src/main/res/drawable/ic_sort_white_24dp.xml index 86a68484f..a0c153ad0 100644 --- a/app/src/main/res/drawable/ic_blank_page_white_24dp.xml +++ b/app/src/main/res/drawable/ic_sort_white_24dp.xml @@ -5,5 +5,5 @@ android:viewportHeight="24.0"> + android:pathData="M3,18h6v-2L3,16v2zM3,6v2h18L21,6L3,6zM3,13h12v-2L3,11v2z"/> diff --git a/app/src/main/res/drawable/ic_stars_black_24dp.xml b/app/src/main/res/drawable/ic_stars_black_24dp.xml new file mode 100644 index 000000000..61c5d7ace --- /dev/null +++ b/app/src/main/res/drawable/ic_stars_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_stars_white_24dp.xml b/app/src/main/res/drawable/ic_stars_white_24dp.xml new file mode 100644 index 000000000..926e5a106 --- /dev/null +++ b/app/src/main/res/drawable/ic_stars_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_subtitles_white_24dp.xml b/app/src/main/res/drawable/ic_subtitles_white_24dp.xml new file mode 100644 index 000000000..1052d1475 --- /dev/null +++ b/app/src/main/res/drawable/ic_subtitles_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_telescope_black_24dp.xml b/app/src/main/res/drawable/ic_telescope_black_24dp.xml new file mode 100644 index 000000000..9c6132ecc --- /dev/null +++ b/app/src/main/res/drawable/ic_telescope_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_telescope_white_24dp.xml b/app/src/main/res/drawable/ic_telescope_white_24dp.xml new file mode 100644 index 000000000..ea870fd87 --- /dev/null +++ b/app/src/main/res/drawable/ic_telescope_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_thumb_down_black_24dp.xml b/app/src/main/res/drawable/ic_thumb_down_black_24dp.xml new file mode 100644 index 000000000..26ba95c85 --- /dev/null +++ b/app/src/main/res/drawable/ic_thumb_down_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_thumb_down_white_24dp.xml b/app/src/main/res/drawable/ic_thumb_down_white_24dp.xml new file mode 100644 index 000000000..72a99e6b0 --- /dev/null +++ b/app/src/main/res/drawable/ic_thumb_down_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_thumb_up_black_24dp.xml b/app/src/main/res/drawable/ic_thumb_up_black_24dp.xml new file mode 100644 index 000000000..34fb51ab3 --- /dev/null +++ b/app/src/main/res/drawable/ic_thumb_up_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_thumb_up_white_24dp.xml b/app/src/main/res/drawable/ic_thumb_up_white_24dp.xml new file mode 100644 index 000000000..d9acf7500 --- /dev/null +++ b/app/src/main/res/drawable/ic_thumb_up_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_trending_up_black_24dp.xml b/app/src/main/res/drawable/ic_trending_up_black_24dp.xml new file mode 100644 index 000000000..4c9da94b9 --- /dev/null +++ b/app/src/main/res/drawable/ic_trending_up_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_trending_up_white_24dp.xml b/app/src/main/res/drawable/ic_trending_up_white_24dp.xml new file mode 100644 index 000000000..4d3859d53 --- /dev/null +++ b/app/src/main/res/drawable/ic_trending_up_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_tv_black_24dp.xml b/app/src/main/res/drawable/ic_tv_black_24dp.xml new file mode 100644 index 000000000..771363883 --- /dev/null +++ b/app/src/main/res/drawable/ic_tv_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_tv_white_24dp.xml b/app/src/main/res/drawable/ic_tv_white_24dp.xml new file mode 100644 index 000000000..0286ef16e --- /dev/null +++ b/app/src/main/res/drawable/ic_tv_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_videogame_asset_black_24dp.xml b/app/src/main/res/drawable/ic_videogame_asset_black_24dp.xml new file mode 100644 index 000000000..52658f650 --- /dev/null +++ b/app/src/main/res/drawable/ic_videogame_asset_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_videogame_asset_white_24dp.xml b/app/src/main/res/drawable/ic_videogame_asset_white_24dp.xml new file mode 100644 index 000000000..46ec002cb --- /dev/null +++ b/app/src/main/res/drawable/ic_videogame_asset_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_volume_down_white_24dp.xml b/app/src/main/res/drawable/ic_volume_down_white_24dp.xml new file mode 100644 index 000000000..3a769637b --- /dev/null +++ b/app/src/main/res/drawable/ic_volume_down_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_volume_down_white_72dp.xml b/app/src/main/res/drawable/ic_volume_down_white_72dp.xml deleted file mode 100644 index a7fafb3a5..000000000 --- a/app/src/main/res/drawable/ic_volume_down_white_72dp.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_volume_mute_white_24dp.xml b/app/src/main/res/drawable/ic_volume_mute_white_24dp.xml new file mode 100644 index 000000000..dac85f981 --- /dev/null +++ b/app/src/main/res/drawable/ic_volume_mute_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_volume_mute_white_72dp.xml b/app/src/main/res/drawable/ic_volume_mute_white_72dp.xml deleted file mode 100644 index 1a8ab7e86..000000000 --- a/app/src/main/res/drawable/ic_volume_mute_white_72dp.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_volume_off_white_72dp.xml b/app/src/main/res/drawable/ic_volume_off_black_24dp.xml similarity index 72% rename from app/src/main/res/drawable/ic_volume_off_white_72dp.xml rename to app/src/main/res/drawable/ic_volume_off_black_24dp.xml index 07f24d7aa..3aed66ddc 100644 --- a/app/src/main/res/drawable/ic_volume_off_white_72dp.xml +++ b/app/src/main/res/drawable/ic_volume_off_black_24dp.xml @@ -1,10 +1,9 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + android:pathData="M16.5,12c0,-1.77 -1.02,-3.29 -2.5,-4.03v2.21l2.45,2.45c0.03,-0.2 0.05,-0.41 0.05,-0.63zM19,12c0,0.94 -0.2,1.82 -0.54,2.64l1.51,1.51C20.63,14.91 21,13.5 21,12c0,-4.28 -2.99,-7.86 -7,-8.77v2.06c2.89,0.86 5,3.54 5,6.71zM4.27,3L3,4.27 7.73,9L3,9v6h4l5,5v-6.73l4.25,4.25c-0.67,0.52 -1.42,0.93 -2.25,1.18v2.06c1.38,-0.31 2.63,-0.95 3.69,-1.81L19.73,21 21,19.73l-9,-9L4.27,3zM12,4L9.91,6.09 12,8.18L12,4z"/> diff --git a/app/src/main/res/drawable/ic_volume_off_white_24dp.xml b/app/src/main/res/drawable/ic_volume_off_white_24dp.xml new file mode 100644 index 000000000..a266d9731 --- /dev/null +++ b/app/src/main/res/drawable/ic_volume_off_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_volume_up_black_24dp.xml b/app/src/main/res/drawable/ic_volume_up_black_24dp.xml new file mode 100644 index 000000000..bb0c74ba1 --- /dev/null +++ b/app/src/main/res/drawable/ic_volume_up_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_volume_up_white_72dp.xml b/app/src/main/res/drawable/ic_volume_up_white_24dp.xml similarity index 67% rename from app/src/main/res/drawable/ic_volume_up_white_72dp.xml rename to app/src/main/res/drawable/ic_volume_up_white_24dp.xml index b2fb235a6..271540946 100644 --- a/app/src/main/res/drawable/ic_volume_up_white_72dp.xml +++ b/app/src/main/res/drawable/ic_volume_up_white_24dp.xml @@ -1,10 +1,9 @@ + android:fillColor="#FFFFFFFF" + android:pathData="M3,9v6h4l5,5L12,4L7,9L3,9zM16.5,12c0,-1.77 -1.02,-3.29 -2.5,-4.03v8.05c1.48,-0.73 2.5,-2.25 2.5,-4.02zM14,3.23v2.06c2.89,0.86 5,3.54 5,6.71s-2.11,5.85 -5,6.71v2.06c4.01,-0.91 7,-4.49 7,-8.77s-2.99,-7.86 -7,-8.77z"/> diff --git a/app/src/main/res/drawable/ic_watch_later_black_24dp.xml b/app/src/main/res/drawable/ic_watch_later_black_24dp.xml new file mode 100644 index 000000000..5a1b9ac74 --- /dev/null +++ b/app/src/main/res/drawable/ic_watch_later_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_watch_later_white_24dp.xml b/app/src/main/res/drawable/ic_watch_later_white_24dp.xml new file mode 100644 index 000000000..f9fffbc43 --- /dev/null +++ b/app/src/main/res/drawable/ic_watch_later_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_wb_sunny_black_24dp.xml b/app/src/main/res/drawable/ic_wb_sunny_black_24dp.xml new file mode 100644 index 000000000..a56fb5049 --- /dev/null +++ b/app/src/main/res/drawable/ic_wb_sunny_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_wb_sunny_white_24dp.xml b/app/src/main/res/drawable/ic_wb_sunny_white_24dp.xml new file mode 100644 index 000000000..5d22bab00 --- /dev/null +++ b/app/src/main/res/drawable/ic_wb_sunny_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_whatshot_black_24dp.xml b/app/src/main/res/drawable/ic_whatshot_black_24dp.xml new file mode 100644 index 000000000..1cbc037f7 --- /dev/null +++ b/app/src/main/res/drawable/ic_whatshot_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_whatshot_white_24dp.xml b/app/src/main/res/drawable/ic_whatshot_white_24dp.xml new file mode 100644 index 000000000..9aa2124f3 --- /dev/null +++ b/app/src/main/res/drawable/ic_whatshot_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_work_black_24dp.xml b/app/src/main/res/drawable/ic_work_black_24dp.xml new file mode 100644 index 000000000..2668f2c43 --- /dev/null +++ b/app/src/main/res/drawable/ic_work_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_work_white_24dp.xml b/app/src/main/res/drawable/ic_work_white_24dp.xml new file mode 100644 index 000000000..8a1db7828 --- /dev/null +++ b/app/src/main/res/drawable/ic_work_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/player_controls_bg.xml b/app/src/main/res/drawable/player_controls_background.xml similarity index 100% rename from app/src/main/res/drawable/player_controls_bg.xml rename to app/src/main/res/drawable/player_controls_background.xml diff --git a/app/src/main/res/drawable/player_top_controls_bg.xml b/app/src/main/res/drawable/player_controls_top_background.xml similarity index 100% rename from app/src/main/res/drawable/player_top_controls_bg.xml rename to app/src/main/res/drawable/player_controls_top_background.xml diff --git a/app/src/main/res/drawable/progress_circular_white.xml b/app/src/main/res/drawable/progress_circular_white.xml index daa6649bc..0de71afec 100644 --- a/app/src/main/res/drawable/progress_circular_white.xml +++ b/app/src/main/res/drawable/progress_circular_white.xml @@ -2,7 +2,7 @@ - - - - - - - - - - - - + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/progress_soundcloud_horizontal_light.xml b/app/src/main/res/drawable/progress_soundcloud_horizontal_light.xml index c326c5c04..0b3000de0 100644 --- a/app/src/main/res/drawable/progress_soundcloud_horizontal_light.xml +++ b/app/src/main/res/drawable/progress_soundcloud_horizontal_light.xml @@ -1,15 +1,15 @@ - - - - - - - - - - - - + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/progress_youtube_horizontal_dark.xml b/app/src/main/res/drawable/progress_youtube_horizontal_dark.xml index 404410f98..7f4520eb8 100644 --- a/app/src/main/res/drawable/progress_youtube_horizontal_dark.xml +++ b/app/src/main/res/drawable/progress_youtube_horizontal_dark.xml @@ -1,15 +1,15 @@ - - - - - - - - - - - - + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/progress_youtube_horizontal_light.xml b/app/src/main/res/drawable/progress_youtube_horizontal_light.xml index 120a6e5fb..d1556de91 100644 --- a/app/src/main/res/drawable/progress_youtube_horizontal_light.xml +++ b/app/src/main/res/drawable/progress_youtube_horizontal_light.xml @@ -1,15 +1,15 @@ - - - - - - - - - - - - + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/dark_checked_selector.xml b/app/src/main/res/drawable/selector_checked_dark.xml similarity index 100% rename from app/src/main/res/drawable/dark_checked_selector.xml rename to app/src/main/res/drawable/selector_checked_dark.xml diff --git a/app/src/main/res/drawable/light_checked_selector.xml b/app/src/main/res/drawable/selector_checked_light.xml similarity index 100% rename from app/src/main/res/drawable/light_checked_selector.xml rename to app/src/main/res/drawable/selector_checked_light.xml diff --git a/app/src/main/res/drawable/dark_selector.xml b/app/src/main/res/drawable/selector_dark.xml similarity index 100% rename from app/src/main/res/drawable/dark_selector.xml rename to app/src/main/res/drawable/selector_dark.xml diff --git a/app/src/main/res/drawable/selector_focused_dark.xml b/app/src/main/res/drawable/selector_focused_dark.xml new file mode 100644 index 000000000..102f40d76 --- /dev/null +++ b/app/src/main/res/drawable/selector_focused_dark.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_focused_light.xml b/app/src/main/res/drawable/selector_focused_light.xml new file mode 100644 index 000000000..102f40d76 --- /dev/null +++ b/app/src/main/res/drawable/selector_focused_light.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/light_selector.xml b/app/src/main/res/drawable/selector_light.xml similarity index 100% rename from app/src/main/res/drawable/light_selector.xml rename to app/src/main/res/drawable/selector_light.xml diff --git a/app/src/main/res/drawable/splash_forground.xml b/app/src/main/res/drawable/splash_foreground.xml similarity index 100% rename from app/src/main/res/drawable/splash_forground.xml rename to app/src/main/res/drawable/splash_foreground.xml diff --git a/app/src/main/res/drawable/tab_selector.xml b/app/src/main/res/drawable/tab_selector.xml index b7307674b..dc472133f 100644 --- a/app/src/main/res/drawable/tab_selector.xml +++ b/app/src/main/res/drawable/tab_selector.xml @@ -1,8 +1,8 @@ - - + \ No newline at end of file diff --git a/app/src/main/res/layout-land/activity_player_queue_control.xml b/app/src/main/res/layout-land/activity_player_queue_control.xml index 7467a79cf..84a29e0c8 100644 --- a/app/src/main/res/layout-land/activity_player_queue_control.xml +++ b/app/src/main/res/layout-land/activity_player_queue_control.xml @@ -111,7 +111,7 @@ tools:ignore="RtlHardcoded"> @@ -139,7 +139,7 @@ android:focusable="true" android:scaleType="fitCenter" android:tint="?attr/colorAccent" - android:src="@drawable/ic_pause_white" + app:srcCompat="@drawable/ic_pause_white_24dp" tools:ignore="ContentDescription"/> @@ -185,8 +185,8 @@ android:orientation="horizontal" tools:ignore="RtlHardcoded"> - + android:background="?attr/selectableItemBackgroundBorderless" + android:clickable="true" + android:focusable="true" + android:scaleType="fitXY" + android:src="@drawable/exo_controls_previous" + android:tint="?attr/colorAccent" + tools:ignore="ContentDescription" /> - + android:background="?attr/selectableItemBackgroundBorderless" + android:clickable="true" + android:focusable="true" + android:scaleType="fitXY" + android:src="@drawable/exo_controls_next" + android:tint="?attr/colorAccent" + tools:ignore="ContentDescription" /> @@ -262,7 +266,7 @@ android:gravity="center" android:orientation="horizontal" android:paddingLeft="16dp" - android:background="@drawable/player_controls_bg" + android:background="@drawable/player_controls_background" android:paddingRight="16dp"> diff --git a/app/src/main/res/layout-large-land/activity_main_player.xml b/app/src/main/res/layout-large-land/activity_main_player.xml index d87930371..1ee2538c0 100644 --- a/app/src/main/res/layout-large-land/activity_main_player.xml +++ b/app/src/main/res/layout-large-land/activity_main_player.xml @@ -89,7 +89,7 @@ android:background="?android:selectableItemBackground" tools:ignore="ContentDescription,RtlHardcoded"/> - @@ -128,13 +128,13 @@ @@ -166,7 +166,7 @@ android:gravity="top" tools:ignore="RtlHardcoded"> - @@ -203,8 +203,6 @@ android:textColor="@android:color/white" android:textSize="15sp" android:textStyle="bold" - android:clickable="true" - android:focusable="true" tools:ignore="RtlHardcoded" tools:text="The Video Title LONG very LONG"/> @@ -219,13 +217,12 @@ android:singleLine="true" android:textColor="@android:color/white" android:textSize="12sp" - android:clickable="true" - android:focusable="true" tools:text="The Video Artist LONG very LONG very Long"/> - - - - @@ -289,8 +286,9 @@ tools:ignore="RtlHardcoded" tools:visibility="visible"> - - @@ -326,7 +327,7 @@ android:layout_height="0dp" android:layout_weight="3"/> - - - + + - @@ -409,7 +424,7 @@ tools:text="1:06:29"/> - - @@ -484,14 +498,14 @@ tools:ignore="ContentDescription"/> - @@ -571,11 +586,11 @@ + tools:src="@drawable/ic_volume_up_white_24dp" /> + tools:src="@drawable/ic_brightness_high_white_24dp" /> + android:id="@+id/video_item_detail" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:baselineAligned="false" + android:focusableInTouchMode="true" + android:orientation="horizontal" + tools:ignore="RtlHardcoded"> - + - + - + - - + + - + - + - + - + - + - + - + - + - - + + - - + + - + - + - + - - + + - - + + - - + + - - + + - - + + - + - + - - + - - + + - + - + - + - + - + - - - + + + - + - - + + - + - + - + - + - + - - + + - + - + - + - + - - + + - + - - + - + - + @@ -637,7 +686,7 @@ android:layout_marginLeft="2dp" android:padding="10dp" android:scaleType="center" - android:src="?attr/close" + app:srcCompat="?attr/ic_close" android:background="?attr/selectableItemBackground" tools:ignore="ContentDescription,RtlHardcoded"/> diff --git a/app/src/main/res/layout-v21/drawer_header.xml b/app/src/main/res/layout-v21/drawer_header.xml deleted file mode 100644 index 9ed9f833a..000000000 --- a/app/src/main/res/layout-v21/drawer_header.xml +++ /dev/null @@ -1,82 +0,0 @@ - - - -