mirror of
https://github.com/revanced/revanced-patches.git
synced 2025-12-07 18:03:55 +01:00
Compare commits
354 Commits
v5.17.0-de
...
v5.29.0-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e22d4e6a4b | ||
|
|
a07f946633 | ||
|
|
29c86ac6a3 | ||
|
|
19cf5667d8 | ||
|
|
fb83e58f79 | ||
|
|
9844081d04 | ||
|
|
439ca37e99 | ||
|
|
113a3d9f19 | ||
|
|
978c24458b | ||
|
|
957bece3e9 | ||
|
|
d32c3ac51d | ||
|
|
26102a70a2 | ||
|
|
2b44bf4c23 | ||
|
|
0e63f49e13 | ||
|
|
674a5b8d29 | ||
|
|
7be374100b | ||
|
|
e48c152b95 | ||
|
|
a678f178e1 | ||
|
|
2d8f5641f9 | ||
|
|
0dbd058099 | ||
|
|
c1a8fd0766 | ||
|
|
d338989cb4 | ||
|
|
b94daacf01 | ||
|
|
c764c4f197 | ||
|
|
6b719dfcd7 | ||
|
|
ccd169121a | ||
|
|
dcfbd8bf93 | ||
|
|
b65697603d | ||
|
|
25da5cca8b | ||
|
|
2b62fc2224 | ||
|
|
a9e9456b6b | ||
|
|
b01523e97d | ||
|
|
b8afb4e821 | ||
|
|
0d2198faed | ||
|
|
5c7c407b82 | ||
|
|
a8d2a1e028 | ||
|
|
d31624cae8 | ||
|
|
e790cfbf59 | ||
|
|
a54d408d3e | ||
|
|
5d3769e921 | ||
|
|
e138501657 | ||
|
|
a9235d6b62 | ||
|
|
90868ff025 | ||
|
|
345ec5c430 | ||
|
|
c8b95d475c | ||
|
|
0a93f44a5e | ||
|
|
a426e2af50 | ||
|
|
9e30c34e74 | ||
|
|
55401368b8 | ||
|
|
c0c56fef23 | ||
|
|
69df47602f | ||
|
|
1cea6bfdff | ||
|
|
e2a9552f91 | ||
|
|
7bbaca77ad | ||
|
|
246f3efe55 | ||
|
|
0fca3e8fb1 | ||
|
|
c09255eaed | ||
|
|
e78d6240ea | ||
|
|
ed0d807d70 | ||
|
|
1c39004350 | ||
|
|
6127f48a9e | ||
|
|
ad416f4aa7 | ||
|
|
adfac8a1f2 | ||
|
|
498488d45b | ||
|
|
f8e31c820a | ||
|
|
826a391591 | ||
|
|
af827e2f1a | ||
|
|
97cd31509e | ||
|
|
c0448dece4 | ||
|
|
f00a95c0d8 | ||
|
|
7a432e5741 | ||
|
|
966a78bd81 | ||
|
|
6aff8e8ca4 | ||
|
|
11aa463fa6 | ||
|
|
bf1b639a2f | ||
|
|
6d5380d44d | ||
|
|
7e1547b5b9 | ||
|
|
c790b45cc5 | ||
|
|
65fc6b43f5 | ||
|
|
2257dd90aa | ||
|
|
4b8499ff2c | ||
|
|
bde3fda972 | ||
|
|
e2e07b5cb2 | ||
|
|
9d10ab6c00 | ||
|
|
d7644152fd | ||
|
|
9be21f4824 | ||
|
|
a2eae0bf04 | ||
|
|
679354b5b3 | ||
|
|
91dec21033 | ||
|
|
1d0c56819b | ||
|
|
4410816c22 | ||
|
|
7e4e48bc9f | ||
|
|
e435b33593 | ||
|
|
bf288b83ae | ||
|
|
7a53580380 | ||
|
|
6439efa2a9 | ||
|
|
bc45433dcb | ||
|
|
8871803e83 | ||
|
|
18954a0285 | ||
|
|
ce5385b28e | ||
|
|
3f4cdf6f83 | ||
|
|
094b4a1ea8 | ||
|
|
a320e35c32 | ||
|
|
5bf5a2d2db | ||
|
|
ff903ba9ac | ||
|
|
1079a54dbe | ||
|
|
2b0e3b4553 | ||
|
|
0265a7791b | ||
|
|
49ae0df224 | ||
|
|
e279491724 | ||
|
|
495260fe2b | ||
|
|
40f069fff7 | ||
|
|
de263c1061 | ||
|
|
bf1f26d8bb | ||
|
|
0ee2ed72d4 | ||
|
|
02373b0bd2 | ||
|
|
97c8e2489d | ||
|
|
08b2b2e104 | ||
|
|
6b386b67d2 | ||
|
|
f8343ae9f6 | ||
|
|
3ba791ac7d | ||
|
|
443b54bf09 | ||
|
|
53587f190d | ||
|
|
83c148addc | ||
|
|
5c8ed05727 | ||
|
|
33833d7a1e | ||
|
|
b712f38017 | ||
|
|
517368eda7 | ||
|
|
2093c0c175 | ||
|
|
a7cfd80bfe | ||
|
|
2990dc6d4e | ||
|
|
c0e52bb6b3 | ||
|
|
93fdd6f538 | ||
|
|
decd249f20 | ||
|
|
d79cb3eea8 | ||
|
|
584b00fd87 | ||
|
|
795016abce | ||
|
|
dc1dbd50a8 | ||
|
|
2984d7362d | ||
|
|
627aed4010 | ||
|
|
4ab1f0cfa9 | ||
|
|
86e8e61ab2 | ||
|
|
e286dab74e | ||
|
|
712a82439f | ||
|
|
4449546c85 | ||
|
|
8d61ba90c3 | ||
|
|
689be79f71 | ||
|
|
b6047fa6b3 | ||
|
|
82bbd603ac | ||
|
|
bc0c3c452d | ||
|
|
fe864d8331 | ||
|
|
4f686935c3 | ||
|
|
798596fd83 | ||
|
|
38b37f182a | ||
|
|
52b9dc5c9f | ||
|
|
dea7108c45 | ||
|
|
24b4579cb9 | ||
|
|
0b52f3d192 | ||
|
|
18c374a81e | ||
|
|
092303e431 | ||
|
|
6bf5bf9d45 | ||
|
|
b2b09a2025 | ||
|
|
4a3a7f1674 | ||
|
|
e59c9e9b3c | ||
|
|
dfb552b01a | ||
|
|
94999c56b1 | ||
|
|
c4fd1f0146 | ||
|
|
4cd0ae9b92 | ||
|
|
9548d581c1 | ||
|
|
a2fe3af6be | ||
|
|
6ef6504d41 | ||
|
|
e58290839f | ||
|
|
e18260bd65 | ||
|
|
b2fcd5a846 | ||
|
|
e68cd70f66 | ||
|
|
14a8f4fb96 | ||
|
|
2593c004f4 | ||
|
|
db68c41d5e | ||
|
|
a4f9cb3cef | ||
|
|
9aec1999bb | ||
|
|
26ecbe646e | ||
|
|
46ba0d8a2e | ||
|
|
f454183646 | ||
|
|
d2b440d800 | ||
|
|
494c5f04a4 | ||
|
|
48d5fdf7e1 | ||
|
|
887c9f0d75 | ||
|
|
7de4c9d41d | ||
|
|
7d3b8d9c42 | ||
|
|
25e1a965d6 | ||
|
|
b29c01cee1 | ||
|
|
639850471b | ||
|
|
796c118fe1 | ||
|
|
edf20e397d | ||
|
|
5f0541407c | ||
|
|
56b7ba9ba7 | ||
|
|
f8bdf744ab | ||
|
|
f4f36ff273 | ||
|
|
5028c1acb3 | ||
|
|
555c9a5823 | ||
|
|
777957e2d0 | ||
|
|
b3316a5915 | ||
|
|
2ca2bb7692 | ||
|
|
23fd720fa7 | ||
|
|
1f08586ae8 | ||
|
|
60fdf4c44c | ||
|
|
63f3342815 | ||
|
|
858c59d728 | ||
|
|
5debf9936d | ||
|
|
f1b85d20a1 | ||
|
|
37d0de5e93 | ||
|
|
96d08d5eb7 | ||
|
|
9b1013e1c2 | ||
|
|
75d6cd7c7b | ||
|
|
5a17f5e1c1 | ||
|
|
1d16de6617 | ||
|
|
aee7cba46d | ||
|
|
ec3faf30a8 | ||
|
|
45b5a51da3 | ||
|
|
8abf176bc9 | ||
|
|
ef35ed7335 | ||
|
|
4fd666b667 | ||
|
|
72e0c01922 | ||
|
|
f69eab3e3b | ||
|
|
7c5c2d95bc | ||
|
|
b2453fecfc | ||
|
|
0d54f8bd80 | ||
|
|
fda16fad1a | ||
|
|
ddd43acd73 | ||
|
|
3451318d53 | ||
|
|
2d94ba9df6 | ||
|
|
aaf3437a5a | ||
|
|
ec8bf06047 | ||
|
|
96512de6c9 | ||
|
|
6114807c43 | ||
|
|
6d69f01421 | ||
|
|
fd4218154d | ||
|
|
8bed8a6622 | ||
|
|
3174047223 | ||
|
|
15053e2b68 | ||
|
|
e5b6aac018 | ||
|
|
d7c9dd0f77 | ||
|
|
a0eb6d5fdb | ||
|
|
55c5eb3d14 | ||
|
|
896de8910a | ||
|
|
e2a7e25c66 | ||
|
|
77ea5c4033 | ||
|
|
6eea2354f5 | ||
|
|
cce21c4d4a | ||
|
|
5e069bde90 | ||
|
|
6a49208982 | ||
|
|
404bb2e86e | ||
|
|
bc869fe359 | ||
|
|
7d166cf82c | ||
|
|
8efbaae65c | ||
|
|
e27ab23279 | ||
|
|
ce42604083 | ||
|
|
fc6282d0cb | ||
|
|
0559fc7fd0 | ||
|
|
7cc6995682 | ||
|
|
476f13bf98 | ||
|
|
f216e16c0b | ||
|
|
f2a8789649 | ||
|
|
5973b64f52 | ||
|
|
102036706e | ||
|
|
2393d0a8f5 | ||
|
|
aea29b9522 | ||
|
|
4db8ef7079 | ||
|
|
7fbd26ccad | ||
|
|
91995ea01d | ||
|
|
86f867fe97 | ||
|
|
0f687ecfd3 | ||
|
|
6c8b7d09c1 | ||
|
|
3d6958f157 | ||
|
|
43d7cc7374 | ||
|
|
5ebd449f1f | ||
|
|
346a061df8 | ||
|
|
13e490a422 | ||
|
|
b4e8540bbc | ||
|
|
775c1baec2 | ||
|
|
9419fb8ec4 | ||
|
|
c510931eb0 | ||
|
|
7160699384 | ||
|
|
9db67a6eb2 | ||
|
|
e684d87dd3 | ||
|
|
2d1752a1eb | ||
|
|
c9ff7092fe | ||
|
|
d451bc6d6d | ||
|
|
741fd36872 | ||
|
|
517f8cf59a | ||
|
|
b78fb24435 | ||
|
|
a3faccb21b | ||
|
|
5f0fddc122 | ||
|
|
854a18ff72 | ||
|
|
b994a16bdc | ||
|
|
f68d06dbf3 | ||
|
|
04c6a2e5f4 | ||
|
|
e6ae55fa99 | ||
|
|
fb62474ff4 | ||
|
|
e084f01fd0 | ||
|
|
d573386e0f | ||
|
|
0f3aeb35e5 | ||
|
|
e30f593af0 | ||
|
|
df965b8a9b | ||
|
|
654587a75e | ||
|
|
9956833781 | ||
|
|
c585b26188 | ||
|
|
de0d11fcfb | ||
|
|
d321504fcf | ||
|
|
6005c97bf5 | ||
|
|
e404d84c83 | ||
|
|
1abed31968 | ||
|
|
a75a88d3c6 | ||
|
|
3d67d90473 | ||
|
|
fa1e137a43 | ||
|
|
ac71a53c73 | ||
|
|
0bff207efc | ||
|
|
e1a8b388a5 | ||
|
|
628d18489c | ||
|
|
36772b8b2e | ||
|
|
49c849979f | ||
|
|
0bdb8cdf2b | ||
|
|
2035c9e2e9 | ||
|
|
7cb38fd3fc | ||
|
|
8ed9d5bf08 | ||
|
|
cd467d6244 | ||
|
|
fdefb67d02 | ||
|
|
5274cd18f0 | ||
|
|
3d68c06146 | ||
|
|
ef3d5bafd5 | ||
|
|
2d7b1b09af | ||
|
|
0572d48fde | ||
|
|
37984b8b99 | ||
|
|
6e63193f06 | ||
|
|
b2384b22a5 | ||
|
|
ccb76983ff | ||
|
|
318b55b8fe | ||
|
|
49ade9efbc | ||
|
|
d77515bd68 | ||
|
|
087bf1e152 | ||
|
|
c2994d583d | ||
|
|
127b0a63fe | ||
|
|
27aafd0ee1 | ||
|
|
49c54c0e54 | ||
|
|
842ba4fc4d | ||
|
|
66ecadce4f | ||
|
|
73ca04da5e | ||
|
|
a5d26208c1 | ||
|
|
497291c478 | ||
|
|
b24278a544 | ||
|
|
135f9ead3c | ||
|
|
ca4f960171 | ||
|
|
7f228cc535 | ||
|
|
bf91e127d8 |
6
.github/workflows/build_pull_request.yml
vendored
6
.github/workflows/build_pull_request.yml
vendored
@@ -19,11 +19,11 @@ jobs:
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: "17"
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: Cache Gradle
|
||||
uses: burrunan/gradle-cache-action@v1
|
||||
uses: burrunan/gradle-cache-action@v3
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
|
||||
23
.github/workflows/release.yml
vendored
23
.github/workflows/release.yml
vendored
@@ -13,24 +13,23 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# Make sure the release step uses its own credentials:
|
||||
# https://github.com/cycjimmy/semantic-release-action#private-packages
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: "17"
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: Cache Gradle
|
||||
uses: burrunan/gradle-cache-action@v1
|
||||
uses: burrunan/gradle-cache-action@v3
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
@@ -40,7 +39,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
node-version: 'lts/*'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
@@ -54,6 +53,14 @@ jobs:
|
||||
fingerprint: ${{ vars.GPG_FINGERPRINT }}
|
||||
|
||||
- name: Release
|
||||
uses: cycjimmy/semantic-release-action@v4
|
||||
id: release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: npm exec semantic-release
|
||||
|
||||
- name: Attest
|
||||
if: steps.release.outputs.new_release_published == 'true'
|
||||
uses: actions/attest-build-provenance@v2
|
||||
with:
|
||||
subject-name: 'ReVanced Patches ${{ steps.release.outputs.new_release_git_tag }}'
|
||||
subject-path: patches/build/libs/patches-*.rvp
|
||||
|
||||
10
.releaserc
10
.releaserc
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"assets": [
|
||||
"CHANGELOG.md",
|
||||
"gradle.properties",
|
||||
"gradle.properties"
|
||||
],
|
||||
"message": "chore: Release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
||||
}
|
||||
@@ -33,16 +33,16 @@
|
||||
"assets": [
|
||||
{
|
||||
"path": "patches/build/libs/patches-!(*sources*|*javadoc*).rvp?(.asc)"
|
||||
},
|
||||
}
|
||||
],
|
||||
successComment: false
|
||||
"successComment": false
|
||||
}
|
||||
],
|
||||
[
|
||||
"@saithodev/semantic-release-backmerge",
|
||||
{
|
||||
backmergeBranches: [{"from": "main", "to": "dev"}],
|
||||
clearWorkspace: true
|
||||
"backmergeBranches": [{"from": "main", "to": "dev"}],
|
||||
"clearWorkspace": true
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
1147
CHANGELOG.md
1147
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
16
extensions/all/misc/adb/hide-adb/build.gradle.kts
Normal file
16
extensions/all/misc/adb/hide-adb/build.gradle.kts
Normal file
@@ -0,0 +1,16 @@
|
||||
android {
|
||||
namespace = "app.revanced.extension"
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 21
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(libs.annotation)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
@@ -0,0 +1,28 @@
|
||||
package app.revanced.extension.all.misc.hide.adb;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.provider.Settings;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class HideAdbPatch {
|
||||
private static final List<String> SPOOF_SETTINGS = Arrays.asList("adb_enabled", "adb_wifi_enabled", "development_settings_enabled");
|
||||
|
||||
public static int getInt(ContentResolver cr, String name) throws Settings.SettingNotFoundException {
|
||||
if (SPOOF_SETTINGS.contains(name)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Settings.Global.getInt(cr, name);
|
||||
}
|
||||
|
||||
public static int getInt(ContentResolver cr, String name, int def) {
|
||||
if (SPOOF_SETTINGS.contains(name)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Settings.Global.getInt(cr, name, def);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package app.revanced.extension.all.connectivity.wifi.spoof;
|
||||
package app.revanced.extension.all.misc.connectivity.wifi.spoof;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
@@ -1,4 +1,4 @@
|
||||
package app.revanced.extension.all.screencapture.removerestriction;
|
||||
package app.revanced.extension.all.misc.screencapture.removerestriction;
|
||||
|
||||
import android.media.AudioAttributes;
|
||||
import android.os.Build;
|
||||
@@ -1,4 +1,4 @@
|
||||
package app.revanced.extension.all.screenshot.removerestriction;
|
||||
package app.revanced.extension.all.misc.screenshot.removerestriction;
|
||||
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
3
extensions/messenger/build.gradle.kts
Normal file
3
extensions/messenger/build.gradle.kts
Normal file
@@ -0,0 +1,3 @@
|
||||
dependencies {
|
||||
compileOnly(project(":extensions:shared:library"))
|
||||
}
|
||||
1
extensions/messenger/src/main/AndroidManifest.xml
Normal file
1
extensions/messenger/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
@@ -0,0 +1,25 @@
|
||||
package app.revanced.extension.messenger.metaai;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class RemoveMetaAIPatch {
|
||||
private static final Set<Long> loggedIDs = Collections.synchronizedSet(new HashSet<>());
|
||||
|
||||
public static boolean overrideBooleanFlag(long id, boolean value) {
|
||||
try {
|
||||
if (Long.toString(id).startsWith("REPLACED_BY_PATCH")) {
|
||||
if (loggedIDs.add(id))
|
||||
Logger.printInfo(() -> "Overriding " + id + " from " + value + " to false");
|
||||
|
||||
return false;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "overrideBooleanFlag failure", ex);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -82,7 +82,7 @@ public class HideAdsPatch {
|
||||
|
||||
// Filter HeaderBlock with known ads until next HeaderBlock.
|
||||
if (currentBlock instanceof HeaderBlock headerBlock) {
|
||||
StyledText headerText = headerBlock.component20();
|
||||
StyledText headerText = headerBlock.getTitle();
|
||||
if (headerText != null) {
|
||||
skipFullHeader = false;
|
||||
for (String blockedHeaderBlock : blockedHeaderBlocks) {
|
||||
|
||||
@@ -3,8 +3,7 @@ package nl.nu.performance.api.client.objects;
|
||||
import nl.nu.performance.api.client.interfaces.Block;
|
||||
|
||||
public class HeaderBlock extends Block {
|
||||
// returns title
|
||||
public final StyledText component20() {
|
||||
public final StyledText getTitle() {
|
||||
throw new UnsupportedOperationException("Stub");
|
||||
}
|
||||
}
|
||||
|
||||
4
extensions/primevideo/build.gradle.kts
Normal file
4
extensions/primevideo/build.gradle.kts
Normal file
@@ -0,0 +1,4 @@
|
||||
dependencies {
|
||||
compileOnly(project(":extensions:shared:library"))
|
||||
compileOnly(project(":extensions:primevideo:stub"))
|
||||
}
|
||||
1
extensions/primevideo/src/main/AndroidManifest.xml
Normal file
1
extensions/primevideo/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
@@ -0,0 +1,36 @@
|
||||
package app.revanced.extension.primevideo.ads;
|
||||
|
||||
import com.amazon.avod.fsm.SimpleTrigger;
|
||||
import com.amazon.avod.media.ads.AdBreak;
|
||||
import com.amazon.avod.media.ads.internal.state.AdBreakTrigger;
|
||||
import com.amazon.avod.media.ads.internal.state.AdEnabledPlayerTriggerType;
|
||||
import com.amazon.avod.media.playback.VideoPlayer;
|
||||
import com.amazon.avod.media.ads.internal.state.ServerInsertedAdBreakState;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class SkipAdsPatch {
|
||||
public static void enterServerInsertedAdBreakState(ServerInsertedAdBreakState state, AdBreakTrigger trigger, VideoPlayer player) {
|
||||
try {
|
||||
AdBreak adBreak = trigger.getBreak();
|
||||
|
||||
// There are two scenarios when entering the original method:
|
||||
// 1. Player naturally entered an ad break while watching a video.
|
||||
// 2. User is skipped/scrubbed to a position on the timeline. If seek position is past an ad break,
|
||||
// user is forced to watch an ad before continuing.
|
||||
//
|
||||
// Scenario 2 is indicated by trigger.getSeekStartPosition() != null, so skip directly to the scrubbing
|
||||
// target. Otherwise, just calculate when the ad break should end and skip to there.
|
||||
if (trigger.getSeekStartPosition() != null)
|
||||
player.seekTo(trigger.getSeekTarget().getTotalMilliseconds());
|
||||
else
|
||||
player.seekTo(player.getCurrentPosition() + adBreak.getDurationExcludingAux().getTotalMilliseconds());
|
||||
|
||||
// Send "end of ads" trigger to state machine so everything doesn't get whacky.
|
||||
state.doTrigger(new SimpleTrigger(AdEnabledPlayerTriggerType.NO_MORE_ADS_SKIP_TRANSITION));
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Failed skipping ads", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
17
extensions/primevideo/stub/build.gradle.kts
Normal file
17
extensions/primevideo/stub/build.gradle.kts
Normal file
@@ -0,0 +1,17 @@
|
||||
plugins {
|
||||
id(libs.plugins.android.library.get().pluginId)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "app.revanced.extension"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 21
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
}
|
||||
1
extensions/primevideo/stub/src/main/AndroidManifest.xml
Normal file
1
extensions/primevideo/stub/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.amazon.avod.fsm;
|
||||
|
||||
public final class SimpleTrigger<T> implements Trigger<T> {
|
||||
public SimpleTrigger(T triggerType) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.amazon.avod.fsm;
|
||||
|
||||
public abstract class StateBase<S, T> {
|
||||
// This method orginally has protected access (modified in patch code).
|
||||
public void doTrigger(Trigger<T> trigger) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.amazon.avod.fsm;
|
||||
|
||||
public interface Trigger<T> {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.amazon.avod.media;
|
||||
|
||||
public final class TimeSpan {
|
||||
public long getTotalMilliseconds() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.amazon.avod.media.ads;
|
||||
|
||||
import com.amazon.avod.media.TimeSpan;
|
||||
|
||||
public interface AdBreak {
|
||||
TimeSpan getDurationExcludingAux();
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.amazon.avod.media.ads.internal.state;
|
||||
|
||||
public abstract class AdBreakState extends AdEnabledPlaybackState {
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.amazon.avod.media.ads.internal.state;
|
||||
|
||||
import com.amazon.avod.media.ads.AdBreak;
|
||||
import com.amazon.avod.media.TimeSpan;
|
||||
|
||||
public class AdBreakTrigger {
|
||||
public AdBreak getBreak() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public TimeSpan getSeekTarget() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public TimeSpan getSeekStartPosition() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.amazon.avod.media.ads.internal.state;
|
||||
|
||||
import com.amazon.avod.fsm.StateBase;
|
||||
import com.amazon.avod.media.playback.state.PlayerStateType;
|
||||
import com.amazon.avod.media.playback.state.trigger.PlayerTriggerType;
|
||||
|
||||
public class AdEnabledPlaybackState extends StateBase<PlayerStateType, PlayerTriggerType> {
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.amazon.avod.media.ads.internal.state;
|
||||
|
||||
public enum AdEnabledPlayerTriggerType {
|
||||
NO_MORE_ADS_SKIP_TRANSITION
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.amazon.avod.media.ads.internal.state;
|
||||
|
||||
public class ServerInsertedAdBreakState extends AdBreakState {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.amazon.avod.media.playback;
|
||||
|
||||
public interface VideoPlayer {
|
||||
long getCurrentPosition();
|
||||
|
||||
void seekTo(long positionMs);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.amazon.avod.media.playback.state;
|
||||
|
||||
public interface PlayerStateType {
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.amazon.avod.media.playback.state.trigger;
|
||||
|
||||
public interface PlayerTriggerType {
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
package app.revanced.extension.shared;
|
||||
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
import static app.revanced.extension.shared.requests.Route.Method.GET;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.app.SearchManager;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
@@ -14,15 +15,20 @@ import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.PowerManager;
|
||||
import android.provider.Settings;
|
||||
import android.util.Pair;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* @noinspection unused
|
||||
*/
|
||||
import app.revanced.extension.shared.requests.Requester;
|
||||
import app.revanced.extension.shared.requests.Route;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class GmsCoreSupport {
|
||||
private static final String PACKAGE_NAME_YOUTUBE = "com.google.android.youtube";
|
||||
private static final String PACKAGE_NAME_YOUTUBE_MUSIC = "com.google.android.apps.youtube.music";
|
||||
@@ -31,10 +37,24 @@ public class GmsCoreSupport {
|
||||
= getGmsCoreVendorGroupId() + ".android.gms";
|
||||
private static final Uri GMS_CORE_PROVIDER
|
||||
= Uri.parse("content://" + getGmsCoreVendorGroupId() + ".android.gsf.gservices/prefix");
|
||||
private static final String DONT_KILL_MY_APP_LINK
|
||||
= "https://dontkillmyapp.com";
|
||||
private static final String DONT_KILL_MY_APP_URL
|
||||
= "https://dontkillmyapp.com/";
|
||||
private static final Route DONT_KILL_MY_APP_MANUFACTURER_API
|
||||
= new Route(GET, "/api/v2/{manufacturer}.json");
|
||||
private static final String DONT_KILL_MY_APP_NAME_PARAMETER
|
||||
= "?app=MicroG";
|
||||
private static final String BUILD_MANUFACTURER
|
||||
= Build.MANUFACTURER.toLowerCase(Locale.ROOT).replace(" ", "-");
|
||||
|
||||
/**
|
||||
* If a manufacturer specific page exists on DontKillMyApp.
|
||||
*/
|
||||
@Nullable
|
||||
private static volatile Boolean DONT_KILL_MY_APP_MANUFACTURER_SUPPORTED;
|
||||
|
||||
private static void open(String queryOrLink) {
|
||||
Logger.printInfo(() -> "Opening link: " + queryOrLink);
|
||||
|
||||
Intent intent;
|
||||
try {
|
||||
// Check if queryOrLink is a valid URL.
|
||||
@@ -59,13 +79,27 @@ public class GmsCoreSupport {
|
||||
// Use a delay to allow the activity to finish initializing.
|
||||
// Otherwise, if device is in dark mode the dialog is shown with wrong color scheme.
|
||||
Utils.runOnMainThreadDelayed(() -> {
|
||||
// Create the custom dialog.
|
||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
||||
context,
|
||||
str("gms_core_dialog_title"), // Title.
|
||||
str(dialogMessageRef), // Message.
|
||||
null, // No EditText.
|
||||
str(positiveButtonTextRef), // OK button text.
|
||||
() -> onPositiveClickListener.onClick(null, 0), // Convert DialogInterface.OnClickListener to Runnable.
|
||||
null, // No Cancel button action.
|
||||
null, // No Neutral button text.
|
||||
null, // No Neutral button action.
|
||||
true // Dismiss dialog when onNeutralClick.
|
||||
);
|
||||
|
||||
Dialog dialog = dialogPair.first;
|
||||
|
||||
// Do not set cancelable to false, to allow using back button to skip the action,
|
||||
// just in case the battery change can never be satisfied.
|
||||
var dialog = new AlertDialog.Builder(context)
|
||||
.setTitle(str("gms_core_dialog_title"))
|
||||
.setMessage(str(dialogMessageRef))
|
||||
.setPositiveButton(str(positiveButtonTextRef), onPositiveClickListener)
|
||||
.create();
|
||||
dialog.setCancelable(true);
|
||||
|
||||
// Show the dialog
|
||||
Utils.showDialog(context, dialog);
|
||||
}, 100);
|
||||
}
|
||||
@@ -73,7 +107,6 @@ public class GmsCoreSupport {
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
public static void checkGmsCore(Activity context) {
|
||||
try {
|
||||
// Verify the user has not included GmsCore for a root installation.
|
||||
@@ -88,7 +121,7 @@ public class GmsCoreSupport {
|
||||
|
||||
// Do not exit. If the app exits before launch completes (and without
|
||||
// opening another activity), then on some devices such as Pixel phone Android 10
|
||||
// no toast will be shown and the app will continually be relaunched
|
||||
// no toast will be shown and the app will continually relaunch
|
||||
// with the appearance of a hung app.
|
||||
}
|
||||
|
||||
@@ -121,15 +154,20 @@ public class GmsCoreSupport {
|
||||
}
|
||||
|
||||
// Check if GmsCore is currently running in the background.
|
||||
try (var client = context.getContentResolver().acquireContentProviderClient(GMS_CORE_PROVIDER)) {
|
||||
var client = context.getContentResolver().acquireContentProviderClient(GMS_CORE_PROVIDER);
|
||||
//noinspection TryFinallyCanBeTryWithResources
|
||||
try {
|
||||
if (client == null) {
|
||||
Logger.printInfo(() -> "GmsCore is not running in the background");
|
||||
checkIfDontKillMyAppSupportsManufacturer();
|
||||
|
||||
showBatteryOptimizationDialog(context,
|
||||
"gms_core_dialog_not_whitelisted_not_allowed_in_background_message",
|
||||
"gms_core_dialog_open_website_text",
|
||||
(dialog, id) -> open(DONT_KILL_MY_APP_LINK));
|
||||
(dialog, id) -> openDontKillMyApp());
|
||||
}
|
||||
} finally {
|
||||
if (client != null) client.close();
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "checkGmsCore failure", ex);
|
||||
@@ -143,10 +181,57 @@ public class GmsCoreSupport {
|
||||
activity.startActivityForResult(intent, 0);
|
||||
}
|
||||
|
||||
private static void checkIfDontKillMyAppSupportsManufacturer() {
|
||||
Utils.runOnBackgroundThread(() -> {
|
||||
try {
|
||||
final long start = System.currentTimeMillis();
|
||||
HttpURLConnection connection = Requester.getConnectionFromRoute(
|
||||
DONT_KILL_MY_APP_URL, DONT_KILL_MY_APP_MANUFACTURER_API, BUILD_MANUFACTURER);
|
||||
connection.setConnectTimeout(5000);
|
||||
connection.setReadTimeout(5000);
|
||||
|
||||
final boolean supported = connection.getResponseCode() == 200;
|
||||
Logger.printInfo(() -> "Manufacturer is " + (supported ? "" : "NOT ")
|
||||
+ "listed on DontKillMyApp: " + BUILD_MANUFACTURER
|
||||
+ " fetch took: " + (System.currentTimeMillis() - start) + "ms");
|
||||
DONT_KILL_MY_APP_MANUFACTURER_SUPPORTED = supported;
|
||||
} catch (Exception ex) {
|
||||
Logger.printInfo(() -> "Could not check if manufacturer is listed on DontKillMyApp: "
|
||||
+ BUILD_MANUFACTURER, ex);
|
||||
DONT_KILL_MY_APP_MANUFACTURER_SUPPORTED = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void openDontKillMyApp() {
|
||||
final Boolean manufacturerSupported = DONT_KILL_MY_APP_MANUFACTURER_SUPPORTED;
|
||||
|
||||
String manufacturerPageToOpen;
|
||||
if (manufacturerSupported == null) {
|
||||
// Fetch has not completed yet. Only happens on extremely slow internet connections
|
||||
// and the user spends less than 1 second reading what's on screen.
|
||||
// Instead of waiting for the fetch (which may timeout),
|
||||
// open the website without a vendor.
|
||||
manufacturerPageToOpen = "";
|
||||
} else if (manufacturerSupported) {
|
||||
manufacturerPageToOpen = BUILD_MANUFACTURER;
|
||||
} else {
|
||||
// No manufacturer specific page exists. Open the general page.
|
||||
manufacturerPageToOpen = "general";
|
||||
}
|
||||
|
||||
open(DONT_KILL_MY_APP_URL + manufacturerPageToOpen + DONT_KILL_MY_APP_NAME_PARAMETER);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return If GmsCore is not whitelisted from battery optimizations.
|
||||
*/
|
||||
private static boolean batteryOptimizationsEnabled(Context context) {
|
||||
//noinspection ObsoleteSdkInt
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
// Android 5.0 does not have battery optimization settings.
|
||||
return false;
|
||||
}
|
||||
var powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
|
||||
return !powerManager.isIgnoringBatteryOptimizations(GMS_CORE_PACKAGE_NAME);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
package app.revanced.extension.shared;
|
||||
|
||||
import static app.revanced.extension.shared.settings.BaseSettings.DEBUG;
|
||||
import static app.revanced.extension.shared.settings.BaseSettings.DEBUG_STACKTRACE;
|
||||
import static app.revanced.extension.shared.settings.BaseSettings.DEBUG_TOAST_ON_ERROR;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
|
||||
import static app.revanced.extension.shared.settings.BaseSettings.*;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
import app.revanced.extension.shared.settings.preference.LogBufferManager;
|
||||
|
||||
/**
|
||||
* ReVanced specific logger. Logging is done to standard device log (accessible thru ADB),
|
||||
* and additionally accessible thru {@link LogBufferManager}.
|
||||
*
|
||||
* All methods are thread safe, and are safe to call even
|
||||
* if {@link Utils#getContext()} is not available.
|
||||
*/
|
||||
public class Logger {
|
||||
|
||||
/**
|
||||
@@ -17,99 +29,173 @@ public class Logger {
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface LogMessage {
|
||||
/**
|
||||
* @return Logger string message. This method is only called if logging is enabled.
|
||||
*/
|
||||
@NonNull
|
||||
String buildMessageString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return For outer classes, this returns {@link Class#getSimpleName()}.
|
||||
* For static, inner, or anonymous classes, this returns the simple name of the enclosing class.
|
||||
* <br>
|
||||
* For example, each of these classes return 'SomethingView':
|
||||
* <code>
|
||||
* com.company.SomethingView
|
||||
* com.company.SomethingView$StaticClass
|
||||
* com.company.SomethingView$1
|
||||
* </code>
|
||||
*/
|
||||
private String findOuterClassSimpleName() {
|
||||
var selfClass = this.getClass();
|
||||
private enum LogLevel {
|
||||
DEBUG,
|
||||
INFO,
|
||||
ERROR
|
||||
}
|
||||
|
||||
String fullClassName = selfClass.getName();
|
||||
final int dollarSignIndex = fullClassName.indexOf('$');
|
||||
if (dollarSignIndex < 0) {
|
||||
return selfClass.getSimpleName(); // Already an outer class.
|
||||
/**
|
||||
* Log tag prefix. Only used for system logging.
|
||||
*/
|
||||
private static final String REVANCED_LOG_TAG_PREFIX = "revanced: ";
|
||||
|
||||
private static final String LOGGER_CLASS_NAME = Logger.class.getName();
|
||||
|
||||
/**
|
||||
* @return For outer classes, this returns {@link Class#getSimpleName()}.
|
||||
* For static, inner, or anonymous classes, this returns the simple name of the enclosing class.
|
||||
* <br>
|
||||
* For example, each of these classes returns 'SomethingView':
|
||||
* <code>
|
||||
* com.company.SomethingView
|
||||
* com.company.SomethingView$StaticClass
|
||||
* com.company.SomethingView$1
|
||||
* </code>
|
||||
*/
|
||||
private static String getOuterClassSimpleName(Object obj) {
|
||||
Class<?> logClass = obj.getClass();
|
||||
String fullClassName = logClass.getName();
|
||||
final int dollarSignIndex = fullClassName.indexOf('$');
|
||||
if (dollarSignIndex < 0) {
|
||||
return logClass.getSimpleName(); // Already an outer class.
|
||||
}
|
||||
|
||||
// Class is inner, static, or anonymous.
|
||||
// Parse the simple name full name.
|
||||
// A class with no package returns index of -1, but incrementing gives index zero which is correct.
|
||||
final int simpleClassNameStartIndex = fullClassName.lastIndexOf('.') + 1;
|
||||
return fullClassName.substring(simpleClassNameStartIndex, dollarSignIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to handle logging to Android Log and {@link LogBufferManager}.
|
||||
* Appends the log message, stack trace (if enabled), and exception (if present) to logBuffer
|
||||
* with class name but without 'revanced:' prefix.
|
||||
*
|
||||
* @param logLevel The log level.
|
||||
* @param message Log message object.
|
||||
* @param ex Optional exception.
|
||||
* @param includeStackTrace If the current stack should be included.
|
||||
* @param showToast If a toast is to be shown.
|
||||
*/
|
||||
private static void logInternal(LogLevel logLevel, LogMessage message, @Nullable Throwable ex,
|
||||
boolean includeStackTrace, boolean showToast) {
|
||||
// It's very important that no Settings are used in this method,
|
||||
// as this code is used when a context is not set and thus referencing
|
||||
// a setting will crash the app.
|
||||
String messageString = message.buildMessageString();
|
||||
String className = getOuterClassSimpleName(message);
|
||||
|
||||
String logText = messageString;
|
||||
|
||||
// Append exception message if present.
|
||||
if (ex != null) {
|
||||
var exceptionMessage = ex.getMessage();
|
||||
if (exceptionMessage != null) {
|
||||
logText += "\nException: " + exceptionMessage;
|
||||
}
|
||||
}
|
||||
|
||||
// Class is inner, static, or anonymous.
|
||||
// Parse the simple name full name.
|
||||
// A class with no package returns index of -1, but incrementing gives index zero which is correct.
|
||||
final int simpleClassNameStartIndex = fullClassName.lastIndexOf('.') + 1;
|
||||
return fullClassName.substring(simpleClassNameStartIndex, dollarSignIndex);
|
||||
if (includeStackTrace) {
|
||||
var sw = new StringWriter();
|
||||
new Throwable().printStackTrace(new PrintWriter(sw));
|
||||
String stackTrace = sw.toString();
|
||||
// Remove the stacktrace elements of this class.
|
||||
final int loggerIndex = stackTrace.lastIndexOf(LOGGER_CLASS_NAME);
|
||||
final int loggerBegins = stackTrace.indexOf('\n', loggerIndex);
|
||||
logText += stackTrace.substring(loggerBegins);
|
||||
}
|
||||
|
||||
// Do not include "revanced:" prefix in clipboard logs.
|
||||
String managerToastString = className + ": " + logText;
|
||||
LogBufferManager.appendToLogBuffer(managerToastString);
|
||||
|
||||
String logTag = REVANCED_LOG_TAG_PREFIX + className;
|
||||
switch (logLevel) {
|
||||
case DEBUG:
|
||||
if (ex == null) Log.d(logTag, logText);
|
||||
else Log.d(logTag, logText, ex);
|
||||
break;
|
||||
case INFO:
|
||||
if (ex == null) Log.i(logTag, logText);
|
||||
else Log.i(logTag, logText, ex);
|
||||
break;
|
||||
case ERROR:
|
||||
if (ex == null) Log.e(logTag, logText);
|
||||
else Log.e(logTag, logText, ex);
|
||||
break;
|
||||
}
|
||||
|
||||
if (showToast) {
|
||||
Utils.showToastLong(managerToastString);
|
||||
}
|
||||
}
|
||||
|
||||
private static final String REVANCED_LOG_PREFIX = "revanced: ";
|
||||
private static boolean shouldLogDebug() {
|
||||
// If the app is still starting up and the context is not yet set,
|
||||
// then allow debug logging regardless what the debug setting actually is.
|
||||
return Utils.context == null || DEBUG.get();
|
||||
}
|
||||
|
||||
private static boolean shouldShowErrorToast() {
|
||||
return Utils.context != null && DEBUG_TOAST_ON_ERROR.get();
|
||||
}
|
||||
|
||||
private static boolean includeStackTrace() {
|
||||
return Utils.context != null && DEBUG_STACKTRACE.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs debug messages under the outer class name of the code calling this method.
|
||||
* Whenever possible, the log string should be constructed entirely inside {@link LogMessage#buildMessageString()}
|
||||
* so the performance cost of building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
|
||||
* <p>
|
||||
* Whenever possible, the log string should be constructed entirely inside
|
||||
* {@link LogMessage#buildMessageString()} so the performance cost of
|
||||
* building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
|
||||
*/
|
||||
public static void printDebug(@NonNull LogMessage message) {
|
||||
public static void printDebug(LogMessage message) {
|
||||
printDebug(message, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs debug messages under the outer class name of the code calling this method.
|
||||
* Whenever possible, the log string should be constructed entirely inside {@link LogMessage#buildMessageString()}
|
||||
* so the performance cost of building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
|
||||
* <p>
|
||||
* Whenever possible, the log string should be constructed entirely inside
|
||||
* {@link LogMessage#buildMessageString()} so the performance cost of
|
||||
* building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
|
||||
*/
|
||||
public static void printDebug(@NonNull LogMessage message, @Nullable Exception ex) {
|
||||
if (DEBUG.get()) {
|
||||
String logMessage = message.buildMessageString();
|
||||
String logTag = REVANCED_LOG_PREFIX + message.findOuterClassSimpleName();
|
||||
|
||||
if (DEBUG_STACKTRACE.get()) {
|
||||
var builder = new StringBuilder(logMessage);
|
||||
var sw = new StringWriter();
|
||||
new Throwable().printStackTrace(new PrintWriter(sw));
|
||||
|
||||
builder.append('\n').append(sw);
|
||||
logMessage = builder.toString();
|
||||
}
|
||||
|
||||
if (ex == null) {
|
||||
Log.d(logTag, logMessage);
|
||||
} else {
|
||||
Log.d(logTag, logMessage, ex);
|
||||
}
|
||||
public static void printDebug(LogMessage message, @Nullable Exception ex) {
|
||||
if (shouldLogDebug()) {
|
||||
logInternal(LogLevel.DEBUG, message, ex, includeStackTrace(), false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs information messages using the outer class name of the code calling this method.
|
||||
*/
|
||||
public static void printInfo(@NonNull LogMessage message) {
|
||||
public static void printInfo(LogMessage message) {
|
||||
printInfo(message, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs information messages using the outer class name of the code calling this method.
|
||||
*/
|
||||
public static void printInfo(@NonNull LogMessage message, @Nullable Exception ex) {
|
||||
String logTag = REVANCED_LOG_PREFIX + message.findOuterClassSimpleName();
|
||||
String logMessage = message.buildMessageString();
|
||||
if (ex == null) {
|
||||
Log.i(logTag, logMessage);
|
||||
} else {
|
||||
Log.i(logTag, logMessage, ex);
|
||||
}
|
||||
public static void printInfo(LogMessage message, @Nullable Exception ex) {
|
||||
logInternal(LogLevel.INFO, message, ex, includeStackTrace(), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs exceptions under the outer class name of the code calling this method.
|
||||
* Appends the log message, exception (if present), and toast message (if enabled) to logBuffer.
|
||||
*/
|
||||
public static void printException(@NonNull LogMessage message) {
|
||||
public static void printException(LogMessage message) {
|
||||
printException(message, null);
|
||||
}
|
||||
|
||||
@@ -122,35 +208,7 @@ public class Logger {
|
||||
* @param message log message
|
||||
* @param ex exception (optional)
|
||||
*/
|
||||
public static void printException(@NonNull LogMessage message, @Nullable Throwable ex) {
|
||||
String messageString = message.buildMessageString();
|
||||
String outerClassSimpleName = message.findOuterClassSimpleName();
|
||||
String logMessage = REVANCED_LOG_PREFIX + outerClassSimpleName;
|
||||
if (ex == null) {
|
||||
Log.e(logMessage, messageString);
|
||||
} else {
|
||||
Log.e(logMessage, messageString, ex);
|
||||
}
|
||||
if (DEBUG_TOAST_ON_ERROR.get()) {
|
||||
Utils.showToastLong(outerClassSimpleName + ": " + messageString);
|
||||
}
|
||||
public static void printException(LogMessage message, @Nullable Throwable ex) {
|
||||
logInternal(LogLevel.ERROR, message, ex, includeStackTrace(), shouldShowErrorToast());
|
||||
}
|
||||
|
||||
/**
|
||||
* Logging to use if {@link BaseSettings#DEBUG} or {@link Utils#getContext()} may not be initialized.
|
||||
* Normally this method should not be used.
|
||||
*/
|
||||
public static void initializationInfo(@NonNull Class<?> callingClass, @NonNull String message) {
|
||||
Log.i(REVANCED_LOG_PREFIX + callingClass.getSimpleName(), message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logging to use if {@link BaseSettings#DEBUG} or {@link Utils#getContext()} may not be initialized.
|
||||
* Normally this method should not be used.
|
||||
*/
|
||||
public static void initializationException(@NonNull Class<?> callingClass, @NonNull String message,
|
||||
@Nullable Exception ex) {
|
||||
Log.e(REVANCED_LOG_PREFIX + callingClass.getSimpleName(), message, ex);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,15 +3,21 @@ package app.revanced.extension.shared.checks;
|
||||
import static android.text.Html.FROM_HTML_MODE_COMPACT;
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
import static app.revanced.extension.shared.Utils.DialogFragmentOnStartAction;
|
||||
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.app.Dialog;
|
||||
import android.content.Intent;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.net.Uri;
|
||||
import android.text.Html;
|
||||
import android.util.Pair;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
@@ -86,38 +92,58 @@ abstract class Check {
|
||||
);
|
||||
|
||||
Utils.runOnMainThreadDelayed(() -> {
|
||||
AlertDialog alert = new AlertDialog.Builder(activity)
|
||||
.setCancelable(false)
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setTitle(str("revanced_check_environment_failed_title"))
|
||||
.setMessage(message)
|
||||
.setPositiveButton(
|
||||
" ",
|
||||
(dialog, which) -> {
|
||||
final var intent = new Intent(Intent.ACTION_VIEW, GOOD_SOURCE);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
activity.startActivity(intent);
|
||||
// Create the custom dialog.
|
||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
||||
activity,
|
||||
str("revanced_check_environment_failed_title"), // Title.
|
||||
message, // Message.
|
||||
null, // No EditText.
|
||||
str("revanced_check_environment_dialog_open_official_source_button"), // OK button text.
|
||||
() -> {
|
||||
// Action for the OK (website) button.
|
||||
final var intent = new Intent(Intent.ACTION_VIEW, GOOD_SOURCE);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
activity.startActivity(intent);
|
||||
|
||||
// Shutdown to prevent the user from navigating back to this app,
|
||||
// which is no longer showing a warning dialog.
|
||||
activity.finishAffinity();
|
||||
System.exit(0);
|
||||
}
|
||||
).setNegativeButton(
|
||||
" ",
|
||||
(dialog, which) -> {
|
||||
// Cleanup data if the user incorrectly imported a huge negative number.
|
||||
final int current = Math.max(0, BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get());
|
||||
BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(current + 1);
|
||||
// Shutdown to prevent the user from navigating back to this app,
|
||||
// which is no longer showing a warning dialog.
|
||||
activity.finishAffinity();
|
||||
System.exit(0);
|
||||
},
|
||||
null, // No cancel button.
|
||||
str("revanced_check_environment_dialog_ignore_button"), // Neutral button text.
|
||||
() -> {
|
||||
// Neutral button action.
|
||||
// Cleanup data if the user incorrectly imported a huge negative number.
|
||||
final int current = Math.max(0, BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get());
|
||||
BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(current + 1);
|
||||
},
|
||||
true // Dismiss dialog when onNeutralClick.
|
||||
);
|
||||
|
||||
dialog.dismiss();
|
||||
}
|
||||
).create();
|
||||
// Get the dialog and main layout.
|
||||
Dialog dialog = dialogPair.first;
|
||||
LinearLayout mainLayout = dialogPair.second;
|
||||
|
||||
Utils.showDialog(activity, alert, false, new DialogFragmentOnStartAction() {
|
||||
// Add icon to the dialog.
|
||||
ImageView iconView = new ImageView(activity);
|
||||
iconView.setImageResource(Utils.getResourceIdentifier("revanced_ic_dialog_alert", "drawable"));
|
||||
iconView.setColorFilter(Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN);
|
||||
iconView.setPadding(0, 0, 0, 0);
|
||||
LinearLayout.LayoutParams iconParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
);
|
||||
iconParams.gravity = Gravity.CENTER;
|
||||
mainLayout.addView(iconView, 0); // Add icon at the top.
|
||||
|
||||
dialog.setCancelable(false);
|
||||
|
||||
// Show the dialog.
|
||||
Utils.showDialog(activity, dialog, false, new DialogFragmentOnStartAction() {
|
||||
boolean hasRun;
|
||||
@Override
|
||||
public void onStart(AlertDialog dialog) {
|
||||
public void onStart(Dialog dialog) {
|
||||
// Only run this once, otherwise if the user changes to a different app
|
||||
// then changes back, this handler will run again and disable the buttons.
|
||||
if (hasRun) {
|
||||
@@ -125,19 +151,43 @@ abstract class Check {
|
||||
}
|
||||
hasRun = true;
|
||||
|
||||
var openWebsiteButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
|
||||
// Get the button container to access buttons.
|
||||
LinearLayout buttonContainer = (LinearLayout) mainLayout.getChildAt(mainLayout.getChildCount() - 1);
|
||||
|
||||
Button openWebsiteButton;
|
||||
Button ignoreButton;
|
||||
|
||||
// Check if buttons are in a single-row layout (buttonContainer has one child: rowContainer).
|
||||
if (buttonContainer.getChildCount() == 1 && buttonContainer.getChildAt(0) instanceof LinearLayout) {
|
||||
LinearLayout rowContainer = (LinearLayout) buttonContainer.getChildAt(0);
|
||||
// Neutral button is the first child (index 0).
|
||||
ignoreButton = (Button) rowContainer.getChildAt(0);
|
||||
// OK button is the last child.
|
||||
openWebsiteButton = (Button) rowContainer.getChildAt(rowContainer.getChildCount() - 1);
|
||||
} else {
|
||||
// Multi-row layout: buttons are in separate containers, ordered OK, Cancel, Neutral.
|
||||
LinearLayout okContainer =
|
||||
(LinearLayout) buttonContainer.getChildAt(0); // OK is first.
|
||||
openWebsiteButton = (Button) okContainer.getChildAt(0);
|
||||
LinearLayout neutralContainer =
|
||||
(LinearLayout)buttonContainer.getChildAt(buttonContainer.getChildCount() - 1); // Neutral is last.
|
||||
ignoreButton = (Button) neutralContainer.getChildAt(0);
|
||||
}
|
||||
|
||||
// Initially set buttons to INVISIBLE and disabled.
|
||||
openWebsiteButton.setVisibility(View.INVISIBLE);
|
||||
openWebsiteButton.setEnabled(false);
|
||||
ignoreButton.setVisibility(View.INVISIBLE);
|
||||
ignoreButton.setEnabled(false);
|
||||
|
||||
var dismissButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE);
|
||||
dismissButton.setEnabled(false);
|
||||
|
||||
getCountdownRunnable(dismissButton, openWebsiteButton).run();
|
||||
// Start the countdown for showing and enabling buttons.
|
||||
getCountdownRunnable(ignoreButton, openWebsiteButton).run();
|
||||
}
|
||||
});
|
||||
}, 1000); // Use a delay, so this dialog is shown on top of any other startup dialogs.
|
||||
}
|
||||
|
||||
private static Runnable getCountdownRunnable(Button dismissButton, Button openWebsiteButton) {
|
||||
private static Runnable getCountdownRunnable(Button ignoreButton, Button openWebsiteButton) {
|
||||
return new Runnable() {
|
||||
private int secondsRemaining = SECONDS_BEFORE_SHOWING_IGNORE_BUTTON;
|
||||
|
||||
@@ -146,17 +196,15 @@ abstract class Check {
|
||||
Utils.verifyOnMainThread();
|
||||
|
||||
if (secondsRemaining > 0) {
|
||||
if (secondsRemaining - SECONDS_BEFORE_SHOWING_WEBSITE_BUTTON == 0) {
|
||||
openWebsiteButton.setText(str("revanced_check_environment_dialog_open_official_source_button"));
|
||||
if (secondsRemaining - SECONDS_BEFORE_SHOWING_WEBSITE_BUTTON <= 0) {
|
||||
openWebsiteButton.setVisibility(View.VISIBLE);
|
||||
openWebsiteButton.setEnabled(true);
|
||||
}
|
||||
|
||||
secondsRemaining--;
|
||||
|
||||
Utils.runOnMainThreadDelayed(this, 1000);
|
||||
} else {
|
||||
dismissButton.setText(str("revanced_check_environment_dialog_ignore_button"));
|
||||
dismissButton.setEnabled(true);
|
||||
ignoreButton.setVisibility(View.VISIBLE);
|
||||
ignoreButton.setEnabled(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -52,7 +52,7 @@ public class Route {
|
||||
|
||||
private int countMatches(CharSequence seq, char c) {
|
||||
int count = 0;
|
||||
for (int i = 0; i < seq.length(); i++) {
|
||||
for (int i = 0, length = seq.length(); i < length; i++) {
|
||||
if (seq.charAt(i) == c)
|
||||
count++;
|
||||
}
|
||||
|
||||
@@ -89,9 +89,11 @@ public enum AppLanguage {
|
||||
ZU;
|
||||
|
||||
private final String language;
|
||||
private final Locale locale;
|
||||
|
||||
AppLanguage() {
|
||||
language = name().toLowerCase(Locale.US);
|
||||
locale = Locale.forLanguageTag(language);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -112,6 +114,6 @@ public enum AppLanguage {
|
||||
return Locale.getDefault();
|
||||
}
|
||||
|
||||
return Locale.forLanguageTag(language);
|
||||
return locale;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,11 @@ public class BaseSettings {
|
||||
|
||||
public static final EnumSetting<AppLanguage> REVANCED_LANGUAGE = new EnumSetting<>("revanced_language", AppLanguage.DEFAULT, true, "revanced_language_user_dialog_message");
|
||||
|
||||
/**
|
||||
* Use the icons declared in the preferences created during patching. If no icons or styles are declared then this setting does nothing.
|
||||
*/
|
||||
public static final BooleanSetting SHOW_MENU_ICONS = new BooleanSetting("revanced_show_menu_icons", TRUE, true);
|
||||
|
||||
public static final BooleanSetting SPOOF_VIDEO_STREAMS = new BooleanSetting("revanced_spoof_video_streams", TRUE, true, "revanced_spoof_video_streams_user_dialog_message");
|
||||
public static final EnumSetting<AppLanguage> SPOOF_VIDEO_STREAMS_LANGUAGE = new EnumSetting<>("revanced_spoof_video_streams_language", AppLanguage.DEFAULT, new AudioStreamLanguageOverrideAvailability());
|
||||
public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE, parent(SPOOF_VIDEO_STREAMS));
|
||||
|
||||
@@ -342,9 +342,12 @@ public abstract class Setting<T> {
|
||||
|
||||
/**
|
||||
* Identical to calling {@link #save(Object)} using {@link #defaultValue}.
|
||||
*
|
||||
* @return The newly saved default value.
|
||||
*/
|
||||
public void resetToDefault() {
|
||||
public T resetToDefault() {
|
||||
save(defaultValue);
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,12 +3,20 @@ package app.revanced.extension.shared.settings.preference;
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.preference.*;
|
||||
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceFragment;
|
||||
import android.preference.PreferenceGroup;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.preference.PreferenceScreen;
|
||||
import android.preference.SwitchPreference;
|
||||
import android.preference.EditTextPreference;
|
||||
import android.preference.ListPreference;
|
||||
import android.util.Pair;
|
||||
import android.widget.LinearLayout;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
@@ -44,7 +52,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
||||
* Set by subclasses if Strings cannot be added as a resource.
|
||||
*/
|
||||
@Nullable
|
||||
protected static String restartDialogButtonText, restartDialogTitle, confirmDialogTitle;
|
||||
protected static String restartDialogButtonText, restartDialogTitle, confirmDialogTitle, restartDialogMessage;
|
||||
|
||||
private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
|
||||
try {
|
||||
@@ -76,7 +84,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
||||
|
||||
updatingPreference = true;
|
||||
// Apply 'Setting <- Preference', unless during importing when it needs to be 'Setting -> Preference'.
|
||||
// Updating here can can cause a recursive call back into this same method.
|
||||
// Updating here can cause a recursive call back into this same method.
|
||||
updatePreference(pref, setting, true, settingImportInProgress);
|
||||
// Update any other preference availability that may now be different.
|
||||
updateUIAvailability();
|
||||
@@ -86,7 +94,6 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Initialize this instance, and do any custom behavior.
|
||||
* <p>
|
||||
@@ -95,7 +102,10 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
||||
* so all app specific {@link Setting} instances are loaded before this method returns.
|
||||
*/
|
||||
protected void initialize() {
|
||||
final var identifier = Utils.getResourceIdentifier("revanced_prefs", "xml");
|
||||
String preferenceResourceName = BaseSettings.SHOW_MENU_ICONS.get()
|
||||
? "revanced_prefs_icons"
|
||||
: "revanced_prefs";
|
||||
final var identifier = Utils.getResourceIdentifier(preferenceResourceName, "xml");
|
||||
if (identifier == 0) return;
|
||||
addPreferencesFromResource(identifier);
|
||||
|
||||
@@ -114,11 +124,14 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
||||
|
||||
showingUserDialogMessage = true;
|
||||
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(confirmDialogTitle)
|
||||
.setMessage(Objects.requireNonNull(setting.userDialogMessage).toString())
|
||||
.setPositiveButton(android.R.string.ok, (dialog, id) -> {
|
||||
// User confirmed, save to the Setting.
|
||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
||||
context,
|
||||
confirmDialogTitle, // Title.
|
||||
Objects.requireNonNull(setting.userDialogMessage).toString(), // No message.
|
||||
null, // No EditText.
|
||||
null, // OK button text.
|
||||
() -> {
|
||||
// OK button action. User confirmed, save to the Setting.
|
||||
updatePreference(pref, setting, true, false);
|
||||
|
||||
// Update availability of other preferences that may be changed.
|
||||
@@ -127,23 +140,27 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
||||
if (setting.rebootApp) {
|
||||
showRestartDialog(context);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, (dialog, id) -> {
|
||||
// Restore whatever the setting was before the change.
|
||||
},
|
||||
() -> {
|
||||
// Cancel button action. Restore whatever the setting was before the change.
|
||||
updatePreference(pref, setting, true, true);
|
||||
})
|
||||
.setOnDismissListener(dialog -> {
|
||||
showingUserDialogMessage = false;
|
||||
})
|
||||
.setCancelable(false)
|
||||
.show();
|
||||
},
|
||||
null, // No Neutral button.
|
||||
null, // No Neutral button action.
|
||||
true // Dismiss dialog when onNeutralClick.
|
||||
);
|
||||
|
||||
dialogPair.first.setOnDismissListener(d -> showingUserDialogMessage = false);
|
||||
|
||||
// Show the dialog.
|
||||
dialogPair.first.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates all Preferences values and their availability using the current values in {@link Setting}.
|
||||
*/
|
||||
protected void updateUIToSettingValues() {
|
||||
updatePreferenceScreen(getPreferenceScreen(), true,true);
|
||||
updatePreferenceScreen(getPreferenceScreen(), true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -278,17 +295,27 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
||||
if (restartDialogTitle == null) {
|
||||
restartDialogTitle = str("revanced_settings_restart_title");
|
||||
}
|
||||
if (restartDialogMessage == null) {
|
||||
restartDialogMessage = str("revanced_settings_restart_dialog_message");
|
||||
}
|
||||
if (restartDialogButtonText == null) {
|
||||
restartDialogButtonText = str("revanced_settings_restart");
|
||||
}
|
||||
|
||||
new AlertDialog.Builder(context)
|
||||
.setMessage(restartDialogTitle)
|
||||
.setPositiveButton(restartDialogButtonText, (dialog, id)
|
||||
-> Utils.restartApp(context))
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setCancelable(false)
|
||||
.show();
|
||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(context,
|
||||
restartDialogTitle, // Title.
|
||||
restartDialogMessage, // Message.
|
||||
null, // No EditText.
|
||||
restartDialogButtonText, // OK button text.
|
||||
() -> Utils.restartApp(context), // OK button action.
|
||||
() -> {}, // Cancel button action (dismiss only).
|
||||
null, // No Neutral button text.
|
||||
null, // No Neutral button action.
|
||||
true // Dismiss dialog when onNeutralClick.
|
||||
);
|
||||
|
||||
// Show the dialog.
|
||||
dialogPair.first.show();
|
||||
}
|
||||
|
||||
@SuppressLint("ResourceType")
|
||||
|
||||
@@ -0,0 +1,449 @@
|
||||
package app.revanced.extension.shared.settings.preference;
|
||||
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
import static app.revanced.extension.shared.Utils.getResourceIdentifier;
|
||||
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Typeface;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.preference.EditTextPreference;
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.text.style.RelativeSizeSpan;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Pair;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewParent;
|
||||
import android.widget.*;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.settings.Setting;
|
||||
import app.revanced.extension.shared.settings.StringSetting;
|
||||
|
||||
/**
|
||||
* A custom preference for selecting a color via a hexadecimal code or a color picker dialog.
|
||||
* Extends {@link EditTextPreference} to display a colored dot in the widget area,
|
||||
* reflecting the currently selected color. The dot is dimmed when the preference is disabled.
|
||||
*/
|
||||
@SuppressWarnings({"unused", "deprecation"})
|
||||
public class ColorPickerPreference extends EditTextPreference {
|
||||
|
||||
/**
|
||||
* Character to show the color appearance.
|
||||
*/
|
||||
public static final String COLOR_DOT_STRING = "⬤";
|
||||
|
||||
/**
|
||||
* Length of a valid color string of format #RRGGBB.
|
||||
*/
|
||||
public static final int COLOR_STRING_LENGTH = 7;
|
||||
|
||||
/**
|
||||
* Matches everything that is not a hex number/letter.
|
||||
*/
|
||||
private static final Pattern PATTERN_NOT_HEX = Pattern.compile("[^0-9A-Fa-f]");
|
||||
|
||||
/**
|
||||
* Alpha for dimming when the preference is disabled.
|
||||
*/
|
||||
private static final float DISABLED_ALPHA = 0.5f; // 50%
|
||||
|
||||
/**
|
||||
* View displaying a colored dot in the widget area.
|
||||
*/
|
||||
private View widgetColorDot;
|
||||
|
||||
/**
|
||||
* Current color in RGB format (without alpha).
|
||||
*/
|
||||
@ColorInt
|
||||
private int currentColor;
|
||||
|
||||
/**
|
||||
* Associated setting for storing the color value.
|
||||
*/
|
||||
private StringSetting colorSetting;
|
||||
|
||||
/**
|
||||
* Dialog TextWatcher for the EditText to monitor color input changes.
|
||||
*/
|
||||
private TextWatcher colorTextWatcher;
|
||||
|
||||
/**
|
||||
* Dialog TextView displaying a colored dot for the selected color preview in the dialog.
|
||||
*/
|
||||
private TextView dialogColorPreview;
|
||||
|
||||
/**
|
||||
* Dialog color picker view.
|
||||
*/
|
||||
private ColorPickerView dialogColorPickerView;
|
||||
|
||||
/**
|
||||
* Removes non valid hex characters, converts to all uppercase,
|
||||
* and adds # character to the start if not present.
|
||||
*/
|
||||
public static String cleanupColorCodeString(String colorString) {
|
||||
// Remove non-hex chars, convert to uppercase, and ensure correct length
|
||||
String result = "#" + PATTERN_NOT_HEX.matcher(colorString)
|
||||
.replaceAll("").toUpperCase(Locale.ROOT);
|
||||
|
||||
if (result.length() < COLOR_STRING_LENGTH) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return result.substring(0, COLOR_STRING_LENGTH);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param color RGB color, without an alpha channel.
|
||||
* @return #RRGGBB hex color string
|
||||
*/
|
||||
public static String getColorString(@ColorInt int color) {
|
||||
String colorString = String.format("#%06X", color);
|
||||
if ((color & 0xFF000000) != 0) {
|
||||
// Likely a bug somewhere.
|
||||
Logger.printException(() -> "getColorString: color has alpha channel: " + colorString);
|
||||
}
|
||||
return colorString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Spanned object for a colored dot using SpannableString.
|
||||
*
|
||||
* @param color The RGB color (without alpha).
|
||||
* @return A Spanned object with the colored dot.
|
||||
*/
|
||||
public static Spanned getColorDot(@ColorInt int color) {
|
||||
SpannableString spannable = new SpannableString(COLOR_DOT_STRING);
|
||||
spannable.setSpan(new ForegroundColorSpan(color | 0xFF000000), 0, COLOR_DOT_STRING.length(),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
spannable.setSpan(new RelativeSizeSpan(1.5f), 0, 1,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
return spannable;
|
||||
}
|
||||
|
||||
public ColorPickerPreference(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public ColorPickerPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public ColorPickerPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the preference by setting up the EditText, loading the color, and set the widget layout.
|
||||
*/
|
||||
private void init() {
|
||||
colorSetting = (StringSetting) Setting.getSettingFromPath(getKey());
|
||||
if (colorSetting == null) {
|
||||
Logger.printException(() -> "Could not find color setting for: " + getKey());
|
||||
}
|
||||
|
||||
EditText editText = getEditText();
|
||||
editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
|
||||
| InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
editText.setAutofillHints((String) null);
|
||||
}
|
||||
|
||||
// Set the widget layout to a custom layout containing the colored dot.
|
||||
setWidgetLayoutResource(getResourceIdentifier("revanced_color_dot_widget", "layout"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the selected color and updates the UI and settings.
|
||||
*
|
||||
* @param colorString The color in hexadecimal format (e.g., "#RRGGBB").
|
||||
* @throws IllegalArgumentException If the color string is invalid.
|
||||
*/
|
||||
@Override
|
||||
public final void setText(String colorString) {
|
||||
try {
|
||||
Logger.printDebug(() -> "setText: " + colorString);
|
||||
super.setText(colorString);
|
||||
|
||||
currentColor = Color.parseColor(colorString) & 0x00FFFFFF;
|
||||
if (colorSetting != null) {
|
||||
colorSetting.save(getColorString(currentColor));
|
||||
}
|
||||
updateColorPreview();
|
||||
updateWidgetColorDot();
|
||||
} catch (IllegalArgumentException ex) {
|
||||
// This code is reached if the user pastes settings json with an invalid color
|
||||
// since this preference is updated with the new setting text.
|
||||
Logger.printDebug(() -> "Parse color error: " + colorString, ex);
|
||||
Utils.showToastShort(str("revanced_settings_color_invalid"));
|
||||
setText(colorSetting.resetToDefault());
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "setText failure: " + colorString, ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBindView(View view) {
|
||||
super.onBindView(view);
|
||||
|
||||
widgetColorDot = view.findViewById(getResourceIdentifier(
|
||||
"revanced_color_dot_widget", "id"));
|
||||
widgetColorDot.setBackgroundResource(getResourceIdentifier(
|
||||
"revanced_settings_circle_background", "drawable"));
|
||||
widgetColorDot.getBackground().setTint(currentColor | 0xFF000000);
|
||||
widgetColorDot.setAlpha(isEnabled() ? 1.0f : DISABLED_ALPHA);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the color preview TextView with a colored dot.
|
||||
*/
|
||||
private void updateColorPreview() {
|
||||
if (dialogColorPreview != null) {
|
||||
dialogColorPreview.setText(getColorDot(currentColor));
|
||||
}
|
||||
}
|
||||
|
||||
private void updateWidgetColorDot() {
|
||||
if (widgetColorDot != null) {
|
||||
widgetColorDot.getBackground().setTint(currentColor | 0xFF000000);
|
||||
widgetColorDot.setAlpha(isEnabled() ? 1.0f : DISABLED_ALPHA);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a TextWatcher to monitor changes in the EditText for color input.
|
||||
*
|
||||
* @return A TextWatcher that updates the color preview on valid input.
|
||||
*/
|
||||
private TextWatcher createColorTextWatcher(ColorPickerView colorPickerView) {
|
||||
return new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable edit) {
|
||||
try {
|
||||
String colorString = edit.toString();
|
||||
|
||||
String sanitizedColorString = cleanupColorCodeString(colorString);
|
||||
if (!sanitizedColorString.equals(colorString)) {
|
||||
edit.replace(0, colorString.length(), sanitizedColorString);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sanitizedColorString.length() != COLOR_STRING_LENGTH) {
|
||||
// User is still typing out the color.
|
||||
return;
|
||||
}
|
||||
|
||||
final int newColor = Color.parseColor(colorString);
|
||||
if (currentColor != newColor) {
|
||||
Logger.printDebug(() -> "afterTextChanged: " + sanitizedColorString);
|
||||
currentColor = newColor;
|
||||
updateColorPreview();
|
||||
updateWidgetColorDot();
|
||||
colorPickerView.setColor(newColor);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
// Should never be reached since input is validated before using.
|
||||
Logger.printException(() -> "afterTextChanged failure", ex);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Dialog with a color preview and EditText for hex color input.
|
||||
*/
|
||||
@Override
|
||||
protected void showDialog(Bundle state) {
|
||||
Context context = getContext();
|
||||
|
||||
// Inflate color picker view.
|
||||
View colorPicker = LayoutInflater.from(context).inflate(
|
||||
getResourceIdentifier("revanced_color_picker", "layout"), null);
|
||||
dialogColorPickerView = colorPicker.findViewById(
|
||||
getResourceIdentifier("revanced_color_picker_view", "id"));
|
||||
dialogColorPickerView.setColor(currentColor);
|
||||
|
||||
// Horizontal layout for preview and EditText.
|
||||
LinearLayout inputLayout = new LinearLayout(context);
|
||||
inputLayout.setOrientation(LinearLayout.HORIZONTAL);
|
||||
|
||||
dialogColorPreview = new TextView(context);
|
||||
LinearLayout.LayoutParams previewParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
);
|
||||
previewParams.setMargins(dipToPixels(15), 0, dipToPixels(10), 0); // text dot has its own indents so 15, instead 16.
|
||||
dialogColorPreview.setLayoutParams(previewParams);
|
||||
inputLayout.addView(dialogColorPreview);
|
||||
updateColorPreview();
|
||||
|
||||
EditText editText = getEditText();
|
||||
ViewParent parent = editText.getParent();
|
||||
if (parent instanceof ViewGroup parentViewGroup) {
|
||||
parentViewGroup.removeView(editText);
|
||||
}
|
||||
editText.setLayoutParams(new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
));
|
||||
String currentColorString = getColorString(currentColor);
|
||||
editText.setText(currentColorString);
|
||||
editText.setSelection(currentColorString.length());
|
||||
editText.setTypeface(Typeface.MONOSPACE);
|
||||
colorTextWatcher = createColorTextWatcher(dialogColorPickerView);
|
||||
editText.addTextChangedListener(colorTextWatcher);
|
||||
inputLayout.addView(editText);
|
||||
|
||||
// Add a dummy view to take up remaining horizontal space,
|
||||
// otherwise it will show an oversize underlined text view.
|
||||
View paddingView = new View(context);
|
||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
||||
0,
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
1f
|
||||
);
|
||||
paddingView.setLayoutParams(params);
|
||||
inputLayout.addView(paddingView);
|
||||
|
||||
// Create content container for color picker and input layout.
|
||||
LinearLayout contentContainer = new LinearLayout(context);
|
||||
contentContainer.setOrientation(LinearLayout.VERTICAL);
|
||||
contentContainer.addView(colorPicker);
|
||||
contentContainer.addView(inputLayout);
|
||||
|
||||
// Create ScrollView to wrap the content container.
|
||||
ScrollView contentScrollView = new ScrollView(context);
|
||||
contentScrollView.setVerticalScrollBarEnabled(false); // Disable vertical scrollbar.
|
||||
contentScrollView.setOverScrollMode(View.OVER_SCROLL_NEVER); // Disable overscroll effect.
|
||||
LinearLayout.LayoutParams scrollViewParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
0,
|
||||
1.0f
|
||||
);
|
||||
contentScrollView.setLayoutParams(scrollViewParams);
|
||||
contentScrollView.addView(contentContainer);
|
||||
|
||||
// Create custom dialog.
|
||||
final int originalColor = currentColor & 0x00FFFFFF;
|
||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
||||
context,
|
||||
getTitle() != null ? getTitle().toString() : str("revanced_settings_color_picker_title"), // Title.
|
||||
null, // No message.
|
||||
null, // No EditText.
|
||||
null, // OK button text.
|
||||
() -> {
|
||||
// OK button action.
|
||||
try {
|
||||
String colorString = editText.getText().toString();
|
||||
if (colorString.length() != COLOR_STRING_LENGTH) {
|
||||
Utils.showToastShort(str("revanced_settings_color_invalid"));
|
||||
setText(getColorString(originalColor));
|
||||
return;
|
||||
}
|
||||
setText(colorString);
|
||||
} catch (Exception ex) {
|
||||
// Should never happen due to a bad color string,
|
||||
// since the text is validated and fixed while the user types.
|
||||
Logger.printException(() -> "OK button failure", ex);
|
||||
}
|
||||
},
|
||||
() -> {
|
||||
// Cancel button action.
|
||||
try {
|
||||
// Restore the original color.
|
||||
setText(getColorString(originalColor));
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Cancel button failure", ex);
|
||||
}
|
||||
},
|
||||
str("revanced_settings_reset_color"), // Neutral button text.
|
||||
() -> {
|
||||
// Neutral button action.
|
||||
try {
|
||||
final int defaultColor = Color.parseColor(colorSetting.defaultValue) & 0x00FFFFFF;
|
||||
// Setting view color causes listener callback into this class.
|
||||
dialogColorPickerView.setColor(defaultColor);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Reset button failure", ex);
|
||||
}
|
||||
},
|
||||
false // Do not dismiss dialog when onNeutralClick.
|
||||
);
|
||||
|
||||
// Add the ScrollView to the dialog's main layout.
|
||||
LinearLayout dialogMainLayout = dialogPair.second;
|
||||
dialogMainLayout.addView(contentScrollView, dialogMainLayout.getChildCount() - 1);
|
||||
|
||||
// Set up color picker listener with debouncing.
|
||||
// Add listener last to prevent callbacks from set calls above.
|
||||
dialogColorPickerView.setOnColorChangedListener(color -> {
|
||||
// Check if it actually changed, since this callback
|
||||
// can be caused by updates in afterTextChanged().
|
||||
if (currentColor == color) {
|
||||
return;
|
||||
}
|
||||
|
||||
String updatedColorString = getColorString(color);
|
||||
Logger.printDebug(() -> "onColorChanged: " + updatedColorString);
|
||||
currentColor = color;
|
||||
editText.setText(updatedColorString);
|
||||
editText.setSelection(updatedColorString.length());
|
||||
|
||||
updateColorPreview();
|
||||
updateWidgetColorDot();
|
||||
});
|
||||
|
||||
// Configure and show the dialog.
|
||||
Dialog dialog = dialogPair.first;
|
||||
dialog.setCanceledOnTouchOutside(false);
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDialogClosed(boolean positiveResult) {
|
||||
super.onDialogClosed(positiveResult);
|
||||
|
||||
if (colorTextWatcher != null) {
|
||||
getEditText().removeTextChangedListener(colorTextWatcher);
|
||||
colorTextWatcher = null;
|
||||
}
|
||||
|
||||
dialogColorPreview = null;
|
||||
dialogColorPickerView = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(boolean enabled) {
|
||||
super.setEnabled(enabled);
|
||||
updateWidgetColorDot();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,500 @@
|
||||
package app.revanced.extension.shared.settings.preference;
|
||||
|
||||
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||
import static app.revanced.extension.shared.settings.preference.ColorPickerPreference.getColorString;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.ComposeShader;
|
||||
import android.graphics.LinearGradient;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.Shader;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
|
||||
/**
|
||||
* A custom color picker view that allows the user to select a color using a hue slider and a saturation-value selector.
|
||||
* This implementation is density-independent and responsive across different screen sizes and DPIs.
|
||||
*
|
||||
* <p>
|
||||
* This view displays two main components for color selection:
|
||||
* <ul>
|
||||
* <li><b>Hue Bar:</b> A horizontal bar at the bottom that allows the user to select the hue component of the color.
|
||||
* <li><b>Saturation-Value Selector:</b> A rectangular area above the hue bar that allows the user to select the saturation and value (brightness)
|
||||
* components of the color based on the selected hue.
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* The view uses {@link LinearGradient} and {@link ComposeShader} to create the color gradients for the hue bar and the
|
||||
* saturation-value selector. It also uses {@link Paint} to draw the selectors (draggable handles).
|
||||
*
|
||||
* <p>
|
||||
* The selected color can be retrieved using {@link #getColor()} and can be set using {@link #setColor(int)}.
|
||||
* An {@link OnColorChangedListener} can be registered to receive notifications when the selected color changes.
|
||||
*/
|
||||
public class ColorPickerView extends View {
|
||||
|
||||
/**
|
||||
* Interface definition for a callback to be invoked when the selected color changes.
|
||||
*/
|
||||
public interface OnColorChangedListener {
|
||||
/**
|
||||
* Called when the selected color has changed.
|
||||
*
|
||||
* Important: Callback color uses RGB format with zero alpha channel.
|
||||
*
|
||||
* @param color The new selected color.
|
||||
*/
|
||||
void onColorChanged(@ColorInt int color);
|
||||
}
|
||||
|
||||
/** Expanded touch area for the hue bar to increase the touch-sensitive area. */
|
||||
public static final float TOUCH_EXPANSION = dipToPixels(20f);
|
||||
|
||||
private static final float MARGIN_BETWEEN_AREAS = dipToPixels(24);
|
||||
private static final float VIEW_PADDING = dipToPixels(16);
|
||||
private static final float HUE_BAR_HEIGHT = dipToPixels(12);
|
||||
private static final float HUE_CORNER_RADIUS = dipToPixels(6);
|
||||
private static final float SELECTOR_RADIUS = dipToPixels(12);
|
||||
private static final float SELECTOR_STROKE_WIDTH = 8;
|
||||
/**
|
||||
* Hue fill radius. Use slightly smaller radius for the selector handle fill,
|
||||
* otherwise the anti-aliasing causes the fill color to bleed past the selector outline.
|
||||
*/
|
||||
private static final float SELECTOR_FILL_RADIUS = SELECTOR_RADIUS - SELECTOR_STROKE_WIDTH / 2;
|
||||
/** Thin dark outline stroke width for the selector rings. */
|
||||
private static final float SELECTOR_EDGE_STROKE_WIDTH = 1;
|
||||
public static final float SELECTOR_EDGE_RADIUS =
|
||||
SELECTOR_RADIUS + SELECTOR_STROKE_WIDTH / 2 + SELECTOR_EDGE_STROKE_WIDTH / 2;
|
||||
|
||||
/** Selector outline inner color. */
|
||||
@ColorInt
|
||||
private static final int SELECTOR_OUTLINE_COLOR = Color.WHITE;
|
||||
|
||||
/** Dark edge color for the selector rings. */
|
||||
@ColorInt
|
||||
private static final int SELECTOR_EDGE_COLOR = Color.parseColor("#CFCFCF");
|
||||
|
||||
private static final int[] HUE_COLORS = new int[361];
|
||||
static {
|
||||
for (int i = 0; i < 361; i++) {
|
||||
HUE_COLORS[i] = Color.HSVToColor(new float[]{i, 1, 1});
|
||||
}
|
||||
}
|
||||
|
||||
/** Hue bar. */
|
||||
private final Paint huePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
/** Saturation-value selector. */
|
||||
private final Paint saturationValuePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
/** Draggable selector. */
|
||||
private final Paint selectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
{
|
||||
selectorPaint.setStrokeWidth(SELECTOR_STROKE_WIDTH);
|
||||
}
|
||||
|
||||
/** Bounds of the hue bar. */
|
||||
private final RectF hueRect = new RectF();
|
||||
/** Bounds of the saturation-value selector. */
|
||||
private final RectF saturationValueRect = new RectF();
|
||||
|
||||
/** HSV color calculations to avoid allocations during drawing. */
|
||||
private final float[] hsvArray = {1, 1, 1};
|
||||
|
||||
/** Current hue value (0-360). */
|
||||
private float hue = 0f;
|
||||
/** Current saturation value (0-1). */
|
||||
private float saturation = 1f;
|
||||
/** Current value (brightness) value (0-1). */
|
||||
private float value = 1f;
|
||||
|
||||
/** The currently selected color in RGB format with no alpha channel. */
|
||||
@ColorInt
|
||||
private int selectedColor;
|
||||
|
||||
private OnColorChangedListener colorChangedListener;
|
||||
|
||||
/** Track if we're currently dragging the hue or saturation handle. */
|
||||
private boolean isDraggingHue;
|
||||
private boolean isDraggingSaturation;
|
||||
|
||||
public ColorPickerView(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public ColorPickerView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public ColorPickerView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
final float DESIRED_ASPECT_RATIO = 0.8f; // height = width * 0.8
|
||||
|
||||
final int minWidth = Utils.dipToPixels(250);
|
||||
final int minHeight = (int) (minWidth * DESIRED_ASPECT_RATIO) + (int) (HUE_BAR_HEIGHT + MARGIN_BETWEEN_AREAS);
|
||||
|
||||
int width = resolveSize(minWidth, widthMeasureSpec);
|
||||
int height = resolveSize(minHeight, heightMeasureSpec);
|
||||
|
||||
// Ensure minimum dimensions for usability.
|
||||
width = Math.max(width, minWidth);
|
||||
height = Math.max(height, minHeight);
|
||||
|
||||
// Adjust height to maintain desired aspect ratio if possible.
|
||||
final int desiredHeight = (int) (width * DESIRED_ASPECT_RATIO) + (int) (HUE_BAR_HEIGHT + MARGIN_BETWEEN_AREAS);
|
||||
if (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY) {
|
||||
height = desiredHeight;
|
||||
}
|
||||
|
||||
setMeasuredDimension(width, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the size of the view changes.
|
||||
* This method calculates and sets the bounds of the hue bar and saturation-value selector.
|
||||
* It also creates the necessary shaders for the gradients.
|
||||
*/
|
||||
@Override
|
||||
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
|
||||
super.onSizeChanged(width, height, oldWidth, oldHeight);
|
||||
|
||||
// Calculate bounds with hue bar at the bottom.
|
||||
final float effectiveWidth = width - (2 * VIEW_PADDING);
|
||||
final float effectiveHeight = height - (2 * VIEW_PADDING) - HUE_BAR_HEIGHT - MARGIN_BETWEEN_AREAS;
|
||||
|
||||
// Adjust rectangles to account for padding and density-independent dimensions.
|
||||
saturationValueRect.set(
|
||||
VIEW_PADDING,
|
||||
VIEW_PADDING,
|
||||
VIEW_PADDING + effectiveWidth,
|
||||
VIEW_PADDING + effectiveHeight
|
||||
);
|
||||
|
||||
hueRect.set(
|
||||
VIEW_PADDING,
|
||||
height - VIEW_PADDING - HUE_BAR_HEIGHT,
|
||||
VIEW_PADDING + effectiveWidth,
|
||||
height - VIEW_PADDING
|
||||
);
|
||||
|
||||
// Update the shaders.
|
||||
updateHueShader();
|
||||
updateSaturationValueShader();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the hue full spectrum (0-360 degrees).
|
||||
*/
|
||||
private void updateHueShader() {
|
||||
LinearGradient hueShader = new LinearGradient(
|
||||
hueRect.left, hueRect.top,
|
||||
hueRect.right, hueRect.top,
|
||||
HUE_COLORS,
|
||||
null,
|
||||
Shader.TileMode.CLAMP
|
||||
);
|
||||
|
||||
huePaint.setShader(hueShader);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the shader for the saturation-value selector based on the currently selected hue.
|
||||
* This method creates a combined shader that blends a saturation gradient with a value gradient.
|
||||
*/
|
||||
private void updateSaturationValueShader() {
|
||||
// Create a saturation-value gradient based on the current hue.
|
||||
// Calculate the start color (white with the selected hue) for the saturation gradient.
|
||||
final int startColor = Color.HSVToColor(new float[]{hue, 0f, 1f});
|
||||
|
||||
// Calculate the middle color (fully saturated color with the selected hue) for the saturation gradient.
|
||||
final int midColor = Color.HSVToColor(new float[]{hue, 1f, 1f});
|
||||
|
||||
// Create a linear gradient for the saturation from startColor to midColor (horizontal).
|
||||
LinearGradient satShader = new LinearGradient(
|
||||
saturationValueRect.left, saturationValueRect.top,
|
||||
saturationValueRect.right, saturationValueRect.top,
|
||||
startColor,
|
||||
midColor,
|
||||
Shader.TileMode.CLAMP
|
||||
);
|
||||
|
||||
// Create a linear gradient for the value (brightness) from white to black (vertical).
|
||||
//noinspection ExtractMethodRecommender
|
||||
LinearGradient valShader = new LinearGradient(
|
||||
saturationValueRect.left, saturationValueRect.top,
|
||||
saturationValueRect.left, saturationValueRect.bottom,
|
||||
Color.WHITE,
|
||||
Color.BLACK,
|
||||
Shader.TileMode.CLAMP
|
||||
);
|
||||
|
||||
// Combine the saturation and value shaders using PorterDuff.Mode.MULTIPLY to create the final color.
|
||||
ComposeShader combinedShader = new ComposeShader(satShader, valShader, PorterDuff.Mode.MULTIPLY);
|
||||
|
||||
// Set the combined shader for the saturation-value paint.
|
||||
saturationValuePaint.setShader(combinedShader);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the color picker view on the canvas.
|
||||
* This method draws the saturation-value selector, the hue bar with rounded corners,
|
||||
* and the draggable handles.
|
||||
*
|
||||
* @param canvas The canvas on which to draw.
|
||||
*/
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
// Draw the saturation-value selector rectangle.
|
||||
canvas.drawRect(saturationValueRect, saturationValuePaint);
|
||||
|
||||
// Draw the hue bar.
|
||||
canvas.drawRoundRect(hueRect, HUE_CORNER_RADIUS, HUE_CORNER_RADIUS, huePaint);
|
||||
|
||||
final float hueSelectorX = hueRect.left + (hue / 360f) * hueRect.width();
|
||||
final float hueSelectorY = hueRect.centerY();
|
||||
|
||||
final float satSelectorX = saturationValueRect.left + saturation * saturationValueRect.width();
|
||||
final float satSelectorY = saturationValueRect.top + (1 - value) * saturationValueRect.height();
|
||||
|
||||
// Draw the saturation and hue selector handle filled with the selected color.
|
||||
hsvArray[0] = hue;
|
||||
final int hueHandleColor = Color.HSVToColor(0xFF, hsvArray);
|
||||
selectorPaint.setStyle(Paint.Style.FILL_AND_STROKE);
|
||||
|
||||
selectorPaint.setColor(hueHandleColor);
|
||||
canvas.drawCircle(hueSelectorX, hueSelectorY, SELECTOR_FILL_RADIUS, selectorPaint);
|
||||
|
||||
selectorPaint.setColor(selectedColor | 0xFF000000);
|
||||
canvas.drawCircle(satSelectorX, satSelectorY, SELECTOR_FILL_RADIUS, selectorPaint);
|
||||
|
||||
// Draw white outlines for the handles.
|
||||
selectorPaint.setColor(SELECTOR_OUTLINE_COLOR);
|
||||
selectorPaint.setStyle(Paint.Style.STROKE);
|
||||
selectorPaint.setStrokeWidth(SELECTOR_STROKE_WIDTH);
|
||||
canvas.drawCircle(hueSelectorX, hueSelectorY, SELECTOR_RADIUS, selectorPaint);
|
||||
canvas.drawCircle(satSelectorX, satSelectorY, SELECTOR_RADIUS, selectorPaint);
|
||||
|
||||
// Draw thin dark outlines for the handles at the outer edge of the white outline.
|
||||
selectorPaint.setColor(SELECTOR_EDGE_COLOR);
|
||||
selectorPaint.setStrokeWidth(SELECTOR_EDGE_STROKE_WIDTH);
|
||||
canvas.drawCircle(hueSelectorX, hueSelectorY, SELECTOR_EDGE_RADIUS, selectorPaint);
|
||||
canvas.drawCircle(satSelectorX, satSelectorY, SELECTOR_EDGE_RADIUS, selectorPaint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles touch events on the view.
|
||||
* This method determines whether the touch event occurred within the hue bar or the saturation-value selector,
|
||||
* updates the corresponding values (hue, saturation, value), and invalidates the view to trigger a redraw.
|
||||
* <p>
|
||||
* In addition to testing if the touch is within the strict rectangles, an expanded hit area (by selectorRadius)
|
||||
* is used so that the draggable handles remain active even when half of the handle is outside the drawn bounds.
|
||||
*
|
||||
* @param event The motion event.
|
||||
* @return True if the event was handled, false otherwise.
|
||||
*/
|
||||
@SuppressLint("ClickableViewAccessibility") // performClick is not overridden, but not needed in this case.
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
try {
|
||||
final float x = event.getX();
|
||||
final float y = event.getY();
|
||||
final int action = event.getAction();
|
||||
Logger.printDebug(() -> "onTouchEvent action: " + action + " x: " + x + " y: " + y);
|
||||
|
||||
// Define touch expansion for the hue bar.
|
||||
RectF expandedHueRect = new RectF(
|
||||
hueRect.left,
|
||||
hueRect.top - TOUCH_EXPANSION,
|
||||
hueRect.right,
|
||||
hueRect.bottom + TOUCH_EXPANSION
|
||||
);
|
||||
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
// Calculate current handle positions.
|
||||
final float hueSelectorX = hueRect.left + (hue / 360f) * hueRect.width();
|
||||
final float hueSelectorY = hueRect.centerY();
|
||||
|
||||
final float satSelectorX = saturationValueRect.left + saturation * saturationValueRect.width();
|
||||
final float valSelectorY = saturationValueRect.top + (1 - value) * saturationValueRect.height();
|
||||
|
||||
// Create hit areas for both handles.
|
||||
RectF hueHitRect = new RectF(
|
||||
hueSelectorX - SELECTOR_RADIUS,
|
||||
hueSelectorY - SELECTOR_RADIUS,
|
||||
hueSelectorX + SELECTOR_RADIUS,
|
||||
hueSelectorY + SELECTOR_RADIUS
|
||||
);
|
||||
RectF satValHitRect = new RectF(
|
||||
satSelectorX - SELECTOR_RADIUS,
|
||||
valSelectorY - SELECTOR_RADIUS,
|
||||
satSelectorX + SELECTOR_RADIUS,
|
||||
valSelectorY + SELECTOR_RADIUS
|
||||
);
|
||||
|
||||
// Check if the touch started on a handle or within the expanded hue bar area.
|
||||
if (hueHitRect.contains(x, y)) {
|
||||
isDraggingHue = true;
|
||||
updateHueFromTouch(x);
|
||||
} else if (satValHitRect.contains(x, y)) {
|
||||
isDraggingSaturation = true;
|
||||
updateSaturationValueFromTouch(x, y);
|
||||
} else if (expandedHueRect.contains(x, y)) {
|
||||
// Handle touch within the expanded hue bar area.
|
||||
isDraggingHue = true;
|
||||
updateHueFromTouch(x);
|
||||
} else if (saturationValueRect.contains(x, y)) {
|
||||
isDraggingSaturation = true;
|
||||
updateSaturationValueFromTouch(x, y);
|
||||
}
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
// Continue updating values even if touch moves outside the view.
|
||||
if (isDraggingHue) {
|
||||
updateHueFromTouch(x);
|
||||
} else if (isDraggingSaturation) {
|
||||
updateSaturationValueFromTouch(x, y);
|
||||
}
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
isDraggingHue = false;
|
||||
isDraggingSaturation = false;
|
||||
break;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "onTouchEvent failure", ex);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the hue value based on touch position, clamping to valid range.
|
||||
*
|
||||
* @param x The x-coordinate of the touch position.
|
||||
*/
|
||||
private void updateHueFromTouch(float x) {
|
||||
// Clamp x to the hue rectangle bounds.
|
||||
final float clampedX = Utils.clamp(x, hueRect.left, hueRect.right);
|
||||
final float updatedHue = ((clampedX - hueRect.left) / hueRect.width()) * 360f;
|
||||
if (hue == updatedHue) {
|
||||
return;
|
||||
}
|
||||
|
||||
hue = updatedHue;
|
||||
updateSaturationValueShader();
|
||||
updateSelectedColor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates saturation and value based on touch position, clamping to valid range.
|
||||
*
|
||||
* @param x The x-coordinate of the touch position.
|
||||
* @param y The y-coordinate of the touch position.
|
||||
*/
|
||||
private void updateSaturationValueFromTouch(float x, float y) {
|
||||
// Clamp x and y to the saturation-value rectangle bounds.
|
||||
final float clampedX = Utils.clamp(x, saturationValueRect.left, saturationValueRect.right);
|
||||
final float clampedY = Utils.clamp(y, saturationValueRect.top, saturationValueRect.bottom);
|
||||
|
||||
final float updatedSaturation = (clampedX - saturationValueRect.left) / saturationValueRect.width();
|
||||
final float updatedValue = 1 - ((clampedY - saturationValueRect.top) / saturationValueRect.height());
|
||||
|
||||
if (saturation == updatedSaturation && value == updatedValue) {
|
||||
return;
|
||||
}
|
||||
saturation = updatedSaturation;
|
||||
value = updatedValue;
|
||||
updateSelectedColor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the selected color and notifies listeners.
|
||||
*/
|
||||
private void updateSelectedColor() {
|
||||
final int updatedColor = Color.HSVToColor(0, new float[]{hue, saturation, value});
|
||||
|
||||
if (selectedColor != updatedColor) {
|
||||
selectedColor = updatedColor;
|
||||
|
||||
if (colorChangedListener != null) {
|
||||
colorChangedListener.onColorChanged(updatedColor);
|
||||
}
|
||||
}
|
||||
|
||||
// Must always redraw, otherwise if saturation is pure grey or black
|
||||
// then the hue slider cannot be changed.
|
||||
invalidate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the currently selected color.
|
||||
*
|
||||
* @param color The color to set in either ARGB or RGB format.
|
||||
*/
|
||||
public void setColor(@ColorInt int color) {
|
||||
color &= 0x00FFFFFF;
|
||||
if (selectedColor == color) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the selected color.
|
||||
selectedColor = color;
|
||||
Logger.printDebug(() -> "setColor: " + getColorString(selectedColor));
|
||||
|
||||
// Convert the ARGB color to HSV values.
|
||||
float[] hsv = new float[3];
|
||||
Color.colorToHSV(color, hsv);
|
||||
|
||||
// Update the hue, saturation, and value.
|
||||
hue = hsv[0];
|
||||
saturation = hsv[1];
|
||||
value = hsv[2];
|
||||
|
||||
// Update the saturation-value shader based on the new hue.
|
||||
updateSaturationValueShader();
|
||||
|
||||
// Notify the listener if it's set.
|
||||
if (colorChangedListener != null) {
|
||||
colorChangedListener.onColorChanged(selectedColor);
|
||||
}
|
||||
|
||||
// Invalidate the view to trigger a redraw.
|
||||
invalidate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the currently selected color.
|
||||
*
|
||||
* @return The selected color in RGB format with no alpha channel.
|
||||
*/
|
||||
@ColorInt
|
||||
public int getColor() {
|
||||
return selectedColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the listener to be notified when the selected color changes.
|
||||
*
|
||||
* @param listener The listener to set.
|
||||
*/
|
||||
public void setOnColorChangedListener(OnColorChangedListener listener) {
|
||||
colorChangedListener = listener;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package app.revanced.extension.shared.settings.preference;
|
||||
|
||||
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.preference.ListPreference;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Pair;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.*;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import app.revanced.extension.shared.Utils;
|
||||
|
||||
/**
|
||||
* A custom ListPreference that uses a styled custom dialog with a custom checkmark indicator.
|
||||
*/
|
||||
@SuppressWarnings({"unused", "deprecation"})
|
||||
public class CustomDialogListPreference extends ListPreference {
|
||||
|
||||
/**
|
||||
* Custom ArrayAdapter to handle checkmark visibility.
|
||||
*/
|
||||
private static class ListPreferenceArrayAdapter extends ArrayAdapter<CharSequence> {
|
||||
private static class SubViewDataContainer {
|
||||
ImageView checkIcon;
|
||||
View placeholder;
|
||||
TextView itemText;
|
||||
}
|
||||
|
||||
final int layoutResourceId;
|
||||
final CharSequence[] entryValues;
|
||||
String selectedValue;
|
||||
|
||||
public ListPreferenceArrayAdapter(Context context, int resource, CharSequence[] entries,
|
||||
CharSequence[] entryValues, String selectedValue) {
|
||||
super(context, resource, entries);
|
||||
this.layoutResourceId = resource;
|
||||
this.entryValues = entryValues;
|
||||
this.selectedValue = selectedValue;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public View getView(int position, View convertView, @NonNull ViewGroup parent) {
|
||||
View view = convertView;
|
||||
SubViewDataContainer holder;
|
||||
|
||||
if (view == null) {
|
||||
LayoutInflater inflater = LayoutInflater.from(getContext());
|
||||
view = inflater.inflate(layoutResourceId, parent, false);
|
||||
holder = new SubViewDataContainer();
|
||||
holder.checkIcon = view.findViewById(Utils.getResourceIdentifier(
|
||||
"revanced_check_icon", "id"));
|
||||
holder.placeholder = view.findViewById(Utils.getResourceIdentifier(
|
||||
"revanced_check_icon_placeholder", "id"));
|
||||
holder.itemText = view.findViewById(Utils.getResourceIdentifier(
|
||||
"revanced_item_text", "id"));
|
||||
view.setTag(holder);
|
||||
} else {
|
||||
holder = (SubViewDataContainer) view.getTag();
|
||||
}
|
||||
|
||||
// Set text.
|
||||
holder.itemText.setText(getItem(position));
|
||||
holder.itemText.setTextColor(Utils.getAppForegroundColor());
|
||||
|
||||
// Show or hide checkmark and placeholder.
|
||||
String currentValue = entryValues[position].toString();
|
||||
boolean isSelected = currentValue.equals(selectedValue);
|
||||
holder.checkIcon.setVisibility(isSelected ? View.VISIBLE : View.GONE);
|
||||
holder.checkIcon.setColorFilter(Utils.getAppForegroundColor());
|
||||
holder.placeholder.setVisibility(isSelected ? View.GONE : View.VISIBLE);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
public void setSelectedValue(String value) {
|
||||
this.selectedValue = value;
|
||||
}
|
||||
}
|
||||
|
||||
public CustomDialogListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
public CustomDialogListPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public CustomDialogListPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public CustomDialogListPreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void showDialog(Bundle state) {
|
||||
Context context = getContext();
|
||||
|
||||
// Create ListView.
|
||||
ListView listView = new ListView(context);
|
||||
listView.setId(android.R.id.list);
|
||||
listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
|
||||
|
||||
// Create custom adapter for the ListView.
|
||||
ListPreferenceArrayAdapter adapter = new ListPreferenceArrayAdapter(
|
||||
context,
|
||||
Utils.getResourceIdentifier("revanced_custom_list_item_checked", "layout"),
|
||||
getEntries(),
|
||||
getEntryValues(),
|
||||
getValue()
|
||||
);
|
||||
listView.setAdapter(adapter);
|
||||
|
||||
// Set checked item.
|
||||
String currentValue = getValue();
|
||||
if (currentValue != null) {
|
||||
CharSequence[] entryValues = getEntryValues();
|
||||
for (int i = 0, length = entryValues.length; i < length; i++) {
|
||||
if (currentValue.equals(entryValues[i].toString())) {
|
||||
listView.setItemChecked(i, true);
|
||||
listView.setSelection(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create the custom dialog without OK button.
|
||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
||||
context,
|
||||
getTitle() != null ? getTitle().toString() : "",
|
||||
null,
|
||||
null,
|
||||
null, // No OK button text.
|
||||
null, // No OK button action.
|
||||
() -> {}, // Cancel button action (just dismiss).
|
||||
null,
|
||||
null,
|
||||
true
|
||||
);
|
||||
|
||||
// Add the ListView to the main layout.
|
||||
LinearLayout mainLayout = dialogPair.second;
|
||||
LinearLayout.LayoutParams listViewParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
0,
|
||||
1.0f
|
||||
);
|
||||
mainLayout.addView(listView, mainLayout.getChildCount() - 1, listViewParams);
|
||||
|
||||
// Handle item click to select value and dismiss dialog.
|
||||
listView.setOnItemClickListener((parent, view, position, id) -> {
|
||||
String selectedValue = getEntryValues()[position].toString();
|
||||
if (callChangeListener(selectedValue)) {
|
||||
setValue(selectedValue);
|
||||
adapter.setSelectedValue(selectedValue);
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
dialogPair.first.dismiss();
|
||||
});
|
||||
|
||||
// Show the dialog.
|
||||
dialogPair.first.show();
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,30 @@
|
||||
package app.revanced.extension.shared.settings.preference;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.preference.EditTextPreference;
|
||||
import android.preference.Preference;
|
||||
import android.text.InputType;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Pair;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import app.revanced.extension.shared.settings.Setting;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.ShapeDrawable;
|
||||
import android.graphics.drawable.shapes.RoundRectShape;
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
import app.revanced.extension.shared.settings.Setting;
|
||||
|
||||
@SuppressWarnings({"unused", "deprecation"})
|
||||
public class ImportExportPreference extends EditTextPreference implements Preference.OnPreferenceClickListener {
|
||||
@@ -54,7 +65,8 @@ public class ImportExportPreference extends EditTextPreference implements Prefer
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
try {
|
||||
// Must set text before preparing dialog, otherwise text is non selectable if this preference is later reopened.
|
||||
// Must set text before showing dialog,
|
||||
// otherwise text is non-selectable if this preference is later reopened.
|
||||
existingSettings = Setting.exportToJson(getContext());
|
||||
getEditText().setText(existingSettings);
|
||||
} catch (Exception ex) {
|
||||
@@ -64,18 +76,32 @@ public class ImportExportPreference extends EditTextPreference implements Prefer
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
||||
protected void showDialog(Bundle state) {
|
||||
try {
|
||||
Utils.setEditTextDialogTheme(builder);
|
||||
Context context = getContext();
|
||||
EditText editText = getEditText();
|
||||
|
||||
// Show the user the settings in JSON format.
|
||||
builder.setNeutralButton(str("revanced_settings_import_copy"), (dialog, which) -> {
|
||||
Utils.setClipboard(getEditText().getText().toString());
|
||||
}).setPositiveButton(str("revanced_settings_import"), (dialog, which) -> {
|
||||
importSettings(builder.getContext(), getEditText().getText().toString());
|
||||
});
|
||||
// Create a custom dialog with the EditText.
|
||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
||||
context,
|
||||
str("revanced_pref_import_export_title"), // Title.
|
||||
null, // No message (EditText replaces it).
|
||||
editText, // Pass the EditText.
|
||||
str("revanced_settings_import"), // OK button text.
|
||||
() -> importSettings(context, editText.getText().toString()), // OK button action.
|
||||
() -> {}, // Cancel button action (dismiss only).
|
||||
str("revanced_settings_import_copy"), // Neutral button (Copy) text.
|
||||
() -> {
|
||||
// Neutral button (Copy) action. Show the user the settings in JSON format.
|
||||
Utils.setClipboard(editText.getText());
|
||||
},
|
||||
true // Dismiss dialog when onNeutralClick.
|
||||
);
|
||||
|
||||
// Show the dialog.
|
||||
dialogPair.first.show();
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "onPrepareDialogBuilder failure", ex);
|
||||
Logger.printException(() -> "showDialog failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +114,7 @@ public class ImportExportPreference extends EditTextPreference implements Prefer
|
||||
|
||||
final boolean rebootNeeded = Setting.importFromJSON(context, replacementSettings);
|
||||
if (rebootNeeded) {
|
||||
AbstractPreferenceFragment.showRestartDialog(getContext());
|
||||
AbstractPreferenceFragment.showRestartDialog(context);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "importSettings failure", ex);
|
||||
@@ -96,5 +122,4 @@ public class ImportExportPreference extends EditTextPreference implements Prefer
|
||||
AbstractPreferenceFragment.settingImportInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
package app.revanced.extension.shared.settings.preference;
|
||||
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
|
||||
import java.util.Deque;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
|
||||
/**
|
||||
* Manages a buffer for storing debug logs from {@link Logger}.
|
||||
* Stores just under 1MB of the most recent log data.
|
||||
*
|
||||
* All methods are thread-safe.
|
||||
*/
|
||||
public final class LogBufferManager {
|
||||
/** Maximum byte size of all buffer entries. Must be less than Android's 1 MB Binder transaction limit. */
|
||||
private static final int BUFFER_MAX_BYTES = 900_000;
|
||||
/** Limit number of log lines. */
|
||||
private static final int BUFFER_MAX_SIZE = 10_000;
|
||||
|
||||
private static final Deque<String> logBuffer = new ConcurrentLinkedDeque<>();
|
||||
private static final AtomicInteger logBufferByteSize = new AtomicInteger();
|
||||
|
||||
/**
|
||||
* Appends a log message to the internal buffer if debugging is enabled.
|
||||
* The buffer is limited to approximately {@link #BUFFER_MAX_BYTES} or {@link #BUFFER_MAX_SIZE}
|
||||
* to prevent excessive memory usage.
|
||||
*
|
||||
* @param message The log message to append.
|
||||
*/
|
||||
public static void appendToLogBuffer(String message) {
|
||||
Objects.requireNonNull(message);
|
||||
|
||||
// It's very important that no Settings are used in this method,
|
||||
// as this code is used when a context is not set and thus referencing
|
||||
// a setting will crash the app.
|
||||
logBuffer.addLast(message);
|
||||
int newSize = logBufferByteSize.addAndGet(message.length());
|
||||
|
||||
// Remove oldest entries if over the log size limits.
|
||||
while (newSize > BUFFER_MAX_BYTES || logBuffer.size() > BUFFER_MAX_SIZE) {
|
||||
String removed = logBuffer.pollFirst();
|
||||
if (removed == null) {
|
||||
// Thread race of two different calls to this method, and the other thread won.
|
||||
return;
|
||||
}
|
||||
|
||||
newSize = logBufferByteSize.addAndGet(-removed.length());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports all logs from the internal buffer to the clipboard.
|
||||
* Displays a toast with the result.
|
||||
*/
|
||||
public static void exportToClipboard() {
|
||||
try {
|
||||
if (!BaseSettings.DEBUG.get()) {
|
||||
Utils.showToastShort(str("revanced_debug_logs_disabled"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (logBuffer.isEmpty()) {
|
||||
Utils.showToastShort(str("revanced_debug_logs_none_found"));
|
||||
clearLogBufferData(); // Clear toast log entry that was just created.
|
||||
return;
|
||||
}
|
||||
|
||||
// Most (but not all) Android 13+ devices always show a "copied to clipboard" toast
|
||||
// and there is no way to programmatically detect if a toast will show or not.
|
||||
// Show a toast even if using Android 13+, but show ReVanced toast first (before copying to clipboard).
|
||||
Utils.showToastShort(str("revanced_debug_logs_copied_to_clipboard"));
|
||||
|
||||
Utils.setClipboard(String.join("\n", logBuffer));
|
||||
} catch (Exception ex) {
|
||||
// Handle security exception if clipboard access is denied.
|
||||
String errorMessage = String.format(str("revanced_debug_logs_failed_to_export"), ex.getMessage());
|
||||
Utils.showToastLong(errorMessage);
|
||||
Logger.printDebug(() -> errorMessage, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void clearLogBufferData() {
|
||||
// Cannot simply clear the log buffer because there is no
|
||||
// write lock for both the deque and the atomic int.
|
||||
// Instead pop off log entries and decrement the size one by one.
|
||||
while (!logBuffer.isEmpty()) {
|
||||
String removed = logBuffer.pollFirst();
|
||||
if (removed != null) {
|
||||
logBufferByteSize.addAndGet(-removed.length());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the internal log buffer and displays a toast with the result.
|
||||
*/
|
||||
public static void clearLogBuffer() {
|
||||
if (!BaseSettings.DEBUG.get()) {
|
||||
Utils.showToastShort(str("revanced_debug_logs_disabled"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Show toast before clearing, otherwise toast log will still remain.
|
||||
Utils.showToastShort(str("revanced_debug_logs_clear_toast"));
|
||||
clearLogBufferData();
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,10 @@ public class NoTitlePreferenceCategory extends PreferenceCategory {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public NoTitlePreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
public NoTitlePreferenceCategory(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package app.revanced.extension.shared.settings.preference;
|
||||
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||
import static app.revanced.extension.shared.requests.Route.Method.GET;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
@@ -8,17 +9,19 @@ import android.app.Dialog;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.ShapeDrawable;
|
||||
import android.graphics.drawable.shapes.RoundRectShape;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.preference.Preference;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -49,28 +52,6 @@ public class ReVancedAboutPreference extends Preference {
|
||||
return text.replace("-", "‑"); // #8209 = non breaking hyphen.
|
||||
}
|
||||
|
||||
private static String getColorHexString(int color) {
|
||||
return String.format("#%06X", (0x00FFFFFF & color));
|
||||
}
|
||||
|
||||
protected boolean isDarkModeEnabled() {
|
||||
return Utils.isDarkModeEnabled(getContext());
|
||||
}
|
||||
|
||||
/**
|
||||
* Subclasses can override this and provide a themed color.
|
||||
*/
|
||||
protected int getLightColor() {
|
||||
return Color.WHITE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subclasses can override this and provide a themed color.
|
||||
*/
|
||||
protected int getDarkColor() {
|
||||
return Color.BLACK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apps that do not support bundling resources must override this.
|
||||
*
|
||||
@@ -87,9 +68,8 @@ public class ReVancedAboutPreference extends Preference {
|
||||
builder.append("<html>");
|
||||
builder.append("<body style=\"text-align: center; padding: 10px;\">");
|
||||
|
||||
final boolean isDarkMode = isDarkModeEnabled();
|
||||
String backgroundColorHex = getColorHexString(isDarkMode ? getDarkColor() : getLightColor());
|
||||
String foregroundColorHex = getColorHexString(isDarkMode ? getLightColor() : getDarkColor());
|
||||
String foregroundColorHex = Utils.getColorHexString(Utils.getAppForegroundColor());
|
||||
String backgroundColorHex = Utils.getColorHexString(Utils.getDialogBackgroundColor());
|
||||
// Apply light/dark mode colors.
|
||||
builder.append(String.format(
|
||||
"<style> body { background-color: %s; color: %s; } a { color: %s; } </style>",
|
||||
@@ -221,14 +201,38 @@ class WebViewDialog extends Dialog {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE); // Remove default title bar.
|
||||
|
||||
// Create main layout.
|
||||
LinearLayout mainLayout = new LinearLayout(getContext());
|
||||
mainLayout.setOrientation(LinearLayout.VERTICAL);
|
||||
|
||||
final int padding = dipToPixels(10);
|
||||
mainLayout.setPadding(padding, padding, padding, padding);
|
||||
// Set rounded rectangle background.
|
||||
ShapeDrawable mainBackground = new ShapeDrawable(new RoundRectShape(
|
||||
Utils.createCornerRadii(28), null, null));
|
||||
mainBackground.getPaint().setColor(Utils.getDialogBackgroundColor());
|
||||
mainLayout.setBackground(mainBackground);
|
||||
|
||||
// Create WebView.
|
||||
WebView webView = new WebView(getContext());
|
||||
webView.setVerticalScrollBarEnabled(false); // Disable the vertical scrollbar.
|
||||
webView.setOverScrollMode(View.OVER_SCROLL_NEVER);
|
||||
webView.getSettings().setJavaScriptEnabled(true);
|
||||
webView.setWebViewClient(new OpenLinksExternallyWebClient());
|
||||
webView.loadDataWithBaseURL(null, htmlContent, "text/html", "utf-8", null);
|
||||
|
||||
setContentView(webView);
|
||||
// Add WebView to layout.
|
||||
mainLayout.addView(webView);
|
||||
|
||||
setContentView(mainLayout);
|
||||
|
||||
// Set dialog window attributes
|
||||
Window window = getWindow();
|
||||
if (window != null) {
|
||||
Utils.setDialogWindowParameters(window);
|
||||
}
|
||||
}
|
||||
|
||||
private class OpenLinksExternallyWebClient extends WebViewClient {
|
||||
@@ -316,7 +320,7 @@ class AboutLinksRoutes {
|
||||
// Do not show an exception toast if the server is down
|
||||
final int responseCode = connection.getResponseCode();
|
||||
if (responseCode != 200) {
|
||||
Logger.printDebug(() -> "Failed to get social links. Response code: " + responseCode);
|
||||
Logger.printDebug(() -> "Failed to get social links. Response code: " + responseCode);
|
||||
return NO_CONNECTION_STATIC_LINKS;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,28 @@
|
||||
package app.revanced.extension.shared.settings.preference;
|
||||
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
import android.graphics.drawable.shapes.RectShape;
|
||||
import android.graphics.drawable.shapes.RoundRectShape;
|
||||
import android.graphics.drawable.ShapeDrawable;
|
||||
import android.graphics.Paint.Style;
|
||||
import android.os.Bundle;
|
||||
import android.preference.EditTextPreference;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Pair;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
@@ -44,41 +58,61 @@ public class ResettableEditTextPreference extends EditTextPreference {
|
||||
this.setting = setting;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
||||
super.onPrepareDialogBuilder(builder);
|
||||
Utils.setEditTextDialogTheme(builder);
|
||||
|
||||
if (setting == null) {
|
||||
String key = getKey();
|
||||
if (key != null) {
|
||||
setting = Setting.getSettingFromPath(key);
|
||||
}
|
||||
}
|
||||
|
||||
if (setting != null) {
|
||||
builder.setNeutralButton(str("revanced_settings_reset"), null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void showDialog(Bundle state) {
|
||||
super.showDialog(state);
|
||||
try {
|
||||
Context context = getContext();
|
||||
EditText editText = getEditText();
|
||||
|
||||
// Override the button click listener to prevent dismissing the dialog.
|
||||
Button button = ((AlertDialog) getDialog()).getButton(AlertDialog.BUTTON_NEUTRAL);
|
||||
if (button == null) {
|
||||
return;
|
||||
}
|
||||
button.setOnClickListener(v -> {
|
||||
try {
|
||||
String defaultStringValue = Objects.requireNonNull(setting).defaultValue.toString();
|
||||
EditText editText = getEditText();
|
||||
editText.setText(defaultStringValue);
|
||||
editText.setSelection(defaultStringValue.length()); // move cursor to end of text
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "reset failure", ex);
|
||||
// Resolve setting if not already set.
|
||||
if (setting == null) {
|
||||
String key = getKey();
|
||||
if (key != null) {
|
||||
setting = Setting.getSettingFromPath(key);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Set initial EditText value to the current persisted value or empty string.
|
||||
String initialValue = getText() != null ? getText() : "";
|
||||
editText.setText(initialValue);
|
||||
editText.setSelection(initialValue.length()); // Move cursor to end.
|
||||
|
||||
// Create custom dialog.
|
||||
String neutralButtonText = (setting != null) ? str("revanced_settings_reset") : null;
|
||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
||||
context,
|
||||
getTitle() != null ? getTitle().toString() : "", // Title.
|
||||
null, // Message is replaced by EditText.
|
||||
editText, // Pass the EditText.
|
||||
null, // OK button text.
|
||||
() -> {
|
||||
// OK button action. Persist the EditText value when OK is clicked.
|
||||
String newValue = editText.getText().toString();
|
||||
if (callChangeListener(newValue)) {
|
||||
setText(newValue);
|
||||
}
|
||||
},
|
||||
() -> {}, // Cancel button action (dismiss only).
|
||||
neutralButtonText, // Neutral button text (Reset).
|
||||
() -> {
|
||||
// Neutral button action.
|
||||
if (setting != null) {
|
||||
try {
|
||||
String defaultStringValue = Objects.requireNonNull(setting).defaultValue.toString();
|
||||
editText.setText(defaultStringValue);
|
||||
editText.setSelection(defaultStringValue.length()); // Move cursor to end of text.
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "reset failure", ex);
|
||||
}
|
||||
}
|
||||
},
|
||||
false // Do not dismiss dialog when onNeutralClick.
|
||||
);
|
||||
|
||||
// Show the dialog.
|
||||
dialogPair.first.show();
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "showDialog failure", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
package app.revanced.extension.shared.settings.preference;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Pair;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import app.revanced.extension.shared.Utils;
|
||||
|
||||
/**
|
||||
* PreferenceList that sorts itself.
|
||||
* By default the first entry is preserved in its original position,
|
||||
* and all other entries are sorted alphabetically.
|
||||
*
|
||||
* Ideally the 'keep first entries to preserve' is an xml parameter,
|
||||
* but currently that's not so simple since Extensions code cannot use
|
||||
* generated code from the Patches repo (which is required for custom xml parameters).
|
||||
*
|
||||
* If any class wants to use a different getFirstEntriesToPreserve value,
|
||||
* it needs to subclass this preference and override {@link #getFirstEntriesToPreserve}.
|
||||
*/
|
||||
@SuppressWarnings({"unused", "deprecation"})
|
||||
public class SortedListPreference extends CustomDialogListPreference {
|
||||
|
||||
/**
|
||||
* Sorts the current list entries.
|
||||
*
|
||||
* @param firstEntriesToPreserve The number of entries to preserve in their original position,
|
||||
* or a negative value to not sort and leave entries
|
||||
* as they current are.
|
||||
*/
|
||||
public void sortEntryAndValues(int firstEntriesToPreserve) {
|
||||
CharSequence[] entries = getEntries();
|
||||
CharSequence[] entryValues = getEntryValues();
|
||||
if (entries == null || entryValues == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int entrySize = entries.length;
|
||||
if (entrySize != entryValues.length) {
|
||||
// Xml array declaration has a missing/extra entry.
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
if (firstEntriesToPreserve < 0) {
|
||||
return; // Nothing to do.
|
||||
}
|
||||
|
||||
List<Pair<CharSequence, CharSequence>> firstEntries = new ArrayList<>(firstEntriesToPreserve);
|
||||
|
||||
// Android does not have a triple class like Kotlin, So instead use a nested pair.
|
||||
// Cannot easily use a SortedMap, because if two entries incorrectly have
|
||||
// identical names then the duplicates entries are not preserved.
|
||||
List<Pair<String, Pair<CharSequence, CharSequence>>> lastEntries = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < entrySize; i++) {
|
||||
Pair<CharSequence, CharSequence> pair = new Pair<>(entries[i], entryValues[i]);
|
||||
if (i < firstEntriesToPreserve) {
|
||||
firstEntries.add(pair);
|
||||
} else {
|
||||
lastEntries.add(new Pair<>(Utils.removePunctuationToLowercase(pair.first), pair));
|
||||
}
|
||||
}
|
||||
|
||||
//noinspection ComparatorCombinators
|
||||
Collections.sort(lastEntries, (pair1, pair2)
|
||||
-> pair1.first.compareTo(pair2.first));
|
||||
|
||||
CharSequence[] sortedEntries = new CharSequence[entrySize];
|
||||
CharSequence[] sortedEntryValues = new CharSequence[entrySize];
|
||||
|
||||
int i = 0;
|
||||
for (Pair<CharSequence, CharSequence> pair : firstEntries) {
|
||||
sortedEntries[i] = pair.first;
|
||||
sortedEntryValues[i] = pair.second;
|
||||
i++;
|
||||
}
|
||||
|
||||
for (Pair<String, Pair<CharSequence, CharSequence>> outer : lastEntries) {
|
||||
Pair<CharSequence, CharSequence> inner = outer.second;
|
||||
sortedEntries[i] = inner.first;
|
||||
sortedEntryValues[i] = inner.second;
|
||||
i++;
|
||||
}
|
||||
|
||||
super.setEntries(sortedEntries);
|
||||
super.setEntryValues(sortedEntryValues);
|
||||
}
|
||||
|
||||
public SortedListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
|
||||
sortEntryAndValues(getFirstEntriesToPreserve());
|
||||
}
|
||||
|
||||
public SortedListPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
|
||||
sortEntryAndValues(getFirstEntriesToPreserve());
|
||||
}
|
||||
|
||||
public SortedListPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
sortEntryAndValues(getFirstEntriesToPreserve());
|
||||
}
|
||||
|
||||
public SortedListPreference(Context context) {
|
||||
super(context);
|
||||
|
||||
sortEntryAndValues(getFirstEntriesToPreserve());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The number of first entries to leave exactly where they are, and do not sort them.
|
||||
* A negative value indicates do not sort any entries.
|
||||
*/
|
||||
protected int getFirstEntriesToPreserve() {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -122,6 +122,21 @@ public class SpoofVideoStreamsPatch {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* Turns off a feature flag that interferes with video playback.
|
||||
*/
|
||||
public static boolean usePlaybackStartFeatureFlag(boolean original) {
|
||||
if (original) {
|
||||
Logger.printDebug(() -> "usePlaybackStartFeatureFlag is set on");
|
||||
}
|
||||
|
||||
if (!SPOOF_STREAMING_DATA) {
|
||||
return original;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
|
||||
@@ -71,9 +71,7 @@ final class PlayerRoutes {
|
||||
return innerTubeBody.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @noinspection SameParameterValue
|
||||
*/
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
static HttpURLConnection getPlayerResponseConnectionFromRoute(Route.CompiledRoute route, ClientType clientType) throws IOException {
|
||||
var connection = Requester.getConnectionFromCompiledRoute(YT_API_URL, route);
|
||||
|
||||
|
||||
@@ -204,7 +204,7 @@ public class StreamingDataRequest {
|
||||
// but empty response body does.
|
||||
if (connection.getContentLength() == 0) {
|
||||
if (BaseSettings.DEBUG.get() && BaseSettings.DEBUG_TOAST_ON_ERROR.get()) {
|
||||
Utils.showToastShort("Ignoring empty spoof stream client: " + clientType);
|
||||
Utils.showToastShort("Debug: Ignoring empty spoof stream client " + clientType);
|
||||
}
|
||||
} else {
|
||||
try (InputStream inputStream = new BufferedInputStream(connection.getInputStream());
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
dependencies {
|
||||
compileOnly(project(":extensions:shared:library"))
|
||||
compileOnly(project(":extensions:spotify:stub"))
|
||||
compileOnly(libs.annotation)
|
||||
}
|
||||
|
||||
android {
|
||||
defaultConfig {
|
||||
minSdk = 21
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package app.revanced.extension.spotify.layout.hide.createbutton;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.spotify.shared.ComponentFilters.*;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class HideCreateButtonPatch {
|
||||
|
||||
/**
|
||||
* A list of component filters that match whether a navigation bar item is the Create button.
|
||||
* The main approach used is matching the resource id for the Create button title.
|
||||
*/
|
||||
private static final List<ComponentFilter> CREATE_BUTTON_COMPONENT_FILTERS = List.of(
|
||||
new ResourceIdComponentFilter("navigationbar_musicappitems_create_title", "string"),
|
||||
// Temporary fallback and fix for APKs merged with AntiSplit-M not having resources properly encoded,
|
||||
// and thus getting the resource identifier for the Create button title always return 0.
|
||||
// FIXME: Remove this once the above issue is no longer relevant.
|
||||
new StringComponentFilter("spotify:create-menu")
|
||||
);
|
||||
|
||||
/**
|
||||
* A component filter for the old id of the resource which contained the Create button title.
|
||||
* Used in older versions of the app.
|
||||
*/
|
||||
private static final ResourceIdComponentFilter OLD_CREATE_BUTTON_COMPONENT_FILTER =
|
||||
new ResourceIdComponentFilter("bottom_navigation_bar_create_tab_title", "string");
|
||||
|
||||
/**
|
||||
* Injection point. This method is called on every navigation bar item to check whether it is the Create button.
|
||||
* If the navigation bar item is the Create button, it returns null to erase it.
|
||||
* The method fingerprint used to patch ensures we can safely return null here.
|
||||
*/
|
||||
public static Object returnNullIfIsCreateButton(Object navigationBarItem) {
|
||||
if (navigationBarItem == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
String stringifiedNavigationBarItem = navigationBarItem.toString();
|
||||
|
||||
for (ComponentFilter componentFilter : CREATE_BUTTON_COMPONENT_FILTERS) {
|
||||
if (componentFilter.filterUnavailable()) {
|
||||
Logger.printInfo(() -> "returnNullIfIsCreateButton: Filter " +
|
||||
componentFilter.getFilterRepresentation() + " not available, skipping");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (stringifiedNavigationBarItem.contains(componentFilter.getFilterValue())) {
|
||||
Logger.printInfo(() -> "Hiding Create button because the navigation bar item " +
|
||||
navigationBarItem + " matched the filter " + componentFilter.getFilterRepresentation());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "returnNullIfIsCreateButton failure", ex);
|
||||
}
|
||||
|
||||
return navigationBarItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point. Called in older versions of the app. Returns whether the old navigation bar item is the old
|
||||
* Create button.
|
||||
*/
|
||||
public static boolean isOldCreateButton(int oldNavigationBarItemTitleResId) {
|
||||
if (OLD_CREATE_BUTTON_COMPONENT_FILTER.filterUnavailable()) {
|
||||
Logger.printInfo(() -> "Skipping hiding old Create button because the resource id for " +
|
||||
OLD_CREATE_BUTTON_COMPONENT_FILTER.resourceName + " is not available");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (oldNavigationBarItemTitleResId == OLD_CREATE_BUTTON_COMPONENT_FILTER.getResourceId()) {
|
||||
Logger.printInfo(() -> "Hiding old Create button because the navigation bar item title resource id" +
|
||||
" matched " + OLD_CREATE_BUTTON_COMPONENT_FILTER.getFilterRepresentation());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package app.revanced.extension.spotify.layout.theme;
|
||||
|
||||
import android.graphics.Color;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class CustomThemePatch {
|
||||
|
||||
private static final int BACKGROUND_COLOR = getColorFromString("@color/gray_7");
|
||||
private static final int BACKGROUND_COLOR_SECONDARY = getColorFromString("@color/gray_15");
|
||||
private static final int ACCENT_COLOR = getColorFromString("@color/spotify_green_157");
|
||||
private static final int ACCENT_PRESSED_COLOR =
|
||||
getColorFromString("@color/dark_brightaccent_background_press");
|
||||
|
||||
/**
|
||||
* Returns an int representation of the color resource or hex code.
|
||||
*/
|
||||
private static int getColorFromString(String colorString) {
|
||||
try {
|
||||
return Utils.getColorFromString(colorString);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Invalid color string: " + colorString, ex);
|
||||
return Color.BLACK;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point. Returns an int representation of the replaced color from the original color.
|
||||
*/
|
||||
public static int replaceColor(int originalColor) {
|
||||
switch (originalColor) {
|
||||
// Playlist background color.
|
||||
case 0xFF121212:
|
||||
return BACKGROUND_COLOR;
|
||||
|
||||
// Share menu background color.
|
||||
case 0xFF1F1F1F:
|
||||
// Home category pills background color.
|
||||
case 0xFF333333:
|
||||
// Settings header background color.
|
||||
case 0xFF282828:
|
||||
// Spotify Connect device list background color.
|
||||
case 0xFF2A2A2A:
|
||||
return BACKGROUND_COLOR_SECONDARY;
|
||||
|
||||
// Some Lottie animations have a color that's slightly off due to rounding errors.
|
||||
case 0xFF1ED760: case 0xFF1ED75F:
|
||||
// Intermediate color used in some animations, same rounding issue.
|
||||
case 0xFF1DB954: case 0xFF1CB854:
|
||||
return ACCENT_COLOR;
|
||||
|
||||
case 0xFF1ABC54:
|
||||
return ACCENT_PRESSED_COLOR;
|
||||
|
||||
default:
|
||||
return originalColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package app.revanced.extension.spotify.misc.privacy;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class SanitizeSharingLinksPatch {
|
||||
|
||||
/**
|
||||
* Parameters that are considered undesirable and should be stripped away.
|
||||
*/
|
||||
private static final List<String> SHARE_PARAMETERS_TO_REMOVE = List.of(
|
||||
"si", // Share tracking parameter.
|
||||
"utm_source" // Share source, such as "copy-link".
|
||||
);
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static String sanitizeUrl(String url) {
|
||||
try {
|
||||
Uri uri = Uri.parse(url);
|
||||
Uri.Builder builder = uri.buildUpon().clearQuery();
|
||||
|
||||
for (String paramName : uri.getQueryParameterNames()) {
|
||||
if (!SHARE_PARAMETERS_TO_REMOVE.contains(paramName)) {
|
||||
for (String value : uri.getQueryParameters(paramName)) {
|
||||
builder.appendQueryParameter(paramName, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String sanitizedUrl = builder.build().toString();
|
||||
Logger.printInfo(() -> "Sanitized url " + url + " to " + sanitizedUrl);
|
||||
return sanitizedUrl;
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "sanitizeUrl failure with " + url, ex);
|
||||
return url;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package app.revanced.extension.spotify.shared;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
|
||||
public final class ComponentFilters {
|
||||
|
||||
public interface ComponentFilter {
|
||||
@NonNull
|
||||
String getFilterValue();
|
||||
String getFilterRepresentation();
|
||||
default boolean filterUnavailable() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class ResourceIdComponentFilter implements ComponentFilter {
|
||||
|
||||
public final String resourceName;
|
||||
public final String resourceType;
|
||||
// Android resources are always positive, so -1 is a valid sentinel value to indicate it has not been loaded.
|
||||
// 0 is returned when a resource has not been found.
|
||||
private int resourceId = -1;
|
||||
@Nullable
|
||||
private String stringfiedResourceId;
|
||||
|
||||
public ResourceIdComponentFilter(String resourceName, String resourceType) {
|
||||
this.resourceName = resourceName;
|
||||
this.resourceType = resourceType;
|
||||
}
|
||||
|
||||
public int getResourceId() {
|
||||
if (resourceId == -1) {
|
||||
resourceId = Utils.getResourceIdentifier(resourceName, resourceType);
|
||||
}
|
||||
return resourceId;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getFilterValue() {
|
||||
if (stringfiedResourceId == null) {
|
||||
stringfiedResourceId = Integer.toString(getResourceId());
|
||||
}
|
||||
return stringfiedResourceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFilterRepresentation() {
|
||||
boolean resourceFound = getResourceId() != 0;
|
||||
return (resourceFound ? getFilterValue() + " (" : "") + resourceName + (resourceFound ? ")" : "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean filterUnavailable() {
|
||||
boolean resourceNotFound = getResourceId() == 0;
|
||||
if (resourceNotFound) {
|
||||
Logger.printInfo(() -> "Resource id for " + resourceName + " was not found");
|
||||
}
|
||||
return resourceNotFound;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class StringComponentFilter implements ComponentFilter {
|
||||
|
||||
public final String string;
|
||||
|
||||
public StringComponentFilter(String string) {
|
||||
this.string = string;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getFilterValue() {
|
||||
return string;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFilterRepresentation() {
|
||||
return string;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,11 @@ android {
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 26
|
||||
minSdk = 21
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.spotify.home.evopage.homeapi.proto;
|
||||
|
||||
public final class Section {
|
||||
public static final int VIDEO_BRAND_AD_FIELD_NUMBER = 20;
|
||||
public static final int IMAGE_BRAND_AD_FIELD_NUMBER = 21;
|
||||
public int featureTypeCase_;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.spotify.useraccount.v1;
|
||||
|
||||
/**
|
||||
* Used for target 8.6.98.900. Class is still present in newer app targets.
|
||||
*/
|
||||
public class AccountAttribute {
|
||||
public Object value_;
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package app.revanced.extension.tiktok;
|
||||
|
||||
import static app.revanced.extension.shared.Utils.isDarkModeEnabled;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
@@ -43,8 +42,8 @@ public class Utils {
|
||||
private static final @ColorInt int TEXT_LIGHT_MODE_SUMMARY
|
||||
= Color.argb(255, 80, 80, 80);
|
||||
|
||||
public static void setTitleAndSummaryColor(Context context, View view) {
|
||||
final boolean darkModeEnabled = isDarkModeEnabled(context);
|
||||
public static void setTitleAndSummaryColor(View view) {
|
||||
final boolean darkModeEnabled = isDarkModeEnabled();
|
||||
|
||||
TextView title = view.findViewById(android.R.id.title);
|
||||
title.setTextColor(darkModeEnabled
|
||||
|
||||
@@ -2,6 +2,7 @@ package app.revanced.extension.tiktok.feedfilter;
|
||||
|
||||
import com.ss.android.ugc.aweme.feed.model.Aweme;
|
||||
import com.ss.android.ugc.aweme.feed.model.FeedItemList;
|
||||
import com.ss.android.ugc.aweme.follow.presenter.FollowFeedList;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
@@ -13,22 +14,41 @@ public final class FeedItemsFilter {
|
||||
new StoryFilter(),
|
||||
new ImageVideoFilter(),
|
||||
new ViewCountFilter(),
|
||||
new LikeCountFilter()
|
||||
new LikeCountFilter(),
|
||||
new ShopFilter()
|
||||
);
|
||||
|
||||
public static void filter(FeedItemList feedItemList) {
|
||||
Iterator<Aweme> feedItemListIterator = feedItemList.items.iterator();
|
||||
while (feedItemListIterator.hasNext()) {
|
||||
Aweme item = feedItemListIterator.next();
|
||||
if (item == null) continue;
|
||||
filterFeedList(feedItemList.items, item -> item);
|
||||
}
|
||||
|
||||
for (IFilter filter : FILTERS) {
|
||||
boolean enabled = filter.getEnabled();
|
||||
if (enabled && filter.getFiltered(item)) {
|
||||
feedItemListIterator.remove();
|
||||
break;
|
||||
}
|
||||
public static void filter(FollowFeedList followFeedList) {
|
||||
filterFeedList(followFeedList.mItems, feed -> (feed != null) ? feed.aweme : null);
|
||||
}
|
||||
|
||||
private static <T> void filterFeedList(List<T> list, AwemeExtractor<T> extractor) {
|
||||
// Could be simplified with removeIf() but requires Android 7.0+ while TikTok supports 4.0+.
|
||||
Iterator<T> iterator = list.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
T container = iterator.next();
|
||||
Aweme item = extractor.extract(container);
|
||||
if (item != null && shouldFilter(item)) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean shouldFilter(Aweme item) {
|
||||
for (IFilter filter : FILTERS) {
|
||||
if (filter.getEnabled() && filter.getFiltered(item)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface AwemeExtractor<T> {
|
||||
Aweme extract(T source);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package app.revanced.extension.tiktok.feedfilter;
|
||||
|
||||
import app.revanced.extension.tiktok.settings.Settings;
|
||||
import com.ss.android.ugc.aweme.feed.model.Aweme;
|
||||
|
||||
public class ShopFilter implements IFilter {
|
||||
private static final String SHOP_INFO = "placeholder_product_id";
|
||||
@Override
|
||||
public boolean getEnabled() {
|
||||
return Settings.HIDE_SHOP.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getFiltered(Aweme item) {
|
||||
return item.getShareUrl().contains(SHOP_INFO);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import app.revanced.extension.shared.settings.StringSetting;
|
||||
public class Settings extends BaseSettings {
|
||||
public static final BooleanSetting REMOVE_ADS = new BooleanSetting("remove_ads", TRUE, true);
|
||||
public static final BooleanSetting HIDE_LIVE = new BooleanSetting("hide_live", FALSE, true);
|
||||
public static final BooleanSetting HIDE_SHOP = new BooleanSetting("hide_shop", FALSE, true);
|
||||
public static final BooleanSetting HIDE_STORY = new BooleanSetting("hide_story", FALSE, true);
|
||||
public static final BooleanSetting HIDE_IMAGE = new BooleanSetting("hide_image", FALSE, true);
|
||||
public static final StringSetting MIN_MAX_VIEWS = new StringSetting("min_max_views", "0-" + Long.MAX_VALUE, true);
|
||||
|
||||
@@ -101,7 +101,7 @@ public class DownloadPathPreference extends DialogPreference {
|
||||
protected void onBindView(View view) {
|
||||
super.onBindView(view);
|
||||
|
||||
Utils.setTitleAndSummaryColor(getContext(), view);
|
||||
Utils.setTitleAndSummaryColor(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -22,6 +22,6 @@ public class InputTextPreference extends EditTextPreference {
|
||||
protected void onBindView(View view) {
|
||||
super.onBindView(view);
|
||||
|
||||
Utils.setTitleAndSummaryColor(getContext(), view);
|
||||
Utils.setTitleAndSummaryColor(view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ public class RangeValuePreference extends DialogPreference {
|
||||
protected void onBindView(View view) {
|
||||
super.onBindView(view);
|
||||
|
||||
Utils.setTitleAndSummaryColor(getContext(), view);
|
||||
Utils.setTitleAndSummaryColor(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -9,7 +9,6 @@ import app.revanced.extension.tiktok.settings.preference.categories.DownloadsPre
|
||||
import app.revanced.extension.tiktok.settings.preference.categories.FeedFilterPreferenceCategory;
|
||||
import app.revanced.extension.tiktok.settings.preference.categories.ExtensionPreferenceCategory;
|
||||
import app.revanced.extension.tiktok.settings.preference.categories.SimSpoofPreferenceCategory;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Preference fragment for ReVanced settings
|
||||
|
||||
@@ -48,6 +48,6 @@ public class ReVancedTikTokAboutPreference extends ReVancedAboutPreference {
|
||||
protected void onBindView(View view) {
|
||||
super.onBindView(view);
|
||||
|
||||
Utils.setTitleAndSummaryColor(getContext(), view);
|
||||
Utils.setTitleAndSummaryColor(view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,6 @@ public class TogglePreference extends SwitchPreference {
|
||||
protected void onBindView(View view) {
|
||||
super.onBindView(view);
|
||||
|
||||
Utils.setTitleAndSummaryColor(getContext(), view);
|
||||
Utils.setTitleAndSummaryColor(view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,11 @@ public class FeedFilterPreferenceCategory extends ConditionalPreferenceCategory
|
||||
"Remove feed ads", "Remove ads from feed.",
|
||||
Settings.REMOVE_ADS
|
||||
));
|
||||
addPreference(new TogglePreference(
|
||||
context,
|
||||
"Hide TikTok Shop", "Hide TikTok shop from feed.",
|
||||
Settings.HIDE_SHOP
|
||||
));
|
||||
addPreference(new TogglePreference(
|
||||
context,
|
||||
"Hide livestreams", "Hide livestreams from feed.",
|
||||
|
||||
@@ -16,9 +16,7 @@ public class SpoofSimPatch {
|
||||
return false;
|
||||
}
|
||||
|
||||
Logger.initializationException(SpoofSimPatch.class,
|
||||
"Context is not yet set, cannot spoof: " + fieldSpoofed, null);
|
||||
|
||||
Logger.printException(() -> "Context is not yet set, cannot spoof: " + fieldSpoofed, null);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,4 +33,8 @@ public class Aweme {
|
||||
public AwemeStatistics getStatistics() {
|
||||
throw new UnsupportedOperationException("Stub");
|
||||
}
|
||||
|
||||
public String getShareUrl() {
|
||||
throw new UnsupportedOperationException("Stub");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.ss.android.ugc.aweme.follow.presenter;
|
||||
|
||||
import com.ss.android.ugc.aweme.feed.model.Aweme;
|
||||
|
||||
//Dummy class
|
||||
public class FollowFeed {
|
||||
public Aweme aweme;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.ss.android.ugc.aweme.follow.presenter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
//Dummy class
|
||||
public class FollowFeedList {
|
||||
public List<FollowFeed> mItems;
|
||||
}
|
||||
@@ -163,7 +163,7 @@ internal object TwiFucker {
|
||||
|
||||
private fun JSONObject.entryIsWhoToFollow(): Boolean =
|
||||
optString("entryId").let {
|
||||
it.startsWith("whoToFollow-") || it.startsWith("who-to-follow-") || it.startsWith("connect-module-")
|
||||
it.startsWith("whoToFollow-") || it.startsWith("who-to-follow-") || it.startsWith("connect-module-") || it.startsWith("who-to-subscribe-")
|
||||
}
|
||||
|
||||
private fun JSONObject.itemContainsPromotedUser(): Boolean =
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
package app.revanced.extension.youtube;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.Color;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
|
||||
public class ThemeHelper {
|
||||
@Nullable
|
||||
private static Integer darkThemeColor, lightThemeColor;
|
||||
private static int themeValue;
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public static void setTheme(Enum<?> value) {
|
||||
final int newOrdinalValue = value.ordinal();
|
||||
if (themeValue != newOrdinalValue) {
|
||||
themeValue = newOrdinalValue;
|
||||
Logger.printDebug(() -> "Theme value: " + newOrdinalValue);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isDarkTheme() {
|
||||
return themeValue == 1;
|
||||
}
|
||||
|
||||
public static void setActivityTheme(Activity activity) {
|
||||
final var theme = isDarkTheme()
|
||||
? "Theme.YouTube.Settings.Dark"
|
||||
: "Theme.YouTube.Settings";
|
||||
activity.setTheme(Utils.getResourceIdentifier(theme, "style"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
@SuppressWarnings("SameReturnValue")
|
||||
private static String darkThemeResourceName() {
|
||||
// Value is changed by Theme patch, if included.
|
||||
return "@color/yt_black3";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The dark theme color as specified by the Theme patch (if included),
|
||||
* or the dark mode background color unpatched YT uses.
|
||||
*/
|
||||
public static int getDarkThemeColor() {
|
||||
if (darkThemeColor == null) {
|
||||
darkThemeColor = getColorInt(darkThemeResourceName());
|
||||
}
|
||||
return darkThemeColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
@SuppressWarnings("SameReturnValue")
|
||||
private static String lightThemeResourceName() {
|
||||
// Value is changed by Theme patch, if included.
|
||||
return "@color/yt_white1";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The light theme color as specified by the Theme patch (if included),
|
||||
* or the non dark mode background color unpatched YT uses.
|
||||
*/
|
||||
public static int getLightThemeColor() {
|
||||
if (lightThemeColor == null) {
|
||||
lightThemeColor = getColorInt(lightThemeResourceName());
|
||||
}
|
||||
return lightThemeColor;
|
||||
}
|
||||
|
||||
private static int getColorInt(String colorString) {
|
||||
if (colorString.startsWith("#")) {
|
||||
return Color.parseColor(colorString);
|
||||
}
|
||||
return Utils.getResourceColor(colorString);
|
||||
}
|
||||
|
||||
public static int getBackgroundColor() {
|
||||
return isDarkTheme() ? getDarkThemeColor() : getLightThemeColor();
|
||||
}
|
||||
|
||||
public static int getForegroundColor() {
|
||||
return isDarkTheme() ? getLightThemeColor() : getDarkThemeColor();
|
||||
}
|
||||
|
||||
public static int getToolbarBackgroundColor() {
|
||||
final String colorName = isDarkTheme()
|
||||
? "yt_black3"
|
||||
: "yt_white1";
|
||||
|
||||
return getColorInt(colorName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import static app.revanced.extension.shared.StringRef.sf;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class AccountCredentialsInvalidTextPatch {
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static String getOfflineNetworkErrorString(String original) {
|
||||
try {
|
||||
if (Utils.isNetworkConnected()) {
|
||||
Logger.printDebug(() -> "Network appears to be online, but app is showing offline error");
|
||||
return '\n' + sf("microg_offline_account_login_error").toString();
|
||||
}
|
||||
|
||||
Logger.printDebug(() -> "Network is offline");
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "getOfflineNetworkErrorString failure", ex);
|
||||
}
|
||||
|
||||
return original;
|
||||
}
|
||||
}
|
||||
@@ -686,7 +686,7 @@ public final class AlternativeThumbnailsPatch {
|
||||
? "" : fullUrl.substring(imageExtensionEndIndex);
|
||||
}
|
||||
|
||||
/** @noinspection SameParameterValue */
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
String createStillsUrl(@NonNull ThumbnailQuality qualityToUse, boolean includeViewTracking) {
|
||||
// Images could be upgraded to webp if they are not already, but this fails quite often,
|
||||
// especially for new videos uploaded in the last hour.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.PlayerType;
|
||||
import app.revanced.extension.youtube.shared.ShortsPlayerState;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@@ -23,7 +24,13 @@ public class BackgroundPlaybackPatch {
|
||||
// 7. Close the Short
|
||||
// 8. Resume playing the regular video
|
||||
// 9. Minimize the app (PIP should appear)
|
||||
return !ShortsPlayerState.isOpen();
|
||||
if (ShortsPlayerState.isOpen()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the video player is opened and it's not playing in the feed.
|
||||
PlayerType current = PlayerType.getCurrent();
|
||||
return !current.isNoneOrHidden() && current != PlayerType.INLINE_MINIMAL;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.settings.Setting;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@@ -81,6 +82,13 @@ public final class ChangeStartPagePatch {
|
||||
}
|
||||
}
|
||||
|
||||
public static class ChangeStartPageTypeAvailability implements Setting.Availability {
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
return Settings.CHANGE_START_PAGE.get() != StartPage.DEFAULT;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Intent action when YouTube is cold started from the launcher.
|
||||
* <p>
|
||||
@@ -93,6 +101,8 @@ public final class ChangeStartPagePatch {
|
||||
|
||||
private static final StartPage START_PAGE = Settings.CHANGE_START_PAGE.get();
|
||||
|
||||
private static final boolean CHANGE_START_PAGE_ALWAYS = Settings.CHANGE_START_PAGE_ALWAYS.get();
|
||||
|
||||
/**
|
||||
* There is an issue where the back button on the toolbar doesn't work properly.
|
||||
* As a workaround for this issue, instead of overriding the browserId multiple times, just override it once.
|
||||
@@ -104,13 +114,13 @@ public final class ChangeStartPagePatch {
|
||||
return original;
|
||||
}
|
||||
|
||||
if (appLaunched) {
|
||||
if (!CHANGE_START_PAGE_ALWAYS && appLaunched) {
|
||||
Logger.printDebug(() -> "Ignore override browseId as the app already launched");
|
||||
return original;
|
||||
}
|
||||
appLaunched = true;
|
||||
|
||||
Logger.printDebug(() -> "Changing browseId to " + START_PAGE.id);
|
||||
Logger.printDebug(() -> "Changing browseId to: " + START_PAGE.id);
|
||||
return START_PAGE.id;
|
||||
}
|
||||
|
||||
@@ -125,14 +135,14 @@ public final class ChangeStartPagePatch {
|
||||
return;
|
||||
}
|
||||
|
||||
if (appLaunched) {
|
||||
if (!CHANGE_START_PAGE_ALWAYS && appLaunched) {
|
||||
Logger.printDebug(() -> "Ignore override intent action as the app already launched");
|
||||
return;
|
||||
}
|
||||
appLaunched = true;
|
||||
|
||||
String intentAction = START_PAGE.id;
|
||||
Logger.printDebug(() -> "Changing intent action to " + intentAction);
|
||||
Logger.printDebug(() -> "Changing intent action to: " + intentAction);
|
||||
intent.setAction(intentAction);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@ package app.revanced.extension.youtube.patches;
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.text.Html;
|
||||
import android.util.Pair;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
@@ -63,18 +66,28 @@ public class CheckWatchHistoryDomainNameResolutionPatch {
|
||||
}
|
||||
|
||||
Utils.runOnMainThread(() -> {
|
||||
var alert = new android.app.AlertDialog.Builder(context)
|
||||
.setTitle(str("revanced_check_watch_history_domain_name_dialog_title"))
|
||||
.setMessage(Html.fromHtml(str("revanced_check_watch_history_domain_name_dialog_message")))
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
}).setNegativeButton(str("revanced_check_watch_history_domain_name_dialog_ignore"), (dialog, which) -> {
|
||||
Settings.CHECK_WATCH_HISTORY_DOMAIN_NAME.save(false);
|
||||
dialog.dismiss();
|
||||
}).create();
|
||||
try {
|
||||
// Create the custom dialog.
|
||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
||||
context,
|
||||
str("revanced_check_watch_history_domain_name_dialog_title"), // Title.
|
||||
Html.fromHtml(str("revanced_check_watch_history_domain_name_dialog_message")), // Message (HTML).
|
||||
null, // No EditText.
|
||||
null, // OK button text.
|
||||
() -> {}, // OK button action (just dismiss).
|
||||
() -> {}, // Cancel button action (just dismiss).
|
||||
str("revanced_check_watch_history_domain_name_dialog_ignore"), // Neutral button text.
|
||||
() -> Settings.CHECK_WATCH_HISTORY_DOMAIN_NAME.save(false), // Neutral button action (Ignore).
|
||||
true // Dismiss dialog on Neutral button click.
|
||||
);
|
||||
|
||||
Utils.showDialog(context, alert, false, null);
|
||||
// Show the dialog.
|
||||
Dialog dialog = dialogPair.first;
|
||||
|
||||
Utils.showDialog(context, dialog, false, null);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "checkDnsResolver dialog creation failure", ex);
|
||||
}
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "checkDnsResolver failure", ex);
|
||||
|
||||
@@ -17,8 +17,7 @@ public class CustomPlayerOverlayOpacityPatch {
|
||||
|
||||
if (opacity < 0 || opacity > 100) {
|
||||
Utils.showToastLong(str("revanced_player_overlay_opacity_invalid_toast"));
|
||||
Settings.PLAYER_OVERLAY_OPACITY.resetToDefault();
|
||||
opacity = Settings.PLAYER_OVERLAY_OPACITY.defaultValue;
|
||||
opacity = Settings.PLAYER_OVERLAY_OPACITY.resetToDefault();
|
||||
}
|
||||
|
||||
PLAYER_OVERLAY_OPACITY_LEVEL = (opacity * 255) / 100;
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.ShortsPlayerState;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class DisableAutoCaptionsPatch {
|
||||
|
||||
/**
|
||||
* Used by injected code. Do not delete.
|
||||
*/
|
||||
public static boolean captionsButtonDisabled;
|
||||
private static volatile boolean captionsButtonStatus;
|
||||
|
||||
public static boolean autoCaptionsEnabled() {
|
||||
return Settings.AUTO_CAPTIONS.get()
|
||||
// Do not use auto captions for Shorts.
|
||||
&& ShortsPlayerState.isOpen();
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean disableAutoCaptions() {
|
||||
return Settings.DISABLE_AUTO_CAPTIONS.get() && !captionsButtonStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void setCaptionsButtonStatus(boolean status) {
|
||||
captionsButtonStatus = status;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class DisableHapticFeedbackPatch {
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean disableChapterVibrate() {
|
||||
return Settings.DISABLE_HAPTIC_FEEDBACK_CHAPTERS.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean disableSeekUndoVibrate() {
|
||||
return Settings.DISABLE_HAPTIC_FEEDBACK_SEEK_UNDO.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean disablePreciseSeekingVibrate() {
|
||||
return Settings.DISABLE_HAPTIC_FEEDBACK_PRECISE_SEEKING.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean disableZoomVibrate() {
|
||||
return Settings.DISABLE_HAPTIC_FEEDBACK_ZOOM.get();
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package app.revanced.extension.youtube.patches;
|
||||
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
/** @noinspection unused*/
|
||||
@SuppressWarnings("unused")
|
||||
public class DisableResumingStartupShortsPlayerPatch {
|
||||
|
||||
/**
|
||||
@@ -11,4 +11,11 @@ public class DisableResumingStartupShortsPlayerPatch {
|
||||
public static boolean disableResumingStartupShortsPlayer() {
|
||||
return Settings.DISABLE_RESUMING_SHORTS_PLAYER.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean disableResumingStartupShortsPlayer(boolean original) {
|
||||
return original && !Settings.DISABLE_RESUMING_SHORTS_PLAYER.get();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
@@ -58,6 +59,22 @@ public final class HidePlayerOverlayButtonsPatch {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void hidePlayerControlButtonsBackground(View rootView) {
|
||||
try {
|
||||
if (!Settings.HIDE_PLAYER_CONTROL_BUTTONS_BACKGROUND.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Each button is an ImageView with a background set to another drawable.
|
||||
removeImageViewsBackgroundRecursive(rootView);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "removePlayerControlButtonsBackground failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void hideView(View parentView, int resourceId) {
|
||||
View nextPreviousButton = parentView.findViewById(resourceId);
|
||||
|
||||
@@ -69,4 +86,16 @@ public final class HidePlayerOverlayButtonsPatch {
|
||||
Logger.printDebug(() -> "Hiding previous/next button");
|
||||
Utils.hideViewByRemovingFromParentUnderCondition(true, nextPreviousButton);
|
||||
}
|
||||
|
||||
private static void removeImageViewsBackgroundRecursive(View currentView) {
|
||||
if (currentView instanceof ImageView imageView) {
|
||||
imageView.setBackground(null);
|
||||
}
|
||||
|
||||
if (currentView instanceof ViewGroup viewGroup) {
|
||||
for (int i = 0; i < viewGroup.getChildCount(); i++) {
|
||||
removeImageViewsBackgroundRecursive(viewGroup.getChildAt(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class HideRelatedVideoOverlayPatch {
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean hideRelatedVideoOverlay() {
|
||||
return Settings.HIDE_RELATED_VIDEO_OVERLAY.get();
|
||||
}
|
||||
}
|
||||
@@ -43,10 +43,13 @@ public final class MiniplayerPatch {
|
||||
MODERN_2(null, 2),
|
||||
MODERN_3(null, 3),
|
||||
/**
|
||||
* Half broken miniplayer, that might be work in progress or left over abandoned code.
|
||||
* Can force this type by editing the import/export settings.
|
||||
* Works and is functional with 20.03+
|
||||
*/
|
||||
MODERN_4(null, 4);
|
||||
MODERN_4(null, 4),
|
||||
/**
|
||||
* Half broken miniplayer, and in 20.02 and earlier is declared as type 4.
|
||||
*/
|
||||
MODERN_5(null, 5);
|
||||
|
||||
/**
|
||||
* Legacy tablet hook value.
|
||||
@@ -126,12 +129,13 @@ public final class MiniplayerPatch {
|
||||
private static final boolean DRAG_AND_DROP_ENABLED =
|
||||
CURRENT_TYPE.isModern() && Settings.MINIPLAYER_DRAG_AND_DROP.get();
|
||||
|
||||
private static final boolean HIDE_EXPAND_CLOSE_ENABLED =
|
||||
Settings.MINIPLAYER_HIDE_EXPAND_CLOSE.get()
|
||||
&& Settings.MINIPLAYER_HIDE_EXPAND_CLOSE.isAvailable();
|
||||
private static final boolean HIDE_OVERLAY_BUTTONS_ENABLED =
|
||||
Settings.MINIPLAYER_HIDE_OVERLAY_BUTTONS.get()
|
||||
&& Settings.MINIPLAYER_HIDE_OVERLAY_BUTTONS.isAvailable();
|
||||
|
||||
private static final boolean HIDE_SUBTEXT_ENABLED =
|
||||
(CURRENT_TYPE == MODERN_1 || CURRENT_TYPE == MODERN_3) && Settings.MINIPLAYER_HIDE_SUBTEXT.get();
|
||||
(CURRENT_TYPE == MODERN_1 || CURRENT_TYPE == MODERN_3 || CURRENT_TYPE == MODERN_4)
|
||||
&& Settings.MINIPLAYER_HIDE_SUBTEXT.get();
|
||||
|
||||
// 19.25 is last version that has forward/back buttons for phones,
|
||||
// but buttons still show for tablets/foldable devices and they don't work well so always hide.
|
||||
@@ -139,7 +143,7 @@ public final class MiniplayerPatch {
|
||||
&& (VersionCheckPatch.IS_19_34_OR_GREATER || Settings.MINIPLAYER_HIDE_REWIND_FORWARD.get());
|
||||
|
||||
private static final boolean MINIPLAYER_ROUNDED_CORNERS_ENABLED =
|
||||
Settings.MINIPLAYER_ROUNDED_CORNERS.get();
|
||||
CURRENT_TYPE.isModern() && Settings.MINIPLAYER_ROUNDED_CORNERS.get();
|
||||
|
||||
private static final boolean MINIPLAYER_HORIZONTAL_DRAG_ENABLED =
|
||||
DRAG_AND_DROP_ENABLED && Settings.MINIPLAYER_HORIZONTAL_DRAG.get();
|
||||
@@ -158,8 +162,7 @@ public final class MiniplayerPatch {
|
||||
|
||||
if (opacity < 0 || opacity > 100) {
|
||||
Utils.showToastLong(str("revanced_miniplayer_opacity_invalid_toast"));
|
||||
Settings.MINIPLAYER_OPACITY.resetToDefault();
|
||||
opacity = Settings.MINIPLAYER_OPACITY.defaultValue;
|
||||
opacity = Settings.MINIPLAYER_OPACITY.resetToDefault();
|
||||
}
|
||||
|
||||
OPACITY_LEVEL = (opacity * 255) / 100;
|
||||
@@ -172,11 +175,12 @@ public final class MiniplayerPatch {
|
||||
}
|
||||
}
|
||||
|
||||
public static final class MiniplayerHideExpandCloseAvailability implements Setting.Availability {
|
||||
public static final class MiniplayerHideOverlayButtonsAvailability implements Setting.Availability {
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
MiniplayerType type = Settings.MINIPLAYER_TYPE.get();
|
||||
return (!IS_19_20_OR_GREATER && (type == MODERN_1 || type == MODERN_3))
|
||||
return type == MODERN_4
|
||||
|| (!IS_19_20_OR_GREATER && (type == MODERN_1 || type == MODERN_3))
|
||||
|| (!IS_19_26_OR_GREATER && type == MODERN_1
|
||||
&& !Settings.MINIPLAYER_DOUBLE_TAP_ACTION.get() && !Settings.MINIPLAYER_DRAG_AND_DROP.get())
|
||||
|| (IS_19_29_OR_GREATER && type == MODERN_3);
|
||||
@@ -227,9 +231,13 @@ public final class MiniplayerPatch {
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void adjustMiniplayerOpacity(ImageView view) {
|
||||
public static void adjustMiniplayerOpacity(View view) {
|
||||
if (CURRENT_TYPE == MODERN_1) {
|
||||
view.setImageAlpha(OPACITY_LEVEL);
|
||||
if (view instanceof ImageView imageView) {
|
||||
imageView.setImageAlpha(OPACITY_LEVEL);
|
||||
} else {
|
||||
Logger.printException(() -> "Unknown miniplayer overlay view: " + view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,7 +255,7 @@ public final class MiniplayerPatch {
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean enableMiniplayerDoubleTapAction(boolean original) {
|
||||
public static boolean getMiniplayerDoubleTapAction(boolean original) {
|
||||
if (CURRENT_TYPE == DEFAULT) {
|
||||
return original;
|
||||
}
|
||||
@@ -258,7 +266,7 @@ public final class MiniplayerPatch {
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean enableMiniplayerDragAndDrop(boolean original) {
|
||||
public static boolean getMiniplayerDragAndDrop(boolean original) {
|
||||
if (CURRENT_TYPE == DEFAULT) {
|
||||
return original;
|
||||
}
|
||||
@@ -266,13 +274,36 @@ public final class MiniplayerPatch {
|
||||
return DRAG_AND_DROP_ENABLED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean getRoundedCorners(boolean original) {
|
||||
if (CURRENT_TYPE == DEFAULT) {
|
||||
return original;
|
||||
}
|
||||
|
||||
return MINIPLAYER_ROUNDED_CORNERS_ENABLED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean setRoundedCorners(boolean original) {
|
||||
if (CURRENT_TYPE.isModern()) {
|
||||
return MINIPLAYER_ROUNDED_CORNERS_ENABLED;
|
||||
public static boolean getHorizontalDrag(boolean original) {
|
||||
if (CURRENT_TYPE == DEFAULT) {
|
||||
return original;
|
||||
}
|
||||
|
||||
return MINIPLAYER_HORIZONTAL_DRAG_ENABLED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean getMaximizeAnimation(boolean original) {
|
||||
// This must be forced on if horizontal drag is enabled,
|
||||
// otherwise the UI has visual glitches when maximizing the miniplayer.
|
||||
if (MINIPLAYER_HORIZONTAL_DRAG_ENABLED) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return original;
|
||||
@@ -281,7 +312,7 @@ public final class MiniplayerPatch {
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static int setMiniplayerDefaultSize(int original) {
|
||||
public static int getMiniplayerDefaultSize(int original) {
|
||||
if (CURRENT_TYPE.isModern()) {
|
||||
return MINIPLAYER_SIZE;
|
||||
}
|
||||
@@ -289,29 +320,26 @@ public final class MiniplayerPatch {
|
||||
return original;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void hideMiniplayerExpandClose(View view) {
|
||||
Utils.hideViewByRemovingFromParentUnderCondition(HIDE_OVERLAY_BUTTONS_ENABLED, view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean setHorizontalDrag(boolean original) {
|
||||
if (CURRENT_TYPE.isModern()) {
|
||||
return MINIPLAYER_HORIZONTAL_DRAG_ENABLED;
|
||||
public static void hideMiniplayerActionButton(View view) {
|
||||
if (CURRENT_TYPE == MODERN_4) {
|
||||
Utils.hideViewByRemovingFromParentUnderCondition(HIDE_OVERLAY_BUTTONS_ENABLED, view);
|
||||
}
|
||||
|
||||
return original;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void hideMiniplayerExpandClose(ImageView view) {
|
||||
Utils.hideViewByRemovingFromParentUnderCondition(HIDE_EXPAND_CLOSE_ENABLED, view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void hideMiniplayerRewindForward(ImageView view) {
|
||||
public static void hideMiniplayerRewindForward(View view) {
|
||||
Utils.hideViewByRemovingFromParentUnderCondition(HIDE_REWIND_FORWARD_ENABLED, view);
|
||||
}
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ public final class NavigationButtonsPatch {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Utils.isDarkModeEnabled(Utils.getContext())
|
||||
return Utils.isDarkModeEnabled()
|
||||
? !DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK
|
||||
: !DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ public class OpenShortsInRegularPlayerPatch {
|
||||
|
||||
private static WeakReference<Activity> mainActivityRef = new WeakReference<>(null);
|
||||
|
||||
private static volatile boolean overrideBackPressToExit;
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
@@ -38,6 +40,18 @@ public class OpenShortsInRegularPlayerPatch {
|
||||
mainActivityRef = new WeakReference<>(activity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean overrideBackPressToExit(boolean original) {
|
||||
if (overrideBackPressToExit) {
|
||||
Logger.printDebug(() -> "Overriding back press to exit activity");
|
||||
return false;
|
||||
}
|
||||
|
||||
return original;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
@@ -45,6 +59,7 @@ public class OpenShortsInRegularPlayerPatch {
|
||||
try {
|
||||
ShortsPlayerType type = Settings.SHORTS_PLAYER_TYPE.get();
|
||||
if (type == ShortsPlayerType.SHORTS_PLAYER) {
|
||||
overrideBackPressToExit = false;
|
||||
return false; // Default unpatched behavior.
|
||||
}
|
||||
|
||||
@@ -61,13 +76,17 @@ public class OpenShortsInRegularPlayerPatch {
|
||||
// set to open in the regular player, so it's ignored as
|
||||
// checking the map makes the patch more complicated.
|
||||
Logger.printDebug(() -> "Ignoring Short with no videoId");
|
||||
overrideBackPressToExit = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (NavigationButton.getSelectedNavigationButton() == NavigationButton.SHORTS) {
|
||||
overrideBackPressToExit = false;
|
||||
return false; // Always use Shorts player for the Shorts nav button.
|
||||
}
|
||||
|
||||
overrideBackPressToExit = true;
|
||||
|
||||
final boolean forceFullScreen = (type == ShortsPlayerType.REGULAR_PLAYER_FULLSCREEN);
|
||||
OpenVideosFullscreenHookPatch.setOpenNextVideoFullscreen(forceFullScreen);
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ public class OpenVideosFullscreenHookPatch {
|
||||
}
|
||||
|
||||
if (!isFullScreenPatchIncluded()) {
|
||||
return false;
|
||||
return original;
|
||||
}
|
||||
|
||||
return Settings.OPEN_VIDEOS_FULLSCREEN_PORTRAIT.get();
|
||||
|
||||
@@ -2,27 +2,22 @@ package app.revanced.extension.youtube.patches;
|
||||
|
||||
import static app.revanced.extension.youtube.returnyoutubedislike.ReturnYouTubeDislike.Vote;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.ShapeDrawable;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.GuardedBy;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.youtube.patches.components.ReturnYouTubeDislikeFilterPatch;
|
||||
import app.revanced.extension.youtube.returnyoutubedislike.ReturnYouTubeDislike;
|
||||
import app.revanced.extension.youtube.returnyoutubedislike.requests.ReturnYouTubeDislikeApi;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.PlayerType;
|
||||
|
||||
@@ -60,12 +55,12 @@ public class ReturnYouTubeDislikePatch {
|
||||
private static volatile ReturnYouTubeDislike lastLithoShortsVideoData;
|
||||
|
||||
/**
|
||||
* Because the litho Shorts spans are created after {@link ReturnYouTubeDislikeFilterPatch}
|
||||
* detects the video ids, after the user votes the litho will update
|
||||
* but {@link #lastLithoShortsVideoData} is not the correct data to use.
|
||||
* If this is true, then instead use {@link #currentVideoData}.
|
||||
* Because litho Shorts spans are created offscreen after {@link ReturnYouTubeDislikeFilterPatch}
|
||||
* detects the video ids, but the current Short can arbitrarily reload the same span,
|
||||
* then use the {@link #lastLithoShortsVideoData} if this value is greater than zero.
|
||||
*/
|
||||
private static volatile boolean lithoShortsShouldUseCurrentData;
|
||||
@GuardedBy("ReturnYouTubeDislikePatch.class")
|
||||
private static int useLithoShortsVideoDataCount;
|
||||
|
||||
/**
|
||||
* Last video id prefetched. Field is to prevent prefetching the same video id multiple times in a row.
|
||||
@@ -73,22 +68,31 @@ public class ReturnYouTubeDislikePatch {
|
||||
@Nullable
|
||||
private static volatile String lastPrefetchedVideoId;
|
||||
|
||||
public static void onRYDStatusChange(boolean rydEnabled) {
|
||||
ReturnYouTubeDislikeApi.resetRateLimits();
|
||||
// Must remove all values to protect against using stale data
|
||||
// if the user enables RYD while a video is on screen.
|
||||
clearData();
|
||||
}
|
||||
|
||||
private static void clearData() {
|
||||
currentVideoData = null;
|
||||
lastLithoShortsVideoData = null;
|
||||
lithoShortsShouldUseCurrentData = false;
|
||||
synchronized (ReturnYouTubeDislike.class) {
|
||||
useLithoShortsVideoDataCount = 0;
|
||||
}
|
||||
|
||||
// Rolling number text should not be cleared,
|
||||
// as it's used if incognito Short is opened/closed
|
||||
// while a regular video is on screen.
|
||||
}
|
||||
|
||||
/**
|
||||
* @return If {@link #useLithoShortsVideoDataCount} was greater than zero.
|
||||
*/
|
||||
private static boolean decrementUseLithoDataIfNeeded() {
|
||||
synchronized (ReturnYouTubeDislikePatch.class) {
|
||||
if (useLithoShortsVideoDataCount > 0) {
|
||||
useLithoShortsVideoDataCount--;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Litho player for both regular videos and Shorts.
|
||||
@@ -148,14 +152,19 @@ public class ReturnYouTubeDislikePatch {
|
||||
return original; // No need to check for Shorts in the context.
|
||||
}
|
||||
|
||||
if (conversionContextString.contains("|shorts_dislike_button.eml")) {
|
||||
if (Utils.containsAny(conversionContextString,
|
||||
"|shorts_dislike_button.eml", "|reel_dislike_button.eml")) {
|
||||
return getShortsSpan(original, true);
|
||||
}
|
||||
|
||||
if (conversionContextString.contains("|shorts_like_button.eml")
|
||||
&& !Utils.containsNumber(original)) {
|
||||
Logger.printDebug(() -> "Replacing hidden likes count");
|
||||
return getShortsSpan(original, false);
|
||||
if (Utils.containsAny(conversionContextString,
|
||||
"|shorts_like_button.eml", "|reel_like_button.eml")) {
|
||||
if (!Utils.containsNumber(original)) {
|
||||
Logger.printDebug(() -> "Replacing hidden likes count");
|
||||
return getShortsSpan(original, false);
|
||||
} else {
|
||||
decrementUseLithoDataIfNeeded();
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "onLithoTextLoaded failure", ex);
|
||||
@@ -170,7 +179,14 @@ public class ReturnYouTubeDislikePatch {
|
||||
return original;
|
||||
}
|
||||
|
||||
ReturnYouTubeDislike videoData = lastLithoShortsVideoData;
|
||||
final ReturnYouTubeDislike videoData;
|
||||
if (decrementUseLithoDataIfNeeded()) {
|
||||
// New Short is loading off screen.
|
||||
videoData = lastLithoShortsVideoData;
|
||||
} else {
|
||||
videoData = currentVideoData;
|
||||
}
|
||||
|
||||
if (videoData == null) {
|
||||
// The Shorts litho video id filter did not detect the video id.
|
||||
// This is normal in incognito mode, but otherwise is abnormal.
|
||||
@@ -178,19 +194,6 @@ public class ReturnYouTubeDislikePatch {
|
||||
return original;
|
||||
}
|
||||
|
||||
// Use the correct dislikes data after voting.
|
||||
if (lithoShortsShouldUseCurrentData) {
|
||||
if (isDislikesSpan) {
|
||||
lithoShortsShouldUseCurrentData = false;
|
||||
}
|
||||
videoData = currentVideoData;
|
||||
if (videoData == null) {
|
||||
Logger.printException(() -> "currentVideoData is null"); // Should never happen
|
||||
return original;
|
||||
}
|
||||
Logger.printDebug(() -> "Using current video data for litho span");
|
||||
}
|
||||
|
||||
return isDislikesSpan
|
||||
? videoData.getDislikeSpanForShort((Spanned) original)
|
||||
: videoData.getLikeSpanForShort((Spanned) original);
|
||||
@@ -265,7 +268,7 @@ public class ReturnYouTubeDislikePatch {
|
||||
Logger.printDebug(() -> "Adding rolling number TextView changes");
|
||||
view.setCompoundDrawablePadding(ReturnYouTubeDislike.leftSeparatorShapePaddingPixels);
|
||||
ShapeDrawable separator = ReturnYouTubeDislike.getLeftSeparatorDrawable();
|
||||
if (Utils.isRightToLeftTextLayout()) {
|
||||
if (Utils.isRightToLeftLocale()) {
|
||||
view.setCompoundDrawables(null, null, separator, null);
|
||||
} else {
|
||||
view.setCompoundDrawables(separator, null, null, null);
|
||||
@@ -360,6 +363,11 @@ public class ReturnYouTubeDislikePatch {
|
||||
if (videoId.equals(lastPrefetchedVideoId)) {
|
||||
return;
|
||||
}
|
||||
if (!Utils.isNetworkConnected()) {
|
||||
Logger.printDebug(() -> "Cannot pre-fetch RYD, network is not connected");
|
||||
lastPrefetchedVideoId = null;
|
||||
return;
|
||||
}
|
||||
|
||||
final boolean videoIdIsShort = VideoInformation.lastPlayerResponseIsShort();
|
||||
// Shorts shelf in home and subscription feed causes player response hook to be called,
|
||||
@@ -414,6 +422,12 @@ public class ReturnYouTubeDislikePatch {
|
||||
}
|
||||
Logger.printDebug(() -> "New video id: " + videoId + " playerType: " + currentPlayerType);
|
||||
|
||||
if (!Utils.isNetworkConnected()) {
|
||||
Logger.printDebug(() -> "Cannot fetch RYD, network is not connected");
|
||||
currentVideoData = null;
|
||||
return;
|
||||
}
|
||||
|
||||
ReturnYouTubeDislike data = ReturnYouTubeDislike.getFetchForVideoId(videoId);
|
||||
// Pre-emptively set the data to short status.
|
||||
// Required to prevent Shorts data from being used on a minimized video in incognito mode.
|
||||
@@ -445,7 +459,10 @@ public class ReturnYouTubeDislikePatch {
|
||||
ReturnYouTubeDislike videoData = ReturnYouTubeDislike.getFetchForVideoId(videoId);
|
||||
videoData.setVideoIdIsShort(true);
|
||||
lastLithoShortsVideoData = videoData;
|
||||
lithoShortsShouldUseCurrentData = false;
|
||||
synchronized (ReturnYouTubeDislikePatch.class) {
|
||||
// Use litho Shorts data for the next like and dislike spans.
|
||||
useLithoShortsVideoDataCount = 2;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean videoIdIsSame(@Nullable ReturnYouTubeDislike fetch, @Nullable String videoId) {
|
||||
@@ -480,13 +497,6 @@ public class ReturnYouTubeDislikePatch {
|
||||
for (Vote v : Vote.values()) {
|
||||
if (v.value == vote) {
|
||||
videoData.sendVote(v);
|
||||
|
||||
if (isNoneHiddenOrMinimized) {
|
||||
if (lastLithoShortsVideoData != null) {
|
||||
lithoShortsShouldUseCurrentData = true;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package app.revanced.extension.youtube.patches;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -76,7 +78,7 @@ public class ShortsAutoplayPatch {
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static Enum<?> changeShortsRepeatBehavior(Enum<?> original) {
|
||||
public static Enum<?> changeShortsRepeatBehavior(@Nullable Enum<?> original) {
|
||||
try {
|
||||
final boolean autoplay;
|
||||
|
||||
@@ -98,17 +100,35 @@ public class ShortsAutoplayPatch {
|
||||
: ShortsLoopBehavior.REPEAT;
|
||||
|
||||
if (behavior.ytEnumValue != null) {
|
||||
Logger.printDebug(() -> behavior.ytEnumValue == original
|
||||
? "Changing Shorts repeat behavior from: " + original.name() + " to: " + behavior.ytEnumValue
|
||||
: "Behavior setting is same as original. Using original: " + original.name()
|
||||
);
|
||||
Logger.printDebug(() -> {
|
||||
String name = (original == null ? "unknown (null)" : original.name());
|
||||
return behavior == original
|
||||
? "Behavior setting is same as original. Using original: " + name
|
||||
: "Changing Shorts repeat behavior from: " + name + " to: " + behavior.name();
|
||||
});
|
||||
|
||||
return behavior.ytEnumValue;
|
||||
}
|
||||
|
||||
if (original == null) {
|
||||
// Cannot return null, as null is used to indicate Short was auto played.
|
||||
// Unpatched app replaces null with unknown enum type (appears to fix for bad api data).
|
||||
Enum<?> unknown = ShortsLoopBehavior.UNKNOWN.ytEnumValue;
|
||||
Logger.printDebug(() -> "Original is null, returning: " + unknown.name());
|
||||
return unknown;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "changeShortsRepeatState failure", ex);
|
||||
Logger.printException(() -> "changeShortsRepeatBehavior failure", ex);
|
||||
}
|
||||
|
||||
return original;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean isAutoPlay(Enum<?> original) {
|
||||
return ShortsLoopBehavior.SINGLE_PLAY.ytEnumValue == original;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,21 @@ package app.revanced.extension.youtube.patches;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
|
||||
public class VersionCheckPatch {
|
||||
public static final boolean IS_19_17_OR_GREATER = Utils.getAppVersionName().compareTo("19.17.00") >= 0;
|
||||
public static final boolean IS_19_20_OR_GREATER = Utils.getAppVersionName().compareTo("19.20.00") >= 0;
|
||||
public static final boolean IS_19_21_OR_GREATER = Utils.getAppVersionName().compareTo("19.21.00") >= 0;
|
||||
public static final boolean IS_19_26_OR_GREATER = Utils.getAppVersionName().compareTo("19.26.00") >= 0;
|
||||
public static final boolean IS_19_29_OR_GREATER = Utils.getAppVersionName().compareTo("19.29.00") >= 0;
|
||||
public static final boolean IS_19_34_OR_GREATER = Utils.getAppVersionName().compareTo("19.34.00") >= 0;
|
||||
public static final boolean IS_19_46_OR_GREATER = Utils.getAppVersionName().compareTo("19.46.00") >= 0;
|
||||
private static boolean isVersionOrGreater(String version) {
|
||||
return Utils.getAppVersionName().compareTo(version) >= 0;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public static final boolean IS_19_17_OR_GREATER = isVersionOrGreater("19.17.00");
|
||||
@Deprecated
|
||||
public static final boolean IS_19_20_OR_GREATER = isVersionOrGreater("19.20.00");
|
||||
@Deprecated
|
||||
public static final boolean IS_19_21_OR_GREATER = isVersionOrGreater("19.21.00");
|
||||
@Deprecated
|
||||
public static final boolean IS_19_26_OR_GREATER = isVersionOrGreater("19.26.00");
|
||||
@Deprecated
|
||||
public static final boolean IS_19_29_OR_GREATER = isVersionOrGreater("19.29.00");
|
||||
@Deprecated
|
||||
public static final boolean IS_19_34_OR_GREATER = isVersionOrGreater("19.34.00");
|
||||
public static final boolean IS_19_46_OR_GREATER = isVersionOrGreater("19.46.00");
|
||||
}
|
||||
|
||||
@@ -354,4 +354,23 @@ public final class VideoInformation {
|
||||
return videoTime >= videoLength && videoLength > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the current playback speed.
|
||||
* Rest of the implementation added by patch.
|
||||
*/
|
||||
public static void overridePlaybackSpeed(float speedOverride) {
|
||||
Logger.printDebug(() -> "Overriding playback speed to: " + speedOverride);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* @param newlyLoadedPlaybackSpeed The current playback speed.
|
||||
*/
|
||||
public static void setPlaybackSpeed(float newlyLoadedPlaybackSpeed) {
|
||||
if (playbackSpeed != newlyLoadedPlaybackSpeed) {
|
||||
Logger.printDebug(() -> "Video speed changed: " + newlyLoadedPlaybackSpeed);
|
||||
playbackSpeed = newlyLoadedPlaybackSpeed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,45 @@
|
||||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class WideSearchbarPatch {
|
||||
|
||||
private static final Boolean WIDE_SEARCHBAR_ENABLED = Settings.WIDE_SEARCHBAR.get();
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean enableWideSearchbar(boolean original) {
|
||||
return Settings.WIDE_SEARCHBAR.get() || original;
|
||||
return WIDE_SEARCHBAR_ENABLED || original;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void setActionBar(View view) {
|
||||
try {
|
||||
if (!WIDE_SEARCHBAR_ENABLED) return;
|
||||
|
||||
View searchBarView = Utils.getChildViewByResourceName(view, "search_bar");
|
||||
|
||||
final int paddingLeft = searchBarView.getPaddingLeft();
|
||||
final int paddingRight = searchBarView.getPaddingRight();
|
||||
final int paddingTop = searchBarView.getPaddingTop();
|
||||
final int paddingBottom = searchBarView.getPaddingBottom();
|
||||
final int paddingStart = Utils.dipToPixels(8);
|
||||
|
||||
if (Utils.isRightToLeftLocale()) {
|
||||
searchBarView.setPadding(paddingLeft, paddingTop, paddingStart, paddingBottom);
|
||||
} else {
|
||||
searchBarView.setPadding(paddingStart, paddingTop, paddingRight, paddingBottom);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "setActionBar failure", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class ZoomHapticsPatch {
|
||||
public static boolean shouldVibrate() {
|
||||
return !Settings.DISABLE_ZOOM_HAPTICS.get();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user