mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2025-11-14 09:27:36 +01:00
Compare commits
1 Commits
v0.27.0
...
fix/peertu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27b2d5de70 |
3
.github/DISCUSSION_TEMPLATE/questions.yml
vendored
3
.github/DISCUSSION_TEMPLATE/questions.yml
vendored
@@ -1,3 +1,6 @@
|
|||||||
|
name: Question
|
||||||
|
description: Ask about anything NewPipe-related
|
||||||
|
labels: [question]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -14,7 +14,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: "Checklist"
|
label: "Checklist"
|
||||||
options:
|
options:
|
||||||
- label: "I am able to reproduce the bug with the latest version given here: [CLICK THIS LINK](https://github.com/TeamNewPipe/NewPipe/releases/latest)."
|
- label: "I am able to reproduce the bug with the [latest version](https://github.com/TeamNewPipe/NewPipe/releases/latest)."
|
||||||
required: true
|
required: true
|
||||||
- label: "I made sure that there are *no existing issues* - [open](https://github.com/TeamNewPipe/NewPipe/issues) or [closed](https://github.com/TeamNewPipe/NewPipe/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
|
- label: "I made sure that there are *no existing issues* - [open](https://github.com/TeamNewPipe/NewPipe/issues) or [closed](https://github.com/TeamNewPipe/NewPipe/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
17
.github/changed-lines-count-labeler.yml
vendored
17
.github/changed-lines-count-labeler.yml
vendored
@@ -1,17 +0,0 @@
|
|||||||
# Add 'size/small' label to any changes with less than 50 lines
|
|
||||||
size/small:
|
|
||||||
max: 49
|
|
||||||
|
|
||||||
# Add 'size/medium' label to any changes between 50 and 249 lines
|
|
||||||
size/medium:
|
|
||||||
min: 50
|
|
||||||
max: 249
|
|
||||||
|
|
||||||
# Add 'size/large' label to any changes between 250 and 749 lines
|
|
||||||
size/large:
|
|
||||||
min: 250
|
|
||||||
max: 749
|
|
||||||
|
|
||||||
# Add 'size/giant' label to any changes for more than 749 lines
|
|
||||||
size/giant:
|
|
||||||
min: 750
|
|
||||||
20
.github/workflows/ci.yml
vendored
20
.github/workflows/ci.yml
vendored
@@ -36,8 +36,8 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- uses: gradle/wrapper-validation-action@v2
|
- uses: gradle/wrapper-validation-action@v1
|
||||||
|
|
||||||
- name: create and checkout branch
|
- name: create and checkout branch
|
||||||
# push events already checked out the branch
|
# push events already checked out the branch
|
||||||
@@ -47,7 +47,7 @@ jobs:
|
|||||||
run: git checkout -B "$BRANCH"
|
run: git checkout -B "$BRANCH"
|
||||||
|
|
||||||
- name: set up JDK 17
|
- name: set up JDK 17
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version: 17
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
@@ -57,7 +57,7 @@ jobs:
|
|||||||
run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace -DskipFormatKtlint
|
run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace -DskipFormatKtlint
|
||||||
|
|
||||||
- name: Upload APK
|
- name: Upload APK
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: app
|
name: app
|
||||||
path: app/build/outputs/apk/debug/*.apk
|
path: app/build/outputs/apk/debug/*.apk
|
||||||
@@ -80,10 +80,10 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: set up JDK 17
|
- name: set up JDK 17
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version: 17
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
@@ -98,7 +98,7 @@ jobs:
|
|||||||
script: ./gradlew connectedCheck --stacktrace
|
script: ./gradlew connectedCheck --stacktrace
|
||||||
|
|
||||||
- name: Upload test report when tests fail # because the printed out stacktrace (console) is too short, see also #7553
|
- name: Upload test report when tests fail # because the printed out stacktrace (console) is too short, see also #7553
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
name: android-test-report-api${{ matrix.api-level }}
|
name: android-test-report-api${{ matrix.api-level }}
|
||||||
@@ -111,19 +111,19 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||||
|
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version: 17
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
cache: 'gradle'
|
cache: 'gradle'
|
||||||
|
|
||||||
- name: Cache SonarCloud packages
|
- name: Cache SonarCloud packages
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: ~/.sonar/cache
|
path: ~/.sonar/cache
|
||||||
key: ${{ runner.os }}-sonar
|
key: ${{ runner.os }}-sonar
|
||||||
|
|||||||
14
.github/workflows/image-minimizer.js
vendored
14
.github/workflows/image-minimizer.js
vendored
@@ -17,8 +17,6 @@ module.exports = async ({github, context}) => {
|
|||||||
initialBody = context.payload.comment.body;
|
initialBody = context.payload.comment.body;
|
||||||
} else if (context.eventName == 'issues') {
|
} else if (context.eventName == 'issues') {
|
||||||
initialBody = context.payload.issue.body;
|
initialBody = context.payload.issue.body;
|
||||||
} else if (context.eventName == 'pull_request') {
|
|
||||||
initialBody = context.payload.pull_request.body;
|
|
||||||
} else {
|
} else {
|
||||||
console.log('Aborting: No body found');
|
console.log('Aborting: No body found');
|
||||||
return;
|
return;
|
||||||
@@ -76,17 +74,9 @@ module.exports = async ({github, context}) => {
|
|||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
body: newBody
|
body: newBody
|
||||||
});
|
});
|
||||||
} else if (context.eventName == 'pull_request') {
|
|
||||||
console.log('Updating pull request', context.payload.pull_request.number);
|
|
||||||
await github.rest.pulls.update({
|
|
||||||
pull_number: context.payload.pull_request.number,
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
body: newBody
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Async replace function from https://stackoverflow.com/a/48032528
|
// Asnyc replace function from https://stackoverflow.com/a/48032528
|
||||||
async function replaceAsync(str, regex, asyncFn) {
|
async function replaceAsync(str, regex, asyncFn) {
|
||||||
const promises = [];
|
const promises = [];
|
||||||
str.replace(regex, (match, ...args) => {
|
str.replace(regex, (match, ...args) => {
|
||||||
@@ -138,7 +128,7 @@ module.exports = async ({github, context}) => {
|
|||||||
if (shouldModify) {
|
if (shouldModify) {
|
||||||
wasMatchModified = true;
|
wasMatchModified = true;
|
||||||
console.log(`Modifying match '${match}'`);
|
console.log(`Modifying match '${match}'`);
|
||||||
return `<img alt="${g1}" src="${g2}" width=${Math.min(600, Math.floor(IMG_MAX_HEIGHT_PX * probeAspectRatio))} />`;
|
return `<img alt="${g1}" src="${g2}" width=${Math.min(600, (IMG_MAX_HEIGHT_PX * probeAspectRatio).toFixed(0))} />`;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Match '${match}' is ok/will not be modified`);
|
console.log(`Match '${match}' is ok/will not be modified`);
|
||||||
|
|||||||
8
.github/workflows/image-minimizer.yml
vendored
8
.github/workflows/image-minimizer.yml
vendored
@@ -5,8 +5,6 @@ on:
|
|||||||
types: [created, edited]
|
types: [created, edited]
|
||||||
issues:
|
issues:
|
||||||
types: [opened, edited]
|
types: [opened, edited]
|
||||||
pull_request:
|
|
||||||
types: [opened, edited]
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
issues: write
|
issues: write
|
||||||
@@ -17,9 +15,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 16
|
||||||
|
|
||||||
@@ -27,7 +25,7 @@ jobs:
|
|||||||
run: npm i probe-image-size@7.2.3 --ignore-scripts
|
run: npm i probe-image-size@7.2.3 --ignore-scripts
|
||||||
|
|
||||||
- name: Minimize simple images
|
- name: Minimize simple images
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@v6
|
||||||
timeout-minutes: 3
|
timeout-minutes: 3
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
|
|||||||
18
.github/workflows/pr-labeler.yml
vendored
18
.github/workflows/pr-labeler.yml
vendored
@@ -1,18 +0,0 @@
|
|||||||
name: "PR size labeler"
|
|
||||||
on: [pull_request_target]
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
changed-lines-count-labeler:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Automatically labelling pull requests based on the changed lines count
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
steps:
|
|
||||||
- name: Set a label
|
|
||||||
uses: TeamNewPipe/changed-lines-count-labeler@main
|
|
||||||
with:
|
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
configuration-path: .github/changed-lines-count-labeler.yml
|
|
||||||
@@ -12,7 +12,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdk 34
|
compileSdk 33
|
||||||
namespace 'org.schabi.newpipe'
|
namespace 'org.schabi.newpipe'
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
@@ -20,8 +20,8 @@ android {
|
|||||||
resValue "string", "app_name", "NewPipe"
|
resValue "string", "app_name", "NewPipe"
|
||||||
minSdk 21
|
minSdk 21
|
||||||
targetSdk 33
|
targetSdk 33
|
||||||
versionCode 997
|
versionCode 993
|
||||||
versionName "0.27.0"
|
versionName "0.25.1"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
@@ -50,6 +50,9 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep the release build type at the end of the list to override 'archivesBaseName' of
|
||||||
|
// debug build. This seems to be a Gradle bug, therefore
|
||||||
|
// TODO: update Gradle version
|
||||||
release {
|
release {
|
||||||
if (System.properties.containsKey('packageSuffix')) {
|
if (System.properties.containsKey('packageSuffix')) {
|
||||||
applicationIdSuffix System.getProperty('packageSuffix')
|
applicationIdSuffix System.getProperty('packageSuffix')
|
||||||
@@ -98,9 +101,7 @@ android {
|
|||||||
resources {
|
resources {
|
||||||
// remove two files which belong to jsoup
|
// remove two files which belong to jsoup
|
||||||
// no idea how they ended up in the META-INF dir...
|
// no idea how they ended up in the META-INF dir...
|
||||||
excludes += ['META-INF/README.md', 'META-INF/CHANGES',
|
excludes += ['META-INF/README.md', 'META-INF/CHANGES']
|
||||||
// 'COPYRIGHT' belongs to RxJava...
|
|
||||||
'META-INF/COPYRIGHT']
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,18 +109,19 @@ android {
|
|||||||
ext {
|
ext {
|
||||||
checkstyleVersion = '10.12.1'
|
checkstyleVersion = '10.12.1'
|
||||||
|
|
||||||
androidxLifecycleVersion = '2.6.2'
|
androidxLifecycleVersion = '2.5.1'
|
||||||
androidxRoomVersion = '2.6.1'
|
androidxRoomVersion = '2.4.3'
|
||||||
androidxWorkVersion = '2.8.1'
|
androidxWorkVersion = '2.7.1'
|
||||||
|
|
||||||
icepickVersion = '3.2.0'
|
icepickVersion = '3.2.0'
|
||||||
exoPlayerVersion = '2.18.7'
|
exoPlayerVersion = '2.18.7'
|
||||||
googleAutoServiceVersion = '1.1.1'
|
googleAutoServiceVersion = '1.0.1'
|
||||||
groupieVersion = '2.10.1'
|
groupieVersion = '2.10.1'
|
||||||
markwonVersion = '4.6.2'
|
markwonVersion = '4.6.2'
|
||||||
|
|
||||||
leakCanaryVersion = '2.12'
|
leakCanaryVersion = '2.9.1'
|
||||||
stethoVersion = '1.6.0'
|
stethoVersion = '1.6.0'
|
||||||
|
mockitoVersion = '4.0.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
configurations {
|
configurations {
|
||||||
@@ -134,7 +136,7 @@ checkstyle {
|
|||||||
toolVersion = checkstyleVersion
|
toolVersion = checkstyleVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register('runCheckstyle', Checkstyle) {
|
task runCheckstyle(type: Checkstyle) {
|
||||||
source 'src'
|
source 'src'
|
||||||
include '**/*.java'
|
include '**/*.java'
|
||||||
exclude '**/gen/**'
|
exclude '**/gen/**'
|
||||||
@@ -155,7 +157,7 @@ tasks.register('runCheckstyle', Checkstyle) {
|
|||||||
def outputDir = "${project.buildDir}/reports/ktlint/"
|
def outputDir = "${project.buildDir}/reports/ktlint/"
|
||||||
def inputFiles = project.fileTree(dir: "src", include: "**/*.kt")
|
def inputFiles = project.fileTree(dir: "src", include: "**/*.kt")
|
||||||
|
|
||||||
tasks.register('runKtlint', JavaExec) {
|
task runKtlint(type: JavaExec) {
|
||||||
inputs.files(inputFiles)
|
inputs.files(inputFiles)
|
||||||
outputs.dir(outputDir)
|
outputs.dir(outputDir)
|
||||||
getMainClass().set("com.pinterest.ktlint.Main")
|
getMainClass().set("com.pinterest.ktlint.Main")
|
||||||
@@ -164,7 +166,7 @@ tasks.register('runKtlint', JavaExec) {
|
|||||||
jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED")
|
jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register('formatKtlint', JavaExec) {
|
task formatKtlint(type: JavaExec) {
|
||||||
inputs.files(inputFiles)
|
inputs.files(inputFiles)
|
||||||
outputs.dir(outputDir)
|
outputs.dir(outputDir)
|
||||||
getMainClass().set("com.pinterest.ktlint.Main")
|
getMainClass().set("com.pinterest.ktlint.Main")
|
||||||
@@ -190,7 +192,7 @@ sonar {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
/** Desugaring **/
|
/** Desugaring **/
|
||||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.0.4'
|
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
|
||||||
|
|
||||||
/** NewPipe libraries **/
|
/** NewPipe libraries **/
|
||||||
// You can use a local version by uncommenting a few lines in settings.gradle
|
// You can use a local version by uncommenting a few lines in settings.gradle
|
||||||
@@ -198,7 +200,7 @@ dependencies {
|
|||||||
// name and the commit hash with the commit hash of the (pushed) commit you want to test
|
// name and the commit hash with the commit hash of the (pushed) commit you want to test
|
||||||
// This works thanks to JitPack: https://jitpack.io/
|
// This works thanks to JitPack: https://jitpack.io/
|
||||||
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
||||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.24.0'
|
implementation 'com.github.TeamNewPipe:NewPipeExtractor:8495ad619e'
|
||||||
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
|
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
|
||||||
|
|
||||||
/** Checkstyle **/
|
/** Checkstyle **/
|
||||||
@@ -206,31 +208,31 @@ dependencies {
|
|||||||
ktlint 'com.pinterest:ktlint:0.45.2'
|
ktlint 'com.pinterest:ktlint:0.45.2'
|
||||||
|
|
||||||
/** Kotlin **/
|
/** Kotlin **/
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlin_version}"
|
||||||
|
|
||||||
/** AndroidX **/
|
/** AndroidX **/
|
||||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
implementation 'androidx.appcompat:appcompat:1.5.1'
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
implementation 'androidx.core:core-ktx:1.12.0'
|
implementation 'androidx.core:core-ktx:1.10.0'
|
||||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.6.2'
|
implementation 'androidx.fragment:fragment-ktx:1.4.1'
|
||||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:${androidxLifecycleVersion}"
|
implementation "androidx.lifecycle:lifecycle-livedata-ktx:${androidxLifecycleVersion}"
|
||||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${androidxLifecycleVersion}"
|
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${androidxLifecycleVersion}"
|
||||||
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
|
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
|
||||||
implementation 'androidx.media:media:1.7.0'
|
implementation 'androidx.media:media:1.6.0'
|
||||||
implementation 'androidx.preference:preference:1.2.1'
|
implementation 'androidx.preference:preference:1.2.0'
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
||||||
implementation "androidx.room:room-runtime:${androidxRoomVersion}"
|
implementation "androidx.room:room-runtime:${androidxRoomVersion}"
|
||||||
implementation "androidx.room:room-rxjava3:${androidxRoomVersion}"
|
implementation "androidx.room:room-rxjava3:${androidxRoomVersion}"
|
||||||
kapt "androidx.room:room-compiler:${androidxRoomVersion}"
|
kapt "androidx.room:room-compiler:${androidxRoomVersion}"
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
// Newer version specified to prevent accessibility regressions with RecyclerView, see:
|
// Newer version specified to prevent accessibility regressions with RecyclerView, see:
|
||||||
// https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01
|
// https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01
|
||||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02'
|
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
||||||
implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}"
|
implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}"
|
||||||
implementation "androidx.work:work-rxjava3:${androidxWorkVersion}"
|
implementation "androidx.work:work-rxjava3:${androidxWorkVersion}"
|
||||||
implementation 'com.google.android.material:material:1.11.0'
|
implementation 'com.google.android.material:material:1.6.1'
|
||||||
|
|
||||||
/** Third-party libraries **/
|
/** Third-party libraries **/
|
||||||
// Instance state boilerplate elimination
|
// Instance state boilerplate elimination
|
||||||
@@ -238,10 +240,10 @@ dependencies {
|
|||||||
kapt "frankiesardo:icepick-processor:${icepickVersion}"
|
kapt "frankiesardo:icepick-processor:${icepickVersion}"
|
||||||
|
|
||||||
// HTML parser
|
// HTML parser
|
||||||
implementation "org.jsoup:jsoup:1.17.2"
|
implementation "org.jsoup:jsoup:1.16.1"
|
||||||
|
|
||||||
// HTTP client
|
// HTTP client
|
||||||
implementation "com.squareup.okhttp3:okhttp:4.12.0"
|
implementation "com.squareup.okhttp3:okhttp:4.11.0"
|
||||||
|
|
||||||
// Media player
|
// Media player
|
||||||
implementation "com.google.android.exoplayer:exoplayer-core:${exoPlayerVersion}"
|
implementation "com.google.android.exoplayer:exoplayer-core:${exoPlayerVersion}"
|
||||||
@@ -270,37 +272,38 @@ dependencies {
|
|||||||
implementation "io.noties.markwon:linkify:${markwonVersion}"
|
implementation "io.noties.markwon:linkify:${markwonVersion}"
|
||||||
|
|
||||||
// Crash reporting
|
// Crash reporting
|
||||||
implementation "ch.acra:acra-core:5.11.3"
|
implementation "ch.acra:acra-core:5.10.1"
|
||||||
|
|
||||||
// Properly restarting
|
// Properly restarting
|
||||||
implementation 'com.jakewharton:process-phoenix:2.1.2'
|
implementation 'com.jakewharton:process-phoenix:2.1.2'
|
||||||
|
|
||||||
// Reactive extensions for Java VM
|
// Reactive extensions for Java VM
|
||||||
implementation "io.reactivex.rxjava3:rxjava:3.1.8"
|
implementation "io.reactivex.rxjava3:rxjava:3.1.6"
|
||||||
implementation "io.reactivex.rxjava3:rxandroid:3.0.2"
|
implementation "io.reactivex.rxjava3:rxandroid:3.0.2"
|
||||||
// RxJava binding APIs for Android UI widgets
|
// RxJava binding APIs for Android UI widgets
|
||||||
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
|
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
|
||||||
|
|
||||||
// Date and time formatting
|
// Date and time formatting
|
||||||
implementation "org.ocpsoft.prettytime:prettytime:5.0.7.Final"
|
implementation "org.ocpsoft.prettytime:prettytime:5.0.6.Final"
|
||||||
|
|
||||||
/** Debugging **/
|
/** Debugging **/
|
||||||
// Memory leak detection
|
// Memory leak detection
|
||||||
debugImplementation "com.squareup.leakcanary:leakcanary-object-watcher-android:${leakCanaryVersion}"
|
implementation "com.squareup.leakcanary:leakcanary-object-watcher-android:${leakCanaryVersion}"
|
||||||
debugImplementation "com.squareup.leakcanary:plumber-android:${leakCanaryVersion}"
|
implementation "com.squareup.leakcanary:plumber-android:${leakCanaryVersion}"
|
||||||
debugImplementation "com.squareup.leakcanary:leakcanary-android-core:${leakCanaryVersion}"
|
debugImplementation "com.squareup.leakcanary:leakcanary-android:${leakCanaryVersion}"
|
||||||
// Debug bridge for Android
|
// Debug bridge for Android
|
||||||
debugImplementation "com.facebook.stetho:stetho:${stethoVersion}"
|
debugImplementation "com.facebook.stetho:stetho:${stethoVersion}"
|
||||||
debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoVersion}"
|
debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoVersion}"
|
||||||
|
|
||||||
/** Testing **/
|
/** Testing **/
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
testImplementation 'org.mockito:mockito-core:5.6.0'
|
testImplementation "org.mockito:mockito-core:${mockitoVersion}"
|
||||||
|
testImplementation "org.mockito:mockito-inline:${mockitoVersion}"
|
||||||
|
|
||||||
androidTestImplementation "androidx.test.ext:junit:1.1.5"
|
androidTestImplementation "androidx.test.ext:junit:1.1.5"
|
||||||
androidTestImplementation "androidx.test:runner:1.5.2"
|
androidTestImplementation "androidx.test:runner:1.5.2"
|
||||||
androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}"
|
androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}"
|
||||||
androidTestImplementation "org.assertj:assertj-core:3.24.2"
|
androidTestImplementation "org.assertj:assertj-core:3.23.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
static String getGitWorkingBranch() {
|
static String getGitWorkingBranch() {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -4,18 +4,15 @@ import android.content.ContentValues
|
|||||||
import android.database.sqlite.SQLiteDatabase
|
import android.database.sqlite.SQLiteDatabase
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import androidx.room.testing.MigrationTestHelper
|
import androidx.room.testing.MigrationTestHelper
|
||||||
|
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
|
||||||
import androidx.test.core.app.ApplicationProvider
|
import androidx.test.core.app.ApplicationProvider
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertNotEquals
|
|
||||||
import org.junit.Assert.assertNull
|
import org.junit.Assert.assertNull
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.schabi.newpipe.database.playlist.model.PlaylistEntity
|
|
||||||
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity
|
|
||||||
import org.schabi.newpipe.extractor.ServiceList
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType
|
import org.schabi.newpipe.extractor.stream.StreamType
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
@@ -24,23 +21,20 @@ class DatabaseMigrationTest {
|
|||||||
private const val DEFAULT_SERVICE_ID = 0
|
private const val DEFAULT_SERVICE_ID = 0
|
||||||
private const val DEFAULT_URL = "https://www.youtube.com/watch?v=cDphUib5iG4"
|
private const val DEFAULT_URL = "https://www.youtube.com/watch?v=cDphUib5iG4"
|
||||||
private const val DEFAULT_TITLE = "Test Title"
|
private const val DEFAULT_TITLE = "Test Title"
|
||||||
private const val DEFAULT_NAME = "Test Name"
|
|
||||||
private val DEFAULT_TYPE = StreamType.VIDEO_STREAM
|
private val DEFAULT_TYPE = StreamType.VIDEO_STREAM
|
||||||
private const val DEFAULT_DURATION = 480L
|
private const val DEFAULT_DURATION = 480L
|
||||||
private const val DEFAULT_UPLOADER_NAME = "Uploader Test"
|
private const val DEFAULT_UPLOADER_NAME = "Uploader Test"
|
||||||
private const val DEFAULT_THUMBNAIL = "https://example.com/example.jpg"
|
private const val DEFAULT_THUMBNAIL = "https://example.com/example.jpg"
|
||||||
|
|
||||||
private const val DEFAULT_SECOND_SERVICE_ID = 1
|
private const val DEFAULT_SECOND_SERVICE_ID = 0
|
||||||
private const val DEFAULT_SECOND_URL = "https://www.youtube.com/watch?v=ncQU6iBn5Fc"
|
private const val DEFAULT_SECOND_URL = "https://www.youtube.com/watch?v=ncQU6iBn5Fc"
|
||||||
|
|
||||||
private const val DEFAULT_THIRD_SERVICE_ID = 2
|
|
||||||
private const val DEFAULT_THIRD_URL = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val testHelper = MigrationTestHelper(
|
val testHelper = MigrationTestHelper(
|
||||||
InstrumentationRegistry.getInstrumentation(),
|
InstrumentationRegistry.getInstrumentation(),
|
||||||
AppDatabase::class.java
|
AppDatabase::class.java.canonicalName,
|
||||||
|
FrameworkSQLiteOpenHelperFactory()
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -114,20 +108,6 @@ class DatabaseMigrationTest {
|
|||||||
Migrations.MIGRATION_6_7
|
Migrations.MIGRATION_6_7
|
||||||
)
|
)
|
||||||
|
|
||||||
testHelper.runMigrationsAndValidate(
|
|
||||||
AppDatabase.DATABASE_NAME,
|
|
||||||
Migrations.DB_VER_8,
|
|
||||||
true,
|
|
||||||
Migrations.MIGRATION_7_8
|
|
||||||
)
|
|
||||||
|
|
||||||
testHelper.runMigrationsAndValidate(
|
|
||||||
AppDatabase.DATABASE_NAME,
|
|
||||||
Migrations.DB_VER_9,
|
|
||||||
true,
|
|
||||||
Migrations.MIGRATION_8_9
|
|
||||||
)
|
|
||||||
|
|
||||||
val migratedDatabaseV3 = getMigratedDatabase()
|
val migratedDatabaseV3 = getMigratedDatabase()
|
||||||
val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst()
|
val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst()
|
||||||
|
|
||||||
@@ -162,157 +142,6 @@ class DatabaseMigrationTest {
|
|||||||
assertNull(secondStreamFromMigratedDatabase.isUploadDateApproximation)
|
assertNull(secondStreamFromMigratedDatabase.isUploadDateApproximation)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun migrateDatabaseFrom7to8() {
|
|
||||||
val databaseInV7 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_7)
|
|
||||||
|
|
||||||
val defaultSearch1 = " abc "
|
|
||||||
val defaultSearch2 = " abc"
|
|
||||||
|
|
||||||
val serviceId = DEFAULT_SERVICE_ID // YouTube
|
|
||||||
// Use id different to YouTube because two searches with the same query
|
|
||||||
// but different service are considered not equal.
|
|
||||||
val otherServiceId = ServiceList.SoundCloud.serviceId
|
|
||||||
|
|
||||||
databaseInV7.run {
|
|
||||||
insert(
|
|
||||||
"search_history", SQLiteDatabase.CONFLICT_FAIL,
|
|
||||||
ContentValues().apply {
|
|
||||||
put("service_id", serviceId)
|
|
||||||
put("search", defaultSearch1)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
insert(
|
|
||||||
"search_history", SQLiteDatabase.CONFLICT_FAIL,
|
|
||||||
ContentValues().apply {
|
|
||||||
put("service_id", serviceId)
|
|
||||||
put("search", defaultSearch2)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
insert(
|
|
||||||
"search_history", SQLiteDatabase.CONFLICT_FAIL,
|
|
||||||
ContentValues().apply {
|
|
||||||
put("service_id", otherServiceId)
|
|
||||||
put("search", defaultSearch1)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
insert(
|
|
||||||
"search_history", SQLiteDatabase.CONFLICT_FAIL,
|
|
||||||
ContentValues().apply {
|
|
||||||
put("service_id", otherServiceId)
|
|
||||||
put("search", defaultSearch2)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
|
|
||||||
testHelper.runMigrationsAndValidate(
|
|
||||||
AppDatabase.DATABASE_NAME, Migrations.DB_VER_8,
|
|
||||||
true, Migrations.MIGRATION_7_8
|
|
||||||
)
|
|
||||||
|
|
||||||
testHelper.runMigrationsAndValidate(
|
|
||||||
AppDatabase.DATABASE_NAME, Migrations.DB_VER_9,
|
|
||||||
true, Migrations.MIGRATION_8_9
|
|
||||||
)
|
|
||||||
|
|
||||||
val migratedDatabaseV8 = getMigratedDatabase()
|
|
||||||
val listFromDB = migratedDatabaseV8.searchHistoryDAO().all.blockingFirst()
|
|
||||||
|
|
||||||
assertEquals(2, listFromDB.size)
|
|
||||||
assertEquals("abc", listFromDB[0].search)
|
|
||||||
assertEquals("abc", listFromDB[1].search)
|
|
||||||
assertNotEquals(listFromDB[0].serviceId, listFromDB[1].serviceId)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun migrateDatabaseFrom8to9() {
|
|
||||||
val databaseInV8 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_8)
|
|
||||||
|
|
||||||
val localUid1: Long
|
|
||||||
val localUid2: Long
|
|
||||||
val remoteUid1: Long
|
|
||||||
val remoteUid2: Long
|
|
||||||
databaseInV8.run {
|
|
||||||
localUid1 = insert(
|
|
||||||
"playlists", SQLiteDatabase.CONFLICT_FAIL,
|
|
||||||
ContentValues().apply {
|
|
||||||
put("name", DEFAULT_NAME + "1")
|
|
||||||
put("is_thumbnail_permanent", false)
|
|
||||||
put("thumbnail_stream_id", -1)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
localUid2 = insert(
|
|
||||||
"playlists", SQLiteDatabase.CONFLICT_FAIL,
|
|
||||||
ContentValues().apply {
|
|
||||||
put("name", DEFAULT_NAME + "2")
|
|
||||||
put("is_thumbnail_permanent", false)
|
|
||||||
put("thumbnail_stream_id", -1)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
delete(
|
|
||||||
"playlists", "uid = ?",
|
|
||||||
Array(1) { localUid1 }
|
|
||||||
)
|
|
||||||
remoteUid1 = insert(
|
|
||||||
"remote_playlists", SQLiteDatabase.CONFLICT_FAIL,
|
|
||||||
ContentValues().apply {
|
|
||||||
put("service_id", DEFAULT_SERVICE_ID)
|
|
||||||
put("url", DEFAULT_URL)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
remoteUid2 = insert(
|
|
||||||
"remote_playlists", SQLiteDatabase.CONFLICT_FAIL,
|
|
||||||
ContentValues().apply {
|
|
||||||
put("service_id", DEFAULT_SECOND_SERVICE_ID)
|
|
||||||
put("url", DEFAULT_SECOND_URL)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
delete(
|
|
||||||
"remote_playlists", "uid = ?",
|
|
||||||
Array(1) { remoteUid2 }
|
|
||||||
)
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
|
|
||||||
testHelper.runMigrationsAndValidate(
|
|
||||||
AppDatabase.DATABASE_NAME,
|
|
||||||
Migrations.DB_VER_9,
|
|
||||||
true,
|
|
||||||
Migrations.MIGRATION_8_9
|
|
||||||
)
|
|
||||||
|
|
||||||
val migratedDatabaseV9 = getMigratedDatabase()
|
|
||||||
var localListFromDB = migratedDatabaseV9.playlistDAO().all.blockingFirst()
|
|
||||||
var remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().all.blockingFirst()
|
|
||||||
|
|
||||||
assertEquals(1, localListFromDB.size)
|
|
||||||
assertEquals(localUid2, localListFromDB[0].uid)
|
|
||||||
assertEquals(-1, localListFromDB[0].displayIndex)
|
|
||||||
assertEquals(1, remoteListFromDB.size)
|
|
||||||
assertEquals(remoteUid1, remoteListFromDB[0].uid)
|
|
||||||
assertEquals(-1, remoteListFromDB[0].displayIndex)
|
|
||||||
|
|
||||||
val localUid3 = migratedDatabaseV9.playlistDAO().insert(
|
|
||||||
PlaylistEntity(DEFAULT_NAME + "3", false, -1, -1)
|
|
||||||
)
|
|
||||||
val remoteUid3 = migratedDatabaseV9.playlistRemoteDAO().insert(
|
|
||||||
PlaylistRemoteEntity(
|
|
||||||
DEFAULT_THIRD_SERVICE_ID, DEFAULT_NAME, DEFAULT_THIRD_URL,
|
|
||||||
DEFAULT_THUMBNAIL, DEFAULT_UPLOADER_NAME, -1, 10
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
localListFromDB = migratedDatabaseV9.playlistDAO().all.blockingFirst()
|
|
||||||
remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().all.blockingFirst()
|
|
||||||
assertEquals(2, localListFromDB.size)
|
|
||||||
assertEquals(localUid3, localListFromDB[1].uid)
|
|
||||||
assertEquals(-1, localListFromDB[1].displayIndex)
|
|
||||||
assertEquals(2, remoteListFromDB.size)
|
|
||||||
assertEquals(remoteUid3, remoteListFromDB[1].uid)
|
|
||||||
assertEquals(-1, remoteListFromDB[1].displayIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getMigratedDatabase(): AppDatabase {
|
private fun getMigratedDatabase(): AppDatabase {
|
||||||
val database: AppDatabase = Room.databaseBuilder(
|
val database: AppDatabase = Room.databaseBuilder(
|
||||||
ApplicationProvider.getApplicationContext(),
|
ApplicationProvider.getApplicationContext(),
|
||||||
|
|||||||
@@ -1,130 +0,0 @@
|
|||||||
package org.schabi.newpipe.database
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.room.Room
|
|
||||||
import androidx.test.core.app.ApplicationProvider
|
|
||||||
import io.reactivex.rxjava3.core.Single
|
|
||||||
import org.junit.After
|
|
||||||
import org.junit.Assert.assertEquals
|
|
||||||
import org.junit.Assert.assertNotNull
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Test
|
|
||||||
import org.schabi.newpipe.database.feed.dao.FeedDAO
|
|
||||||
import org.schabi.newpipe.database.feed.model.FeedEntity
|
|
||||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
|
||||||
import org.schabi.newpipe.database.stream.StreamWithState
|
|
||||||
import org.schabi.newpipe.database.stream.dao.StreamDAO
|
|
||||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
|
||||||
import org.schabi.newpipe.database.subscription.SubscriptionDAO
|
|
||||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
|
||||||
import org.schabi.newpipe.extractor.ServiceList
|
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfo
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType
|
|
||||||
import java.io.IOException
|
|
||||||
import java.time.OffsetDateTime
|
|
||||||
import kotlin.streams.toList
|
|
||||||
|
|
||||||
class FeedDAOTest {
|
|
||||||
private lateinit var db: AppDatabase
|
|
||||||
private lateinit var feedDAO: FeedDAO
|
|
||||||
private lateinit var streamDAO: StreamDAO
|
|
||||||
private lateinit var subscriptionDAO: SubscriptionDAO
|
|
||||||
|
|
||||||
private val serviceId = ServiceList.YouTube.serviceId
|
|
||||||
|
|
||||||
private val stream1 = StreamEntity(1, serviceId, "https://youtube.com/watch?v=1", "stream 1", StreamType.VIDEO_STREAM, 1000, "channel-1", "https://youtube.com/channel/1", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-01-01", OffsetDateTime.parse("2023-01-01T00:00:00Z"))
|
|
||||||
private val stream2 = StreamEntity(2, serviceId, "https://youtube.com/watch?v=2", "stream 2", StreamType.VIDEO_STREAM, 1000, "channel-1", "https://youtube.com/channel/1", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-01-02", OffsetDateTime.parse("2023-01-02T00:00:00Z"))
|
|
||||||
private val stream3 = StreamEntity(3, serviceId, "https://youtube.com/watch?v=3", "stream 3", StreamType.LIVE_STREAM, 1000, "channel-1", "https://youtube.com/channel/1", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-01-03", OffsetDateTime.parse("2023-01-03T00:00:00Z"))
|
|
||||||
private val stream4 = StreamEntity(4, serviceId, "https://youtube.com/watch?v=4", "stream 4", StreamType.VIDEO_STREAM, 1000, "channel-2", "https://youtube.com/channel/2", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-08-10", OffsetDateTime.parse("2023-08-10T00:00:00Z"))
|
|
||||||
private val stream5 = StreamEntity(5, serviceId, "https://youtube.com/watch?v=5", "stream 5", StreamType.VIDEO_STREAM, 1000, "channel-2", "https://youtube.com/channel/2", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-08-20", OffsetDateTime.parse("2023-08-20T00:00:00Z"))
|
|
||||||
private val stream6 = StreamEntity(6, serviceId, "https://youtube.com/watch?v=6", "stream 6", StreamType.VIDEO_STREAM, 1000, "channel-3", "https://youtube.com/channel/3", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-09-01", OffsetDateTime.parse("2023-09-01T00:00:00Z"))
|
|
||||||
private val stream7 = StreamEntity(7, serviceId, "https://youtube.com/watch?v=7", "stream 7", StreamType.VIDEO_STREAM, 1000, "channel-4", "https://youtube.com/channel/4", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-08-10", OffsetDateTime.parse("2023-08-10T00:00:00Z"))
|
|
||||||
|
|
||||||
private val allStreams = listOf(
|
|
||||||
stream1, stream2, stream3, stream4, stream5, stream6, stream7
|
|
||||||
)
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun createDb() {
|
|
||||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
|
||||||
db = Room.inMemoryDatabaseBuilder(
|
|
||||||
context, AppDatabase::class.java
|
|
||||||
).build()
|
|
||||||
feedDAO = db.feedDAO()
|
|
||||||
streamDAO = db.streamDAO()
|
|
||||||
subscriptionDAO = db.subscriptionDAO()
|
|
||||||
}
|
|
||||||
|
|
||||||
@After
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun closeDb() {
|
|
||||||
db.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testUnlinkStreamsOlderThan_KeepOne() {
|
|
||||||
setupUnlinkDelete("2023-08-15T00:00:00Z")
|
|
||||||
val streams = feedDAO.getStreams(
|
|
||||||
FeedGroupEntity.GROUP_ALL_ID, includePlayed = true, includePartiallyPlayed = true, null
|
|
||||||
)
|
|
||||||
.blockingGet()
|
|
||||||
val allowedStreams = listOf(stream3, stream5, stream6, stream7)
|
|
||||||
assertEqual(streams, allowedStreams)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testUnlinkStreamsOlderThan_KeepMultiple() {
|
|
||||||
setupUnlinkDelete("2023-08-01T00:00:00Z")
|
|
||||||
val streams = feedDAO.getStreams(
|
|
||||||
FeedGroupEntity.GROUP_ALL_ID, includePlayed = true, includePartiallyPlayed = true, null
|
|
||||||
)
|
|
||||||
.blockingGet()
|
|
||||||
val allowedStreams = listOf(stream3, stream4, stream5, stream6, stream7)
|
|
||||||
assertEqual(streams, allowedStreams)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun assertEqual(streams: List<StreamWithState>?, allowedStreams: List<StreamEntity>) {
|
|
||||||
assertNotNull(streams)
|
|
||||||
assertEquals(
|
|
||||||
allowedStreams,
|
|
||||||
streams!!
|
|
||||||
.map { it.stream }
|
|
||||||
.sortedBy { it.uid }
|
|
||||||
.toList()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupUnlinkDelete(time: String) {
|
|
||||||
clearAndFillTables()
|
|
||||||
Single.fromCallable {
|
|
||||||
feedDAO.unlinkStreamsOlderThan(OffsetDateTime.parse(time))
|
|
||||||
}.blockingSubscribe()
|
|
||||||
Single.fromCallable {
|
|
||||||
streamDAO.deleteOrphans()
|
|
||||||
}.blockingSubscribe()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun clearAndFillTables() {
|
|
||||||
db.clearAllTables()
|
|
||||||
streamDAO.insertAll(allStreams)
|
|
||||||
subscriptionDAO.insertAll(
|
|
||||||
listOf(
|
|
||||||
SubscriptionEntity.from(ChannelInfo(serviceId, "1", "https://youtube.com/channel/1", "https://youtube.com/channel/1", "channel-1")),
|
|
||||||
SubscriptionEntity.from(ChannelInfo(serviceId, "2", "https://youtube.com/channel/2", "https://youtube.com/channel/2", "channel-2")),
|
|
||||||
SubscriptionEntity.from(ChannelInfo(serviceId, "3", "https://youtube.com/channel/3", "https://youtube.com/channel/3", "channel-3")),
|
|
||||||
SubscriptionEntity.from(ChannelInfo(serviceId, "4", "https://youtube.com/channel/4", "https://youtube.com/channel/4", "channel-4")),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
feedDAO.insertAll(
|
|
||||||
listOf(
|
|
||||||
FeedEntity(1, 1),
|
|
||||||
FeedEntity(2, 1),
|
|
||||||
FeedEntity(3, 1),
|
|
||||||
FeedEntity(4, 2),
|
|
||||||
FeedEntity(5, 2),
|
|
||||||
FeedEntity(6, 3),
|
|
||||||
FeedEntity(7, 4),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
package org.schabi.newpipe.local.subscription;
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
|
||||||
|
|
||||||
import androidx.test.core.app.ApplicationProvider;
|
|
||||||
|
|
||||||
import org.junit.After;
|
|
||||||
import org.junit.Before;
|
|
||||||
import org.junit.Rule;
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.schabi.newpipe.database.AppDatabase;
|
|
||||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity;
|
|
||||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
|
||||||
import org.schabi.newpipe.testUtil.TestDatabase;
|
|
||||||
import org.schabi.newpipe.testUtil.TrampolineSchedulerRule;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class SubscriptionManagerTest {
|
|
||||||
private AppDatabase database;
|
|
||||||
private SubscriptionManager manager;
|
|
||||||
|
|
||||||
@Rule
|
|
||||||
public TrampolineSchedulerRule trampolineScheduler = new TrampolineSchedulerRule();
|
|
||||||
|
|
||||||
|
|
||||||
private SubscriptionEntity getAssertOneSubscriptionEntity() {
|
|
||||||
final List<SubscriptionEntity> entities = manager
|
|
||||||
.getSubscriptions(FeedGroupEntity.GROUP_ALL_ID, "", false)
|
|
||||||
.blockingFirst();
|
|
||||||
assertEquals(1, entities.size());
|
|
||||||
return entities.get(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Before
|
|
||||||
public void setup() {
|
|
||||||
database = TestDatabase.Companion.createReplacingNewPipeDatabase();
|
|
||||||
manager = new SubscriptionManager(ApplicationProvider.getApplicationContext());
|
|
||||||
}
|
|
||||||
|
|
||||||
@After
|
|
||||||
public void cleanUp() {
|
|
||||||
database.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testInsert() throws ExtractionException, IOException {
|
|
||||||
final ChannelInfo info = ChannelInfo.getInfo("https://www.youtube.com/c/3blue1brown");
|
|
||||||
final SubscriptionEntity subscription = SubscriptionEntity.from(info);
|
|
||||||
|
|
||||||
manager.insertSubscription(subscription);
|
|
||||||
final SubscriptionEntity readSubscription = getAssertOneSubscriptionEntity();
|
|
||||||
|
|
||||||
// the uid has changed, since the uid is chosen upon inserting, but the rest should match
|
|
||||||
assertEquals(subscription.getServiceId(), readSubscription.getServiceId());
|
|
||||||
assertEquals(subscription.getUrl(), readSubscription.getUrl());
|
|
||||||
assertEquals(subscription.getName(), readSubscription.getName());
|
|
||||||
assertEquals(subscription.getAvatarUrl(), readSubscription.getAvatarUrl());
|
|
||||||
assertEquals(subscription.getSubscriberCount(), readSubscription.getSubscriberCount());
|
|
||||||
assertEquals(subscription.getDescription(), readSubscription.getDescription());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testUpdateNotificationMode() throws ExtractionException, IOException {
|
|
||||||
final ChannelInfo info = ChannelInfo.getInfo("https://www.youtube.com/c/veritasium");
|
|
||||||
final SubscriptionEntity subscription = SubscriptionEntity.from(info);
|
|
||||||
subscription.setNotificationMode(0);
|
|
||||||
|
|
||||||
manager.insertSubscription(subscription);
|
|
||||||
manager.updateNotificationMode(subscription.getServiceId(), subscription.getUrl(), 1)
|
|
||||||
.blockingAwait();
|
|
||||||
final SubscriptionEntity anotherSubscription = getAssertOneSubscriptionEntity();
|
|
||||||
|
|
||||||
assertEquals(0, subscription.getNotificationMode());
|
|
||||||
assertEquals(subscription.getUrl(), anotherSubscription.getUrl());
|
|
||||||
assertEquals(1, anotherSubscription.getNotificationMode());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,21 +12,15 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
|||||||
import androidx.test.filters.MediumTest
|
import androidx.test.filters.MediumTest
|
||||||
import androidx.test.internal.runner.junit4.statement.UiThreadStatement
|
import androidx.test.internal.runner.junit4.statement.UiThreadStatement
|
||||||
import org.junit.Assert
|
import org.junit.Assert
|
||||||
import org.junit.Assert.assertEquals
|
|
||||||
import org.junit.Assert.assertFalse
|
|
||||||
import org.junit.Assert.assertNull
|
|
||||||
import org.junit.Assert.assertTrue
|
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.extractor.MediaFormat
|
import org.schabi.newpipe.extractor.MediaFormat
|
||||||
import org.schabi.newpipe.extractor.downloader.Response
|
|
||||||
import org.schabi.newpipe.extractor.stream.AudioStream
|
import org.schabi.newpipe.extractor.stream.AudioStream
|
||||||
import org.schabi.newpipe.extractor.stream.Stream
|
import org.schabi.newpipe.extractor.stream.Stream
|
||||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream
|
import org.schabi.newpipe.extractor.stream.SubtitlesStream
|
||||||
import org.schabi.newpipe.extractor.stream.VideoStream
|
import org.schabi.newpipe.extractor.stream.VideoStream
|
||||||
import org.schabi.newpipe.util.StreamItemAdapter.StreamInfoWrapper
|
|
||||||
|
|
||||||
@MediumTest
|
@MediumTest
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
@@ -90,7 +84,7 @@ class StreamItemAdapterTest {
|
|||||||
@Test
|
@Test
|
||||||
fun subtitleStreams_noIcon() {
|
fun subtitleStreams_noIcon() {
|
||||||
val adapter = StreamItemAdapter<SubtitlesStream, Stream>(
|
val adapter = StreamItemAdapter<SubtitlesStream, Stream>(
|
||||||
StreamItemAdapter.StreamInfoWrapper(
|
StreamItemAdapter.StreamSizeWrapper(
|
||||||
(0 until 5).map {
|
(0 until 5).map {
|
||||||
SubtitlesStream.Builder()
|
SubtitlesStream.Builder()
|
||||||
.setContent("https://example.com", true)
|
.setContent("https://example.com", true)
|
||||||
@@ -111,7 +105,7 @@ class StreamItemAdapterTest {
|
|||||||
@Test
|
@Test
|
||||||
fun audioStreams_noIcon() {
|
fun audioStreams_noIcon() {
|
||||||
val adapter = StreamItemAdapter<AudioStream, Stream>(
|
val adapter = StreamItemAdapter<AudioStream, Stream>(
|
||||||
StreamItemAdapter.StreamInfoWrapper(
|
StreamItemAdapter.StreamSizeWrapper(
|
||||||
(0 until 5).map {
|
(0 until 5).map {
|
||||||
AudioStream.Builder()
|
AudioStream.Builder()
|
||||||
.setId(Stream.ID_UNKNOWN)
|
.setId(Stream.ID_UNKNOWN)
|
||||||
@@ -129,109 +123,12 @@ class StreamItemAdapterTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun retrieveMediaFormatFromFileTypeHeaders() {
|
|
||||||
val streams = getIncompleteAudioStreams(5)
|
|
||||||
val wrapper = StreamInfoWrapper(streams, context)
|
|
||||||
val retrieveMediaFormat = { stream: AudioStream, response: Response ->
|
|
||||||
StreamInfoWrapper.retrieveMediaFormatFromFileTypeHeaders(stream, wrapper, response)
|
|
||||||
}
|
|
||||||
val helper = AssertionHelper(streams, wrapper, retrieveMediaFormat)
|
|
||||||
|
|
||||||
helper.assertInvalidResponse(getResponse(mapOf(Pair("content-length", "mp3"))), 0)
|
|
||||||
helper.assertInvalidResponse(getResponse(mapOf(Pair("file-type", "mp0"))), 1)
|
|
||||||
|
|
||||||
helper.assertValidResponse(getResponse(mapOf(Pair("x-amz-meta-file-type", "aiff"))), 2, MediaFormat.AIFF)
|
|
||||||
helper.assertValidResponse(getResponse(mapOf(Pair("file-type", "mp3"))), 3, MediaFormat.MP3)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun retrieveMediaFormatFromContentDispositionHeader() {
|
|
||||||
val streams = getIncompleteAudioStreams(11)
|
|
||||||
val wrapper = StreamInfoWrapper(streams, context)
|
|
||||||
val retrieveMediaFormat = { stream: AudioStream, response: Response ->
|
|
||||||
StreamInfoWrapper.retrieveMediaFormatFromContentDispositionHeader(stream, wrapper, response)
|
|
||||||
}
|
|
||||||
val helper = AssertionHelper(streams, wrapper, retrieveMediaFormat)
|
|
||||||
|
|
||||||
helper.assertInvalidResponse(getResponse(mapOf(Pair("content-length", "mp3"))), 0)
|
|
||||||
helper.assertInvalidResponse(
|
|
||||||
getResponse(mapOf(Pair("Content-Disposition", "filename=\"train.png\""))), 1
|
|
||||||
)
|
|
||||||
helper.assertInvalidResponse(
|
|
||||||
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"data.csv\""))), 2
|
|
||||||
)
|
|
||||||
helper.assertInvalidResponse(
|
|
||||||
getResponse(mapOf(Pair("Content-Disposition", "form-data; filename=\"data.csv\""))), 3
|
|
||||||
)
|
|
||||||
helper.assertInvalidResponse(
|
|
||||||
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"fieldName\"; filename*=\"filename.jpg\""))), 4
|
|
||||||
)
|
|
||||||
|
|
||||||
helper.assertValidResponse(
|
|
||||||
getResponse(mapOf(Pair("Content-Disposition", "filename=\"train.ogg\""))),
|
|
||||||
5, MediaFormat.OGG
|
|
||||||
)
|
|
||||||
helper.assertValidResponse(
|
|
||||||
getResponse(mapOf(Pair("Content-Disposition", "some-form-data; filename=\"audio.flac\""))),
|
|
||||||
6, MediaFormat.FLAC
|
|
||||||
)
|
|
||||||
helper.assertValidResponse(
|
|
||||||
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"audio.aiff\"; filename=\"audio.aiff\""))),
|
|
||||||
7, MediaFormat.AIFF
|
|
||||||
)
|
|
||||||
helper.assertValidResponse(
|
|
||||||
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"alien?\"; filename*=UTF-8''%CE%B1%CE%BB%CE%B9%CF%B5%CE%BD.m4a"))),
|
|
||||||
8, MediaFormat.M4A
|
|
||||||
)
|
|
||||||
helper.assertValidResponse(
|
|
||||||
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"audio.mp3\"; filename=\"audio.opus\"; filename*=UTF-8''alien.opus"))),
|
|
||||||
9, MediaFormat.OPUS
|
|
||||||
)
|
|
||||||
helper.assertValidResponse(
|
|
||||||
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"audio.mp3\"; filename=\"audio.opus\"; filename*=\"UTF-8''alien.opus\""))),
|
|
||||||
10, MediaFormat.OPUS
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun retrieveMediaFormatFromContentTypeHeader() {
|
|
||||||
val streams = getIncompleteAudioStreams(12)
|
|
||||||
val wrapper = StreamInfoWrapper(streams, context)
|
|
||||||
val retrieveMediaFormat = { stream: AudioStream, response: Response ->
|
|
||||||
StreamInfoWrapper.retrieveMediaFormatFromContentTypeHeader(stream, wrapper, response)
|
|
||||||
}
|
|
||||||
val helper = AssertionHelper(streams, wrapper, retrieveMediaFormat)
|
|
||||||
|
|
||||||
helper.assertInvalidResponse(getResponse(mapOf(Pair("content-length", "984501"))), 0)
|
|
||||||
helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "audio/xyz"))), 1)
|
|
||||||
helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "mp3"))), 2)
|
|
||||||
helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "mp3"))), 3)
|
|
||||||
helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "audio/mpeg"))), 4)
|
|
||||||
helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "audio/aif"))), 5)
|
|
||||||
helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "whatever"))), 6)
|
|
||||||
helper.assertInvalidResponse(getResponse(mapOf()), 7)
|
|
||||||
|
|
||||||
helper.assertValidResponse(
|
|
||||||
getResponse(mapOf(Pair("Content-Type", "audio/flac"))), 8, MediaFormat.FLAC
|
|
||||||
)
|
|
||||||
helper.assertValidResponse(
|
|
||||||
getResponse(mapOf(Pair("Content-Type", "audio/wav"))), 9, MediaFormat.WAV
|
|
||||||
)
|
|
||||||
helper.assertValidResponse(
|
|
||||||
getResponse(mapOf(Pair("Content-Type", "audio/opus"))), 10, MediaFormat.OPUS
|
|
||||||
)
|
|
||||||
helper.assertValidResponse(
|
|
||||||
getResponse(mapOf(Pair("Content-Type", "audio/aiff"))), 11, MediaFormat.AIFF
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return a list of video streams, in which their video only property mirrors the provided
|
* @return a list of video streams, in which their video only property mirrors the provided
|
||||||
* [videoOnly] vararg.
|
* [videoOnly] vararg.
|
||||||
*/
|
*/
|
||||||
private fun getVideoStreams(vararg videoOnly: Boolean) =
|
private fun getVideoStreams(vararg videoOnly: Boolean) =
|
||||||
StreamItemAdapter.StreamInfoWrapper(
|
StreamItemAdapter.StreamSizeWrapper(
|
||||||
videoOnly.map {
|
videoOnly.map {
|
||||||
VideoStream.Builder()
|
VideoStream.Builder()
|
||||||
.setId(Stream.ID_UNKNOWN)
|
.setId(Stream.ID_UNKNOWN)
|
||||||
@@ -264,19 +161,6 @@ class StreamItemAdapterTest {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun getIncompleteAudioStreams(size: Int): List<AudioStream> {
|
|
||||||
val list = ArrayList<AudioStream>(size)
|
|
||||||
for (i in 1..size) {
|
|
||||||
list.add(
|
|
||||||
AudioStream.Builder()
|
|
||||||
.setId(Stream.ID_UNKNOWN)
|
|
||||||
.setContent("https://example.com/$i", true)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return list
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether the item at [position] in the [spinner] has the correct icon visibility when
|
* Checks whether the item at [position] in the [spinner] has the correct icon visibility when
|
||||||
* it is shown in normal mode (selected) and in dropdown mode (user is choosing one of a list).
|
* it is shown in normal mode (selected) and in dropdown mode (user is choosing one of a list).
|
||||||
@@ -312,56 +196,11 @@ class StreamItemAdapterTest {
|
|||||||
streams.forEachIndexed { index, stream ->
|
streams.forEachIndexed { index, stream ->
|
||||||
val secondaryStreamHelper: SecondaryStreamHelper<T>? = stream?.let {
|
val secondaryStreamHelper: SecondaryStreamHelper<T>? = stream?.let {
|
||||||
SecondaryStreamHelper(
|
SecondaryStreamHelper(
|
||||||
StreamItemAdapter.StreamInfoWrapper(streams, context),
|
StreamItemAdapter.StreamSizeWrapper(streams, context),
|
||||||
it
|
it
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
put(index, secondaryStreamHelper)
|
put(index, secondaryStreamHelper)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getResponse(headers: Map<String, String>): Response {
|
|
||||||
val listHeaders = HashMap<String, List<String>>()
|
|
||||||
headers.forEach { entry ->
|
|
||||||
listHeaders[entry.key] = listOf(entry.value)
|
|
||||||
}
|
|
||||||
return Response(200, null, listHeaders, "", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper class for assertion related to extractions of [MediaFormat]s.
|
|
||||||
*/
|
|
||||||
class AssertionHelper<T : Stream>(
|
|
||||||
private val streams: List<T>,
|
|
||||||
private val wrapper: StreamInfoWrapper<T>,
|
|
||||||
private val retrieveMediaFormat: (stream: T, response: Response) -> Boolean
|
|
||||||
) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assert that an invalid response does not result in wrongly extracted [MediaFormat].
|
|
||||||
*/
|
|
||||||
fun assertInvalidResponse(
|
|
||||||
response: Response,
|
|
||||||
index: Int
|
|
||||||
) {
|
|
||||||
assertFalse(
|
|
||||||
"invalid header returns valid value", retrieveMediaFormat(streams[index], response)
|
|
||||||
)
|
|
||||||
assertNull("Media format extracted although stated otherwise", wrapper.getFormat(index))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assert that a valid response results in correctly extracted and handled [MediaFormat].
|
|
||||||
*/
|
|
||||||
fun assertValidResponse(
|
|
||||||
response: Response,
|
|
||||||
index: Int,
|
|
||||||
format: MediaFormat
|
|
||||||
) {
|
|
||||||
assertTrue(
|
|
||||||
"header was not recognized", retrieveMediaFormat(streams[index], response)
|
|
||||||
)
|
|
||||||
assertEquals("Wrong media format extracted", format, wrapper.getFormat(index))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package org.schabi.newpipe
|
|||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.facebook.stetho.Stetho
|
import com.facebook.stetho.Stetho
|
||||||
import com.facebook.stetho.okhttp3.StethoInterceptor
|
import com.facebook.stetho.okhttp3.StethoInterceptor
|
||||||
|
import leakcanary.AppWatcher
|
||||||
import leakcanary.LeakCanary
|
import leakcanary.LeakCanary
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import org.schabi.newpipe.extractor.downloader.Downloader
|
import org.schabi.newpipe.extractor.downloader.Downloader
|
||||||
@@ -12,6 +13,8 @@ class DebugApp : App() {
|
|||||||
super.onCreate()
|
super.onCreate()
|
||||||
initStetho()
|
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(
|
LeakCanary.config = LeakCanary.config.copy(
|
||||||
dumpHeap = PreferenceManager
|
dumpHeap = PreferenceManager
|
||||||
.getDefaultSharedPreferences(this).getBoolean(
|
.getDefaultSharedPreferences(this).getBoolean(
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import android.view.ViewGroup;
|
|||||||
import androidx.annotation.IntDef;
|
import androidx.annotation.IntDef;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.core.os.BundleCompat;
|
|
||||||
import androidx.lifecycle.Lifecycle;
|
import androidx.lifecycle.Lifecycle;
|
||||||
import androidx.viewpager.widget.PagerAdapter;
|
import androidx.viewpager.widget.PagerAdapter;
|
||||||
|
|
||||||
@@ -285,7 +284,7 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
|||||||
Bundle state = null;
|
Bundle state = null;
|
||||||
if (!mSavedState.isEmpty()) {
|
if (!mSavedState.isEmpty()) {
|
||||||
state = new Bundle();
|
state = new Bundle();
|
||||||
state.putParcelableArrayList("states", mSavedState);
|
state.putParcelableArray("states", mSavedState.toArray(new Fragment.SavedState[0]));
|
||||||
}
|
}
|
||||||
for (int i = 0; i < mFragments.size(); i++) {
|
for (int i = 0; i < mFragments.size(); i++) {
|
||||||
final Fragment f = mFragments.get(i);
|
final Fragment f = mFragments.get(i);
|
||||||
@@ -312,12 +311,13 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
|||||||
if (state != null) {
|
if (state != null) {
|
||||||
final Bundle bundle = (Bundle) state;
|
final Bundle bundle = (Bundle) state;
|
||||||
bundle.setClassLoader(loader);
|
bundle.setClassLoader(loader);
|
||||||
final var states = BundleCompat.getParcelableArrayList(bundle, "states",
|
final Parcelable[] fss = bundle.getParcelableArray("states");
|
||||||
Fragment.SavedState.class);
|
|
||||||
mSavedState.clear();
|
mSavedState.clear();
|
||||||
mFragments.clear();
|
mFragments.clear();
|
||||||
if (states != null) {
|
if (fss != null) {
|
||||||
mSavedState.addAll(states);
|
for (final Parcelable parcelable : fss) {
|
||||||
|
mSavedState.add((Fragment.SavedState) parcelable);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
final Iterable<String> keys = bundle.keySet();
|
final Iterable<String> keys = bundle.keySet();
|
||||||
for (final String key : keys) {
|
for (final String key : keys) {
|
||||||
|
|||||||
@@ -20,11 +20,9 @@ import org.schabi.newpipe.extractor.downloader.Downloader;
|
|||||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
import org.schabi.newpipe.util.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
import org.schabi.newpipe.util.StateSaver;
|
import org.schabi.newpipe.util.StateSaver;
|
||||||
import org.schabi.newpipe.util.image.PreferredImageQuality;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InterruptedIOException;
|
import java.io.InterruptedIOException;
|
||||||
@@ -60,8 +58,6 @@ import io.reactivex.rxjava3.plugins.RxJavaPlugins;
|
|||||||
public class App extends Application {
|
public class App extends Application {
|
||||||
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID;
|
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID;
|
||||||
private static final String TAG = App.class.toString();
|
private static final String TAG = App.class.toString();
|
||||||
|
|
||||||
private boolean isFirstRun = false;
|
|
||||||
private static App app;
|
private static App app;
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@@ -87,13 +83,7 @@ public class App extends Application {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if the last used preference version is set
|
// Initialize settings first because others inits can use its values
|
||||||
// to determine whether this is the first app run
|
|
||||||
final int lastUsedPrefVersion = PreferenceManager.getDefaultSharedPreferences(this)
|
|
||||||
.getInt(getString(R.string.last_used_preferences_version), -1);
|
|
||||||
isFirstRun = lastUsedPrefVersion == -1;
|
|
||||||
|
|
||||||
// Initialize settings first because other initializations can use its values
|
|
||||||
NewPipeSettings.initSettings(this);
|
NewPipeSettings.initSettings(this);
|
||||||
|
|
||||||
NewPipe.init(getDownloader(),
|
NewPipe.init(getDownloader(),
|
||||||
@@ -109,9 +99,8 @@ public class App extends Application {
|
|||||||
// Initialize image loader
|
// Initialize image loader
|
||||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||||
PicassoHelper.init(this);
|
PicassoHelper.init(this);
|
||||||
ImageStrategy.setPreferredImageQuality(PreferredImageQuality.fromPreferenceKey(this,
|
PicassoHelper.setShouldLoadImages(
|
||||||
prefs.getString(getString(R.string.image_quality_key),
|
prefs.getBoolean(getString(R.string.download_thumbnail_key), true));
|
||||||
getString(R.string.image_quality_default))));
|
|
||||||
PicassoHelper.setIndicatorsEnabled(MainActivity.DEBUG
|
PicassoHelper.setIndicatorsEnabled(MainActivity.DEBUG
|
||||||
&& prefs.getBoolean(getString(R.string.show_image_indicators_key), false));
|
&& prefs.getBoolean(getString(R.string.show_image_indicators_key), false));
|
||||||
|
|
||||||
@@ -263,7 +252,4 @@ public class App extends Application {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isFirstRun() {
|
|
||||||
return isFirstRun;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import androidx.fragment.app.FragmentManager;
|
|||||||
|
|
||||||
import icepick.Icepick;
|
import icepick.Icepick;
|
||||||
import icepick.State;
|
import icepick.State;
|
||||||
|
import leakcanary.AppWatcher;
|
||||||
|
|
||||||
public abstract class BaseFragment extends Fragment {
|
public abstract class BaseFragment extends Fragment {
|
||||||
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
|
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
|
||||||
@@ -76,33 +77,20 @@ public abstract class BaseFragment extends Fragment {
|
|||||||
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
|
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
|
||||||
|
AppWatcher.INSTANCE.getObjectWatcher().watch(this);
|
||||||
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Init
|
// Init
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
/**
|
|
||||||
* This method is called in {@link #onViewCreated(View, Bundle)} to initialize the views.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* {@link #initListeners()} is called after this method to initialize the corresponding
|
|
||||||
* listeners.
|
|
||||||
* </p>
|
|
||||||
* @param rootView The inflated view for this fragment
|
|
||||||
* (provided by {@link #onViewCreated(View, Bundle)})
|
|
||||||
* @param savedInstanceState The saved state of this fragment
|
|
||||||
* (provided by {@link #onViewCreated(View, Bundle)})
|
|
||||||
*/
|
|
||||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the listeners for this fragment.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* This method is called after {@link #initViews(View, Bundle)}
|
|
||||||
* in {@link #onViewCreated(View, Bundle)}.
|
|
||||||
* </p>
|
|
||||||
*/
|
|
||||||
protected void initListeners() {
|
protected void initListeners() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,20 +108,9 @@ public abstract class BaseFragment extends Fragment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds the root fragment by looping through all of the parent fragments. The root fragment
|
|
||||||
* is supposed to be {@link org.schabi.newpipe.fragments.MainFragment}, and is the fragment that
|
|
||||||
* handles keeping the backstack of opened fragments in NewPipe, and also the player bottom
|
|
||||||
* sheet. This function therefore returns the fragment manager of said fragment.
|
|
||||||
*
|
|
||||||
* @return the fragment manager of the root fragment, i.e.
|
|
||||||
* {@link org.schabi.newpipe.fragments.MainFragment}
|
|
||||||
*/
|
|
||||||
protected FragmentManager getFM() {
|
protected FragmentManager getFM() {
|
||||||
Fragment current = this;
|
return getParentFragment() == null
|
||||||
while (current.getParentFragment() != null) {
|
? getFragmentManager()
|
||||||
current = current.getParentFragment();
|
: getParentFragment().getFragmentManager();
|
||||||
}
|
|
||||||
return current.getFragmentManager();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ import android.widget.FrameLayout;
|
|||||||
import android.widget.Spinner;
|
import android.widget.Spinner;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.ActionBar;
|
import androidx.appcompat.app.ActionBar;
|
||||||
import androidx.appcompat.app.ActionBarDrawerToggle;
|
import androidx.appcompat.app.ActionBarDrawerToggle;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
@@ -52,7 +51,6 @@ import androidx.core.app.ActivityCompat;
|
|||||||
import androidx.core.view.GravityCompat;
|
import androidx.core.view.GravityCompat;
|
||||||
import androidx.drawerlayout.widget.DrawerLayout;
|
import androidx.drawerlayout.widget.DrawerLayout;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentContainerView;
|
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
@@ -65,21 +63,19 @@ import org.schabi.newpipe.databinding.InstanceSpinnerLayoutBinding;
|
|||||||
import org.schabi.newpipe.databinding.ToolbarLayoutBinding;
|
import org.schabi.newpipe.databinding.ToolbarLayoutBinding;
|
||||||
import org.schabi.newpipe.error.ErrorUtil;
|
import org.schabi.newpipe.error.ErrorUtil;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
|
import org.schabi.newpipe.extractor.ServiceList;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
|
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
|
||||||
import org.schabi.newpipe.fragments.BackPressable;
|
import org.schabi.newpipe.fragments.BackPressable;
|
||||||
import org.schabi.newpipe.fragments.MainFragment;
|
import org.schabi.newpipe.fragments.MainFragment;
|
||||||
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
||||||
import org.schabi.newpipe.fragments.list.comments.CommentRepliesFragment;
|
|
||||||
import org.schabi.newpipe.fragments.list.search.SearchFragment;
|
import org.schabi.newpipe.fragments.list.search.SearchFragment;
|
||||||
import org.schabi.newpipe.local.feed.notifications.NotificationWorker;
|
import org.schabi.newpipe.local.feed.notifications.NotificationWorker;
|
||||||
import org.schabi.newpipe.player.Player;
|
import org.schabi.newpipe.player.Player;
|
||||||
import org.schabi.newpipe.player.event.OnKeyDownListener;
|
import org.schabi.newpipe.player.event.OnKeyDownListener;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.settings.UpdateSettingsFragment;
|
|
||||||
import org.schabi.newpipe.util.Constants;
|
import org.schabi.newpipe.util.Constants;
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
import org.schabi.newpipe.util.KioskTranslator;
|
import org.schabi.newpipe.util.KioskTranslator;
|
||||||
@@ -87,7 +83,6 @@ import org.schabi.newpipe.util.Localization;
|
|||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.PeertubeHelper;
|
import org.schabi.newpipe.util.PeertubeHelper;
|
||||||
import org.schabi.newpipe.util.PermissionHelper;
|
import org.schabi.newpipe.util.PermissionHelper;
|
||||||
import org.schabi.newpipe.util.ReleaseVersionUtil;
|
|
||||||
import org.schabi.newpipe.util.SerializedCache;
|
import org.schabi.newpipe.util.SerializedCache;
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
import org.schabi.newpipe.util.StateSaver;
|
import org.schabi.newpipe.util.StateSaver;
|
||||||
@@ -169,11 +164,6 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
// if this is enabled by the user.
|
// if this is enabled by the user.
|
||||||
NotificationWorker.initialize(this);
|
NotificationWorker.initialize(this);
|
||||||
}
|
}
|
||||||
if (!UpdateSettingsFragment.wasUserAskedForConsent(this)
|
|
||||||
&& !App.getApp().isFirstRun()
|
|
||||||
&& ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
|
|
||||||
UpdateSettingsFragment.askForConsentToUpdateChecks(this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -183,8 +173,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
final App app = App.getApp();
|
final App app = App.getApp();
|
||||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
|
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
|
||||||
|
|
||||||
if (prefs.getBoolean(app.getString(R.string.update_app_key), false)
|
if (prefs.getBoolean(app.getString(R.string.update_app_key), true)) {
|
||||||
&& prefs.getBoolean(app.getString(R.string.update_check_consent_key), false)) {
|
|
||||||
// Start the worker which is checking all conditions
|
// Start the worker which is checking all conditions
|
||||||
// and eventually searching for a new version.
|
// and eventually searching for a new version.
|
||||||
NewVersionWorker.enqueueNewVersionCheckingWork(app, false);
|
NewVersionWorker.enqueueNewVersionCheckingWork(app, false);
|
||||||
@@ -231,14 +220,14 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
|
final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||||
final StreamingService service = NewPipe.getService(currentServiceId);
|
final StreamingService service = NewPipe.getService(currentServiceId);
|
||||||
|
|
||||||
int kioskMenuItemId = 0;
|
int kioskId = 0;
|
||||||
|
|
||||||
for (final String ks : service.getKioskList().getAvailableKiosks()) {
|
for (final String ks : service.getKioskList().getAvailableKiosks()) {
|
||||||
drawerLayoutBinding.navigation.getMenu()
|
drawerLayoutBinding.navigation.getMenu()
|
||||||
.add(R.id.menu_tabs_group, kioskMenuItemId, 0, KioskTranslator
|
.add(R.id.menu_tabs_group, kioskId, 0, KioskTranslator
|
||||||
.getTranslatedKioskName(ks, this))
|
.getTranslatedKioskName(ks, this))
|
||||||
.setIcon(KioskTranslator.getKioskIcon(ks));
|
.setIcon(KioskTranslator.getKioskIcon(ks));
|
||||||
kioskMenuItemId++;
|
kioskId++;
|
||||||
}
|
}
|
||||||
|
|
||||||
drawerLayoutBinding.navigation.getMenu()
|
drawerLayoutBinding.navigation.getMenu()
|
||||||
@@ -270,8 +259,15 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
private boolean drawerItemSelected(final MenuItem item) {
|
private boolean drawerItemSelected(final MenuItem item) {
|
||||||
switch (item.getGroupId()) {
|
switch (item.getGroupId()) {
|
||||||
case R.id.menu_services_group:
|
case R.id.menu_services_group:
|
||||||
changeService(item);
|
if (item.getItemId() == ServiceList.PeerTube.getServiceId()
|
||||||
break;
|
&& DeviceUtils.isTv(getApplicationContext())
|
||||||
|
&& !item.isActionViewExpanded()) {
|
||||||
|
((Spinner) item.getActionView()).performClick();
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
changeService(item);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case R.id.menu_tabs_group:
|
case R.id.menu_tabs_group:
|
||||||
try {
|
try {
|
||||||
tabSelected(item);
|
tabSelected(item);
|
||||||
@@ -318,16 +314,20 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
NavigationHelper.openStatisticFragment(getSupportFragmentManager());
|
NavigationHelper.openStatisticFragment(getSupportFragmentManager());
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
final StreamingService currentService = ServiceHelper.getSelectedService(this);
|
final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||||
int kioskMenuItemId = 0;
|
final StreamingService service = NewPipe.getService(currentServiceId);
|
||||||
for (final String kioskId : currentService.getKioskList().getAvailableKiosks()) {
|
String serviceName = "";
|
||||||
if (kioskMenuItemId == item.getItemId()) {
|
|
||||||
NavigationHelper.openKioskFragment(getSupportFragmentManager(),
|
int kioskId = 0;
|
||||||
currentService.getServiceId(), kioskId);
|
for (final String ks : service.getKioskList().getAvailableKiosks()) {
|
||||||
break;
|
if (kioskId == item.getItemId()) {
|
||||||
|
serviceName = ks;
|
||||||
}
|
}
|
||||||
kioskMenuItemId++;
|
kioskId++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NavigationHelper.openKioskFragment(getSupportFragmentManager(), currentServiceId,
|
||||||
|
serviceName);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -391,8 +391,8 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
.add(R.id.menu_services_group, s.getServiceId(), ORDER, title)
|
.add(R.id.menu_services_group, s.getServiceId(), ORDER, title)
|
||||||
.setIcon(ServiceHelper.getIcon(s.getServiceId()));
|
.setIcon(ServiceHelper.getIcon(s.getServiceId()));
|
||||||
|
|
||||||
// peertube specifics
|
// PeerTube specifics
|
||||||
if (s.getServiceId() == 3) {
|
if (s == ServiceList.PeerTube) {
|
||||||
enhancePeertubeMenu(menuItem);
|
enhancePeertubeMenu(menuItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -558,21 +558,14 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
// interacts with a fragment inside fragment_holder so all back presses should be
|
// interacts with a fragment inside fragment_holder so all back presses should be
|
||||||
// handled by it
|
// handled by it
|
||||||
if (bottomSheetHiddenOrCollapsed()) {
|
if (bottomSheetHiddenOrCollapsed()) {
|
||||||
final FragmentManager fm = getSupportFragmentManager();
|
final Fragment fragment = getSupportFragmentManager()
|
||||||
final Fragment fragment = fm.findFragmentById(R.id.fragment_holder);
|
.findFragmentById(R.id.fragment_holder);
|
||||||
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
|
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
|
||||||
// delegate the back press to it
|
// delegate the back press to it
|
||||||
if (fragment instanceof BackPressable) {
|
if (fragment instanceof BackPressable) {
|
||||||
if (((BackPressable) fragment).onBackPressed()) {
|
if (((BackPressable) fragment).onBackPressed()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if (fragment instanceof CommentRepliesFragment) {
|
|
||||||
// expand DetailsFragment if CommentRepliesFragment was opened
|
|
||||||
// to show the top level comments again
|
|
||||||
// Expand DetailsFragment if CommentRepliesFragment was opened
|
|
||||||
// and no other CommentRepliesFragments are on top of the back stack
|
|
||||||
// to show the top level comments again.
|
|
||||||
openDetailFragmentFromCommentReplies(fm, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
@@ -648,17 +641,10 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
* </pre>
|
* </pre>
|
||||||
*/
|
*/
|
||||||
private void onHomeButtonPressed() {
|
private void onHomeButtonPressed() {
|
||||||
final FragmentManager fm = getSupportFragmentManager();
|
// If search fragment wasn't found in the backstack...
|
||||||
final Fragment fragment = fm.findFragmentById(R.id.fragment_holder);
|
if (!NavigationHelper.tryGotoSearchFragment(getSupportFragmentManager())) {
|
||||||
|
// ...go to the main fragment
|
||||||
if (fragment instanceof CommentRepliesFragment) {
|
NavigationHelper.gotoMainFragment(getSupportFragmentManager());
|
||||||
// Expand DetailsFragment if CommentRepliesFragment was opened
|
|
||||||
// and no other CommentRepliesFragments are on top of the back stack
|
|
||||||
// to show the top level comments again.
|
|
||||||
openDetailFragmentFromCommentReplies(fm, true);
|
|
||||||
} else if (!NavigationHelper.tryGotoSearchFragment(fm)) {
|
|
||||||
// If search fragment wasn't found in the backstack go to the main fragment
|
|
||||||
NavigationHelper.gotoMainFragment(fm);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -854,68 +840,6 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openDetailFragmentFromCommentReplies(
|
|
||||||
@NonNull final FragmentManager fm,
|
|
||||||
final boolean popBackStack
|
|
||||||
) {
|
|
||||||
// obtain the name of the fragment under the replies fragment that's going to be popped
|
|
||||||
@Nullable final String fragmentUnderEntryName;
|
|
||||||
if (fm.getBackStackEntryCount() < 2) {
|
|
||||||
fragmentUnderEntryName = null;
|
|
||||||
} else {
|
|
||||||
fragmentUnderEntryName = fm.getBackStackEntryAt(fm.getBackStackEntryCount() - 2)
|
|
||||||
.getName();
|
|
||||||
}
|
|
||||||
|
|
||||||
// the root comment is the comment for which the user opened the replies page
|
|
||||||
@Nullable final CommentRepliesFragment repliesFragment =
|
|
||||||
(CommentRepliesFragment) fm.findFragmentByTag(CommentRepliesFragment.TAG);
|
|
||||||
@Nullable final CommentsInfoItem rootComment =
|
|
||||||
repliesFragment == null ? null : repliesFragment.getCommentsInfoItem();
|
|
||||||
|
|
||||||
// sometimes this function pops the backstack, other times it's handled by the system
|
|
||||||
if (popBackStack) {
|
|
||||||
fm.popBackStackImmediate();
|
|
||||||
}
|
|
||||||
|
|
||||||
// only expand the bottom sheet back if there are no more nested comment replies fragments
|
|
||||||
// stacked under the one that is currently being popped
|
|
||||||
if (CommentRepliesFragment.TAG.equals(fragmentUnderEntryName)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final BottomSheetBehavior<FragmentContainerView> behavior = BottomSheetBehavior
|
|
||||||
.from(mainBinding.fragmentPlayerHolder);
|
|
||||||
// do not return to the comment if the details fragment was closed
|
|
||||||
if (behavior.getState() == BottomSheetBehavior.STATE_HIDDEN) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// scroll to the root comment once the bottom sheet expansion animation is finished
|
|
||||||
behavior.addBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
|
|
||||||
@Override
|
|
||||||
public void onStateChanged(@NonNull final View bottomSheet,
|
|
||||||
final int newState) {
|
|
||||||
if (newState == BottomSheetBehavior.STATE_EXPANDED) {
|
|
||||||
final Fragment detailFragment = fm.findFragmentById(
|
|
||||||
R.id.fragment_player_holder);
|
|
||||||
if (detailFragment instanceof VideoDetailFragment && rootComment != null) {
|
|
||||||
// should always be the case
|
|
||||||
((VideoDetailFragment) detailFragment).scrollToComment(rootComment);
|
|
||||||
}
|
|
||||||
behavior.removeBottomSheetCallback(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSlide(@NonNull final View bottomSheet, final float slideOffset) {
|
|
||||||
// not needed, listener is removed once the sheet is expanded
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean bottomSheetHiddenOrCollapsed() {
|
private boolean bottomSheetHiddenOrCollapsed() {
|
||||||
final BottomSheetBehavior<FrameLayout> bottomSheetBehavior =
|
final BottomSheetBehavior<FrameLayout> bottomSheetBehavior =
|
||||||
BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder);
|
BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder);
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import static org.schabi.newpipe.database.Migrations.MIGRATION_3_4;
|
|||||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_4_5;
|
import static org.schabi.newpipe.database.Migrations.MIGRATION_4_5;
|
||||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_5_6;
|
import static org.schabi.newpipe.database.Migrations.MIGRATION_5_6;
|
||||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_6_7;
|
import static org.schabi.newpipe.database.Migrations.MIGRATION_6_7;
|
||||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_7_8;
|
|
||||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_8_9;
|
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
@@ -29,7 +27,7 @@ public final class NewPipeDatabase {
|
|||||||
return Room
|
return Room
|
||||||
.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
|
.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
|
||||||
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5,
|
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5,
|
||||||
MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9)
|
MIGRATION_5_6, MIGRATION_6_7)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ import com.grack.nanojson.JsonParser
|
|||||||
import com.grack.nanojson.JsonParserException
|
import com.grack.nanojson.JsonParserException
|
||||||
import org.schabi.newpipe.extractor.downloader.Response
|
import org.schabi.newpipe.extractor.downloader.Response
|
||||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
|
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
|
||||||
import org.schabi.newpipe.util.ReleaseVersionUtil
|
import org.schabi.newpipe.util.ReleaseVersionUtil.coerceUpdateCheckExpiry
|
||||||
|
import org.schabi.newpipe.util.ReleaseVersionUtil.isLastUpdateCheckExpired
|
||||||
|
import org.schabi.newpipe.util.ReleaseVersionUtil.isReleaseApk
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
class NewVersionWorker(
|
class NewVersionWorker(
|
||||||
@@ -82,7 +84,7 @@ class NewVersionWorker(
|
|||||||
@Throws(IOException::class, ReCaptchaException::class)
|
@Throws(IOException::class, ReCaptchaException::class)
|
||||||
private fun checkNewVersion() {
|
private fun checkNewVersion() {
|
||||||
// Check if the current apk is a github one or not.
|
// Check if the current apk is a github one or not.
|
||||||
if (!ReleaseVersionUtil.isReleaseApk) {
|
if (!isReleaseApk()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +93,7 @@ class NewVersionWorker(
|
|||||||
// Check if the last request has happened a certain time ago
|
// Check if the last request has happened a certain time ago
|
||||||
// to reduce the number of API requests.
|
// to reduce the number of API requests.
|
||||||
val expiry = prefs.getLong(applicationContext.getString(R.string.update_expiry_key), 0)
|
val expiry = prefs.getLong(applicationContext.getString(R.string.update_expiry_key), 0)
|
||||||
if (!ReleaseVersionUtil.isLastUpdateCheckExpired(expiry)) {
|
if (!isLastUpdateCheckExpired(expiry)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,7 +108,7 @@ class NewVersionWorker(
|
|||||||
try {
|
try {
|
||||||
// Store a timestamp which needs to be exceeded,
|
// Store a timestamp which needs to be exceeded,
|
||||||
// before a new request to the API is made.
|
// before a new request to the API is made.
|
||||||
val newExpiry = ReleaseVersionUtil.coerceUpdateCheckExpiry(response.getHeader("expires"))
|
val newExpiry = coerceUpdateCheckExpiry(response.getHeader("expires"))
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
putLong(applicationContext.getString(R.string.update_expiry_key), newExpiry)
|
putLong(applicationContext.getString(R.string.update_expiry_key), newExpiry)
|
||||||
}
|
}
|
||||||
@@ -118,13 +120,13 @@ class NewVersionWorker(
|
|||||||
|
|
||||||
// Parse the json from the response.
|
// Parse the json from the response.
|
||||||
try {
|
try {
|
||||||
val newpipeVersionInfo = JsonParser.`object`()
|
val githubStableObject = JsonParser.`object`()
|
||||||
.from(response.responseBody()).getObject("flavors")
|
.from(response.responseBody()).getObject("flavors")
|
||||||
.getObject("newpipe")
|
.getObject("github").getObject("stable")
|
||||||
|
|
||||||
val versionName = newpipeVersionInfo.getString("version")
|
val versionName = githubStableObject.getString("version")
|
||||||
val versionCode = newpipeVersionInfo.getInt("version_code")
|
val versionCode = githubStableObject.getInt("version_code")
|
||||||
val apkLocationUrl = newpipeVersionInfo.getString("apk")
|
val apkLocationUrl = githubStableObject.getString("apk")
|
||||||
compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode)
|
compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode)
|
||||||
} catch (e: JsonParserException) {
|
} catch (e: JsonParserException) {
|
||||||
// Most likely something is wrong in data received from NEWPIPE_API_URL.
|
// Most likely something is wrong in data received from NEWPIPE_API_URL.
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ public final class QueueItemMenuUtil {
|
|||||||
return true;
|
return true;
|
||||||
case R.id.menu_item_share:
|
case R.id.menu_item_share:
|
||||||
shareText(context, item.getTitle(), item.getUrl(),
|
shareText(context, item.getTitle(), item.getUrl(),
|
||||||
item.getThumbnails());
|
item.getThumbnailUrl());
|
||||||
return true;
|
return true;
|
||||||
case R.id.menu_item_download:
|
case R.id.menu_item_download:
|
||||||
fetchStreamInfoAndSaveToDatabase(context, item.getServiceId(), item.getUrl(),
|
fetchStreamInfoAndSaveToDatabase(context, item.getServiceId(), item.getUrl(),
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ import org.schabi.newpipe.database.stream.model.StreamEntity;
|
|||||||
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
|
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
|
||||||
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
|
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
|
||||||
import org.schabi.newpipe.download.DownloadDialog;
|
import org.schabi.newpipe.download.DownloadDialog;
|
||||||
import org.schabi.newpipe.download.LoadingDialog;
|
|
||||||
import org.schabi.newpipe.error.ErrorInfo;
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
import org.schabi.newpipe.error.ErrorUtil;
|
import org.schabi.newpipe.error.ErrorUtil;
|
||||||
import org.schabi.newpipe.error.ReCaptchaActivity;
|
import org.schabi.newpipe.error.ReCaptchaActivity;
|
||||||
@@ -65,7 +64,6 @@ import org.schabi.newpipe.extractor.exceptions.PrivateContentException;
|
|||||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException;
|
import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException;
|
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException;
|
||||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
|
||||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||||
@@ -73,11 +71,10 @@ import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
|||||||
import org.schabi.newpipe.player.PlayerType;
|
import org.schabi.newpipe.player.PlayerType;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||||
import org.schabi.newpipe.player.playqueue.ChannelTabPlayQueue;
|
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||||
import org.schabi.newpipe.util.ChannelTabHelper;
|
|
||||||
import org.schabi.newpipe.util.Constants;
|
import org.schabi.newpipe.util.Constants;
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
@@ -792,10 +789,10 @@ public class RouterActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}, () ->
|
}, () -> {
|
||||||
// this branch is executed if there is no activity context
|
// this branch is executed if there is no activity context
|
||||||
inFlight(false)
|
inFlight(false);
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
<T> Single<T> pleaseWait(final Single<T> single) {
|
<T> Single<T> pleaseWait(final Single<T> single) {
|
||||||
@@ -815,24 +812,19 @@ public class RouterActivity extends AppCompatActivity {
|
|||||||
@SuppressLint("CheckResult")
|
@SuppressLint("CheckResult")
|
||||||
private void openDownloadDialog(final int currentServiceId, final String currentUrl) {
|
private void openDownloadDialog(final int currentServiceId, final String currentUrl) {
|
||||||
inFlight(true);
|
inFlight(true);
|
||||||
final LoadingDialog loadingDialog = new LoadingDialog(R.string.loading_metadata_title);
|
|
||||||
loadingDialog.show(getParentFragmentManager(), "loadingDialog");
|
|
||||||
disposables.add(ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, true)
|
disposables.add(ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, true)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.compose(this::pleaseWait)
|
.compose(this::pleaseWait)
|
||||||
.subscribe(result ->
|
.subscribe(result ->
|
||||||
runOnVisible(ctx -> {
|
runOnVisible(ctx -> {
|
||||||
loadingDialog.dismiss();
|
|
||||||
final FragmentManager fm = ctx.getSupportFragmentManager();
|
final FragmentManager fm = ctx.getSupportFragmentManager();
|
||||||
final DownloadDialog downloadDialog = new DownloadDialog(ctx, result);
|
final DownloadDialog downloadDialog = new DownloadDialog(ctx, result);
|
||||||
// dismiss listener to be handled by FragmentManager
|
// dismiss listener to be handled by FragmentManager
|
||||||
downloadDialog.show(fm, "downloadDialog");
|
downloadDialog.show(fm, "downloadDialog");
|
||||||
}
|
}
|
||||||
), throwable -> runOnVisible(ctx -> {
|
), throwable -> runOnVisible(ctx ->
|
||||||
loadingDialog.dismiss();
|
((RouterActivity) ctx).showUnsupportedUrlDialog(currentUrl))));
|
||||||
((RouterActivity) ctx).showUnsupportedUrlDialog(currentUrl);
|
|
||||||
})));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openAddToPlaylistDialog(final int currentServiceId, final String currentUrl) {
|
private void openAddToPlaylistDialog(final int currentServiceId, final String currentUrl) {
|
||||||
@@ -1024,16 +1016,7 @@ public class RouterActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
playQueue = new SinglePlayQueue((StreamInfo) info);
|
playQueue = new SinglePlayQueue((StreamInfo) info);
|
||||||
} else if (info instanceof ChannelInfo) {
|
} else if (info instanceof ChannelInfo) {
|
||||||
final Optional<ListLinkHandler> playableTab = ((ChannelInfo) info).getTabs()
|
playQueue = new ChannelPlayQueue((ChannelInfo) info);
|
||||||
.stream()
|
|
||||||
.filter(ChannelTabHelper::isStreamsTab)
|
|
||||||
.findFirst();
|
|
||||||
|
|
||||||
if (playableTab.isPresent()) {
|
|
||||||
playQueue = new ChannelTabPlayQueue(info.getServiceId(), playableTab.get());
|
|
||||||
} else {
|
|
||||||
return; // there is no playable tab
|
|
||||||
}
|
|
||||||
} else if (info instanceof PlaylistInfo) {
|
} else if (info instanceof PlaylistInfo) {
|
||||||
playQueue = new PlaylistPlayQueue((PlaylistInfo) info);
|
playQueue = new PlaylistPlayQueue((PlaylistInfo) info);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ class AboutActivity : AppCompatActivity() {
|
|||||||
/**
|
/**
|
||||||
* List of all software components.
|
* List of all software components.
|
||||||
*/
|
*/
|
||||||
private val SOFTWARE_COMPONENTS = arrayListOf(
|
private val SOFTWARE_COMPONENTS = arrayOf(
|
||||||
SoftwareComponent(
|
SoftwareComponent(
|
||||||
"ACRA", "2013", "Kevin Gaudin",
|
"ACRA", "2013", "Kevin Gaudin",
|
||||||
"https://github.com/ACRA/acra", StandardLicenses.APACHE2
|
"https://github.com/ACRA/acra", StandardLicenses.APACHE2
|
||||||
|
|||||||
@@ -1,40 +1,30 @@
|
|||||||
package org.schabi.newpipe.about
|
package org.schabi.newpipe.about
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Base64
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.webkit.WebView
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
|
||||||
import io.reactivex.rxjava3.core.Observable
|
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
import io.reactivex.rxjava3.disposables.Disposable
|
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
|
||||||
import org.schabi.newpipe.BuildConfig
|
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.databinding.FragmentLicensesBinding
|
import org.schabi.newpipe.databinding.FragmentLicensesBinding
|
||||||
import org.schabi.newpipe.databinding.ItemSoftwareComponentBinding
|
import org.schabi.newpipe.databinding.ItemSoftwareComponentBinding
|
||||||
import org.schabi.newpipe.ktx.parcelableArrayList
|
|
||||||
import org.schabi.newpipe.util.Localization
|
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fragment containing the software licenses.
|
* Fragment containing the software licenses.
|
||||||
*/
|
*/
|
||||||
class LicenseFragment : Fragment() {
|
class LicenseFragment : Fragment() {
|
||||||
private lateinit var softwareComponents: List<SoftwareComponent>
|
private lateinit var softwareComponents: Array<SoftwareComponent>
|
||||||
private var activeSoftwareComponent: SoftwareComponent? = null
|
private var activeLicense: License? = null
|
||||||
private val compositeDisposable = CompositeDisposable()
|
private val compositeDisposable = CompositeDisposable()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
softwareComponents = arguments?.parcelableArrayList<SoftwareComponent>(ARG_COMPONENTS)!!
|
softwareComponents = arguments?.getParcelableArray(ARG_COMPONENTS) as Array<SoftwareComponent>
|
||||||
.sortedBy { it.name } // Sort components by name
|
activeLicense = savedInstanceState?.getSerializable(LICENSE_KEY) as? License
|
||||||
activeSoftwareComponent = savedInstanceState?.getSerializable(SOFTWARE_COMPONENT_KEY) as? SoftwareComponent
|
// Sort components by name
|
||||||
|
softwareComponents.sortBy { it.name }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
@@ -49,8 +39,9 @@ class LicenseFragment : Fragment() {
|
|||||||
): View {
|
): View {
|
||||||
val binding = FragmentLicensesBinding.inflate(inflater, container, false)
|
val binding = FragmentLicensesBinding.inflate(inflater, container, false)
|
||||||
binding.licensesAppReadLicense.setOnClickListener {
|
binding.licensesAppReadLicense.setOnClickListener {
|
||||||
|
activeLicense = StandardLicenses.GPL3
|
||||||
compositeDisposable.add(
|
compositeDisposable.add(
|
||||||
showLicense(NEWPIPE_SOFTWARE_COMPONENT)
|
showLicense(activity, StandardLicenses.GPL3)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
for (component in softwareComponents) {
|
for (component in softwareComponents) {
|
||||||
@@ -66,72 +57,27 @@ class LicenseFragment : Fragment() {
|
|||||||
val root: View = componentBinding.root
|
val root: View = componentBinding.root
|
||||||
root.tag = component
|
root.tag = component
|
||||||
root.setOnClickListener {
|
root.setOnClickListener {
|
||||||
|
activeLicense = component.license
|
||||||
compositeDisposable.add(
|
compositeDisposable.add(
|
||||||
showLicense(component)
|
showLicense(activity, component)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
binding.licensesSoftwareComponents.addView(root)
|
binding.licensesSoftwareComponents.addView(root)
|
||||||
registerForContextMenu(root)
|
registerForContextMenu(root)
|
||||||
}
|
}
|
||||||
activeSoftwareComponent?.let { compositeDisposable.add(showLicense(it)) }
|
activeLicense?.let { compositeDisposable.add(showLicense(activity, it)) }
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(savedInstanceState: Bundle) {
|
override fun onSaveInstanceState(savedInstanceState: Bundle) {
|
||||||
super.onSaveInstanceState(savedInstanceState)
|
super.onSaveInstanceState(savedInstanceState)
|
||||||
activeSoftwareComponent?.let { savedInstanceState.putSerializable(SOFTWARE_COMPONENT_KEY, it) }
|
activeLicense?.let { savedInstanceState.putSerializable(LICENSE_KEY, it) }
|
||||||
}
|
|
||||||
|
|
||||||
private fun showLicense(
|
|
||||||
softwareComponent: SoftwareComponent
|
|
||||||
): Disposable {
|
|
||||||
return if (context == null) {
|
|
||||||
Disposable.empty()
|
|
||||||
} else {
|
|
||||||
val context = requireContext()
|
|
||||||
activeSoftwareComponent = softwareComponent
|
|
||||||
Observable.fromCallable { getFormattedLicense(context, softwareComponent.license) }
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe { formattedLicense ->
|
|
||||||
val webViewData = Base64.encodeToString(
|
|
||||||
formattedLicense.toByteArray(), Base64.NO_PADDING
|
|
||||||
)
|
|
||||||
val webView = WebView(context)
|
|
||||||
webView.loadData(webViewData, "text/html; charset=UTF-8", "base64")
|
|
||||||
|
|
||||||
Localization.assureCorrectAppLanguage(context)
|
|
||||||
val builder = AlertDialog.Builder(requireContext())
|
|
||||||
.setTitle(softwareComponent.name)
|
|
||||||
.setView(webView)
|
|
||||||
.setOnCancelListener { activeSoftwareComponent = null }
|
|
||||||
.setOnDismissListener { activeSoftwareComponent = null }
|
|
||||||
.setPositiveButton(R.string.done) { dialog, _ -> dialog.dismiss() }
|
|
||||||
|
|
||||||
if (softwareComponent != NEWPIPE_SOFTWARE_COMPONENT) {
|
|
||||||
builder.setNeutralButton(R.string.open_website_license) { _, _ ->
|
|
||||||
ShareUtils.openUrlInApp(requireContext(), softwareComponent.link)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val ARG_COMPONENTS = "components"
|
private const val ARG_COMPONENTS = "components"
|
||||||
private const val SOFTWARE_COMPONENT_KEY = "ACTIVE_SOFTWARE_COMPONENT"
|
private const val LICENSE_KEY = "ACTIVE_LICENSE"
|
||||||
private val NEWPIPE_SOFTWARE_COMPONENT = SoftwareComponent(
|
fun newInstance(softwareComponents: Array<SoftwareComponent>): LicenseFragment {
|
||||||
"NewPipe",
|
|
||||||
"2014-2023",
|
|
||||||
"Team NewPipe",
|
|
||||||
"https://newpipe.net/",
|
|
||||||
StandardLicenses.GPL3,
|
|
||||||
BuildConfig.VERSION_NAME
|
|
||||||
)
|
|
||||||
|
|
||||||
fun newInstance(softwareComponents: ArrayList<SoftwareComponent>): LicenseFragment {
|
|
||||||
val fragment = LicenseFragment()
|
val fragment = LicenseFragment()
|
||||||
fragment.arguments = bundleOf(ARG_COMPONENTS to softwareComponents)
|
fragment.arguments = bundleOf(ARG_COMPONENTS to softwareComponents)
|
||||||
return fragment
|
return fragment
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
package org.schabi.newpipe.about
|
package org.schabi.newpipe.about
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.util.Base64
|
||||||
|
import android.webkit.WebView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.rxjava3.core.Observable
|
||||||
|
import io.reactivex.rxjava3.disposables.Disposable
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.util.Localization
|
||||||
import org.schabi.newpipe.util.ThemeHelper
|
import org.schabi.newpipe.util.ThemeHelper
|
||||||
|
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -11,7 +20,7 @@ import java.io.IOException
|
|||||||
* @return String which contains a HTML formatted license page
|
* @return String which contains a HTML formatted license page
|
||||||
* styled according to the context's theme
|
* styled according to the context's theme
|
||||||
*/
|
*/
|
||||||
fun getFormattedLicense(context: Context, license: License): String {
|
private fun getFormattedLicense(context: Context, license: License): String {
|
||||||
try {
|
try {
|
||||||
return context.assets.open(license.filename).bufferedReader().use { it.readText() }
|
return context.assets.open(license.filename).bufferedReader().use { it.readText() }
|
||||||
// split the HTML file and insert the stylesheet into the HEAD of the file
|
// split the HTML file and insert the stylesheet into the HEAD of the file
|
||||||
@@ -25,7 +34,7 @@ fun getFormattedLicense(context: Context, license: License): String {
|
|||||||
* @param context the Android context
|
* @param context the Android context
|
||||||
* @return String which is a CSS stylesheet according to the context's theme
|
* @return String which is a CSS stylesheet according to the context's theme
|
||||||
*/
|
*/
|
||||||
fun getLicenseStylesheet(context: Context): String {
|
private fun getLicenseStylesheet(context: Context): String {
|
||||||
val isLightTheme = ThemeHelper.isLightThemeSelected(context)
|
val isLightTheme = ThemeHelper.isLightThemeSelected(context)
|
||||||
val licenseBackgroundColor = getHexRGBColor(
|
val licenseBackgroundColor = getHexRGBColor(
|
||||||
context, if (isLightTheme) R.color.light_license_background_color else R.color.dark_license_background_color
|
context, if (isLightTheme) R.color.light_license_background_color else R.color.dark_license_background_color
|
||||||
@@ -47,6 +56,48 @@ fun getLicenseStylesheet(context: Context): String {
|
|||||||
* @param color the color number from R.color
|
* @param color the color number from R.color
|
||||||
* @return a six characters long String with hexadecimal RGB values
|
* @return a six characters long String with hexadecimal RGB values
|
||||||
*/
|
*/
|
||||||
fun getHexRGBColor(context: Context, color: Int): String {
|
private fun getHexRGBColor(context: Context, color: Int): String {
|
||||||
return context.getString(color).substring(3)
|
return context.getString(color).substring(3)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showLicense(context: Context?, component: SoftwareComponent): Disposable {
|
||||||
|
return showLicense(context, component.license) {
|
||||||
|
setPositiveButton(R.string.dismiss) { dialog, _ ->
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
setNeutralButton(R.string.open_website_license) { _, _ ->
|
||||||
|
ShareUtils.openUrlInApp(context!!, component.link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showLicense(context: Context?, license: License) = showLicense(context, license) {
|
||||||
|
setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showLicense(
|
||||||
|
context: Context?,
|
||||||
|
license: License,
|
||||||
|
block: AlertDialog.Builder.() -> AlertDialog.Builder
|
||||||
|
): Disposable {
|
||||||
|
return if (context == null) {
|
||||||
|
Disposable.empty()
|
||||||
|
} else {
|
||||||
|
Observable.fromCallable { getFormattedLicense(context, license) }
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe { formattedLicense ->
|
||||||
|
val webViewData =
|
||||||
|
Base64.encodeToString(formattedLicense.toByteArray(), Base64.NO_PADDING)
|
||||||
|
val webView = WebView(context)
|
||||||
|
webView.loadData(webViewData, "text/html; charset=UTF-8", "base64")
|
||||||
|
|
||||||
|
Localization.assureCorrectAppLanguage(context)
|
||||||
|
AlertDialog.Builder(context)
|
||||||
|
.setTitle(license.name)
|
||||||
|
.setView(webView)
|
||||||
|
.block()
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package org.schabi.newpipe.about
|
|||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import java.io.Serializable
|
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class SoftwareComponent
|
class SoftwareComponent
|
||||||
@@ -14,4 +13,4 @@ constructor(
|
|||||||
val link: String,
|
val link: String,
|
||||||
val license: License,
|
val license: License,
|
||||||
val version: String? = null
|
val version: String? = null
|
||||||
) : Parcelable, Serializable
|
) : Parcelable
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package org.schabi.newpipe.database;
|
package org.schabi.newpipe.database;
|
||||||
|
|
||||||
import static org.schabi.newpipe.database.Migrations.DB_VER_9;
|
import static org.schabi.newpipe.database.Migrations.DB_VER_7;
|
||||||
|
|
||||||
import androidx.room.Database;
|
import androidx.room.Database;
|
||||||
import androidx.room.RoomDatabase;
|
import androidx.room.RoomDatabase;
|
||||||
@@ -38,7 +38,7 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
|||||||
FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class,
|
FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class,
|
||||||
FeedLastUpdatedEntity.class
|
FeedLastUpdatedEntity.class
|
||||||
},
|
},
|
||||||
version = DB_VER_9
|
version = DB_VER_7
|
||||||
)
|
)
|
||||||
public abstract class AppDatabase extends RoomDatabase {
|
public abstract class AppDatabase extends RoomDatabase {
|
||||||
public static final String DATABASE_NAME = "newpipe.db";
|
public static final String DATABASE_NAME = "newpipe.db";
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import java.time.Instant
|
|||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
|
|
||||||
class Converters {
|
object Converters {
|
||||||
/**
|
/**
|
||||||
* Convert a long value to a [OffsetDateTime].
|
* Convert a long value to a [OffsetDateTime].
|
||||||
*
|
*
|
||||||
@@ -47,6 +47,6 @@ class Converters {
|
|||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun feedGroupIconOf(id: Int): FeedGroupIcon {
|
fun feedGroupIconOf(id: Int): FeedGroupIcon {
|
||||||
return FeedGroupIcon.entries.first { it.id == id }
|
return FeedGroupIcon.values().first { it.id == id }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,8 +25,6 @@ public final class Migrations {
|
|||||||
public static final int DB_VER_5 = 5;
|
public static final int DB_VER_5 = 5;
|
||||||
public static final int DB_VER_6 = 6;
|
public static final int DB_VER_6 = 6;
|
||||||
public static final int DB_VER_7 = 7;
|
public static final int DB_VER_7 = 7;
|
||||||
public static final int DB_VER_8 = 8;
|
|
||||||
public static final int DB_VER_9 = 9;
|
|
||||||
|
|
||||||
private static final String TAG = Migrations.class.getName();
|
private static final String TAG = Migrations.class.getName();
|
||||||
public static final boolean DEBUG = MainActivity.DEBUG;
|
public static final boolean DEBUG = MainActivity.DEBUG;
|
||||||
@@ -188,7 +186,7 @@ public final class Migrations {
|
|||||||
@Override
|
@Override
|
||||||
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
||||||
database.execSQL("ALTER TABLE `subscriptions` ADD COLUMN `notification_mode` "
|
database.execSQL("ALTER TABLE `subscriptions` ADD COLUMN `notification_mode` "
|
||||||
+ "INTEGER NOT NULL DEFAULT 0");
|
+ "INTEGER NOT NULL DEFAULT 0");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -237,71 +235,6 @@ public final class Migrations {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public static final Migration MIGRATION_7_8 = new Migration(DB_VER_7, DB_VER_8) {
|
|
||||||
@Override
|
|
||||||
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
|
||||||
database.execSQL("DELETE FROM search_history WHERE id NOT IN (SELECT id FROM (SELECT "
|
|
||||||
+ "MIN(id) as id FROM search_history GROUP BY trim(search), service_id ) tmp)");
|
|
||||||
database.execSQL("UPDATE search_history SET search = trim(search)");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public static final Migration MIGRATION_8_9 = new Migration(DB_VER_8, DB_VER_9) {
|
|
||||||
@Override
|
|
||||||
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
|
||||||
try {
|
|
||||||
database.beginTransaction();
|
|
||||||
|
|
||||||
// Update playlists.
|
|
||||||
// Create a temp table to initialize display_index.
|
|
||||||
database.execSQL("CREATE TABLE `playlists_tmp` "
|
|
||||||
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
|
|
||||||
+ "`name` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL, "
|
|
||||||
+ "`thumbnail_stream_id` INTEGER NOT NULL, "
|
|
||||||
+ "`display_index` INTEGER NOT NULL)");
|
|
||||||
database.execSQL("INSERT INTO `playlists_tmp` "
|
|
||||||
+ "(`uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, "
|
|
||||||
+ "`display_index`) "
|
|
||||||
+ "SELECT `uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, "
|
|
||||||
+ "-1 "
|
|
||||||
+ "FROM `playlists`");
|
|
||||||
|
|
||||||
// Replace the old table, note that this also removes the index on the name which
|
|
||||||
// we don't need anymore.
|
|
||||||
database.execSQL("DROP TABLE `playlists`");
|
|
||||||
database.execSQL("ALTER TABLE `playlists_tmp` RENAME TO `playlists`");
|
|
||||||
|
|
||||||
|
|
||||||
// Update remote_playlists.
|
|
||||||
// Create a temp table to initialize display_index.
|
|
||||||
database.execSQL("CREATE TABLE `remote_playlists_tmp` "
|
|
||||||
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
|
|
||||||
+ "`service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, "
|
|
||||||
+ "`thumbnail_url` TEXT, `uploader` TEXT, "
|
|
||||||
+ "`display_index` INTEGER NOT NULL,"
|
|
||||||
+ "`stream_count` INTEGER)");
|
|
||||||
database.execSQL("INSERT INTO `remote_playlists_tmp` (`uid`, `service_id`, "
|
|
||||||
+ "`name`, `url`, `thumbnail_url`, `uploader`, `display_index`, "
|
|
||||||
+ "`stream_count`)"
|
|
||||||
+ "SELECT `uid`, `service_id`, `name`, `url`, `thumbnail_url`, `uploader`, "
|
|
||||||
+ "-1, `stream_count` FROM `remote_playlists`");
|
|
||||||
|
|
||||||
// Replace the old table, note that this also removes the index on the name which
|
|
||||||
// we don't need anymore.
|
|
||||||
database.execSQL("DROP TABLE `remote_playlists`");
|
|
||||||
database.execSQL("ALTER TABLE `remote_playlists_tmp` RENAME TO `remote_playlists`");
|
|
||||||
|
|
||||||
// Create index on the new table.
|
|
||||||
database.execSQL("CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` "
|
|
||||||
+ "ON `remote_playlists` (`service_id`, `url`)");
|
|
||||||
|
|
||||||
database.setTransactionSuccessful();
|
|
||||||
} finally {
|
|
||||||
database.endTransaction();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private Migrations() {
|
private Migrations() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,30 +93,18 @@ abstract class FeedDAO {
|
|||||||
uploadDateBefore: OffsetDateTime?
|
uploadDateBefore: OffsetDateTime?
|
||||||
): Maybe<List<StreamWithState>>
|
): Maybe<List<StreamWithState>>
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove links to streams that are older than the given date
|
|
||||||
* **but keep at least one stream per uploader**.
|
|
||||||
*
|
|
||||||
* One stream per uploader is kept because it is needed as reference
|
|
||||||
* when fetching new streams to check if they are new or not.
|
|
||||||
* @param offsetDateTime the newest date to keep, older streams are removed
|
|
||||||
*/
|
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
DELETE FROM feed
|
DELETE FROM feed WHERE
|
||||||
WHERE feed.stream_id IN (SELECT uid from (
|
|
||||||
SELECT s.uid,
|
feed.stream_id IN (
|
||||||
(SELECT MAX(upload_date)
|
SELECT s.uid FROM streams s
|
||||||
FROM streams s1
|
|
||||||
INNER JOIN feed f1
|
INNER JOIN feed f
|
||||||
ON s1.uid = f1.stream_id
|
ON s.uid = f.stream_id
|
||||||
WHERE f1.subscription_id = f.subscription_id) max_upload_date
|
|
||||||
FROM streams s
|
WHERE s.upload_date < :offsetDateTime
|
||||||
INNER JOIN feed f
|
)
|
||||||
ON s.uid = f.stream_id
|
|
||||||
|
|
||||||
WHERE s.upload_date < :offsetDateTime
|
|
||||||
AND s.upload_date <> max_upload_date))
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
abstract fun unlinkStreamsOlderThan(offsetDateTime: OffsetDateTime)
|
abstract fun unlinkStreamsOlderThan(offsetDateTime: OffsetDateTime)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user