mirror of
https://github.com/revanced/revanced-patches.git
synced 2025-12-18 07:02:28 +01:00
Compare commits
365 Commits
v5.26.1-de
...
v5.37.0-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a5c102c0d | ||
|
|
e6c79f1383 | ||
|
|
2a582eced8 | ||
|
|
2db0948bea | ||
|
|
a3ba92e742 | ||
|
|
2a85a3b290 | ||
|
|
eee72208dd | ||
|
|
dcd42454bd | ||
|
|
782353c18a | ||
|
|
b53b870e8f | ||
|
|
09b941abf0 | ||
|
|
678ef4052e | ||
|
|
0abfab79d7 | ||
|
|
61cadf72cd | ||
|
|
e12359b94f | ||
|
|
c001daba4a | ||
|
|
e136f62d6e | ||
|
|
8ec405a359 | ||
|
|
2f4b3a887b | ||
|
|
d1fabb242b | ||
|
|
a53b00dd51 | ||
|
|
850c13e98e | ||
|
|
4310789a26 | ||
|
|
c4a720fbd3 | ||
|
|
3bdb8dbce0 | ||
|
|
4894f33c96 | ||
|
|
7f6093ee66 | ||
|
|
9d4aa5cd16 | ||
|
|
5ace6f587c | ||
|
|
796f56745e | ||
|
|
88b47ef414 | ||
|
|
8cd8e59bbc | ||
|
|
6e72b14d07 | ||
|
|
52b088327b | ||
|
|
8e934cc56b | ||
|
|
b3140d909b | ||
|
|
97645aa9f4 | ||
|
|
603e2d018c | ||
|
|
144af2f07e | ||
|
|
b8629aacb6 | ||
|
|
3951527f51 | ||
|
|
7a8b618c4e | ||
|
|
c66c42e946 | ||
|
|
b340769cf3 | ||
|
|
0a8cd7a7db | ||
|
|
39f90e4b11 | ||
|
|
9256aa4548 | ||
|
|
7973c75552 | ||
|
|
2b2307416a | ||
|
|
1dbc2d4057 | ||
|
|
f6917dc361 | ||
|
|
d2f043e11a | ||
|
|
a392bc0dfd | ||
|
|
dfc127048a | ||
|
|
ed31d0cab6 | ||
|
|
0df6315f9c | ||
|
|
f14259f9ef | ||
|
|
1473db0bef | ||
|
|
829ca58a55 | ||
|
|
aace741e25 | ||
|
|
189529151a | ||
|
|
51237c177a | ||
|
|
23496c7c36 | ||
|
|
e6823d8924 | ||
|
|
43597dab21 | ||
|
|
c0824db142 | ||
|
|
1b7f84b7fa | ||
|
|
6d87c848d6 | ||
|
|
150bee2833 | ||
|
|
c3ee6eca44 | ||
|
|
01a04c338c | ||
|
|
3130225d9d | ||
|
|
16b27fb872 | ||
|
|
bedabd3fa3 | ||
|
|
84f3c6f02d | ||
|
|
25470baeee | ||
|
|
b86da73a87 | ||
|
|
4aaa7ca895 | ||
|
|
d3f63461e7 | ||
|
|
7a3ace2231 | ||
|
|
c89668a540 | ||
|
|
40ac8e1142 | ||
|
|
26c6420de5 | ||
|
|
bfd3989995 | ||
|
|
7e812ae1a8 | ||
|
|
c23a926b07 | ||
|
|
fe66baedb7 | ||
|
|
959f23d1e4 | ||
|
|
56fbd8cce0 | ||
|
|
1bb8c53ed3 | ||
|
|
5fc0631a15 | ||
|
|
bdbe96beba | ||
|
|
6bd9e49c7a | ||
|
|
f904ca6d7e | ||
|
|
e579c56921 | ||
|
|
83f239065a | ||
|
|
6499318f33 | ||
|
|
809e013c4e | ||
|
|
182829d51c | ||
|
|
61824ade23 | ||
|
|
ff4308e961 | ||
|
|
b5eb13c0a8 | ||
|
|
b702dceda0 | ||
|
|
d616652058 | ||
|
|
c3e571e765 | ||
|
|
30176a3318 | ||
|
|
9c0638d128 | ||
|
|
d7eb6e87a5 | ||
|
|
562e005772 | ||
|
|
f61218de52 | ||
|
|
a19b670e19 | ||
|
|
300d816350 | ||
|
|
63d64a5c87 | ||
|
|
0cfc31c8f7 | ||
|
|
a28891e5f3 | ||
|
|
36036b082d | ||
|
|
1bc63e50a7 | ||
|
|
4b2b5e3029 | ||
|
|
9afa7d2ac6 | ||
|
|
1a8146dbc8 | ||
|
|
178eed7fcd | ||
|
|
621292644c | ||
|
|
1dd01cf54a | ||
|
|
8c31374c53 | ||
|
|
2e177a8839 | ||
|
|
cfffd422f8 | ||
|
|
37aab8382e | ||
|
|
f4950ec2ea | ||
|
|
7bdc32867a | ||
|
|
6e60ac6963 | ||
|
|
1adbd563b2 | ||
|
|
9ccf13b680 | ||
|
|
7b8ca9c018 | ||
|
|
ae6dd23d08 | ||
|
|
b1d164b446 | ||
|
|
87c39dd485 | ||
|
|
1549ac12aa | ||
|
|
5d08fdddb8 | ||
|
|
98114e5bde | ||
|
|
a4817dfdd0 | ||
|
|
d4f05351e1 | ||
|
|
d92362b0d9 | ||
|
|
afc7c75df1 | ||
|
|
f0d4e9bfb4 | ||
|
|
e9e4cf39b6 | ||
|
|
0579a9f760 | ||
|
|
1c0acef3f3 | ||
|
|
2419adb77b | ||
|
|
9e4113555b | ||
|
|
125855540b | ||
|
|
a8eee825e6 | ||
|
|
63859f0ef9 | ||
|
|
1c9000dbda | ||
|
|
8ec857a175 | ||
|
|
f56c7868f5 | ||
|
|
cfd77800d6 | ||
|
|
707deaef0b | ||
|
|
9ddb3ac39d | ||
|
|
a7d3b7c287 | ||
|
|
30bac0397e | ||
|
|
c5fc187a35 | ||
|
|
f46dbcd084 | ||
|
|
2136573cb6 | ||
|
|
86ec08993c | ||
|
|
44da5a71c5 | ||
|
|
e4e81b89ea | ||
|
|
165df659a1 | ||
|
|
bb87afe0f6 | ||
|
|
ac5fb17937 | ||
|
|
e88356b3c5 | ||
|
|
dead9c2d94 | ||
|
|
ca640b2839 | ||
|
|
c972267cd8 | ||
|
|
d0d2c13d16 | ||
|
|
e7b4ab53cf | ||
|
|
f994264d9c | ||
|
|
eb61c1f5d1 | ||
|
|
e578347277 | ||
|
|
294b2dce2e | ||
|
|
aa37105ea3 | ||
|
|
eb57a2697b | ||
|
|
19bc5b63c5 | ||
|
|
2b93ff6cfc | ||
|
|
cc6984e919 | ||
|
|
8bf575e778 | ||
|
|
2e625ee1a2 | ||
|
|
6bcba48ee7 | ||
|
|
c3034edc43 | ||
|
|
82255a09d3 | ||
|
|
594dce13cd | ||
|
|
479e205808 | ||
|
|
3d1b7e8101 | ||
|
|
e951184b7a | ||
|
|
d088b1e7ed | ||
|
|
a38f635514 | ||
|
|
b3e6c215cc | ||
|
|
c9cc3d5c41 | ||
|
|
536e64565c | ||
|
|
65cbf3c1eb | ||
|
|
61c1a7a75a | ||
|
|
1e39db06b8 | ||
|
|
e019f83232 | ||
|
|
3b57a5f8c0 | ||
|
|
eafe3dfc45 | ||
|
|
d56d8d990c | ||
|
|
37a8682901 | ||
|
|
11ba7d4e3e | ||
|
|
6833d37c26 | ||
|
|
e6f72bcb7d | ||
|
|
e8a227c082 | ||
|
|
0472ec2830 | ||
|
|
6412a5cb1a | ||
|
|
cc548689ac | ||
|
|
a3d47e72e3 | ||
|
|
f37482443a | ||
|
|
cc4aef89d3 | ||
|
|
1c0a0eb4b5 | ||
|
|
b1d6c46763 | ||
|
|
42195b9f63 | ||
|
|
a4e08ea13d | ||
|
|
bd2a939a72 | ||
|
|
a89179ab79 | ||
|
|
b0129d383a | ||
|
|
23b6c42630 | ||
|
|
10f4464735 | ||
|
|
4e5addbba5 | ||
|
|
8d11ede927 | ||
|
|
83a3f4da00 | ||
|
|
caf3b69731 | ||
|
|
3135203b55 | ||
|
|
8d113a7c67 | ||
|
|
4e742075f3 | ||
|
|
04caa66662 | ||
|
|
dacc85f5e7 | ||
|
|
f9abec358a | ||
|
|
7e11514cc1 | ||
|
|
2e9c8df8f6 | ||
|
|
4c8cfc8800 | ||
|
|
0ba6fad33f | ||
|
|
3eac215e13 | ||
|
|
90a3262f68 | ||
|
|
f7f49b834e | ||
|
|
89ec5d5bc6 | ||
|
|
e3bc8be936 | ||
|
|
6c5c3f5a4d | ||
|
|
629bd0644b | ||
|
|
b4005079e3 | ||
|
|
a354c443ad | ||
|
|
d1313e3ea1 | ||
|
|
11338008c6 | ||
|
|
8b9e04475d | ||
|
|
d3c9dc6ed7 | ||
|
|
d7ed32571f | ||
|
|
d3935f03c0 | ||
|
|
b2e601f0f0 | ||
|
|
d3ec219a29 | ||
|
|
5ed07d4aaa | ||
|
|
209a3a3626 | ||
|
|
2b3419571f | ||
|
|
bbe504e616 | ||
|
|
6c32591f62 | ||
|
|
ad6da67281 | ||
|
|
14dc593eba | ||
|
|
e52ee41222 | ||
|
|
6ee94f8532 | ||
|
|
21688201af | ||
|
|
f08474369b | ||
|
|
ed617094ea | ||
|
|
9131c50f1b | ||
|
|
69600d08a4 | ||
|
|
5dba77612b | ||
|
|
92b588c866 | ||
|
|
da20e565cd | ||
|
|
ca694c78d2 | ||
|
|
e169056b70 | ||
|
|
b6bf1e026c | ||
|
|
9fa89d48c0 | ||
|
|
5d2c21540c | ||
|
|
1a8aacdff6 | ||
|
|
1804bd9bfc | ||
|
|
7eb4e62762 | ||
|
|
b8e10b5c1f | ||
|
|
a7c11b9b08 | ||
|
|
443c0a74d5 | ||
|
|
84a0f7f7d7 | ||
|
|
558bf8bca8 | ||
|
|
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 |
2
.github/workflows/build_pull_request.yml
vendored
2
.github/workflows/build_pull_request.yml
vendored
@@ -13,8 +13,6 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
|
|||||||
1
.github/workflows/pull_strings.yml
vendored
1
.github/workflows/pull_strings.yml
vendored
@@ -17,7 +17,6 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: dev
|
ref: dev
|
||||||
fetch-depth: 0
|
|
||||||
clean: true
|
clean: true
|
||||||
|
|
||||||
- name: Pull strings
|
- name: Pull strings
|
||||||
|
|||||||
2
.github/workflows/push_strings.yml
vendored
2
.github/workflows/push_strings.yml
vendored
@@ -15,8 +15,6 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Preprocess strings
|
- name: Preprocess strings
|
||||||
env:
|
env:
|
||||||
|
|||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -19,8 +19,6 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
|
|||||||
1150
CHANGELOG.md
1150
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
8
adsfund.json
Normal file
8
adsfund.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"info": "This is verification file for ads.fund project",
|
||||||
|
"project": {
|
||||||
|
"name": "Revanced Patches",
|
||||||
|
"walletAddress": "0x7ab4091e00363654bf84B34151225742cd92FCE5",
|
||||||
|
"tokenAddress": "0xadf325f255083a3f3d9a9d01ffb3db52a148d802"
|
||||||
|
}
|
||||||
|
}
|
||||||
3
build.gradle.kts
Normal file
3
build.gradle.kts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.library) apply false
|
||||||
|
}
|
||||||
5
extensions/baconreader/build.gradle.kts
Normal file
5
extensions/baconreader/build.gradle.kts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
dependencies {
|
||||||
|
compileOnly(project(":extensions:shared:library"))
|
||||||
|
compileOnly(libs.annotation)
|
||||||
|
compileOnly(libs.okhttp)
|
||||||
|
}
|
||||||
1
extensions/baconreader/src/main/AndroidManifest.xml
Normal file
1
extensions/baconreader/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest/>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package app.revanced.extension.baconreader;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.fixes.redgifs.BaseFixRedgifsApiPatch;
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @noinspection unused
|
||||||
|
*/
|
||||||
|
public class FixRedgifsApiPatch extends BaseFixRedgifsApiPatch {
|
||||||
|
static {
|
||||||
|
INSTANCE = new FixRedgifsApiPatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDefaultUserAgent() {
|
||||||
|
// BaconReader uses a static user agent for Redgifs API calls
|
||||||
|
return "BaconReader";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static OkHttpClient install(OkHttpClient.Builder builder) {
|
||||||
|
return builder.addInterceptor(INSTANCE).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
dependencies {
|
dependencies {
|
||||||
compileOnly(project(":extensions:shared:library"))
|
compileOnly(project(":extensions:shared:library"))
|
||||||
compileOnly(project(":extensions:boostforreddit:stub"))
|
compileOnly(project(":extensions:boostforreddit:stub"))
|
||||||
|
compileOnly(libs.annotation)
|
||||||
|
compileOnly(libs.okhttp)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package app.revanced.extension.boostforreddit;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.fixes.redgifs.BaseFixRedgifsApiPatch;
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @noinspection unused
|
||||||
|
*/
|
||||||
|
public class FixRedgifsApiPatch extends BaseFixRedgifsApiPatch {
|
||||||
|
static {
|
||||||
|
INSTANCE = new FixRedgifsApiPatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDefaultUserAgent() {
|
||||||
|
// Boost uses a static user agent for Redgifs API calls
|
||||||
|
return "Boost";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static OkHttpClient createClient() {
|
||||||
|
return new OkHttpClient.Builder().addInterceptor(INSTANCE).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id(libs.plugins.android.library.get().pluginId)
|
alias(libs.plugins.android.library)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|||||||
4
extensions/cricbuzz/build.gradle.kts
Normal file
4
extensions/cricbuzz/build.gradle.kts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
dependencies {
|
||||||
|
compileOnly(project(":extensions:shared:library"))
|
||||||
|
compileOnly(project(":extensions:cricbuzz:stub"))
|
||||||
|
}
|
||||||
1
extensions/cricbuzz/src/main/AndroidManifest.xml
Normal file
1
extensions/cricbuzz/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest/>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package app.revanced.extension.cricbuzz.ads;
|
||||||
|
|
||||||
|
import com.cricbuzz.android.data.rest.model.BottomBar;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class HideAdsPatch {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static void filterCb11(List<BottomBar> list) {
|
||||||
|
try {
|
||||||
|
Iterator<BottomBar> iterator = list.iterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
BottomBar bar = iterator.next();
|
||||||
|
if (bar.getName().equals("Cricbuzz11")) {
|
||||||
|
Logger.printInfo(() -> "Removing Cricbuzz11 bar: " + bar);
|
||||||
|
iterator.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "filterCb11 failure", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
extensions/cricbuzz/stub/build.gradle.kts
Normal file
17
extensions/cricbuzz/stub/build.gradle.kts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.library)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "app.revanced.extension"
|
||||||
|
compileSdk = 34
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 21
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
|
}
|
||||||
|
}
|
||||||
1
extensions/cricbuzz/stub/src/main/AndroidManifest.xml
Normal file
1
extensions/cricbuzz/stub/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest/>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.cricbuzz.android.data.rest.model;
|
||||||
|
|
||||||
|
public final class BottomBar {
|
||||||
|
public final String getName() { throw new UnsupportedOperationException(); }
|
||||||
|
}
|
||||||
@@ -1,14 +1,24 @@
|
|||||||
package app.revanced.extension.messenger.metaai;
|
package app.revanced.extension.messenger.metaai;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public class RemoveMetaAIPatch {
|
public class RemoveMetaAIPatch {
|
||||||
public static boolean overrideConfigBool(long id, boolean value) {
|
private static final Set<Long> loggedIDs = Collections.synchronizedSet(new HashSet<>());
|
||||||
// It seems like all configs starting with 363219 are related to Meta AI.
|
|
||||||
// A list of specific ones that need disabling would probably be better,
|
public static boolean overrideBooleanFlag(long id, boolean value) {
|
||||||
// but these config numbers seem to change slightly with each update.
|
try {
|
||||||
// These first 6 digits don't though.
|
if (Long.toString(id).startsWith("REPLACED_BY_PATCH")) {
|
||||||
if (Long.toString(id).startsWith("363219"))
|
if (loggedIDs.add(id))
|
||||||
|
Logger.printInfo(() -> "Overriding " + id + " from " + value + " to false");
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "overrideBooleanFlag failure", ex);
|
||||||
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
dependencies {
|
||||||
|
compileOnly(project(":extensions:shared:library"))
|
||||||
|
compileOnly(project(":extensions:youtube:stub"))
|
||||||
|
compileOnly(libs.annotation)
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package app.revanced.extension.music.patches.spoof;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_43_32;
|
||||||
|
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_61_48;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.spoof.ClientType;
|
||||||
|
import app.revanced.extension.shared.spoof.requests.StreamingDataRequest;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class SpoofVideoStreamsPatch {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static void setClientOrderToUse() {
|
||||||
|
ClientType[] availableClients = {
|
||||||
|
ANDROID_VR_1_43_32,
|
||||||
|
ANDROID_VR_1_61_48,
|
||||||
|
};
|
||||||
|
|
||||||
|
StreamingDataRequest.setClientOrderToUse(availableClients, ANDROID_VR_1_43_32);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package app.revanced.extension.music.spoof;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @noinspection unused
|
|
||||||
*/
|
|
||||||
public class SpoofClientPatch {
|
|
||||||
private static final int CLIENT_TYPE_ID = 26;
|
|
||||||
private static final String CLIENT_VERSION = "6.21";
|
|
||||||
private static final String DEVICE_MODEL = "iPhone16,2";
|
|
||||||
private static final String OS_VERSION = "17.7.2.21H221";
|
|
||||||
|
|
||||||
public static int getClientId() {
|
|
||||||
return CLIENT_TYPE_ID;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getClientVersion() {
|
|
||||||
return CLIENT_VERSION;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getClientModel() {
|
|
||||||
return DEVICE_MODEL;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getOsVersion() {
|
|
||||||
return OS_VERSION;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id(libs.plugins.android.library.get().pluginId)
|
alias(libs.plugins.android.library)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|||||||
@@ -0,0 +1,207 @@
|
|||||||
|
package app.revanced.extension.primevideo.videoplayer;
|
||||||
|
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.RectF;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.ColorFilter;
|
||||||
|
import android.graphics.PixelFormat;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
|
||||||
|
import com.amazon.video.sdk.player.Player;
|
||||||
|
|
||||||
|
public class PlaybackSpeedPatch {
|
||||||
|
private static Player player;
|
||||||
|
private static final float[] SPEED_VALUES = {0.5f, 0.7f, 0.8f, 0.9f, 0.95f, 1.0f, 1.05f, 1.1f, 1.2f, 1.3f, 1.5f, 2.0f};
|
||||||
|
private static final String SPEED_BUTTON_TAG = "speed_overlay";
|
||||||
|
|
||||||
|
public static void setPlayer(Player playerInstance) {
|
||||||
|
player = playerInstance;
|
||||||
|
if (player != null) {
|
||||||
|
// Reset playback rate when switching between episodes to ensure correct display.
|
||||||
|
player.setPlaybackRate(1.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void initializeSpeedOverlay(View userControlsView) {
|
||||||
|
try {
|
||||||
|
LinearLayout buttonContainer = Utils.getChildViewByResourceName(userControlsView, "ButtonContainerPlayerTop");
|
||||||
|
|
||||||
|
// If the speed overlay exists we should return early.
|
||||||
|
if (Utils.getChildView(buttonContainer, false, child ->
|
||||||
|
child instanceof ImageView && SPEED_BUTTON_TAG.equals(child.getTag())) != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageView speedButton = createSpeedButton(userControlsView.getContext());
|
||||||
|
speedButton.setOnClickListener(v -> changePlaybackSpeed(speedButton));
|
||||||
|
buttonContainer.addView(speedButton, 0);
|
||||||
|
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
Logger.printException(() -> "initializeSpeedOverlay, no button container found", e);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.printException(() -> "initializeSpeedOverlay failure", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ImageView createSpeedButton(Context context) {
|
||||||
|
ImageView speedButton = new ImageView(context);
|
||||||
|
speedButton.setContentDescription("Playback Speed");
|
||||||
|
speedButton.setTag(SPEED_BUTTON_TAG);
|
||||||
|
speedButton.setClickable(true);
|
||||||
|
speedButton.setFocusable(true);
|
||||||
|
speedButton.setScaleType(ImageView.ScaleType.CENTER);
|
||||||
|
|
||||||
|
SpeedIconDrawable speedIcon = new SpeedIconDrawable();
|
||||||
|
speedButton.setImageDrawable(speedIcon);
|
||||||
|
|
||||||
|
int buttonSize = Utils.dipToPixels(48);
|
||||||
|
speedButton.setMinimumWidth(buttonSize);
|
||||||
|
speedButton.setMinimumHeight(buttonSize);
|
||||||
|
|
||||||
|
return speedButton;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String[] getSpeedOptions() {
|
||||||
|
String[] options = new String[SPEED_VALUES.length];
|
||||||
|
for (int i = 0; i < SPEED_VALUES.length; i++) {
|
||||||
|
options[i] = SPEED_VALUES[i] + "x";
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void changePlaybackSpeed(ImageView imageView) {
|
||||||
|
if (player == null) {
|
||||||
|
Logger.printException(() -> "Player not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
player.pause();
|
||||||
|
AlertDialog dialog = createSpeedPlaybackDialog(imageView);
|
||||||
|
dialog.setOnDismissListener(dialogInterface -> player.play());
|
||||||
|
dialog.show();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.printException(() -> "changePlaybackSpeed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AlertDialog createSpeedPlaybackDialog(ImageView imageView) {
|
||||||
|
Context context = imageView.getContext();
|
||||||
|
int currentSelection = getCurrentSpeedSelection();
|
||||||
|
|
||||||
|
return new AlertDialog.Builder(context)
|
||||||
|
.setTitle("Select Playback Speed")
|
||||||
|
.setSingleChoiceItems(getSpeedOptions(), currentSelection,
|
||||||
|
PlaybackSpeedPatch::handleSpeedSelection)
|
||||||
|
.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getCurrentSpeedSelection() {
|
||||||
|
try {
|
||||||
|
float currentRate = player.getPlaybackRate();
|
||||||
|
int index = Arrays.binarySearch(SPEED_VALUES, currentRate);
|
||||||
|
return Math.max(index, 0); // Use slowest speed if not found.
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.printException(() -> "getCurrentSpeedSelection error getting current playback speed", e);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void handleSpeedSelection(android.content.DialogInterface dialog, int selectedIndex) {
|
||||||
|
try {
|
||||||
|
float selectedSpeed = SPEED_VALUES[selectedIndex];
|
||||||
|
player.setPlaybackRate(selectedSpeed);
|
||||||
|
player.play();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.printException(() -> "handleSpeedSelection error setting playback speed", e);
|
||||||
|
} finally {
|
||||||
|
dialog.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SpeedIconDrawable extends Drawable {
|
||||||
|
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void draw(Canvas canvas) {
|
||||||
|
int w = getBounds().width();
|
||||||
|
int h = getBounds().height();
|
||||||
|
float centerX = w / 2f;
|
||||||
|
// Position gauge in lower portion.
|
||||||
|
float centerY = h * 0.7f;
|
||||||
|
float radius = Math.min(w, h) / 2f * 0.8f;
|
||||||
|
|
||||||
|
paint.setColor(Color.WHITE);
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
paint.setStrokeWidth(radius * 0.1f);
|
||||||
|
|
||||||
|
// Draw semicircle.
|
||||||
|
RectF oval = new RectF(centerX - radius, centerY - radius, centerX + radius, centerY + radius);
|
||||||
|
canvas.drawArc(oval, 180, 180, false, paint);
|
||||||
|
|
||||||
|
// Draw three tick marks.
|
||||||
|
paint.setStrokeWidth(radius * 0.06f);
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
float angle = 180 + (i * 45); // 180°, 225°, 270°.
|
||||||
|
float angleRad = (float) Math.toRadians(angle);
|
||||||
|
|
||||||
|
float startX = centerX + (radius * 0.8f) * (float) Math.cos(angleRad);
|
||||||
|
float startY = centerY + (radius * 0.8f) * (float) Math.sin(angleRad);
|
||||||
|
float endX = centerX + radius * (float) Math.cos(angleRad);
|
||||||
|
float endY = centerY + radius * (float) Math.sin(angleRad);
|
||||||
|
|
||||||
|
canvas.drawLine(startX, startY, endX, endY, paint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw needle.
|
||||||
|
paint.setStrokeWidth(radius * 0.08f);
|
||||||
|
float needleAngle = 200; // Slightly right of center.
|
||||||
|
float needleAngleRad = (float) Math.toRadians(needleAngle);
|
||||||
|
|
||||||
|
float needleEndX = centerX + (radius * 0.6f) * (float) Math.cos(needleAngleRad);
|
||||||
|
float needleEndY = centerY + (radius * 0.6f) * (float) Math.sin(needleAngleRad);
|
||||||
|
|
||||||
|
canvas.drawLine(centerX, centerY, needleEndX, needleEndY, paint);
|
||||||
|
|
||||||
|
// Center dot.
|
||||||
|
paint.setStyle(Paint.Style.FILL);
|
||||||
|
canvas.drawCircle(centerX, centerY, radius * 0.06f, paint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAlpha(int alpha) {
|
||||||
|
paint.setAlpha(alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setColorFilter(ColorFilter colorFilter) {
|
||||||
|
paint.setColorFilter(colorFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOpacity() {
|
||||||
|
return PixelFormat.TRANSLUCENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getIntrinsicWidth() {
|
||||||
|
return Utils.dipToPixels(32);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getIntrinsicHeight() {
|
||||||
|
return Utils.dipToPixels(32);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id(libs.plugins.android.library.get().pluginId)
|
alias(libs.plugins.android.library)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|||||||
@@ -4,4 +4,10 @@ public interface VideoPlayer {
|
|||||||
long getCurrentPosition();
|
long getCurrentPosition();
|
||||||
|
|
||||||
void seekTo(long positionMs);
|
void seekTo(long positionMs);
|
||||||
|
|
||||||
|
void pause();
|
||||||
|
|
||||||
|
void play();
|
||||||
|
|
||||||
|
boolean isPlaying();
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.amazon.video.sdk.player;
|
||||||
|
|
||||||
|
public interface Player {
|
||||||
|
float getPlaybackRate();
|
||||||
|
|
||||||
|
void setPlaybackRate(float rate);
|
||||||
|
|
||||||
|
void play();
|
||||||
|
|
||||||
|
void pause();
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id(libs.plugins.android.library.get().pluginId)
|
alias(libs.plugins.android.library)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":extensions:shared:library"))
|
implementation(project(":extensions:shared:library"))
|
||||||
|
compileOnly(libs.okhttp)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("com.android.library")
|
alias(libs.plugins.android.library)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@@ -18,4 +18,5 @@ android {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly(libs.annotation)
|
compileOnly(libs.annotation)
|
||||||
|
compileOnly(libs.okhttp)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import static app.revanced.extension.shared.requests.Route.Method.GET;
|
|||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.AlertDialog;
|
import android.app.Dialog;
|
||||||
import android.app.SearchManager;
|
import android.app.SearchManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
@@ -15,9 +15,10 @@ import android.net.Uri;
|
|||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.PowerManager;
|
import android.os.PowerManager;
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
|
import android.util.Pair;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.RequiresApi;
|
|
||||||
|
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
@@ -78,13 +79,27 @@ public class GmsCoreSupport {
|
|||||||
// Use a delay to allow the activity to finish initializing.
|
// 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.
|
// Otherwise, if device is in dark mode the dialog is shown with wrong color scheme.
|
||||||
Utils.runOnMainThreadDelayed(() -> {
|
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,
|
// 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.
|
// just in case the battery change can never be satisfied.
|
||||||
var dialog = new AlertDialog.Builder(context)
|
dialog.setCancelable(true);
|
||||||
.setTitle(str("gms_core_dialog_title"))
|
|
||||||
.setMessage(str(dialogMessageRef))
|
// Show the dialog
|
||||||
.setPositiveButton(str(positiveButtonTextRef), onPositiveClickListener)
|
|
||||||
.create();
|
|
||||||
Utils.showDialog(context, dialog);
|
Utils.showDialog(context, dialog);
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
@@ -92,7 +107,6 @@ public class GmsCoreSupport {
|
|||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
|
||||||
public static void checkGmsCore(Activity context) {
|
public static void checkGmsCore(Activity context) {
|
||||||
try {
|
try {
|
||||||
// Verify the user has not included GmsCore for a root installation.
|
// Verify the user has not included GmsCore for a root installation.
|
||||||
@@ -140,7 +154,9 @@ public class GmsCoreSupport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if GmsCore is currently running in the background.
|
// 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) {
|
if (client == null) {
|
||||||
Logger.printInfo(() -> "GmsCore is not running in the background");
|
Logger.printInfo(() -> "GmsCore is not running in the background");
|
||||||
checkIfDontKillMyAppSupportsManufacturer();
|
checkIfDontKillMyAppSupportsManufacturer();
|
||||||
@@ -150,6 +166,8 @@ public class GmsCoreSupport {
|
|||||||
"gms_core_dialog_open_website_text",
|
"gms_core_dialog_open_website_text",
|
||||||
(dialog, id) -> openDontKillMyApp());
|
(dialog, id) -> openDontKillMyApp());
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
if (client != null) client.close();
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "checkGmsCore failure", ex);
|
Logger.printException(() -> "checkGmsCore failure", ex);
|
||||||
@@ -209,6 +227,11 @@ public class GmsCoreSupport {
|
|||||||
* @return If GmsCore is not whitelisted from battery optimizations.
|
* @return If GmsCore is not whitelisted from battery optimizations.
|
||||||
*/
|
*/
|
||||||
private static boolean batteryOptimizationsEnabled(Context context) {
|
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);
|
var powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
|
||||||
return !powerManager.isIgnoringBatteryOptimizations(GMS_CORE_PACKAGE_NAME);
|
return !powerManager.isIgnoringBatteryOptimizations(GMS_CORE_PACKAGE_NAME);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ import app.revanced.extension.shared.settings.preference.LogBufferManager;
|
|||||||
* ReVanced specific logger. Logging is done to standard device log (accessible thru ADB),
|
* ReVanced specific logger. Logging is done to standard device log (accessible thru ADB),
|
||||||
* and additionally accessible thru {@link LogBufferManager}.
|
* and additionally accessible thru {@link LogBufferManager}.
|
||||||
*
|
*
|
||||||
* All methods are thread safe.
|
* All methods are thread safe, and are safe to call even
|
||||||
|
* if {@link Utils#getContext()} is not available.
|
||||||
*/
|
*/
|
||||||
public class Logger {
|
public class Logger {
|
||||||
|
|
||||||
@@ -138,6 +139,20 @@ public class Logger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.
|
* Logs debug messages under the outer class name of the code calling this method.
|
||||||
* <p>
|
* <p>
|
||||||
@@ -157,8 +172,8 @@ public class Logger {
|
|||||||
* building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
|
* building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
|
||||||
*/
|
*/
|
||||||
public static void printDebug(LogMessage message, @Nullable Exception ex) {
|
public static void printDebug(LogMessage message, @Nullable Exception ex) {
|
||||||
if (DEBUG.get()) {
|
if (shouldLogDebug()) {
|
||||||
logInternal(LogLevel.DEBUG, message, ex, DEBUG_STACKTRACE.get(), false);
|
logInternal(LogLevel.DEBUG, message, ex, includeStackTrace(), false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,7 +188,7 @@ public class Logger {
|
|||||||
* Logs information messages using the outer class name of the code calling this method.
|
* Logs information messages using the outer class name of the code calling this method.
|
||||||
*/
|
*/
|
||||||
public static void printInfo(LogMessage message, @Nullable Exception ex) {
|
public static void printInfo(LogMessage message, @Nullable Exception ex) {
|
||||||
logInternal(LogLevel.INFO, message, ex, DEBUG_STACKTRACE.get(), false);
|
logInternal(LogLevel.INFO, message, ex, includeStackTrace(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -194,22 +209,6 @@ public class Logger {
|
|||||||
* @param ex exception (optional)
|
* @param ex exception (optional)
|
||||||
*/
|
*/
|
||||||
public static void printException(LogMessage message, @Nullable Throwable ex) {
|
public static void printException(LogMessage message, @Nullable Throwable ex) {
|
||||||
logInternal(LogLevel.ERROR, message, ex, DEBUG_STACKTRACE.get(), DEBUG_TOAST_ON_ERROR.get());
|
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(LogMessage message) {
|
|
||||||
logInternal(LogLevel.INFO, message, null, false, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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(LogMessage message, @Nullable Exception ex) {
|
|
||||||
logInternal(LogLevel.ERROR, message, ex, false, false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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 android.text.Html.FROM_HTML_MODE_COMPACT;
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
import static app.revanced.extension.shared.Utils.DialogFragmentOnStartAction;
|
import static app.revanced.extension.shared.Utils.DialogFragmentOnStartAction;
|
||||||
|
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.AlertDialog;
|
import android.app.Dialog;
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.graphics.PorterDuff;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.text.Html;
|
import android.text.Html;
|
||||||
|
import android.util.Pair;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.View;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
@@ -86,14 +92,15 @@ abstract class Check {
|
|||||||
);
|
);
|
||||||
|
|
||||||
Utils.runOnMainThreadDelayed(() -> {
|
Utils.runOnMainThreadDelayed(() -> {
|
||||||
AlertDialog alert = new AlertDialog.Builder(activity)
|
// Create the custom dialog.
|
||||||
.setCancelable(false)
|
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
||||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
activity,
|
||||||
.setTitle(str("revanced_check_environment_failed_title"))
|
str("revanced_check_environment_failed_title"), // Title.
|
||||||
.setMessage(message)
|
message, // Message.
|
||||||
.setPositiveButton(
|
null, // No EditText.
|
||||||
" ",
|
str("revanced_check_environment_dialog_open_official_source_button"), // OK button text.
|
||||||
(dialog, which) -> {
|
() -> {
|
||||||
|
// Action for the OK (website) button.
|
||||||
final var intent = new Intent(Intent.ACTION_VIEW, GOOD_SOURCE);
|
final var intent = new Intent(Intent.ACTION_VIEW, GOOD_SOURCE);
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
activity.startActivity(intent);
|
activity.startActivity(intent);
|
||||||
@@ -102,22 +109,41 @@ abstract class Check {
|
|||||||
// which is no longer showing a warning dialog.
|
// which is no longer showing a warning dialog.
|
||||||
activity.finishAffinity();
|
activity.finishAffinity();
|
||||||
System.exit(0);
|
System.exit(0);
|
||||||
}
|
},
|
||||||
).setNegativeButton(
|
null, // No cancel button.
|
||||||
" ",
|
str("revanced_check_environment_dialog_ignore_button"), // Neutral button text.
|
||||||
(dialog, which) -> {
|
() -> {
|
||||||
|
// Neutral button action.
|
||||||
// Cleanup data if the user incorrectly imported a huge negative number.
|
// Cleanup data if the user incorrectly imported a huge negative number.
|
||||||
final int current = Math.max(0, BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get());
|
final int current = Math.max(0, BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get());
|
||||||
BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(current + 1);
|
BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(current + 1);
|
||||||
|
},
|
||||||
|
true // Dismiss dialog when onNeutralClick.
|
||||||
|
);
|
||||||
|
|
||||||
dialog.dismiss();
|
// Get the dialog and main layout.
|
||||||
}
|
Dialog dialog = dialogPair.first;
|
||||||
).create();
|
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;
|
boolean hasRun;
|
||||||
@Override
|
@Override
|
||||||
public void onStart(AlertDialog dialog) {
|
public void onStart(Dialog dialog) {
|
||||||
// Only run this once, otherwise if the user changes to a different app
|
// 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.
|
// then changes back, this handler will run again and disable the buttons.
|
||||||
if (hasRun) {
|
if (hasRun) {
|
||||||
@@ -125,19 +151,43 @@ abstract class Check {
|
|||||||
}
|
}
|
||||||
hasRun = true;
|
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);
|
openWebsiteButton.setEnabled(false);
|
||||||
|
ignoreButton.setVisibility(View.INVISIBLE);
|
||||||
|
ignoreButton.setEnabled(false);
|
||||||
|
|
||||||
var dismissButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE);
|
// Start the countdown for showing and enabling buttons.
|
||||||
dismissButton.setEnabled(false);
|
getCountdownRunnable(ignoreButton, openWebsiteButton).run();
|
||||||
|
|
||||||
getCountdownRunnable(dismissButton, openWebsiteButton).run();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, 1000); // Use a delay, so this dialog is shown on top of any other startup dialogs.
|
}, 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() {
|
return new Runnable() {
|
||||||
private int secondsRemaining = SECONDS_BEFORE_SHOWING_IGNORE_BUTTON;
|
private int secondsRemaining = SECONDS_BEFORE_SHOWING_IGNORE_BUTTON;
|
||||||
|
|
||||||
@@ -146,17 +196,15 @@ abstract class Check {
|
|||||||
Utils.verifyOnMainThread();
|
Utils.verifyOnMainThread();
|
||||||
|
|
||||||
if (secondsRemaining > 0) {
|
if (secondsRemaining > 0) {
|
||||||
if (secondsRemaining - SECONDS_BEFORE_SHOWING_WEBSITE_BUTTON == 0) {
|
if (secondsRemaining - SECONDS_BEFORE_SHOWING_WEBSITE_BUTTON <= 0) {
|
||||||
openWebsiteButton.setText(str("revanced_check_environment_dialog_open_official_source_button"));
|
openWebsiteButton.setVisibility(View.VISIBLE);
|
||||||
openWebsiteButton.setEnabled(true);
|
openWebsiteButton.setEnabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
secondsRemaining--;
|
secondsRemaining--;
|
||||||
|
|
||||||
Utils.runOnMainThreadDelayed(this, 1000);
|
Utils.runOnMainThreadDelayed(this, 1000);
|
||||||
} else {
|
} else {
|
||||||
dismissButton.setText(str("revanced_check_environment_dialog_ignore_button"));
|
ignoreButton.setVisibility(View.VISIBLE);
|
||||||
dismissButton.setEnabled(true);
|
ignoreButton.setEnabled(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package app.revanced.extension.shared.fixes.redgifs;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import okhttp3.Interceptor;
|
||||||
|
import okhttp3.MediaType;
|
||||||
|
import okhttp3.Protocol;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
import okhttp3.ResponseBody;
|
||||||
|
|
||||||
|
|
||||||
|
public abstract class BaseFixRedgifsApiPatch implements Interceptor {
|
||||||
|
protected static BaseFixRedgifsApiPatch INSTANCE;
|
||||||
|
public abstract String getDefaultUserAgent();
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Response intercept(@NonNull Chain chain) throws IOException {
|
||||||
|
Request request = chain.request();
|
||||||
|
if (!request.url().host().equals("api.redgifs.com")) {
|
||||||
|
return chain.proceed(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
String userAgent = getDefaultUserAgent();
|
||||||
|
|
||||||
|
if (request.header("Authorization") != null) {
|
||||||
|
Response response = chain.proceed(request.newBuilder().header("User-Agent", userAgent).build());
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
// It's possible that the user agent is being overwritten later down in the interceptor
|
||||||
|
// chain, so make sure we grab the new user agent from the request headers.
|
||||||
|
userAgent = response.request().header("User-Agent");
|
||||||
|
response.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
RedgifsTokenManager.RedgifsToken token = RedgifsTokenManager.refreshToken(userAgent);
|
||||||
|
|
||||||
|
// Emulate response for old OAuth endpoint
|
||||||
|
if (request.url().encodedPath().equals("/v2/oauth/client")) {
|
||||||
|
String responseBody = RedgifsTokenManager.getEmulatedOAuthResponseBody(token);
|
||||||
|
return new Response.Builder()
|
||||||
|
.message("OK")
|
||||||
|
.code(HttpURLConnection.HTTP_OK)
|
||||||
|
.protocol(Protocol.HTTP_1_1)
|
||||||
|
.request(request)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.body(ResponseBody.create(
|
||||||
|
responseBody, MediaType.get("application/json")))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
Request modifiedRequest = request.newBuilder()
|
||||||
|
.header("Authorization", "Bearer " + token.getAccessToken())
|
||||||
|
.header("User-Agent", userAgent)
|
||||||
|
.build();
|
||||||
|
return chain.proceed(modifiedRequest);
|
||||||
|
} catch (JSONException ex) {
|
||||||
|
Logger.printException(() -> "Could not parse Redgifs response", ex);
|
||||||
|
throw new IOException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package app.revanced.extension.shared.fixes.redgifs;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.requests.Route.Method.GET;
|
||||||
|
|
||||||
|
import androidx.annotation.GuardedBy;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.requests.Requester;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages Redgifs token lifecycle.
|
||||||
|
*/
|
||||||
|
public class RedgifsTokenManager {
|
||||||
|
public static class RedgifsToken {
|
||||||
|
// Expire after 23 hours to provide some breathing room
|
||||||
|
private static final long EXPIRY_SECONDS = 23 * 60 * 60;
|
||||||
|
|
||||||
|
private final String accessToken;
|
||||||
|
private final long refreshTimeInSeconds;
|
||||||
|
|
||||||
|
public RedgifsToken(String accessToken, long refreshTime) {
|
||||||
|
this.accessToken = accessToken;
|
||||||
|
this.refreshTimeInSeconds = refreshTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAccessToken() {
|
||||||
|
return accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getExpiryTimeInSeconds() {
|
||||||
|
return refreshTimeInSeconds + EXPIRY_SECONDS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isValid() {
|
||||||
|
if (accessToken == null) return false;
|
||||||
|
return getExpiryTimeInSeconds() >= System.currentTimeMillis() / 1000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public static final String REDGIFS_API_HOST = "https://api.redgifs.com";
|
||||||
|
private static final String GET_TEMPORARY_TOKEN = REDGIFS_API_HOST + "/v2/auth/temporary";
|
||||||
|
@GuardedBy("itself")
|
||||||
|
private static final Map<String, RedgifsToken> tokenMap = new HashMap<>();
|
||||||
|
|
||||||
|
private static String getToken(String userAgent) throws IOException, JSONException {
|
||||||
|
HttpURLConnection connection = (HttpURLConnection) new URL(GET_TEMPORARY_TOKEN).openConnection();
|
||||||
|
connection.setFixedLengthStreamingMode(0);
|
||||||
|
connection.setRequestMethod(GET.name());
|
||||||
|
connection.setRequestProperty("User-Agent", userAgent);
|
||||||
|
connection.setRequestProperty("Content-Type", "application/json");
|
||||||
|
connection.setRequestProperty("Accept", "application/json");
|
||||||
|
connection.setUseCaches(false);
|
||||||
|
|
||||||
|
JSONObject responseObject = Requester.parseJSONObject(connection);
|
||||||
|
return responseObject.getString("token");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RedgifsToken refreshToken(String userAgent) throws IOException, JSONException {
|
||||||
|
synchronized(tokenMap) {
|
||||||
|
// Reference: https://github.com/JeffreyCA/Apollo-ImprovedCustomApi/pull/67
|
||||||
|
RedgifsToken token = tokenMap.get(userAgent);
|
||||||
|
if (token != null && token.isValid()) {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy user agent from original request if present because Redgifs verifies
|
||||||
|
// that the user agent in subsequent requests matches the one in the OAuth token.
|
||||||
|
String accessToken = getToken(userAgent);
|
||||||
|
long refreshTime = System.currentTimeMillis() / 1000;
|
||||||
|
token = new RedgifsToken(accessToken, refreshTime);
|
||||||
|
tokenMap.put(userAgent, token);
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getEmulatedOAuthResponseBody(RedgifsToken token) throws JSONException {
|
||||||
|
// Reference: https://github.com/JeffreyCA/Apollo-ImprovedCustomApi/pull/67
|
||||||
|
JSONObject responseObject = new JSONObject();
|
||||||
|
responseObject.put("access_token", token.accessToken);
|
||||||
|
responseObject.put("expiry_time", token.getExpiryTimeInSeconds() - (System.currentTimeMillis() / 1000));
|
||||||
|
responseObject.put("scope", "read");
|
||||||
|
responseObject.put("token_type", "Bearer");
|
||||||
|
return responseObject.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,7 +52,7 @@ public class Route {
|
|||||||
|
|
||||||
private int countMatches(CharSequence seq, char c) {
|
private int countMatches(CharSequence seq, char c) {
|
||||||
int count = 0;
|
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)
|
if (seq.charAt(i) == c)
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,5 @@ public class BaseSettings {
|
|||||||
public static final BooleanSetting SPOOF_VIDEO_STREAMS_IOS_FORCE_AVC = new BooleanSetting("revanced_spoof_video_streams_ios_force_avc", FALSE, true,
|
public static final BooleanSetting SPOOF_VIDEO_STREAMS_IOS_FORCE_AVC = new BooleanSetting("revanced_spoof_video_streams_ios_force_avc", FALSE, true,
|
||||||
"revanced_spoof_video_streams_ios_force_avc_user_dialog_message", new SpoofiOSAvailability());
|
"revanced_spoof_video_streams_ios_force_avc_user_dialog_message", new SpoofiOSAvailability());
|
||||||
// Client type must be last spoof setting due to cyclic references.
|
// Client type must be last spoof setting due to cyclic references.
|
||||||
public static final EnumSetting<ClientType> SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_video_streams_client_type", ClientType.ANDROID_UNPLUGGED, true, parent(SPOOF_VIDEO_STREAMS));
|
public static final EnumSetting<ClientType> SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_video_streams_client_type", ClientType.ANDROID_VR_1_61_48, true, parent(SPOOF_VIDEO_STREAMS));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,15 +71,20 @@ public class EnumSetting<T extends Enum<?>> extends Setting<T> {
|
|||||||
json.put(importExportKey, value.name().toLowerCase(Locale.ENGLISH));
|
json.put(importExportKey, value.name().toLowerCase(Locale.ENGLISH));
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
/**
|
||||||
private T getEnumFromString(String enumName) {
|
* @param enumName Enum name. Casing does not matter.
|
||||||
|
* @return Enum of this type with the same declared name.
|
||||||
|
* @throws IllegalArgumentException if the name is not a valid enum of this type.
|
||||||
|
*/
|
||||||
|
protected T getEnumFromString(String enumName) {
|
||||||
//noinspection ConstantConditions
|
//noinspection ConstantConditions
|
||||||
for (Enum<?> value : defaultValue.getClass().getEnumConstants()) {
|
for (Enum<?> value : defaultValue.getClass().getEnumConstants()) {
|
||||||
if (value.name().equalsIgnoreCase(enumName)) {
|
if (value.name().equalsIgnoreCase(enumName)) {
|
||||||
// noinspection unchecked
|
//noinspection unchecked
|
||||||
return (T) value;
|
return (T) value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new IllegalArgumentException("Unknown enum value: " + enumName);
|
throw new IllegalArgumentException("Unknown enum value: " + enumName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +108,9 @@ public class EnumSetting<T extends Enum<?>> extends Setting<T> {
|
|||||||
* Availability based on if this setting is currently set to any of the provided types.
|
* Availability based on if this setting is currently set to any of the provided types.
|
||||||
*/
|
*/
|
||||||
@SafeVarargs
|
@SafeVarargs
|
||||||
public final Setting.Availability availability(@NonNull T... types) {
|
public final Setting.Availability availability(T... types) {
|
||||||
|
Objects.requireNonNull(types);
|
||||||
|
|
||||||
return () -> {
|
return () -> {
|
||||||
T currentEnumType = get();
|
T currentEnumType = get();
|
||||||
for (T enumType : types) {
|
for (T enumType : types) {
|
||||||
|
|||||||
@@ -28,16 +28,14 @@ public abstract class Setting<T> {
|
|||||||
/**
|
/**
|
||||||
* Availability based on a single parent setting being enabled.
|
* Availability based on a single parent setting being enabled.
|
||||||
*/
|
*/
|
||||||
@NonNull
|
public static Availability parent(BooleanSetting parent) {
|
||||||
public static Availability parent(@NonNull BooleanSetting parent) {
|
|
||||||
return parent::get;
|
return parent::get;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Availability based on all parents being enabled.
|
* Availability based on all parents being enabled.
|
||||||
*/
|
*/
|
||||||
@NonNull
|
public static Availability parentsAll(BooleanSetting... parents) {
|
||||||
public static Availability parentsAll(@NonNull BooleanSetting... parents) {
|
|
||||||
return () -> {
|
return () -> {
|
||||||
for (BooleanSetting parent : parents) {
|
for (BooleanSetting parent : parents) {
|
||||||
if (!parent.get()) return false;
|
if (!parent.get()) return false;
|
||||||
@@ -49,8 +47,7 @@ public abstract class Setting<T> {
|
|||||||
/**
|
/**
|
||||||
* Availability based on any parent being enabled.
|
* Availability based on any parent being enabled.
|
||||||
*/
|
*/
|
||||||
@NonNull
|
public static Availability parentsAny(BooleanSetting... parents) {
|
||||||
public static Availability parentsAny(@NonNull BooleanSetting... parents) {
|
|
||||||
return () -> {
|
return () -> {
|
||||||
for (BooleanSetting parent : parents) {
|
for (BooleanSetting parent : parents) {
|
||||||
if (parent.get()) return true;
|
if (parent.get()) return true;
|
||||||
@@ -79,7 +76,7 @@ public abstract class Setting<T> {
|
|||||||
/**
|
/**
|
||||||
* Adds a callback for {@link #importFromJSON(Context, String)} and {@link #exportToJson(Context)}.
|
* Adds a callback for {@link #importFromJSON(Context, String)} and {@link #exportToJson(Context)}.
|
||||||
*/
|
*/
|
||||||
public static void addImportExportCallback(@NonNull ImportExportCallback callback) {
|
public static void addImportExportCallback(ImportExportCallback callback) {
|
||||||
importExportCallbacks.add(Objects.requireNonNull(callback));
|
importExportCallbacks.add(Objects.requireNonNull(callback));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,14 +97,13 @@ public abstract class Setting<T> {
|
|||||||
public static final SharedPrefCategory preferences = new SharedPrefCategory("revanced_prefs");
|
public static final SharedPrefCategory preferences = new SharedPrefCategory("revanced_prefs");
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public static Setting<?> getSettingFromPath(@NonNull String str) {
|
public static Setting<?> getSettingFromPath(String str) {
|
||||||
return PATH_TO_SETTINGS.get(str);
|
return PATH_TO_SETTINGS.get(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return All settings that have been created.
|
* @return All settings that have been created.
|
||||||
*/
|
*/
|
||||||
@NonNull
|
|
||||||
public static List<Setting<?>> allLoadedSettings() {
|
public static List<Setting<?>> allLoadedSettings() {
|
||||||
return Collections.unmodifiableList(SETTINGS);
|
return Collections.unmodifiableList(SETTINGS);
|
||||||
}
|
}
|
||||||
@@ -115,7 +111,6 @@ public abstract class Setting<T> {
|
|||||||
/**
|
/**
|
||||||
* @return All settings that have been created, sorted by keys.
|
* @return All settings that have been created, sorted by keys.
|
||||||
*/
|
*/
|
||||||
@NonNull
|
|
||||||
private static List<Setting<?>> allLoadedSettingsSorted() {
|
private static List<Setting<?>> allLoadedSettingsSorted() {
|
||||||
Collections.sort(SETTINGS, (Setting<?> o1, Setting<?> o2) -> o1.key.compareTo(o2.key));
|
Collections.sort(SETTINGS, (Setting<?> o1, Setting<?> o2) -> o1.key.compareTo(o2.key));
|
||||||
return allLoadedSettings();
|
return allLoadedSettings();
|
||||||
@@ -124,13 +119,11 @@ public abstract class Setting<T> {
|
|||||||
/**
|
/**
|
||||||
* The key used to store the value in the shared preferences.
|
* The key used to store the value in the shared preferences.
|
||||||
*/
|
*/
|
||||||
@NonNull
|
|
||||||
public final String key;
|
public final String key;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default value of the setting.
|
* The default value of the setting.
|
||||||
*/
|
*/
|
||||||
@NonNull
|
|
||||||
public final T defaultValue;
|
public final T defaultValue;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -161,7 +154,6 @@ public abstract class Setting<T> {
|
|||||||
/**
|
/**
|
||||||
* The value of the setting.
|
* The value of the setting.
|
||||||
*/
|
*/
|
||||||
@NonNull
|
|
||||||
protected volatile T value;
|
protected volatile T value;
|
||||||
|
|
||||||
public Setting(String key, T defaultValue) {
|
public Setting(String key, T defaultValue) {
|
||||||
@@ -199,8 +191,8 @@ public abstract class Setting<T> {
|
|||||||
* @param userDialogMessage Confirmation message to display, if the user tries to change the setting from the default value.
|
* @param userDialogMessage Confirmation message to display, if the user tries to change the setting from the default value.
|
||||||
* @param availability Condition that must be true, for this setting to be available to configure.
|
* @param availability Condition that must be true, for this setting to be available to configure.
|
||||||
*/
|
*/
|
||||||
public Setting(@NonNull String key,
|
public Setting(String key,
|
||||||
@NonNull T defaultValue,
|
T defaultValue,
|
||||||
boolean rebootApp,
|
boolean rebootApp,
|
||||||
boolean includeWithImportExport,
|
boolean includeWithImportExport,
|
||||||
@Nullable String userDialogMessage,
|
@Nullable String userDialogMessage,
|
||||||
@@ -227,7 +219,7 @@ public abstract class Setting<T> {
|
|||||||
/**
|
/**
|
||||||
* Migrate a setting value if the path is renamed but otherwise the old and new settings are identical.
|
* Migrate a setting value if the path is renamed but otherwise the old and new settings are identical.
|
||||||
*/
|
*/
|
||||||
public static <T> void migrateOldSettingToNew(@NonNull Setting<T> oldSetting, @NonNull Setting<T> newSetting) {
|
public static <T> void migrateOldSettingToNew(Setting<T> oldSetting, Setting<T> newSetting) {
|
||||||
if (oldSetting == newSetting) throw new IllegalArgumentException();
|
if (oldSetting == newSetting) throw new IllegalArgumentException();
|
||||||
|
|
||||||
if (!oldSetting.isSetToDefault()) {
|
if (!oldSetting.isSetToDefault()) {
|
||||||
@@ -243,7 +235,7 @@ public abstract class Setting<T> {
|
|||||||
* This method will be deleted in the future.
|
* This method will be deleted in the future.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("rawtypes")
|
@SuppressWarnings("rawtypes")
|
||||||
public static void migrateFromOldPreferences(@NonNull SharedPrefCategory oldPrefs, @NonNull Setting setting, String settingKey) {
|
public static void migrateFromOldPreferences(SharedPrefCategory oldPrefs, Setting setting, String settingKey) {
|
||||||
if (!oldPrefs.preferences.contains(settingKey)) {
|
if (!oldPrefs.preferences.contains(settingKey)) {
|
||||||
return; // Nothing to do.
|
return; // Nothing to do.
|
||||||
}
|
}
|
||||||
@@ -285,7 +277,7 @@ public abstract class Setting<T> {
|
|||||||
* This intentionally is a static method to deter
|
* This intentionally is a static method to deter
|
||||||
* accidental usage when {@link #save(Object)} was intended.
|
* accidental usage when {@link #save(Object)} was intended.
|
||||||
*/
|
*/
|
||||||
public static void privateSetValueFromString(@NonNull Setting<?> setting, @NonNull String newValue) {
|
public static void privateSetValueFromString(Setting<?> setting, String newValue) {
|
||||||
setting.setValueFromString(newValue);
|
setting.setValueFromString(newValue);
|
||||||
|
|
||||||
// Clear the preference value since default is used, to allow changing
|
// Clear the preference value since default is used, to allow changing
|
||||||
@@ -299,7 +291,7 @@ public abstract class Setting<T> {
|
|||||||
/**
|
/**
|
||||||
* Sets the value of {@link #value}, but do not save to {@link #preferences}.
|
* Sets the value of {@link #value}, but do not save to {@link #preferences}.
|
||||||
*/
|
*/
|
||||||
protected abstract void setValueFromString(@NonNull String newValue);
|
protected abstract void setValueFromString(String newValue);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load and set the value of {@link #value}.
|
* Load and set the value of {@link #value}.
|
||||||
@@ -309,7 +301,7 @@ public abstract class Setting<T> {
|
|||||||
/**
|
/**
|
||||||
* Persistently saves the value.
|
* Persistently saves the value.
|
||||||
*/
|
*/
|
||||||
public final void save(@NonNull T newValue) {
|
public final void save(T newValue) {
|
||||||
if (value.equals(newValue)) {
|
if (value.equals(newValue)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -406,7 +398,6 @@ public abstract class Setting<T> {
|
|||||||
json.put(importExportKey, value);
|
json.put(importExportKey, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public static String exportToJson(@Nullable Context alertDialogContext) {
|
public static String exportToJson(@Nullable Context alertDialogContext) {
|
||||||
try {
|
try {
|
||||||
JSONObject json = new JSONObject();
|
JSONObject json = new JSONObject();
|
||||||
@@ -445,7 +436,7 @@ public abstract class Setting<T> {
|
|||||||
/**
|
/**
|
||||||
* @return if any settings that require a reboot were changed.
|
* @return if any settings that require a reboot were changed.
|
||||||
*/
|
*/
|
||||||
public static boolean importFromJSON(@NonNull Context alertDialogContext, @NonNull String settingsJsonString) {
|
public static boolean importFromJSON(Context alertDialogContext, String settingsJsonString) {
|
||||||
try {
|
try {
|
||||||
if (!settingsJsonString.matches("[\\s\\S]*\\{")) {
|
if (!settingsJsonString.matches("[\\s\\S]*\\{")) {
|
||||||
settingsJsonString = '{' + settingsJsonString + '}'; // Restore outer JSON braces
|
settingsJsonString = '{' + settingsJsonString + '}'; // Restore outer JSON braces
|
||||||
|
|||||||
@@ -3,12 +3,20 @@ package app.revanced.extension.shared.settings.preference;
|
|||||||
import static app.revanced.extension.shared.StringRef.str;
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.AlertDialog;
|
import android.app.Dialog;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.os.Bundle;
|
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.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
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.
|
* Set by subclasses if Strings cannot be added as a resource.
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
protected static String restartDialogButtonText, restartDialogTitle, confirmDialogTitle;
|
protected static String restartDialogButtonText, restartDialogTitle, confirmDialogTitle, restartDialogMessage;
|
||||||
|
|
||||||
private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
|
private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
|
||||||
try {
|
try {
|
||||||
@@ -76,7 +84,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
|||||||
|
|
||||||
updatingPreference = true;
|
updatingPreference = true;
|
||||||
// Apply 'Setting <- Preference', unless during importing when it needs to be 'Setting -> Preference'.
|
// 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);
|
updatePreference(pref, setting, true, settingImportInProgress);
|
||||||
// Update any other preference availability that may now be different.
|
// Update any other preference availability that may now be different.
|
||||||
updateUIAvailability();
|
updateUIAvailability();
|
||||||
@@ -116,11 +124,14 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
|||||||
|
|
||||||
showingUserDialogMessage = true;
|
showingUserDialogMessage = true;
|
||||||
|
|
||||||
new AlertDialog.Builder(context)
|
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
||||||
.setTitle(confirmDialogTitle)
|
context,
|
||||||
.setMessage(Objects.requireNonNull(setting.userDialogMessage).toString())
|
confirmDialogTitle, // Title.
|
||||||
.setPositiveButton(android.R.string.ok, (dialog, id) -> {
|
Objects.requireNonNull(setting.userDialogMessage).toString(), // No message.
|
||||||
// User confirmed, save to the Setting.
|
null, // No EditText.
|
||||||
|
null, // OK button text.
|
||||||
|
() -> {
|
||||||
|
// OK button action. User confirmed, save to the Setting.
|
||||||
updatePreference(pref, setting, true, false);
|
updatePreference(pref, setting, true, false);
|
||||||
|
|
||||||
// Update availability of other preferences that may be changed.
|
// Update availability of other preferences that may be changed.
|
||||||
@@ -129,23 +140,27 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
|||||||
if (setting.rebootApp) {
|
if (setting.rebootApp) {
|
||||||
showRestartDialog(context);
|
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);
|
updatePreference(pref, setting, true, true);
|
||||||
})
|
},
|
||||||
.setOnDismissListener(dialog -> {
|
null, // No Neutral button.
|
||||||
showingUserDialogMessage = false;
|
null, // No Neutral button action.
|
||||||
})
|
true // Dismiss dialog when onNeutralClick.
|
||||||
.setCancelable(false)
|
);
|
||||||
.show();
|
|
||||||
|
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}.
|
* Updates all Preferences values and their availability using the current values in {@link Setting}.
|
||||||
*/
|
*/
|
||||||
protected void updateUIToSettingValues() {
|
protected void updateUIToSettingValues() {
|
||||||
updatePreferenceScreen(getPreferenceScreen(), true,true);
|
updatePreferenceScreen(getPreferenceScreen(), true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -280,17 +295,27 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
|||||||
if (restartDialogTitle == null) {
|
if (restartDialogTitle == null) {
|
||||||
restartDialogTitle = str("revanced_settings_restart_title");
|
restartDialogTitle = str("revanced_settings_restart_title");
|
||||||
}
|
}
|
||||||
|
if (restartDialogMessage == null) {
|
||||||
|
restartDialogMessage = str("revanced_settings_restart_dialog_message");
|
||||||
|
}
|
||||||
if (restartDialogButtonText == null) {
|
if (restartDialogButtonText == null) {
|
||||||
restartDialogButtonText = str("revanced_settings_restart");
|
restartDialogButtonText = str("revanced_settings_restart");
|
||||||
}
|
}
|
||||||
|
|
||||||
new AlertDialog.Builder(context)
|
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(context,
|
||||||
.setMessage(restartDialogTitle)
|
restartDialogTitle, // Title.
|
||||||
.setPositiveButton(restartDialogButtonText, (dialog, id)
|
restartDialogMessage, // Message.
|
||||||
-> Utils.restartApp(context))
|
null, // No EditText.
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
restartDialogButtonText, // OK button text.
|
||||||
.setCancelable(false)
|
() -> Utils.restartApp(context), // OK button action.
|
||||||
.show();
|
() -> {}, // 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")
|
@SuppressLint("ResourceType")
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ package app.revanced.extension.shared.settings.preference;
|
|||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
import static app.revanced.extension.shared.Utils.getResourceIdentifier;
|
import static app.revanced.extension.shared.Utils.getResourceIdentifier;
|
||||||
|
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||||
|
|
||||||
import android.app.AlertDialog;
|
import android.app.Dialog;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.graphics.Typeface;
|
import android.graphics.Typeface;
|
||||||
@@ -18,14 +19,12 @@ import android.text.TextWatcher;
|
|||||||
import android.text.style.ForegroundColorSpan;
|
import android.text.style.ForegroundColorSpan;
|
||||||
import android.text.style.RelativeSizeSpan;
|
import android.text.style.RelativeSizeSpan;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
import android.util.Pair;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.ViewParent;
|
import android.view.ViewParent;
|
||||||
import android.widget.Button;
|
import android.widget.*;
|
||||||
import android.widget.EditText;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.ColorInt;
|
import androidx.annotation.ColorInt;
|
||||||
|
|
||||||
@@ -216,86 +215,6 @@ public class ColorPickerPreference extends EditTextPreference {
|
|||||||
widgetColorDot.setAlpha(isEnabled() ? 1.0f : DISABLED_ALPHA);
|
widgetColorDot.setAlpha(isEnabled() ? 1.0f : DISABLED_ALPHA);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a layout with a color preview and EditText for hex color input.
|
|
||||||
*
|
|
||||||
* @param context The context for creating the layout.
|
|
||||||
* @return A LinearLayout containing the color preview and EditText.
|
|
||||||
*/
|
|
||||||
private LinearLayout createDialogLayout(Context context) {
|
|
||||||
LinearLayout layout = new LinearLayout(context);
|
|
||||||
layout.setOrientation(LinearLayout.VERTICAL);
|
|
||||||
layout.setPadding(70, 0, 70, 0);
|
|
||||||
|
|
||||||
// Inflate color picker.
|
|
||||||
View colorPicker = LayoutInflater.from(context).inflate(
|
|
||||||
getResourceIdentifier("revanced_color_picker", "layout"), null);
|
|
||||||
dialogColorPickerView = colorPicker.findViewById(
|
|
||||||
getResourceIdentifier("color_picker_view", "id"));
|
|
||||||
dialogColorPickerView.setColor(currentColor);
|
|
||||||
layout.addView(colorPicker);
|
|
||||||
|
|
||||||
// Horizontal layout for preview and EditText.
|
|
||||||
LinearLayout inputLayout = new LinearLayout(context);
|
|
||||||
inputLayout.setOrientation(LinearLayout.HORIZONTAL);
|
|
||||||
inputLayout.setPadding(0, 20, 0, 0);
|
|
||||||
|
|
||||||
dialogColorPreview = new TextView(context);
|
|
||||||
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);
|
|
||||||
|
|
||||||
layout.addView(inputLayout);
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
});
|
|
||||||
|
|
||||||
return layout;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the color preview TextView with a colored dot.
|
* Updates the color preview TextView with a colored dot.
|
||||||
*/
|
*/
|
||||||
@@ -360,65 +279,153 @@ public class ColorPickerPreference extends EditTextPreference {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepares the dialog builder with a custom view and reset button.
|
* Creates a Dialog with a color preview and EditText for hex color input.
|
||||||
*
|
|
||||||
* @param builder The AlertDialog.Builder to configure.
|
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
protected void showDialog(Bundle state) {
|
||||||
Utils.setEditTextDialogTheme(builder);
|
Context context = getContext();
|
||||||
LinearLayout dialogLayout = createDialogLayout(builder.getContext());
|
|
||||||
builder.setView(dialogLayout);
|
|
||||||
final int originalColor = currentColor;
|
|
||||||
|
|
||||||
builder.setNeutralButton(str("revanced_settings_reset_color"), null);
|
// 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);
|
||||||
|
|
||||||
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
// 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 {
|
try {
|
||||||
String colorString = getEditText().getText().toString();
|
String colorString = editText.getText().toString();
|
||||||
|
|
||||||
if (colorString.length() != COLOR_STRING_LENGTH) {
|
if (colorString.length() != COLOR_STRING_LENGTH) {
|
||||||
Utils.showToastShort(str("revanced_settings_color_invalid"));
|
Utils.showToastShort(str("revanced_settings_color_invalid"));
|
||||||
setText(getColorString(originalColor));
|
setText(getColorString(originalColor));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setText(colorString);
|
setText(colorString);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
// Should never happen due to a bad color string,
|
// Should never happen due to a bad color string,
|
||||||
// since the text is validated and fixed while the user types.
|
// since the text is validated and fixed while the user types.
|
||||||
Logger.printException(() -> "setPositiveButton failure", ex);
|
Logger.printException(() -> "OK button failure", ex);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
() -> {
|
||||||
builder.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
|
// Cancel button action.
|
||||||
try {
|
try {
|
||||||
// Restore the original color.
|
// Restore the original color.
|
||||||
setText(getColorString(originalColor));
|
setText(getColorString(originalColor));
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "setNegativeButton failure", ex);
|
Logger.printException(() -> "Cancel button failure", ex);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
}
|
str("revanced_settings_reset_color"), // Neutral button text.
|
||||||
|
() -> {
|
||||||
@Override
|
// Neutral button action.
|
||||||
protected void showDialog(Bundle state) {
|
|
||||||
super.showDialog(state);
|
|
||||||
|
|
||||||
AlertDialog dialog = (AlertDialog) getDialog();
|
|
||||||
dialog.setCanceledOnTouchOutside(false);
|
|
||||||
|
|
||||||
// Do not close dialog when reset is pressed.
|
|
||||||
Button button = dialog.getButton(AlertDialog.BUTTON_NEUTRAL);
|
|
||||||
button.setOnClickListener(view -> {
|
|
||||||
try {
|
try {
|
||||||
final int defaultColor = Color.parseColor(colorSetting.defaultValue) & 0x00FFFFFF;
|
final int defaultColor = Color.parseColor(colorSetting.defaultValue) & 0x00FFFFFF;
|
||||||
// Setting view color causes listener callback into this class.
|
// Setting view color causes listener callback into this class.
|
||||||
dialogColorPickerView.setColor(defaultColor);
|
dialogColorPickerView.setColor(defaultColor);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "setOnClickListener failure", 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
|
@Override
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ import app.revanced.extension.shared.Utils;
|
|||||||
* <p>
|
* <p>
|
||||||
* This view displays two main components for color selection:
|
* This view displays two main components for color selection:
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li><b>Hue Bar:</b> A vertical bar on the right that allows the user to select the hue component of the color.
|
* <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 that allows the user to select the saturation and value (brightness)
|
* <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.
|
* components of the color based on the selected hue.
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
@@ -63,7 +63,7 @@ public class ColorPickerView extends View {
|
|||||||
|
|
||||||
private static final float MARGIN_BETWEEN_AREAS = dipToPixels(24);
|
private static final float MARGIN_BETWEEN_AREAS = dipToPixels(24);
|
||||||
private static final float VIEW_PADDING = dipToPixels(16);
|
private static final float VIEW_PADDING = dipToPixels(16);
|
||||||
private static final float HUE_BAR_WIDTH = dipToPixels(12);
|
private static final float HUE_BAR_HEIGHT = dipToPixels(12);
|
||||||
private static final float HUE_CORNER_RADIUS = dipToPixels(6);
|
private static final float HUE_CORNER_RADIUS = dipToPixels(6);
|
||||||
private static final float SELECTOR_RADIUS = dipToPixels(12);
|
private static final float SELECTOR_RADIUS = dipToPixels(12);
|
||||||
private static final float SELECTOR_STROKE_WIDTH = 8;
|
private static final float SELECTOR_STROKE_WIDTH = 8;
|
||||||
@@ -144,17 +144,17 @@ public class ColorPickerView extends View {
|
|||||||
final float DESIRED_ASPECT_RATIO = 0.8f; // height = width * 0.8
|
final float DESIRED_ASPECT_RATIO = 0.8f; // height = width * 0.8
|
||||||
|
|
||||||
final int minWidth = Utils.dipToPixels(250);
|
final int minWidth = Utils.dipToPixels(250);
|
||||||
final int minHeight = (int) (minWidth * DESIRED_ASPECT_RATIO);
|
final int minHeight = (int) (minWidth * DESIRED_ASPECT_RATIO) + (int) (HUE_BAR_HEIGHT + MARGIN_BETWEEN_AREAS);
|
||||||
|
|
||||||
int width = resolveSize(minWidth, widthMeasureSpec);
|
int width = resolveSize(minWidth, widthMeasureSpec);
|
||||||
int height = resolveSize(minHeight, heightMeasureSpec);
|
int height = resolveSize(minHeight, heightMeasureSpec);
|
||||||
|
|
||||||
// Ensure minimum dimensions for usability
|
// Ensure minimum dimensions for usability.
|
||||||
width = Math.max(width, minWidth);
|
width = Math.max(width, minWidth);
|
||||||
height = Math.max(height, minHeight);
|
height = Math.max(height, minHeight);
|
||||||
|
|
||||||
// Adjust height to maintain desired aspect ratio if possible
|
// Adjust height to maintain desired aspect ratio if possible.
|
||||||
final int desiredHeight = (int) (width * DESIRED_ASPECT_RATIO);
|
final int desiredHeight = (int) (width * DESIRED_ASPECT_RATIO) + (int) (HUE_BAR_HEIGHT + MARGIN_BETWEEN_AREAS);
|
||||||
if (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY) {
|
if (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY) {
|
||||||
height = desiredHeight;
|
height = desiredHeight;
|
||||||
}
|
}
|
||||||
@@ -171,22 +171,22 @@ public class ColorPickerView extends View {
|
|||||||
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
|
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
|
||||||
super.onSizeChanged(width, height, oldWidth, oldHeight);
|
super.onSizeChanged(width, height, oldWidth, oldHeight);
|
||||||
|
|
||||||
// Calculate bounds with hue bar on the right
|
// Calculate bounds with hue bar at the bottom.
|
||||||
final float effectiveWidth = width - (2 * VIEW_PADDING);
|
final float effectiveWidth = width - (2 * VIEW_PADDING);
|
||||||
final float selectorWidth = effectiveWidth - HUE_BAR_WIDTH - MARGIN_BETWEEN_AREAS;
|
final float effectiveHeight = height - (2 * VIEW_PADDING) - HUE_BAR_HEIGHT - MARGIN_BETWEEN_AREAS;
|
||||||
|
|
||||||
// Adjust rectangles to account for padding and density-independent dimensions
|
// Adjust rectangles to account for padding and density-independent dimensions.
|
||||||
saturationValueRect.set(
|
saturationValueRect.set(
|
||||||
VIEW_PADDING,
|
VIEW_PADDING,
|
||||||
VIEW_PADDING,
|
VIEW_PADDING,
|
||||||
VIEW_PADDING + selectorWidth,
|
VIEW_PADDING + effectiveWidth,
|
||||||
height - VIEW_PADDING
|
VIEW_PADDING + effectiveHeight
|
||||||
);
|
);
|
||||||
|
|
||||||
hueRect.set(
|
hueRect.set(
|
||||||
width - VIEW_PADDING - HUE_BAR_WIDTH,
|
|
||||||
VIEW_PADDING,
|
VIEW_PADDING,
|
||||||
width - VIEW_PADDING,
|
height - VIEW_PADDING - HUE_BAR_HEIGHT,
|
||||||
|
VIEW_PADDING + effectiveWidth,
|
||||||
height - VIEW_PADDING
|
height - VIEW_PADDING
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -201,7 +201,7 @@ public class ColorPickerView extends View {
|
|||||||
private void updateHueShader() {
|
private void updateHueShader() {
|
||||||
LinearGradient hueShader = new LinearGradient(
|
LinearGradient hueShader = new LinearGradient(
|
||||||
hueRect.left, hueRect.top,
|
hueRect.left, hueRect.top,
|
||||||
hueRect.left, hueRect.bottom,
|
hueRect.right, hueRect.top,
|
||||||
HUE_COLORS,
|
HUE_COLORS,
|
||||||
null,
|
null,
|
||||||
Shader.TileMode.CLAMP
|
Shader.TileMode.CLAMP
|
||||||
@@ -263,8 +263,8 @@ public class ColorPickerView extends View {
|
|||||||
// Draw the hue bar.
|
// Draw the hue bar.
|
||||||
canvas.drawRoundRect(hueRect, HUE_CORNER_RADIUS, HUE_CORNER_RADIUS, huePaint);
|
canvas.drawRoundRect(hueRect, HUE_CORNER_RADIUS, HUE_CORNER_RADIUS, huePaint);
|
||||||
|
|
||||||
final float hueSelectorX = hueRect.centerX();
|
final float hueSelectorX = hueRect.left + (hue / 360f) * hueRect.width();
|
||||||
final float hueSelectorY = hueRect.top + (hue / 360f) * hueRect.height();
|
final float hueSelectorY = hueRect.centerY();
|
||||||
|
|
||||||
final float satSelectorX = saturationValueRect.left + saturation * saturationValueRect.width();
|
final float satSelectorX = saturationValueRect.left + saturation * saturationValueRect.width();
|
||||||
final float satSelectorY = saturationValueRect.top + (1 - value) * saturationValueRect.height();
|
final float satSelectorY = saturationValueRect.top + (1 - value) * saturationValueRect.height();
|
||||||
@@ -316,17 +316,17 @@ public class ColorPickerView extends View {
|
|||||||
|
|
||||||
// Define touch expansion for the hue bar.
|
// Define touch expansion for the hue bar.
|
||||||
RectF expandedHueRect = new RectF(
|
RectF expandedHueRect = new RectF(
|
||||||
hueRect.left - TOUCH_EXPANSION,
|
hueRect.left,
|
||||||
hueRect.top,
|
hueRect.top - TOUCH_EXPANSION,
|
||||||
hueRect.right + TOUCH_EXPANSION,
|
hueRect.right,
|
||||||
hueRect.bottom
|
hueRect.bottom + TOUCH_EXPANSION
|
||||||
);
|
);
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case MotionEvent.ACTION_DOWN:
|
case MotionEvent.ACTION_DOWN:
|
||||||
// Calculate current handle positions.
|
// Calculate current handle positions.
|
||||||
final float hueSelectorX = hueRect.centerX();
|
final float hueSelectorX = hueRect.left + (hue / 360f) * hueRect.width();
|
||||||
final float hueSelectorY = hueRect.top + (hue / 360f) * hueRect.height();
|
final float hueSelectorY = hueRect.centerY();
|
||||||
|
|
||||||
final float satSelectorX = saturationValueRect.left + saturation * saturationValueRect.width();
|
final float satSelectorX = saturationValueRect.left + saturation * saturationValueRect.width();
|
||||||
final float valSelectorY = saturationValueRect.top + (1 - value) * saturationValueRect.height();
|
final float valSelectorY = saturationValueRect.top + (1 - value) * saturationValueRect.height();
|
||||||
@@ -348,14 +348,14 @@ public class ColorPickerView extends View {
|
|||||||
// Check if the touch started on a handle or within the expanded hue bar area.
|
// Check if the touch started on a handle or within the expanded hue bar area.
|
||||||
if (hueHitRect.contains(x, y)) {
|
if (hueHitRect.contains(x, y)) {
|
||||||
isDraggingHue = true;
|
isDraggingHue = true;
|
||||||
updateHueFromTouch(y);
|
updateHueFromTouch(x);
|
||||||
} else if (satValHitRect.contains(x, y)) {
|
} else if (satValHitRect.contains(x, y)) {
|
||||||
isDraggingSaturation = true;
|
isDraggingSaturation = true;
|
||||||
updateSaturationValueFromTouch(x, y);
|
updateSaturationValueFromTouch(x, y);
|
||||||
} else if (expandedHueRect.contains(x, y)) {
|
} else if (expandedHueRect.contains(x, y)) {
|
||||||
// Handle touch within the expanded hue bar area.
|
// Handle touch within the expanded hue bar area.
|
||||||
isDraggingHue = true;
|
isDraggingHue = true;
|
||||||
updateHueFromTouch(y);
|
updateHueFromTouch(x);
|
||||||
} else if (saturationValueRect.contains(x, y)) {
|
} else if (saturationValueRect.contains(x, y)) {
|
||||||
isDraggingSaturation = true;
|
isDraggingSaturation = true;
|
||||||
updateSaturationValueFromTouch(x, y);
|
updateSaturationValueFromTouch(x, y);
|
||||||
@@ -365,7 +365,7 @@ public class ColorPickerView extends View {
|
|||||||
case MotionEvent.ACTION_MOVE:
|
case MotionEvent.ACTION_MOVE:
|
||||||
// Continue updating values even if touch moves outside the view.
|
// Continue updating values even if touch moves outside the view.
|
||||||
if (isDraggingHue) {
|
if (isDraggingHue) {
|
||||||
updateHueFromTouch(y);
|
updateHueFromTouch(x);
|
||||||
} else if (isDraggingSaturation) {
|
} else if (isDraggingSaturation) {
|
||||||
updateSaturationValueFromTouch(x, y);
|
updateSaturationValueFromTouch(x, y);
|
||||||
}
|
}
|
||||||
@@ -387,12 +387,12 @@ public class ColorPickerView extends View {
|
|||||||
/**
|
/**
|
||||||
* Updates the hue value based on touch position, clamping to valid range.
|
* Updates the hue value based on touch position, clamping to valid range.
|
||||||
*
|
*
|
||||||
* @param y The y-coordinate of the touch position.
|
* @param x The x-coordinate of the touch position.
|
||||||
*/
|
*/
|
||||||
private void updateHueFromTouch(float y) {
|
private void updateHueFromTouch(float x) {
|
||||||
// Clamp y to the hue rectangle bounds.
|
// Clamp x to the hue rectangle bounds.
|
||||||
final float clampedY = Utils.clamp(y, hueRect.top, hueRect.bottom);
|
final float clampedX = Utils.clamp(x, hueRect.left, hueRect.right);
|
||||||
final float updatedHue = ((clampedY - hueRect.top) / hueRect.height()) * 360f;
|
final float updatedHue = ((clampedX - hueRect.left) / hueRect.width()) * 360f;
|
||||||
if (hue == updatedHue) {
|
if (hue == updatedHue) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
package app.revanced.extension.shared.settings.preference;
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
public 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;
|
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.content.Context;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
import android.preference.EditTextPreference;
|
import android.preference.EditTextPreference;
|
||||||
import android.preference.Preference;
|
import android.preference.Preference;
|
||||||
import android.text.InputType;
|
import android.text.InputType;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
import android.util.Pair;
|
||||||
import android.util.TypedValue;
|
import android.util.TypedValue;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
import android.widget.EditText;
|
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.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.shared.settings.Setting;
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
|
||||||
|
|
||||||
@SuppressWarnings({"unused", "deprecation"})
|
@SuppressWarnings({"unused", "deprecation"})
|
||||||
public class ImportExportPreference extends EditTextPreference implements Preference.OnPreferenceClickListener {
|
public class ImportExportPreference extends EditTextPreference implements Preference.OnPreferenceClickListener {
|
||||||
@@ -54,7 +65,8 @@ public class ImportExportPreference extends EditTextPreference implements Prefer
|
|||||||
@Override
|
@Override
|
||||||
public boolean onPreferenceClick(Preference preference) {
|
public boolean onPreferenceClick(Preference preference) {
|
||||||
try {
|
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());
|
existingSettings = Setting.exportToJson(getContext());
|
||||||
getEditText().setText(existingSettings);
|
getEditText().setText(existingSettings);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
@@ -64,18 +76,32 @@ public class ImportExportPreference extends EditTextPreference implements Prefer
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
protected void showDialog(Bundle state) {
|
||||||
try {
|
try {
|
||||||
Utils.setEditTextDialogTheme(builder);
|
Context context = getContext();
|
||||||
|
EditText editText = getEditText();
|
||||||
|
|
||||||
// Show the user the settings in JSON format.
|
// Create a custom dialog with the EditText.
|
||||||
builder.setNeutralButton(str("revanced_settings_import_copy"), (dialog, which) -> {
|
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
||||||
Utils.setClipboard(getEditText().getText());
|
context,
|
||||||
}).setPositiveButton(str("revanced_settings_import"), (dialog, which) -> {
|
str("revanced_pref_import_export_title"), // Title.
|
||||||
importSettings(builder.getContext(), getEditText().getText().toString());
|
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) {
|
} 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);
|
final boolean rebootNeeded = Setting.importFromJSON(context, replacementSettings);
|
||||||
if (rebootNeeded) {
|
if (rebootNeeded) {
|
||||||
AbstractPreferenceFragment.showRestartDialog(getContext());
|
AbstractPreferenceFragment.showRestartDialog(context);
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "importSettings failure", ex);
|
Logger.printException(() -> "importSettings failure", ex);
|
||||||
@@ -96,5 +122,4 @@ public class ImportExportPreference extends EditTextPreference implements Prefer
|
|||||||
AbstractPreferenceFragment.settingImportInProgress = false;
|
AbstractPreferenceFragment.settingImportInProgress = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package app.revanced.extension.shared.settings.preference;
|
package app.revanced.extension.shared.settings.preference;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
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 static app.revanced.extension.shared.requests.Route.Method.GET;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
@@ -8,16 +9,19 @@ import android.app.Dialog;
|
|||||||
import android.app.ProgressDialog;
|
import android.app.ProgressDialog;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.graphics.Color;
|
import android.graphics.drawable.ShapeDrawable;
|
||||||
|
import android.graphics.drawable.shapes.RoundRectShape;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.preference.Preference;
|
import android.preference.Preference;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
import android.view.View;
|
||||||
import android.view.Window;
|
import android.view.Window;
|
||||||
import android.webkit.WebView;
|
import android.webkit.WebView;
|
||||||
import android.webkit.WebViewClient;
|
import android.webkit.WebViewClient;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
@@ -48,28 +52,6 @@ public class ReVancedAboutPreference extends Preference {
|
|||||||
return text.replace("-", "‑"); // #8209 = non breaking hyphen.
|
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
* Apps that do not support bundling resources must override this.
|
||||||
*
|
*
|
||||||
@@ -86,9 +68,8 @@ public class ReVancedAboutPreference extends Preference {
|
|||||||
builder.append("<html>");
|
builder.append("<html>");
|
||||||
builder.append("<body style=\"text-align: center; padding: 10px;\">");
|
builder.append("<body style=\"text-align: center; padding: 10px;\">");
|
||||||
|
|
||||||
final boolean isDarkMode = isDarkModeEnabled();
|
String foregroundColorHex = Utils.getColorHexString(Utils.getAppForegroundColor());
|
||||||
String backgroundColorHex = getColorHexString(isDarkMode ? getDarkColor() : getLightColor());
|
String backgroundColorHex = Utils.getColorHexString(Utils.getDialogBackgroundColor());
|
||||||
String foregroundColorHex = getColorHexString(isDarkMode ? getLightColor() : getDarkColor());
|
|
||||||
// Apply light/dark mode colors.
|
// Apply light/dark mode colors.
|
||||||
builder.append(String.format(
|
builder.append(String.format(
|
||||||
"<style> body { background-color: %s; color: %s; } a { color: %s; } </style>",
|
"<style> body { background-color: %s; color: %s; } a { color: %s; } </style>",
|
||||||
@@ -220,14 +201,38 @@ class WebViewDialog extends Dialog {
|
|||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(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 webView = new WebView(getContext());
|
||||||
|
webView.setVerticalScrollBarEnabled(false); // Disable the vertical scrollbar.
|
||||||
|
webView.setOverScrollMode(View.OVER_SCROLL_NEVER);
|
||||||
webView.getSettings().setJavaScriptEnabled(true);
|
webView.getSettings().setJavaScriptEnabled(true);
|
||||||
webView.setWebViewClient(new OpenLinksExternallyWebClient());
|
webView.setWebViewClient(new OpenLinksExternallyWebClient());
|
||||||
webView.loadDataWithBaseURL(null, htmlContent, "text/html", "utf-8", null);
|
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 {
|
private class OpenLinksExternallyWebClient extends WebViewClient {
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ package app.revanced.extension.shared.settings.preference;
|
|||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
|
||||||
import android.app.AlertDialog;
|
import android.app.Dialog;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.EditTextPreference;
|
import android.preference.EditTextPreference;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.widget.Button;
|
import android.util.Pair;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
@@ -45,10 +46,12 @@ public class ResettableEditTextPreference extends EditTextPreference {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
protected void showDialog(Bundle state) {
|
||||||
super.onPrepareDialogBuilder(builder);
|
try {
|
||||||
Utils.setEditTextDialogTheme(builder);
|
Context context = getContext();
|
||||||
|
EditText editText = getEditText();
|
||||||
|
|
||||||
|
// Resolve setting if not already set.
|
||||||
if (setting == null) {
|
if (setting == null) {
|
||||||
String key = getKey();
|
String key = getKey();
|
||||||
if (key != null) {
|
if (key != null) {
|
||||||
@@ -56,29 +59,47 @@ public class ResettableEditTextPreference extends EditTextPreference {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
if (setting != null) {
|
||||||
builder.setNeutralButton(str("revanced_settings_reset"), null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void showDialog(Bundle state) {
|
|
||||||
super.showDialog(state);
|
|
||||||
|
|
||||||
// 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 {
|
try {
|
||||||
String defaultStringValue = Objects.requireNonNull(setting).defaultValue.toString();
|
String defaultStringValue = Objects.requireNonNull(setting).defaultValue.toString();
|
||||||
EditText editText = getEditText();
|
|
||||||
editText.setText(defaultStringValue);
|
editText.setText(defaultStringValue);
|
||||||
editText.setSelection(defaultStringValue.length()); // move cursor to end of text
|
editText.setSelection(defaultStringValue.length()); // Move cursor to end of text.
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "reset failure", 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package app.revanced.extension.shared.settings.preference;
|
package app.revanced.extension.shared.settings.preference;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.preference.ListPreference;
|
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
|
|
||||||
@@ -24,12 +23,14 @@ import app.revanced.extension.shared.Utils;
|
|||||||
* it needs to subclass this preference and override {@link #getFirstEntriesToPreserve}.
|
* it needs to subclass this preference and override {@link #getFirstEntriesToPreserve}.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings({"unused", "deprecation"})
|
@SuppressWarnings({"unused", "deprecation"})
|
||||||
public class SortedListPreference extends ListPreference {
|
public class SortedListPreference extends CustomDialogListPreference {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sorts the current list entries.
|
* Sorts the current list entries.
|
||||||
*
|
*
|
||||||
* @param firstEntriesToPreserve The number of entries to preserve in their original position.
|
* @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) {
|
public void sortEntryAndValues(int firstEntriesToPreserve) {
|
||||||
CharSequence[] entries = getEntries();
|
CharSequence[] entries = getEntries();
|
||||||
@@ -44,6 +45,10 @@ public class SortedListPreference extends ListPreference {
|
|||||||
throw new IllegalStateException();
|
throw new IllegalStateException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (firstEntriesToPreserve < 0) {
|
||||||
|
return; // Nothing to do.
|
||||||
|
}
|
||||||
|
|
||||||
List<Pair<CharSequence, CharSequence>> firstEntries = new ArrayList<>(firstEntriesToPreserve);
|
List<Pair<CharSequence, CharSequence>> firstEntries = new ArrayList<>(firstEntriesToPreserve);
|
||||||
|
|
||||||
// Android does not have a triple class like Kotlin, So instead use a nested pair.
|
// Android does not have a triple class like Kotlin, So instead use a nested pair.
|
||||||
@@ -85,10 +90,6 @@ public class SortedListPreference extends ListPreference {
|
|||||||
super.setEntryValues(sortedEntryValues);
|
super.setEntryValues(sortedEntryValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected int getFirstEntriesToPreserve() {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public SortedListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
public SortedListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
super(context, attrs, defStyleAttr, defStyleRes);
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
|
||||||
@@ -112,4 +113,12 @@ public class SortedListPreference extends ListPreference {
|
|||||||
|
|
||||||
sortEntryAndValues(getFirstEntriesToPreserve());
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import app.revanced.extension.shared.settings.BaseSettings;
|
|||||||
|
|
||||||
public enum ClientType {
|
public enum ClientType {
|
||||||
// https://dumps.tadiphone.dev/dumps/oculus/eureka
|
// https://dumps.tadiphone.dev/dumps/oculus/eureka
|
||||||
ANDROID_VR_NO_AUTH(
|
ANDROID_VR_1_61_48(
|
||||||
28,
|
28,
|
||||||
"ANDROID_VR",
|
"ANDROID_VR",
|
||||||
"com.google.android.apps.youtube.vr.oculus",
|
"com.google.android.apps.youtube.vr.oculus",
|
||||||
@@ -27,7 +27,7 @@ public enum ClientType {
|
|||||||
"1.61.48",
|
"1.61.48",
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
"Android VR No auth"
|
"Android VR 1.61"
|
||||||
),
|
),
|
||||||
// Chromecast with Google TV 4K.
|
// Chromecast with Google TV 4K.
|
||||||
// https://dumps.tadiphone.dev/dumps/google/kirkwood
|
// https://dumps.tadiphone.dev/dumps/google/kirkwood
|
||||||
@@ -97,21 +97,25 @@ public enum ClientType {
|
|||||||
? "iOS TV Force AVC"
|
? "iOS TV Force AVC"
|
||||||
: "iOS TV"
|
: "iOS TV"
|
||||||
),
|
),
|
||||||
ANDROID_VR_AUTH(
|
/**
|
||||||
ANDROID_VR_NO_AUTH.id,
|
* Uses non adaptive bitrate, which fixes audio stuttering with YT Music.
|
||||||
ANDROID_VR_NO_AUTH.clientName,
|
* Uses VP9 and not AV1.
|
||||||
ANDROID_VR_NO_AUTH.packageName,
|
*/
|
||||||
ANDROID_VR_NO_AUTH.deviceMake,
|
ANDROID_VR_1_43_32(
|
||||||
ANDROID_VR_NO_AUTH.deviceModel,
|
ANDROID_VR_1_61_48.id,
|
||||||
ANDROID_VR_NO_AUTH.osName,
|
ANDROID_VR_1_61_48.clientName,
|
||||||
ANDROID_VR_NO_AUTH.osVersion,
|
ANDROID_VR_1_61_48.packageName,
|
||||||
ANDROID_VR_NO_AUTH.androidSdkVersion,
|
ANDROID_VR_1_61_48.deviceMake,
|
||||||
ANDROID_VR_NO_AUTH.buildId,
|
ANDROID_VR_1_61_48.deviceModel,
|
||||||
ANDROID_VR_NO_AUTH.cronetVersion,
|
ANDROID_VR_1_61_48.osName,
|
||||||
ANDROID_VR_NO_AUTH.clientVersion,
|
ANDROID_VR_1_61_48.osVersion,
|
||||||
ANDROID_VR_NO_AUTH.requiresAuth,
|
ANDROID_VR_1_61_48.androidSdkVersion,
|
||||||
true,
|
ANDROID_VR_1_61_48.buildId,
|
||||||
"Android VR Auth"
|
"107.0.5284.2",
|
||||||
|
"1.43.32",
|
||||||
|
ANDROID_VR_1_61_48.requiresAuth,
|
||||||
|
ANDROID_VR_1_61_48.useAuth,
|
||||||
|
"Android VR 1.43"
|
||||||
);
|
);
|
||||||
|
|
||||||
private static boolean forceAVC() {
|
private static boolean forceAVC() {
|
||||||
|
|||||||
@@ -252,8 +252,9 @@ public class SpoofVideoStreamsPatch {
|
|||||||
public static final class AudioStreamLanguageOverrideAvailability implements Setting.Availability {
|
public static final class AudioStreamLanguageOverrideAvailability implements Setting.Availability {
|
||||||
@Override
|
@Override
|
||||||
public boolean isAvailable() {
|
public boolean isAvailable() {
|
||||||
|
ClientType clientType = BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get();
|
||||||
return BaseSettings.SPOOF_VIDEO_STREAMS.get()
|
return BaseSettings.SPOOF_VIDEO_STREAMS.get()
|
||||||
&& BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ClientType.ANDROID_VR_NO_AUTH;
|
&& (clientType == ClientType.ANDROID_VR_1_61_48 || clientType == ClientType.ANDROID_VR_1_43_32);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ final class PlayerRoutes {
|
|||||||
// but if this is a fall over client it will set the language even though
|
// but if this is a fall over client it will set the language even though
|
||||||
// the audio language is not selectable in the UI.
|
// the audio language is not selectable in the UI.
|
||||||
ClientType userSelectedClient = BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get();
|
ClientType userSelectedClient = BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get();
|
||||||
Locale streamLocale = userSelectedClient == ClientType.ANDROID_VR_NO_AUTH
|
Locale streamLocale = (userSelectedClient == ClientType.ANDROID_VR_1_61_48
|
||||||
|
|| userSelectedClient == ClientType.ANDROID_VR_1_43_32)
|
||||||
? BaseSettings.SPOOF_VIDEO_STREAMS_LANGUAGE.get().getLocale()
|
? BaseSettings.SPOOF_VIDEO_STREAMS_LANGUAGE.get().getLocale()
|
||||||
: Locale.getDefault();
|
: Locale.getDefault();
|
||||||
|
|
||||||
|
|||||||
@@ -35,21 +35,22 @@ import app.revanced.extension.shared.spoof.ClientType;
|
|||||||
*/
|
*/
|
||||||
public class StreamingDataRequest {
|
public class StreamingDataRequest {
|
||||||
|
|
||||||
private static final ClientType[] CLIENT_ORDER_TO_USE;
|
private static volatile ClientType[] clientOrderToUse = ClientType.values();
|
||||||
|
|
||||||
static {
|
public static void setClientOrderToUse(ClientType[] availableClients, ClientType preferredClient) {
|
||||||
ClientType[] allClientTypes = ClientType.values();
|
Objects.requireNonNull(availableClients);
|
||||||
ClientType preferredClient = BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get();
|
|
||||||
|
|
||||||
CLIENT_ORDER_TO_USE = new ClientType[allClientTypes.length];
|
clientOrderToUse = new ClientType[availableClients.length];
|
||||||
CLIENT_ORDER_TO_USE[0] = preferredClient;
|
clientOrderToUse[0] = preferredClient;
|
||||||
|
|
||||||
int i = 1;
|
int i = 1;
|
||||||
for (ClientType c : allClientTypes) {
|
for (ClientType c : availableClients) {
|
||||||
if (c != preferredClient) {
|
if (c != preferredClient) {
|
||||||
CLIENT_ORDER_TO_USE[i++] = c;
|
clientOrderToUse[i++] = c;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger.printDebug(() -> "Available spoof clients: " + Arrays.toString(clientOrderToUse));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String AUTHORIZATION_HEADER = "Authorization";
|
private static final String AUTHORIZATION_HEADER = "Authorization";
|
||||||
@@ -193,9 +194,9 @@ public class StreamingDataRequest {
|
|||||||
|
|
||||||
// Retry with different client if empty response body is received.
|
// Retry with different client if empty response body is received.
|
||||||
int i = 0;
|
int i = 0;
|
||||||
for (ClientType clientType : CLIENT_ORDER_TO_USE) {
|
for (ClientType clientType : clientOrderToUse) {
|
||||||
// Show an error if the last client type fails, or if debug is enabled then show for all attempts.
|
// Show an error if the last client type fails, or if debug is enabled then show for all attempts.
|
||||||
final boolean showErrorToast = (++i == CLIENT_ORDER_TO_USE.length) || debugEnabled;
|
final boolean showErrorToast = (++i == clientOrderToUse.length) || debugEnabled;
|
||||||
|
|
||||||
HttpURLConnection connection = send(clientType, videoId, playerHeaders, showErrorToast);
|
HttpURLConnection connection = send(clientType, videoId, playerHeaders, showErrorToast);
|
||||||
if (connection != null) {
|
if (connection != null) {
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.protobuf)
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly(project(":extensions:shared:library"))
|
compileOnly(project(":extensions:shared:library"))
|
||||||
compileOnly(project(":extensions:spotify:stub"))
|
compileOnly(project(":extensions:spotify:stub"))
|
||||||
compileOnly(libs.annotation)
|
compileOnly(libs.annotation)
|
||||||
|
|
||||||
|
implementation(libs.nanohttpd)
|
||||||
|
implementation(libs.protobuf.javalite)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@@ -14,3 +21,19 @@ android {
|
|||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protobuf {
|
||||||
|
protoc {
|
||||||
|
artifact = libs.protobuf.protoc.get().toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
generateProtoTasks {
|
||||||
|
all().forEach { task ->
|
||||||
|
task.builtins {
|
||||||
|
create("java") {
|
||||||
|
option("lite")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,25 +1,33 @@
|
|||||||
package app.revanced.extension.spotify.layout.hide.createbutton;
|
package app.revanced.extension.spotify.layout.hide.createbutton;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.spotify.shared.ComponentFilters.ComponentFilter;
|
||||||
|
import app.revanced.extension.spotify.shared.ComponentFilters.ResourceIdComponentFilter;
|
||||||
|
import app.revanced.extension.spotify.shared.ComponentFilters.StringComponentFilter;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public final class HideCreateButtonPatch {
|
public final class HideCreateButtonPatch {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list of ids of resources which contain the Create button title.
|
* 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<String> CREATE_BUTTON_TITLE_RES_ID_LIST = List.of(
|
private static final List<ComponentFilter> CREATE_BUTTON_COMPONENT_FILTERS = List.of(
|
||||||
Integer.toString(Utils.getResourceIdentifier("navigationbar_musicappitems_create_title", "string"))
|
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")
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The old id of the resource which contained the Create button title. Used in older versions of the app.
|
* 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 int OLD_CREATE_BUTTON_TITLE_RES_ID =
|
private static final ResourceIdComponentFilter OLD_CREATE_BUTTON_COMPONENT_FILTER =
|
||||||
Utils.getResourceIdentifier("bottom_navigation_bar_create_tab_title", "string");
|
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.
|
* Injection point. This method is called on every navigation bar item to check whether it is the Create button.
|
||||||
@@ -31,24 +39,27 @@ public final class HideCreateButtonPatch {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
String stringifiedNavigationBarItem = navigationBarItem.toString();
|
String stringifiedNavigationBarItem = navigationBarItem.toString();
|
||||||
|
|
||||||
boolean isCreateButton = false;
|
for (ComponentFilter componentFilter : CREATE_BUTTON_COMPONENT_FILTERS) {
|
||||||
String matchedTitleResId = null;
|
if (componentFilter.filterUnavailable()) {
|
||||||
|
Logger.printInfo(() -> "returnNullIfIsCreateButton: Filter " +
|
||||||
for (String titleResId : CREATE_BUTTON_TITLE_RES_ID_LIST) {
|
componentFilter.getFilterRepresentation() + " not available, skipping");
|
||||||
if (stringifiedNavigationBarItem.contains(titleResId)) {
|
continue;
|
||||||
isCreateButton = true;
|
|
||||||
matchedTitleResId = titleResId;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCreateButton) {
|
if (stringifiedNavigationBarItem.contains(componentFilter.getFilterValue())) {
|
||||||
String finalMatchedTitleResId = matchedTitleResId;
|
Logger.printInfo(() -> "Hiding Create button because the navigation bar item " +
|
||||||
Logger.printInfo(() -> "Hiding Create button because the navigation bar item " + navigationBarItem +
|
navigationBarItem + " matched the filter " + componentFilter.getFilterRepresentation());
|
||||||
" matched the title resource id " + finalMatchedTitleResId);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable ex) {
|
||||||
|
// Catch Throwable as calling toString can cause crashes with wrongfully generated code that throws
|
||||||
|
// NoSuchMethod errors.
|
||||||
|
Logger.printException(() -> "returnNullIfIsCreateButton failure", ex);
|
||||||
|
}
|
||||||
|
|
||||||
return navigationBarItem;
|
return navigationBarItem;
|
||||||
}
|
}
|
||||||
@@ -58,11 +69,15 @@ public final class HideCreateButtonPatch {
|
|||||||
* Create button.
|
* Create button.
|
||||||
*/
|
*/
|
||||||
public static boolean isOldCreateButton(int oldNavigationBarItemTitleResId) {
|
public static boolean isOldCreateButton(int oldNavigationBarItemTitleResId) {
|
||||||
boolean isCreateButton = oldNavigationBarItemTitleResId == OLD_CREATE_BUTTON_TITLE_RES_ID;
|
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 (isCreateButton) {
|
if (oldNavigationBarItemTitleResId == OLD_CREATE_BUTTON_COMPONENT_FILTER.getResourceId()) {
|
||||||
Logger.printInfo(() -> "Hiding old Create button because the navigation bar item title resource id" +
|
Logger.printInfo(() -> "Hiding old Create button because the navigation bar item title resource id" +
|
||||||
" matched " + OLD_CREATE_BUTTON_TITLE_RES_ID);
|
" matched " + OLD_CREATE_BUTTON_COMPONENT_FILTER.getFilterRepresentation());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package app.revanced.extension.spotify.misc.fix;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.spotify.misc.fix.clienttoken.data.v0.ClienttokenHttp.*;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
|
||||||
|
import static app.revanced.extension.spotify.misc.fix.Constants.*;
|
||||||
|
|
||||||
|
class ClientTokenService {
|
||||||
|
private static final String IOS_CLIENT_ID = "58bd3c95768941ea9eb4350aaa033eb3";
|
||||||
|
private static final String IOS_USER_AGENT;
|
||||||
|
|
||||||
|
static {
|
||||||
|
String clientVersion = getClientVersion();
|
||||||
|
int commitHashIndex = clientVersion.lastIndexOf(".");
|
||||||
|
String version = clientVersion.substring(
|
||||||
|
clientVersion.indexOf("-") + 1,
|
||||||
|
clientVersion.lastIndexOf(".", commitHashIndex - 1)
|
||||||
|
);
|
||||||
|
|
||||||
|
IOS_USER_AGENT = "Spotify/" + version + " iOS/" + getSystemVersion() + " (" + getHardwareMachine() + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final ConnectivitySdkData.Builder IOS_CONNECTIVITY_SDK_DATA =
|
||||||
|
ConnectivitySdkData.newBuilder()
|
||||||
|
.setPlatformSpecificData(PlatformSpecificData.newBuilder()
|
||||||
|
.setIos(NativeIOSData.newBuilder()
|
||||||
|
.setHwMachine(getHardwareMachine())
|
||||||
|
.setSystemVersion(getSystemVersion())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
private static final ClientDataRequest.Builder IOS_CLIENT_DATA_REQUEST =
|
||||||
|
ClientDataRequest.newBuilder()
|
||||||
|
.setClientVersion(getClientVersion())
|
||||||
|
.setClientId(IOS_CLIENT_ID);
|
||||||
|
|
||||||
|
private static final ClientTokenRequest.Builder IOS_CLIENT_TOKEN_REQUEST =
|
||||||
|
ClientTokenRequest.newBuilder()
|
||||||
|
.setRequestType(ClientTokenRequestType.REQUEST_CLIENT_DATA_REQUEST);
|
||||||
|
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
static ClientTokenRequest newIOSClientTokenRequest(String deviceId) {
|
||||||
|
Logger.printInfo(() -> "Creating new iOS client token request with device ID: " + deviceId);
|
||||||
|
|
||||||
|
return IOS_CLIENT_TOKEN_REQUEST
|
||||||
|
.setClientData(IOS_CLIENT_DATA_REQUEST
|
||||||
|
.setConnectivitySdkData(IOS_CONNECTIVITY_SDK_DATA
|
||||||
|
.setDeviceId(deviceId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
static ClientTokenResponse getClientTokenResponse(@NonNull ClientTokenRequest request) {
|
||||||
|
if (request.getRequestType() == ClientTokenRequestType.REQUEST_CLIENT_DATA_REQUEST) {
|
||||||
|
Logger.printInfo(() -> "Requesting iOS client token");
|
||||||
|
String deviceId = request.getClientData().getConnectivitySdkData().getDeviceId();
|
||||||
|
request = newIOSClientTokenRequest(deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientTokenResponse response;
|
||||||
|
try {
|
||||||
|
response = requestClientToken(request);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
Logger.printException(() -> "Failed to handle request", ex);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private static ClientTokenResponse requestClientToken(@NonNull ClientTokenRequest request) throws IOException {
|
||||||
|
HttpURLConnection urlConnection = (HttpURLConnection) new URL(CLIENT_TOKEN_API_URL).openConnection();
|
||||||
|
urlConnection.setRequestMethod("POST");
|
||||||
|
urlConnection.setDoOutput(true);
|
||||||
|
urlConnection.setRequestProperty("Content-Type", "application/x-protobuf");
|
||||||
|
urlConnection.setRequestProperty("Accept", "application/x-protobuf");
|
||||||
|
urlConnection.setRequestProperty("User-Agent", IOS_USER_AGENT);
|
||||||
|
|
||||||
|
byte[] requestArray = request.toByteArray();
|
||||||
|
urlConnection.setFixedLengthStreamingMode(requestArray.length);
|
||||||
|
urlConnection.getOutputStream().write(requestArray);
|
||||||
|
|
||||||
|
try (InputStream inputStream = urlConnection.getInputStream()) {
|
||||||
|
return ClientTokenResponse.parseFrom(inputStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
static ClientTokenResponse serveClientTokenRequest(@NonNull InputStream inputStream) {
|
||||||
|
ClientTokenRequest request;
|
||||||
|
try {
|
||||||
|
request = ClientTokenRequest.parseFrom(inputStream);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
Logger.printException(() -> "Failed to parse request from input stream", ex);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Logger.printInfo(() -> "Request of type: " + request.getRequestType());
|
||||||
|
|
||||||
|
ClientTokenResponse response = getClientTokenResponse(request);
|
||||||
|
if (response != null) Logger.printInfo(() -> "Response of type: " + response.getResponseType());
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package app.revanced.extension.spotify.misc.fix;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
class Constants {
|
||||||
|
static final String CLIENT_TOKEN_API_PATH = "/v1/clienttoken";
|
||||||
|
static final String CLIENT_TOKEN_API_URL = "https://clienttoken.spotify.com" + CLIENT_TOKEN_API_PATH;
|
||||||
|
|
||||||
|
// Modified by a patch. Do not touch.
|
||||||
|
@NonNull
|
||||||
|
static String getClientVersion() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modified by a patch. Do not touch.
|
||||||
|
@NonNull
|
||||||
|
static String getSystemVersion() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modified by a patch. Do not touch.
|
||||||
|
@NonNull
|
||||||
|
static String getHardwareMachine() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package app.revanced.extension.spotify.misc.fix;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.spotify.misc.fix.clienttoken.data.v0.ClienttokenHttp.ClientTokenResponse;
|
||||||
|
import com.google.protobuf.MessageLite;
|
||||||
|
import fi.iki.elonen.NanoHTTPD;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.FilterInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import static app.revanced.extension.spotify.misc.fix.ClientTokenService.serveClientTokenRequest;
|
||||||
|
import static app.revanced.extension.spotify.misc.fix.Constants.CLIENT_TOKEN_API_PATH;
|
||||||
|
import static fi.iki.elonen.NanoHTTPD.Response.Status.INTERNAL_ERROR;
|
||||||
|
|
||||||
|
class RequestListener extends NanoHTTPD {
|
||||||
|
RequestListener(int port) {
|
||||||
|
super(port);
|
||||||
|
|
||||||
|
try {
|
||||||
|
start();
|
||||||
|
} catch (IOException ex) {
|
||||||
|
Logger.printException(() -> "Failed to start request listener on port " + port, ex);
|
||||||
|
throw new RuntimeException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Response serve(@NonNull IHTTPSession session) {
|
||||||
|
String uri = session.getUri();
|
||||||
|
if (!uri.equals(CLIENT_TOKEN_API_PATH)) return INTERNAL_ERROR_RESPONSE;
|
||||||
|
|
||||||
|
Logger.printInfo(() -> "Serving request for URI: " + uri);
|
||||||
|
|
||||||
|
ClientTokenResponse response = serveClientTokenRequest(getInputStream(session));
|
||||||
|
if (response != null) return newResponse(Response.Status.OK, response);
|
||||||
|
|
||||||
|
Logger.printException(() -> "Failed to serve client token request");
|
||||||
|
return INTERNAL_ERROR_RESPONSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private static InputStream newLimitedInputStream(InputStream inputStream, long contentLength) {
|
||||||
|
return new FilterInputStream(inputStream) {
|
||||||
|
private long remaining = contentLength;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read() throws IOException {
|
||||||
|
if (remaining <= 0) return -1;
|
||||||
|
int result = super.read();
|
||||||
|
if (result != -1) remaining--;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] b, int off, int len) throws IOException {
|
||||||
|
if (remaining <= 0) return -1;
|
||||||
|
len = (int) Math.min(len, remaining);
|
||||||
|
int result = super.read(b, off, len);
|
||||||
|
if (result != -1) remaining -= result;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private static InputStream getInputStream(@NonNull IHTTPSession session) {
|
||||||
|
long requestContentLength = Long.parseLong(Objects.requireNonNull(session.getHeaders().get("content-length")));
|
||||||
|
return newLimitedInputStream(session.getInputStream(), requestContentLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final Response INTERNAL_ERROR_RESPONSE = newResponse(INTERNAL_ERROR);
|
||||||
|
|
||||||
|
@SuppressWarnings("SameParameterValue")
|
||||||
|
@NonNull
|
||||||
|
private static Response newResponse(Response.Status status) {
|
||||||
|
return newResponse(status, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private static Response newResponse(Response.IStatus status, MessageLite messageLite) {
|
||||||
|
if (messageLite == null) {
|
||||||
|
return newFixedLengthResponse(status, "application/x-protobuf", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] messageBytes = messageLite.toByteArray();
|
||||||
|
InputStream stream = new ByteArrayInputStream(messageBytes);
|
||||||
|
return newFixedLengthResponse(status, "application/x-protobuf", stream, messageBytes.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package app.revanced.extension.spotify.misc.fix;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class SpoofClientPatch {
|
||||||
|
private static RequestListener listener;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point. Launch requests listener server.
|
||||||
|
*/
|
||||||
|
public synchronized static void launchListener(int port) {
|
||||||
|
if (listener != null) {
|
||||||
|
Logger.printInfo(() -> "Listener already running on port " + port);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Logger.printInfo(() -> "Launching listener on port " + port);
|
||||||
|
listener = new RequestListener(port);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "launchListener failure", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package spotify.clienttoken.data.v0;
|
||||||
|
|
||||||
|
option optimize_for = LITE_RUNTIME;
|
||||||
|
option java_package = "app.revanced.extension.spotify.misc.fix.clienttoken.data.v0";
|
||||||
|
|
||||||
|
message ClientTokenRequest {
|
||||||
|
ClientTokenRequestType request_type = 1;
|
||||||
|
|
||||||
|
oneof request {
|
||||||
|
ClientDataRequest client_data = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ClientTokenRequestType {
|
||||||
|
REQUEST_UNKNOWN = 0;
|
||||||
|
REQUEST_CLIENT_DATA_REQUEST = 1;
|
||||||
|
REQUEST_CHALLENGE_ANSWERS_REQUEST = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ClientDataRequest {
|
||||||
|
string client_version = 1;
|
||||||
|
string client_id = 2;
|
||||||
|
|
||||||
|
oneof data {
|
||||||
|
ConnectivitySdkData connectivity_sdk_data = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message ConnectivitySdkData {
|
||||||
|
PlatformSpecificData platform_specific_data = 1;
|
||||||
|
string device_id = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PlatformSpecificData {
|
||||||
|
oneof data {
|
||||||
|
NativeIOSData ios = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message NativeIOSData {
|
||||||
|
int32 user_interface_idiom = 1;
|
||||||
|
bool target_iphone_simulator = 2;
|
||||||
|
string hw_machine = 3;
|
||||||
|
string system_version = 4;
|
||||||
|
string simulator_model_identifier = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ClientTokenResponse {
|
||||||
|
ClientTokenResponseType response_type = 1;
|
||||||
|
|
||||||
|
oneof response {
|
||||||
|
GrantedTokenResponse granted_token = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ClientTokenResponseType {
|
||||||
|
RESPONSE_UNKNOWN = 0;
|
||||||
|
RESPONSE_GRANTED_TOKEN_RESPONSE = 1;
|
||||||
|
RESPONSE_CHALLENGES_RESPONSE = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GrantedTokenResponse {
|
||||||
|
string token = 1;
|
||||||
|
int32 expires_after_seconds = 2;
|
||||||
|
int32 refresh_after_seconds = 3;
|
||||||
|
repeated TokenDomain domains = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TokenDomain {
|
||||||
|
string domain = 1;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id(libs.plugins.android.library.get().pluginId)
|
alias(libs.plugins.android.library)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package app.revanced;
|
||||||
|
|
||||||
|
public interface ContextMenuItemPlaceholder {
|
||||||
|
Object getViewModel();
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.spotify.browsita.v1.resolved;
|
||||||
|
|
||||||
|
public final class Section {
|
||||||
|
public static final int BRAND_ADS_FIELD_NUMBER = 6;
|
||||||
|
public int sectionTypeCase_;
|
||||||
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
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,4 +2,5 @@ dependencies {
|
|||||||
compileOnly(project(":extensions:shared:library"))
|
compileOnly(project(":extensions:shared:library"))
|
||||||
compileOnly(project(":extensions:syncforreddit:stub"))
|
compileOnly(project(":extensions:syncforreddit:stub"))
|
||||||
compileOnly(libs.annotation)
|
compileOnly(libs.annotation)
|
||||||
|
compileOnly(libs.okhttp)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package app.revanced.extension.syncforreddit;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.fixes.redgifs.BaseFixRedgifsApiPatch;
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @noinspection unused
|
||||||
|
*/
|
||||||
|
public class FixRedgifsApiPatch extends BaseFixRedgifsApiPatch {
|
||||||
|
static {
|
||||||
|
INSTANCE = new FixRedgifsApiPatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDefaultUserAgent() {
|
||||||
|
// To be filled in by patch
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static OkHttpClient install(OkHttpClient.Builder builder) {
|
||||||
|
return builder.addInterceptor(INSTANCE).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id(libs.plugins.android.library.get().pluginId)
|
alias(libs.plugins.android.library)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ public class SpoofSimPatch {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.initializationException(() -> "Context is not yet set, cannot spoof: " + fieldSpoofed, null);
|
Logger.printException(() -> "Context is not yet set, cannot spoof: " + fieldSpoofed, null);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id(libs.plugins.android.library.get().pluginId)
|
alias(libs.plugins.android.library)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
android.namespace = "app.revanced.extension"
|
android.namespace = "app.revanced.extension"
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id(libs.plugins.android.library.get().pluginId)
|
alias(libs.plugins.android.library)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id(libs.plugins.android.library.get().pluginId)
|
alias(libs.plugins.android.library)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
package app.revanced.extension.youtube
|
package app.revanced.extension.youtube
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger
|
||||||
|
import java.util.Collections
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* generic event provider class
|
* generic event provider class
|
||||||
*/
|
*/
|
||||||
class Event<T> {
|
class Event<T> {
|
||||||
private val eventListeners = mutableSetOf<(T) -> Unit>()
|
private val eventListeners = Collections.synchronizedSet(mutableSetOf<(T) -> Unit>())
|
||||||
|
|
||||||
operator fun plusAssign(observer: (T) -> Unit) {
|
operator fun plusAssign(observer: (T) -> Unit) {
|
||||||
addObserver(observer)
|
addObserver(observer)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addObserver(observer: (T) -> Unit) {
|
fun addObserver(observer: (T) -> Unit) {
|
||||||
|
Logger.printDebug { "Adding observer: $observer" }
|
||||||
eventListeners.add(observer)
|
eventListeners.add(observer)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,7 +27,8 @@ class Event<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
operator fun invoke(value: T) {
|
operator fun invoke(value: T) {
|
||||||
for (observer in eventListeners)
|
for (observer in eventListeners) {
|
||||||
observer.invoke(value)
|
observer.invoke(value)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,179 +0,0 @@
|
|||||||
package app.revanced.extension.youtube;
|
|
||||||
|
|
||||||
import static app.revanced.extension.shared.Utils.clamp;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.graphics.Canvas;
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.graphics.Paint;
|
|
||||||
import android.graphics.RectF;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.text.style.ReplacementSpan;
|
|
||||||
import android.text.TextPaint;
|
|
||||||
import android.view.Window;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
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";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int getThemeColor(String resourceName, int defaultColor) {
|
|
||||||
try {
|
|
||||||
return Utils.getColorFromString(resourceName);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
// User entered an invalid custom theme color.
|
|
||||||
// Normally this should never be reached, and no localized strings are needed.
|
|
||||||
Utils.showToastLong("Invalid custom theme color: " + resourceName);
|
|
||||||
return defaultColor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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 = getThemeColor(darkThemeResourceName(), Color.BLACK);
|
|
||||||
}
|
|
||||||
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 = getThemeColor(lightThemeResourceName(), Color.WHITE);
|
|
||||||
}
|
|
||||||
return lightThemeColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int getBackgroundColor() {
|
|
||||||
return isDarkTheme() ? getDarkThemeColor() : getLightThemeColor();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int getForegroundColor() {
|
|
||||||
return isDarkTheme() ? getLightThemeColor() : getDarkThemeColor();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int getDialogBackgroundColor() {
|
|
||||||
final String colorName = isDarkTheme()
|
|
||||||
? "yt_black1"
|
|
||||||
: "yt_white1";
|
|
||||||
|
|
||||||
return Utils.getColorFromString(colorName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int getToolbarBackgroundColor() {
|
|
||||||
final String colorName = isDarkTheme()
|
|
||||||
? "yt_black3"
|
|
||||||
: "yt_white1";
|
|
||||||
|
|
||||||
return Utils.getColorFromString(colorName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the system navigation bar color for the activity.
|
|
||||||
* Applies the background color obtained from {@link #getBackgroundColor()} to the navigation bar.
|
|
||||||
* For Android 10 (API 29) and above, enforces navigation bar contrast to ensure visibility.
|
|
||||||
*/
|
|
||||||
public static void setNavigationBarColor(@Nullable Window window) {
|
|
||||||
if (window == null) {
|
|
||||||
Logger.printDebug(() -> "Cannot set navigation bar color, window is null");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.setNavigationBarColor(getBackgroundColor());
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
window.setNavigationBarContrastEnforced(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adjusts the brightness of a color by lightening or darkening it based on the given factor.
|
|
||||||
* <p>
|
|
||||||
* If the factor is greater than 1, the color is lightened by interpolating toward white (#FFFFFF).
|
|
||||||
* If the factor is less than or equal to 1, the color is darkened by scaling its RGB components toward black (#000000).
|
|
||||||
* The alpha channel remains unchanged.
|
|
||||||
*
|
|
||||||
* @param color The input color to adjust, in ARGB format.
|
|
||||||
* @param factor The adjustment factor. Use values > 1.0f to lighten (e.g., 1.11f for slight lightening)
|
|
||||||
* or values <= 1.0f to darken (e.g., 0.95f for slight darkening).
|
|
||||||
* @return The adjusted color in ARGB format.
|
|
||||||
*/
|
|
||||||
public static int adjustColorBrightness(int color, float factor) {
|
|
||||||
final int alpha = Color.alpha(color);
|
|
||||||
int red = Color.red(color);
|
|
||||||
int green = Color.green(color);
|
|
||||||
int blue = Color.blue(color);
|
|
||||||
|
|
||||||
if (factor > 1.0f) {
|
|
||||||
// Lighten: Interpolate toward white (255)
|
|
||||||
final float t = 1.0f - (1.0f / factor); // Interpolation parameter
|
|
||||||
red = Math.round(red + (255 - red) * t);
|
|
||||||
green = Math.round(green + (255 - green) * t);
|
|
||||||
blue = Math.round(blue + (255 - blue) * t);
|
|
||||||
} else {
|
|
||||||
// Darken or no change: Scale toward black
|
|
||||||
red = (int) (red * factor);
|
|
||||||
green = (int) (green * factor);
|
|
||||||
blue = (int) (blue * factor);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure values are within [0, 255]
|
|
||||||
red = clamp(red, 0, 255);
|
|
||||||
green = clamp(green, 0, 255);
|
|
||||||
blue = clamp(blue, 0, 255);
|
|
||||||
|
|
||||||
return Color.argb(alpha, red, green, blue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package app.revanced.extension.youtube.patches;
|
||||||
|
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class ChangeHeaderPatch {
|
||||||
|
|
||||||
|
public enum HeaderLogo {
|
||||||
|
DEFAULT(null, null),
|
||||||
|
REGULAR("ytWordmarkHeader", "yt_ringo2_wordmark_header"),
|
||||||
|
PREMIUM("ytPremiumWordmarkHeader", "yt_ringo2_premium_wordmark_header"),
|
||||||
|
REVANCED("revanced_header_logo", "revanced_header_logo"),
|
||||||
|
REVANCED_MINIMAL("revanced_header_logo_minimal", "revanced_header_logo_minimal"),
|
||||||
|
CUSTOM("custom_header", "custom_header");
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private final String attributeName;
|
||||||
|
@Nullable
|
||||||
|
private final String drawableName;
|
||||||
|
|
||||||
|
HeaderLogo(@Nullable String attributeName, @Nullable String drawableName) {
|
||||||
|
this.attributeName = attributeName;
|
||||||
|
this.drawableName = drawableName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The attribute id of this header logo, or NULL if the logo should not be replaced.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
private Integer getAttributeId() {
|
||||||
|
if (attributeName == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int identifier = Utils.getResourceIdentifier(attributeName, "attr");
|
||||||
|
if (identifier == 0) {
|
||||||
|
// Identifier is zero if custom header setting was included in imported settings
|
||||||
|
// and a custom image was not included during patching.
|
||||||
|
Logger.printDebug(() -> "Could not find attribute: " + drawableName);
|
||||||
|
Settings.HEADER_LOGO.resetToDefault();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return identifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public Drawable getDrawable() {
|
||||||
|
if (drawableName == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String drawableFullName = drawableName + (Utils.isDarkModeEnabled()
|
||||||
|
? "_dark"
|
||||||
|
: "_light");
|
||||||
|
|
||||||
|
final int identifier = Utils.getResourceIdentifier(drawableFullName, "drawable");
|
||||||
|
if (identifier == 0) {
|
||||||
|
Logger.printDebug(() -> "Could not find drawable: " + drawableFullName);
|
||||||
|
Settings.HEADER_LOGO.resetToDefault();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Utils.getContext().getDrawable(identifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static int getHeaderAttributeId(int original) {
|
||||||
|
return Objects.requireNonNullElse(Settings.HEADER_LOGO.get().getAttributeId(), original);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Drawable getDrawable(Drawable original) {
|
||||||
|
Drawable logo = Settings.HEADER_LOGO.get().getDrawable();
|
||||||
|
if (logo != null) {
|
||||||
|
return logo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: If 'Hide Doodles' is enabled, this will force the regular logo regardless
|
||||||
|
// what account the user has. This can be improved the next time a Doodle is
|
||||||
|
// active and the attribute id is passed to this method so the correct
|
||||||
|
// regular/premium logo is returned.
|
||||||
|
logo = HeaderLogo.REGULAR.getDrawable();
|
||||||
|
if (logo != null) {
|
||||||
|
return logo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should never happen.
|
||||||
|
Logger.printException(() -> "Could not find regular header logo resource");
|
||||||
|
return original;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,10 @@ package app.revanced.extension.youtube.patches;
|
|||||||
import static app.revanced.extension.shared.StringRef.str;
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.app.Dialog;
|
||||||
import android.text.Html;
|
import android.text.Html;
|
||||||
|
import android.util.Pair;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
@@ -63,18 +66,28 @@ public class CheckWatchHistoryDomainNameResolutionPatch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Utils.runOnMainThread(() -> {
|
Utils.runOnMainThread(() -> {
|
||||||
var alert = new android.app.AlertDialog.Builder(context)
|
try {
|
||||||
.setTitle(str("revanced_check_watch_history_domain_name_dialog_title"))
|
// Create the custom dialog.
|
||||||
.setMessage(Html.fromHtml(str("revanced_check_watch_history_domain_name_dialog_message")))
|
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
||||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
context,
|
||||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
str("revanced_check_watch_history_domain_name_dialog_title"), // Title.
|
||||||
dialog.dismiss();
|
Html.fromHtml(str("revanced_check_watch_history_domain_name_dialog_message")), // Message (HTML).
|
||||||
}).setNegativeButton(str("revanced_check_watch_history_domain_name_dialog_ignore"), (dialog, which) -> {
|
null, // No EditText.
|
||||||
Settings.CHECK_WATCH_HISTORY_DOMAIN_NAME.save(false);
|
null, // OK button text.
|
||||||
dialog.dismiss();
|
() -> {}, // OK button action (just dismiss).
|
||||||
}).create();
|
() -> {}, // 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) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "checkDnsResolver failure", ex);
|
Logger.printException(() -> "checkDnsResolver failure", ex);
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package app.revanced.extension.youtube.patches;
|
||||||
|
|
||||||
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public final class DisableDoubleTapActionsPatch {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*
|
||||||
|
* @return If "should skip to chapter start" flag is set.
|
||||||
|
*/
|
||||||
|
public static boolean disableDoubleTapChapters(boolean original) {
|
||||||
|
return original && !Settings.DISABLE_CHAPTER_SKIP_DOUBLE_TAP.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package app.revanced.extension.youtube.patches;
|
package app.revanced.extension.youtube.patches;
|
||||||
|
|
||||||
|
import android.view.Display;
|
||||||
|
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
@@ -8,8 +10,10 @@ public class DisableHdrPatch {
|
|||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
public static boolean disableHDRVideo() {
|
public static int[] disableHdrVideo(Display.HdrCapabilities capabilities) {
|
||||||
return !Settings.DISABLE_HDR_VIDEO.get();
|
return Settings.DISABLE_HDR_VIDEO.get()
|
||||||
|
? new int[0]
|
||||||
|
: capabilities.getSupportedHdrTypes();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package app.revanced.extension.youtube.patches;
|
||||||
|
|
||||||
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class DisableSignInToTvPopupPatch {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static boolean disableSignInToTvPopup() {
|
||||||
|
return Settings.DISABLE_SIGNIN_TO_TV_POPUP.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,15 @@
|
|||||||
package app.revanced.extension.youtube.patches;
|
package app.revanced.extension.youtube.patches;
|
||||||
|
|
||||||
|
import static app.revanced.extension.youtube.settings.preference.ExternalDownloaderPreference.showDialogIfAppIsNotInstalled;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.StringRef;
|
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
@@ -36,7 +34,7 @@ public final class DownloadsPatch {
|
|||||||
*
|
*
|
||||||
* Appears to always be called from the main thread.
|
* Appears to always be called from the main thread.
|
||||||
*/
|
*/
|
||||||
public static boolean inAppDownloadButtonOnClick(@NonNull String videoId) {
|
public static boolean inAppDownloadButtonOnClick(String videoId) {
|
||||||
try {
|
try {
|
||||||
if (!Settings.EXTERNAL_DOWNLOADER_ACTION_BUTTON.get()) {
|
if (!Settings.EXTERNAL_DOWNLOADER_ACTION_BUTTON.get()) {
|
||||||
return false;
|
return false;
|
||||||
@@ -48,6 +46,9 @@ public final class DownloadsPatch {
|
|||||||
boolean isActivityContext = true;
|
boolean isActivityContext = true;
|
||||||
if (context == null) {
|
if (context == null) {
|
||||||
// Utils context is the application context, and not an activity context.
|
// Utils context is the application context, and not an activity context.
|
||||||
|
//
|
||||||
|
// Edit: This check may no longer be needed since YT can now
|
||||||
|
// only be launched from the main Activity (embedded usage in other apps no longer works).
|
||||||
context = Utils.getContext();
|
context = Utils.getContext();
|
||||||
isActivityContext = false;
|
isActivityContext = false;
|
||||||
}
|
}
|
||||||
@@ -64,8 +65,7 @@ public final class DownloadsPatch {
|
|||||||
* @param isActivityContext If the context parameter is for an Activity. If this is false, then
|
* @param isActivityContext If the context parameter is for an Activity. If this is false, then
|
||||||
* the downloader is opened as a new task (which forces YT to minimize).
|
* the downloader is opened as a new task (which forces YT to minimize).
|
||||||
*/
|
*/
|
||||||
public static void launchExternalDownloader(@NonNull String videoId,
|
public static void launchExternalDownloader(String videoId, Context context, boolean isActivityContext) {
|
||||||
@NonNull Context context, boolean isActivityContext) {
|
|
||||||
try {
|
try {
|
||||||
Objects.requireNonNull(videoId);
|
Objects.requireNonNull(videoId);
|
||||||
Logger.printDebug(() -> "Launching external downloader with context: " + context);
|
Logger.printDebug(() -> "Launching external downloader with context: " + context);
|
||||||
@@ -73,16 +73,8 @@ public final class DownloadsPatch {
|
|||||||
// Trim string to avoid any accidental whitespace.
|
// Trim string to avoid any accidental whitespace.
|
||||||
var downloaderPackageName = Settings.EXTERNAL_DOWNLOADER_PACKAGE_NAME.get().trim();
|
var downloaderPackageName = Settings.EXTERNAL_DOWNLOADER_PACKAGE_NAME.get().trim();
|
||||||
|
|
||||||
boolean packageEnabled = false;
|
// If the package is not installed, show a dialog.
|
||||||
try {
|
if (showDialogIfAppIsNotInstalled(context, downloaderPackageName)) {
|
||||||
packageEnabled = context.getPackageManager().getApplicationInfo(downloaderPackageName, 0).enabled;
|
|
||||||
} catch (PackageManager.NameNotFoundException error) {
|
|
||||||
Logger.printDebug(() -> "External downloader could not be found: " + error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the package is not installed, show the toast
|
|
||||||
if (!packageEnabled) {
|
|
||||||
Utils.showToastLong(StringRef.str("revanced_external_downloader_not_installed_warning", downloaderPackageName));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,16 @@ public class ForceOriginalAudioPatch {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static boolean ignoreDefaultAudioStream(boolean original) {
|
||||||
|
if (Settings.FORCE_ORIGINAL_AUDIO.get()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return original;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
@@ -50,7 +60,6 @@ public class ForceOriginalAudioPatch {
|
|||||||
return isOriginal;
|
return isOriginal;
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "isDefaultAudioStream failure", ex);
|
Logger.printException(() -> "isDefaultAudioStream failure", ex);
|
||||||
|
|
||||||
return isDefault;
|
return isDefault;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package app.revanced.extension.youtube.patches;
|
package app.revanced.extension.youtube.patches;
|
||||||
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
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) {
|
private static void hideView(View parentView, int resourceId) {
|
||||||
View nextPreviousButton = parentView.findViewById(resourceId);
|
View nextPreviousButton = parentView.findViewById(resourceId);
|
||||||
|
|
||||||
@@ -69,4 +86,16 @@ public final class HidePlayerOverlayButtonsPatch {
|
|||||||
Logger.printDebug(() -> "Hiding previous/next button");
|
Logger.printDebug(() -> "Hiding previous/next button");
|
||||||
Utils.hideViewByRemovingFromParentUnderCondition(true, nextPreviousButton);
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,6 @@ public final class HideRelatedVideoOverlayPatch {
|
|||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
public static boolean hideRelatedVideoOverlay() {
|
public static boolean hideRelatedVideoOverlay() {
|
||||||
return Settings.HIDE_RELATED_VIDEO_OVERLAY.get();
|
return Settings.HIDE_RELATED_VIDEOS_OVERLAY.get();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,11 +57,4 @@ public class PlayerControlsPatch {
|
|||||||
private static void fullscreenButtonVisibilityChanged(boolean isVisible) {
|
private static void fullscreenButtonVisibilityChanged(boolean isVisible) {
|
||||||
// Code added during patching.
|
// Code added during patching.
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point.
|
|
||||||
*/
|
|
||||||
public static String getPlayerTopControlsLayoutResourceName(String original) {
|
|
||||||
return "default";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package app.revanced.extension.youtube.patches;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import app.revanced.extension.youtube.shared.PlayerControlsVisibility;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class PlayerControlsVisibilityHookPatch {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static void setPlayerControlsVisibility(@Nullable Enum<?> youTubePlayerControlsVisibility) {
|
||||||
|
if (youTubePlayerControlsVisibility == null) return;
|
||||||
|
|
||||||
|
PlayerControlsVisibility.setFromString(youTubePlayerControlsVisibility.name());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ import java.util.Objects;
|
|||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.youtube.patches.components.ReturnYouTubeDislikeFilterPatch;
|
import app.revanced.extension.youtube.patches.components.ReturnYouTubeDislikeFilter;
|
||||||
import app.revanced.extension.youtube.returnyoutubedislike.ReturnYouTubeDislike;
|
import app.revanced.extension.youtube.returnyoutubedislike.ReturnYouTubeDislike;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
import app.revanced.extension.youtube.shared.PlayerType;
|
import app.revanced.extension.youtube.shared.PlayerType;
|
||||||
@@ -55,7 +55,7 @@ public class ReturnYouTubeDislikePatch {
|
|||||||
private static volatile ReturnYouTubeDislike lastLithoShortsVideoData;
|
private static volatile ReturnYouTubeDislike lastLithoShortsVideoData;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Because litho Shorts spans are created offscreen after {@link ReturnYouTubeDislikeFilterPatch}
|
* Because litho Shorts spans are created offscreen after {@link ReturnYouTubeDislikeFilter}
|
||||||
* detects the video ids, but the current Short can arbitrarily reload the same span,
|
* 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.
|
* then use the {@link #lastLithoShortsVideoData} if this value is greater than zero.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -7,11 +7,17 @@ public class VersionCheckPatch {
|
|||||||
return Utils.getAppVersionName().compareTo(version) >= 0;
|
return Utils.getAppVersionName().compareTo(version) >= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
public static final boolean IS_19_17_OR_GREATER = isVersionOrGreater("19.17.00");
|
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");
|
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");
|
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");
|
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");
|
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_34_OR_GREATER = isVersionOrGreater("19.34.00");
|
||||||
public static final boolean IS_19_46_OR_GREATER = isVersionOrGreater("19.46.00");
|
public static final boolean IS_19_46_OR_GREATER = isVersionOrGreater("19.46.00");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
package app.revanced.extension.youtube.patches;
|
package app.revanced.extension.youtube.patches;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.google.android.libraries.youtube.innertube.model.media.VideoQuality;
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.youtube.Event;
|
||||||
|
import app.revanced.extension.youtube.shared.ShortsPlayerState;
|
||||||
import app.revanced.extension.youtube.shared.VideoState;
|
import app.revanced.extension.youtube.shared.VideoState;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -16,11 +22,30 @@ import app.revanced.extension.youtube.shared.VideoState;
|
|||||||
public final class VideoInformation {
|
public final class VideoInformation {
|
||||||
|
|
||||||
public interface PlaybackController {
|
public interface PlaybackController {
|
||||||
// Methods are added to YT classes during patching.
|
// Methods are added during patching.
|
||||||
boolean seekTo(long videoTime);
|
boolean patch_seekTo(long videoTime);
|
||||||
void seekToRelative(long videoTimeOffset);
|
void patch_seekToRelative(long videoTimeOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface to use obfuscated methods.
|
||||||
|
*/
|
||||||
|
public interface VideoQualityMenuInterface {
|
||||||
|
// Method is added during patching.
|
||||||
|
void patch_setQuality(VideoQuality quality);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Video resolution of the automatic quality option..
|
||||||
|
*/
|
||||||
|
public static final int AUTOMATIC_VIDEO_QUALITY_VALUE = -2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Video quality names are the same text for all languages.
|
||||||
|
* Premium can be "1080p Premium" or "1080p60 Premium"
|
||||||
|
*/
|
||||||
|
public static final String VIDEO_QUALITY_PREMIUM_NAME = "Premium";
|
||||||
|
|
||||||
private static final float DEFAULT_YOUTUBE_PLAYBACK_SPEED = 1.0f;
|
private static final float DEFAULT_YOUTUBE_PLAYBACK_SPEED = 1.0f;
|
||||||
/**
|
/**
|
||||||
* Prefix present in all Short player parameters signature.
|
* Prefix present in all Short player parameters signature.
|
||||||
@@ -30,12 +55,10 @@ public final class VideoInformation {
|
|||||||
private static WeakReference<PlaybackController> playerControllerRef = new WeakReference<>(null);
|
private static WeakReference<PlaybackController> playerControllerRef = new WeakReference<>(null);
|
||||||
private static WeakReference<PlaybackController> mdxPlayerDirectorRef = new WeakReference<>(null);
|
private static WeakReference<PlaybackController> mdxPlayerDirectorRef = new WeakReference<>(null);
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private static String videoId = "";
|
private static String videoId = "";
|
||||||
private static long videoLength = 0;
|
private static long videoLength = 0;
|
||||||
private static long videoTime = -1;
|
private static long videoTime = -1;
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private static volatile String playerResponseVideoId = "";
|
private static volatile String playerResponseVideoId = "";
|
||||||
private static volatile boolean playerResponseVideoIdIsShort;
|
private static volatile boolean playerResponseVideoIdIsShort;
|
||||||
private static volatile boolean videoIdIsShort;
|
private static volatile boolean videoIdIsShort;
|
||||||
@@ -45,6 +68,44 @@ public final class VideoInformation {
|
|||||||
*/
|
*/
|
||||||
private static float playbackSpeed = DEFAULT_YOUTUBE_PLAYBACK_SPEED;
|
private static float playbackSpeed = DEFAULT_YOUTUBE_PLAYBACK_SPEED;
|
||||||
|
|
||||||
|
private static int desiredVideoResolution = AUTOMATIC_VIDEO_QUALITY_VALUE;
|
||||||
|
|
||||||
|
private static boolean qualityNeedsUpdating;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The available qualities of the current video.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
private static VideoQuality[] currentQualities;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current quality of the video playing.
|
||||||
|
* This is always the actual quality even if Automatic quality is active.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
private static VideoQuality currentQuality;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current VideoQualityMenuInterface, set during setVideoQuality.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
private static VideoQualityMenuInterface currentMenuInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for when the current quality changes.
|
||||||
|
*/
|
||||||
|
public static final Event<VideoQuality> onQualityChange = new Event<>();
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static VideoQuality[] getCurrentQualities() {
|
||||||
|
return currentQualities;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static VideoQuality getCurrentQuality() {
|
||||||
|
return currentQuality;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*
|
*
|
||||||
@@ -52,12 +113,18 @@ public final class VideoInformation {
|
|||||||
*/
|
*/
|
||||||
public static void initialize(@NonNull PlaybackController playerController) {
|
public static void initialize(@NonNull PlaybackController playerController) {
|
||||||
try {
|
try {
|
||||||
|
Logger.printDebug(() -> "newVideoStarted");
|
||||||
|
|
||||||
playerControllerRef = new WeakReference<>(Objects.requireNonNull(playerController));
|
playerControllerRef = new WeakReference<>(Objects.requireNonNull(playerController));
|
||||||
videoTime = -1;
|
videoTime = -1;
|
||||||
videoLength = 0;
|
videoLength = 0;
|
||||||
playbackSpeed = DEFAULT_YOUTUBE_PLAYBACK_SPEED;
|
playbackSpeed = DEFAULT_YOUTUBE_PLAYBACK_SPEED;
|
||||||
|
desiredVideoResolution = AUTOMATIC_VIDEO_QUALITY_VALUE;
|
||||||
|
currentQualities = null;
|
||||||
|
currentMenuInterface = null;
|
||||||
|
setCurrentQuality(null);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "Failed to initialize", ex);
|
Logger.printException(() -> "initialize failure", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,14 +264,14 @@ public final class VideoInformation {
|
|||||||
if (controller == null) {
|
if (controller == null) {
|
||||||
Logger.printDebug(() -> "Cannot seekTo because player controller is null");
|
Logger.printDebug(() -> "Cannot seekTo because player controller is null");
|
||||||
} else {
|
} else {
|
||||||
if (controller.seekTo(adjustedSeekTime)) return true;
|
if (controller.patch_seekTo(adjustedSeekTime)) return true;
|
||||||
Logger.printDebug(() -> "seekTo did not succeeded. Trying MXD.");
|
Logger.printDebug(() -> "seekTo did not succeeded. Trying MXD.");
|
||||||
// Else the video is loading or changing videos, or video is casting to a different device.
|
// Else the video is loading or changing videos, or video is casting to a different device.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try calling the seekTo method of the MDX player director (called when casting).
|
// Try calling the seekTo method of the MDX player director (called when casting).
|
||||||
// The difference has to be a different second mark in order to avoid infinite skip loops
|
// The difference has to be a different second mark in order to avoid infinite skip loops
|
||||||
// as the Lounge API only supports seconds.
|
// as the Lounge API only supports whole seconds.
|
||||||
if (adjustedSeekTime / 1000 == videoTime / 1000) {
|
if (adjustedSeekTime / 1000 == videoTime / 1000) {
|
||||||
Logger.printDebug(() -> "Skipping seekTo for MDX because seek time is too small "
|
Logger.printDebug(() -> "Skipping seekTo for MDX because seek time is too small "
|
||||||
+ "(" + (adjustedSeekTime - videoTime) + "ms)");
|
+ "(" + (adjustedSeekTime - videoTime) + "ms)");
|
||||||
@@ -217,9 +284,9 @@ public final class VideoInformation {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return controller.seekTo(adjustedSeekTime);
|
return controller.patch_seekTo(adjustedSeekTime);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "Failed to seek", ex);
|
Logger.printException(() -> "seekTo failure", ex);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -239,7 +306,7 @@ public final class VideoInformation {
|
|||||||
if (controller == null) {
|
if (controller == null) {
|
||||||
Logger.printDebug(() -> "Cannot seek relative as player controller is null");
|
Logger.printDebug(() -> "Cannot seek relative as player controller is null");
|
||||||
} else {
|
} else {
|
||||||
controller.seekToRelative(seekTime);
|
controller.patch_seekToRelative(seekTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adjust the fine adjustment function so it's at least 1 second before/after.
|
// Adjust the fine adjustment function so it's at least 1 second before/after.
|
||||||
@@ -255,10 +322,10 @@ public final class VideoInformation {
|
|||||||
if (controller == null) {
|
if (controller == null) {
|
||||||
Logger.printDebug(() -> "Cannot seek relative as MXD player controller is null");
|
Logger.printDebug(() -> "Cannot seek relative as MXD player controller is null");
|
||||||
} else {
|
} else {
|
||||||
controller.seekToRelative(adjustedSeekTime);
|
controller.patch_seekToRelative(adjustedSeekTime);
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "Failed to seek relative", ex);
|
Logger.printException(() -> "seekToRelative failure", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,14 +406,13 @@ public final class VideoInformation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return If the playback is at the end of the video.
|
|
||||||
* <p>
|
|
||||||
* If video is playing in the background with no video visible,
|
* If video is playing in the background with no video visible,
|
||||||
* this always returns false (even if the video is actually at the end).
|
* this always returns false (even if the video is actually at the end).
|
||||||
* <p>
|
* <p>
|
||||||
* This is equivalent to checking for {@link VideoState#ENDED},
|
* This is equivalent to checking for {@link VideoState#ENDED},
|
||||||
* but can give a more up-to-date result for code calling from some hooks.
|
* but can give a more up-to-date result for code calling from some hooks.
|
||||||
*
|
*
|
||||||
|
* @return If the playback is at the end of the video.
|
||||||
* @see VideoState
|
* @see VideoState
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||||
@@ -373,4 +439,137 @@ public final class VideoInformation {
|
|||||||
playbackSpeed = newlyLoadedPlaybackSpeed;
|
playbackSpeed = newlyLoadedPlaybackSpeed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param resolution The desired video quality resolution to use.
|
||||||
|
*/
|
||||||
|
public static void setDesiredVideoResolution(int resolution) {
|
||||||
|
Utils.verifyOnMainThread();
|
||||||
|
Logger.printDebug(() -> "Setting desired video resolution: " + resolution);
|
||||||
|
desiredVideoResolution = resolution;
|
||||||
|
qualityNeedsUpdating = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setCurrentQuality(@Nullable VideoQuality quality) {
|
||||||
|
Utils.verifyOnMainThread();
|
||||||
|
if (currentQuality != quality) {
|
||||||
|
Logger.printDebug(() -> "Current quality changed to: " + quality);
|
||||||
|
currentQuality = quality;
|
||||||
|
onQualityChange.invoke(quality);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forcefully changes the video quality of the currently playing video.
|
||||||
|
*/
|
||||||
|
public static void changeQuality(VideoQuality quality) {
|
||||||
|
Utils.verifyOnMainThread();
|
||||||
|
|
||||||
|
if (currentMenuInterface == null) {
|
||||||
|
Logger.printException(() -> "Cannot change quality, menu interface is null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentMenuInterface.patch_setQuality(quality);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point. Fixes bad data used by YouTube.
|
||||||
|
* Issue can be reproduced by selecting 480p quality on any Short,
|
||||||
|
* and occasionally with random regular videos.
|
||||||
|
*/
|
||||||
|
public static int fixVideoQualityResolution(String name, int quality) {
|
||||||
|
try {
|
||||||
|
if (!name.startsWith(Integer.toString(quality))) {
|
||||||
|
final int suffixIndex = name.indexOf('p');
|
||||||
|
if (suffixIndex > 0) {
|
||||||
|
final int fixedQuality = Integer.parseInt(name.substring(0, suffixIndex));
|
||||||
|
Logger.printDebug(() -> "Fixing wrong quality resolution from: " +
|
||||||
|
name + "(" + quality + ") to: " + name + ")" + fixedQuality + ")");
|
||||||
|
return fixedQuality;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "fixVideoQualityResolution failed", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return quality;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*
|
||||||
|
* @param qualities Video qualities available, ordered from largest to smallest, with index 0 being the 'automatic' value of -2
|
||||||
|
* @param originalQualityIndex quality index to use, as chosen by YouTube
|
||||||
|
*/
|
||||||
|
public static int setVideoQuality(VideoQuality[] qualities, VideoQualityMenuInterface menu, int originalQualityIndex) {
|
||||||
|
try {
|
||||||
|
Utils.verifyOnMainThread();
|
||||||
|
currentMenuInterface = menu;
|
||||||
|
|
||||||
|
final boolean availableQualitiesChanged = (currentQualities == null)
|
||||||
|
|| !Arrays.equals(currentQualities, qualities);
|
||||||
|
if (availableQualitiesChanged) {
|
||||||
|
currentQualities = qualities;
|
||||||
|
Logger.printDebug(() -> "VideoQualities: " + Arrays.toString(currentQualities));
|
||||||
|
}
|
||||||
|
|
||||||
|
// On extremely slow internet connections the index can initially be -1
|
||||||
|
originalQualityIndex = Math.max(0, originalQualityIndex);
|
||||||
|
|
||||||
|
VideoQuality updatedCurrentQuality = qualities[originalQualityIndex];
|
||||||
|
if (updatedCurrentQuality.patch_getResolution() != AUTOMATIC_VIDEO_QUALITY_VALUE
|
||||||
|
&& (currentQuality == null || currentQuality != updatedCurrentQuality)) {
|
||||||
|
setCurrentQuality(updatedCurrentQuality);
|
||||||
|
}
|
||||||
|
|
||||||
|
final int preferredQuality = desiredVideoResolution;
|
||||||
|
if (preferredQuality == AUTOMATIC_VIDEO_QUALITY_VALUE) {
|
||||||
|
return originalQualityIndex; // Nothing to do.
|
||||||
|
}
|
||||||
|
|
||||||
|
// After changing videos the qualities can initially be for the prior video.
|
||||||
|
// If the qualities have changed and the default is not auto then an update is needed.
|
||||||
|
if (qualityNeedsUpdating) {
|
||||||
|
qualityNeedsUpdating = false;
|
||||||
|
} else if (!availableQualitiesChanged) {
|
||||||
|
return originalQualityIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the highest quality that is equal to or less than the preferred.
|
||||||
|
int i = 0;
|
||||||
|
final int lastQualityIndex = qualities.length - 1;
|
||||||
|
for (VideoQuality quality : qualities) {
|
||||||
|
final int qualityResolution = quality.patch_getResolution();
|
||||||
|
if ((qualityResolution != AUTOMATIC_VIDEO_QUALITY_VALUE && qualityResolution <= preferredQuality)
|
||||||
|
// Use the lowest video quality if the default is lower than all available.
|
||||||
|
|| i == lastQualityIndex) {
|
||||||
|
final boolean qualityNeedsChange = (i != originalQualityIndex);
|
||||||
|
Logger.printDebug(() -> qualityNeedsChange
|
||||||
|
? "Changing video quality from: " + updatedCurrentQuality + " to: " + quality
|
||||||
|
: "Video is already the preferred quality: " + quality
|
||||||
|
);
|
||||||
|
|
||||||
|
// On first load of a new regular video, if the video is already the
|
||||||
|
// desired quality then the quality flyout will show 'Auto' (ie: Auto (720p)).
|
||||||
|
//
|
||||||
|
// To prevent user confusion, set the video index even if the
|
||||||
|
// quality is already correct so the UI picker will not display "Auto".
|
||||||
|
//
|
||||||
|
// Only change Shorts quality if the quality actually needs to change,
|
||||||
|
// because the "auto" option is not shown in the flyout
|
||||||
|
// and setting the same quality again can cause the Short to restart.
|
||||||
|
if (qualityNeedsChange || !ShortsPlayerState.isOpen()) {
|
||||||
|
changeQuality(quality);
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalQualityIndex;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "setVideoQuality failure", ex);
|
||||||
|
}
|
||||||
|
return originalQualityIndex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,16 @@ package app.revanced.extension.youtube.patches.announcements;
|
|||||||
|
|
||||||
import static android.text.Html.FROM_HTML_MODE_COMPACT;
|
import static android.text.Html.FROM_HTML_MODE_COMPACT;
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||||
import static app.revanced.extension.youtube.patches.announcements.requests.AnnouncementsRoutes.GET_LATEST_ANNOUNCEMENTS;
|
import static app.revanced.extension.youtube.patches.announcements.requests.AnnouncementsRoutes.GET_LATEST_ANNOUNCEMENTS;
|
||||||
import static app.revanced.extension.youtube.patches.announcements.requests.AnnouncementsRoutes.GET_LATEST_ANNOUNCEMENT_IDS;
|
import static app.revanced.extension.youtube.patches.announcements.requests.AnnouncementsRoutes.GET_LATEST_ANNOUNCEMENT_IDS;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.AlertDialog;
|
import android.app.Dialog;
|
||||||
import android.text.Html;
|
import android.text.Html;
|
||||||
import android.text.method.LinkMovementMethod;
|
import android.util.Pair;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
@@ -56,10 +59,11 @@ public final class AnnouncementsPatch {
|
|||||||
int id = Settings.ANNOUNCEMENT_LAST_ID.defaultValue;
|
int id = Settings.ANNOUNCEMENT_LAST_ID.defaultValue;
|
||||||
try {
|
try {
|
||||||
final var announcementIds = new JSONArray(jsonString);
|
final var announcementIds = new JSONArray(jsonString);
|
||||||
id = announcementIds.getJSONObject(0).getInt("id");
|
if (announcementIds.length() == 0) return true;
|
||||||
|
|
||||||
|
id = announcementIds.getJSONObject(0).getInt("id");
|
||||||
} catch (Throwable ex) {
|
} catch (Throwable ex) {
|
||||||
Logger.printException(() -> "Failed to parse announcement IDs", ex);
|
Logger.printException(() -> "Failed to parse announcement ID", ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do not show the announcement, if the last announcement id is the same as the current one.
|
// Do not show the announcement, if the last announcement id is the same as the current one.
|
||||||
@@ -120,25 +124,38 @@ public final class AnnouncementsPatch {
|
|||||||
final Level finalLevel = level;
|
final Level finalLevel = level;
|
||||||
|
|
||||||
Utils.runOnMainThread(() -> {
|
Utils.runOnMainThread(() -> {
|
||||||
// Show the announcement.
|
// Create the custom dialog and show the announcement.
|
||||||
var alert = new AlertDialog.Builder(context)
|
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
||||||
.setTitle(finalTitle)
|
context,
|
||||||
.setMessage(finalMessage)
|
finalTitle, // Title.
|
||||||
.setIcon(finalLevel.icon)
|
finalMessage, // Message.
|
||||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
null, // No EditText.
|
||||||
Settings.ANNOUNCEMENT_LAST_ID.save(finalId);
|
null, // OK button text.
|
||||||
dialog.dismiss();
|
() -> Settings.ANNOUNCEMENT_LAST_ID.save(finalId), // OK button action.
|
||||||
}).setNegativeButton(str("revanced_announcements_dialog_dismiss"), (dialog, which) -> {
|
() -> {}, // Cancel button action (dismiss only).
|
||||||
dialog.dismiss();
|
str("revanced_announcements_dialog_dismiss"), // Neutral button text.
|
||||||
})
|
() -> {}, // Neutral button action (dismiss only).
|
||||||
.setCancelable(false)
|
true // Dismiss dialog when onNeutralClick.
|
||||||
.create();
|
);
|
||||||
|
|
||||||
Utils.showDialog(context, alert, false, (AlertDialog dialog) -> {
|
Dialog dialog = dialogPair.first;
|
||||||
// Make links clickable.
|
LinearLayout mainLayout = dialogPair.second;
|
||||||
((TextView) dialog.findViewById(android.R.id.message))
|
|
||||||
.setMovementMethod(LinkMovementMethod.getInstance());
|
// Set the icon for the title TextView
|
||||||
});
|
for (int i = 0, childCould = mainLayout.getChildCount(); i < childCould; i++) {
|
||||||
|
View child = mainLayout.getChildAt(i);
|
||||||
|
if (child instanceof TextView childTextView && finalTitle.equals(childTextView.getText().toString())) {
|
||||||
|
childTextView.setCompoundDrawablesWithIntrinsicBounds(
|
||||||
|
finalLevel.icon, 0, 0, 0);
|
||||||
|
childTextView.setCompoundDrawablePadding(dipToPixels(8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set dialog as non-cancelable.
|
||||||
|
dialog.setCancelable(false);
|
||||||
|
|
||||||
|
// Show the dialog.
|
||||||
|
Utils.showDialog(context, dialog);
|
||||||
});
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
final var message = "Failed to get announcement";
|
final var message = "Failed to get announcement";
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import static app.revanced.extension.shared.requests.Route.Method.GET;
|
|||||||
|
|
||||||
public class AnnouncementsRoutes {
|
public class AnnouncementsRoutes {
|
||||||
private static final String ANNOUNCEMENTS_PROVIDER = "https://api.revanced.app/v4";
|
private static final String ANNOUNCEMENTS_PROVIDER = "https://api.revanced.app/v4";
|
||||||
public static final Route GET_LATEST_ANNOUNCEMENT_IDS = new Route(GET, "/announcements/latest/id?tag=youtube");
|
public static final Route GET_LATEST_ANNOUNCEMENT_IDS = new Route(GET, "/announcements/latest/id?tag=\uD83C\uDF9E\uFE0F%20YouTube");
|
||||||
public static final Route GET_LATEST_ANNOUNCEMENTS = new Route(GET, "/announcements/latest?tag=youtube");
|
public static final Route GET_LATEST_ANNOUNCEMENTS = new Route(GET, "/announcements/latest?tag=\uD83C\uDF9E\uFE0F%20YouTube");
|
||||||
|
|
||||||
private AnnouncementsRoutes() {
|
private AnnouncementsRoutes() {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import android.app.Instrumentation;
|
|||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
@@ -34,10 +32,6 @@ public final class AdsFilter extends Filter {
|
|||||||
private final StringFilterGroup playerShoppingShelf;
|
private final StringFilterGroup playerShoppingShelf;
|
||||||
private final ByteArrayFilterGroup playerShoppingShelfBuffer;
|
private final ByteArrayFilterGroup playerShoppingShelfBuffer;
|
||||||
|
|
||||||
private final StringFilterGroup channelProfile;
|
|
||||||
private final ByteArrayFilterGroup visitStoreButton;
|
|
||||||
|
|
||||||
private final StringFilterGroup shoppingLinks;
|
|
||||||
|
|
||||||
public AdsFilter() {
|
public AdsFilter() {
|
||||||
exceptions.addPatterns(
|
exceptions.addPatterns(
|
||||||
@@ -91,6 +85,7 @@ public final class AdsFilter extends Filter {
|
|||||||
"text_image_no_button_layout", // Tablet layout search results.
|
"text_image_no_button_layout", // Tablet layout search results.
|
||||||
"video_display_button_group_layout",
|
"video_display_button_group_layout",
|
||||||
"video_display_carousel_button_group_layout",
|
"video_display_carousel_button_group_layout",
|
||||||
|
"video_display_carousel_buttoned_short_dr_layout",
|
||||||
"video_display_full_buttoned_short_dr_layout",
|
"video_display_full_buttoned_short_dr_layout",
|
||||||
"video_display_full_layout",
|
"video_display_full_layout",
|
||||||
"watch_metadata_app_promo"
|
"watch_metadata_app_promo"
|
||||||
@@ -107,37 +102,25 @@ public final class AdsFilter extends Filter {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final var viewProducts = new StringFilterGroup(
|
final var viewProducts = new StringFilterGroup(
|
||||||
Settings.HIDE_PRODUCTS_BANNER,
|
Settings.HIDE_VIEW_PRODUCTS_BANNER,
|
||||||
"product_item",
|
"product_item",
|
||||||
"products_in_video",
|
"products_in_video",
|
||||||
"shopping_overlay.eml", // Video player overlay shopping links.
|
"shopping_overlay.eml" // Video player overlay shopping links.
|
||||||
"shopping_carousel.eml" // Channel profile shopping shelf.
|
|
||||||
);
|
);
|
||||||
|
|
||||||
shoppingLinks = new StringFilterGroup(
|
final var shoppingLinks = new StringFilterGroup(
|
||||||
Settings.HIDE_SHOPPING_LINKS,
|
Settings.HIDE_SHOPPING_LINKS,
|
||||||
"expandable_list"
|
"shopping_description_shelf.eml"
|
||||||
);
|
);
|
||||||
|
|
||||||
playerShoppingShelf = new StringFilterGroup(
|
playerShoppingShelf = new StringFilterGroup(
|
||||||
Settings.HIDE_PLAYER_STORE_SHELF,
|
Settings.HIDE_CREATOR_STORE_SHELF,
|
||||||
"horizontal_shelf.eml"
|
"horizontal_shelf.eml"
|
||||||
);
|
);
|
||||||
|
|
||||||
playerShoppingShelfBuffer = new ByteArrayFilterGroup(
|
playerShoppingShelfBuffer = new ByteArrayFilterGroup(
|
||||||
null,
|
null,
|
||||||
"shopping_item_card_list.eml"
|
"shopping_item_card_list"
|
||||||
);
|
|
||||||
|
|
||||||
channelProfile = new StringFilterGroup(
|
|
||||||
Settings.HIDE_VISIT_STORE_BUTTON,
|
|
||||||
"channel_profile.eml",
|
|
||||||
"page_header.eml"
|
|
||||||
);
|
|
||||||
|
|
||||||
visitStoreButton = new ByteArrayFilterGroup(
|
|
||||||
null,
|
|
||||||
"header_store_button"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
final var webLinkPanel = new StringFilterGroup(
|
final var webLinkPanel = new StringFilterGroup(
|
||||||
@@ -147,7 +130,8 @@ public final class AdsFilter extends Filter {
|
|||||||
|
|
||||||
final var merchandise = new StringFilterGroup(
|
final var merchandise = new StringFilterGroup(
|
||||||
Settings.HIDE_MERCHANDISE_BANNERS,
|
Settings.HIDE_MERCHANDISE_BANNERS,
|
||||||
"product_carousel"
|
"product_carousel",
|
||||||
|
"shopping_carousel.eml" // Channel profile shopping shelf.
|
||||||
);
|
);
|
||||||
|
|
||||||
final var selfSponsor = new StringFilterGroup(
|
final var selfSponsor = new StringFilterGroup(
|
||||||
@@ -156,29 +140,23 @@ public final class AdsFilter extends Filter {
|
|||||||
);
|
);
|
||||||
|
|
||||||
addPathCallbacks(
|
addPathCallbacks(
|
||||||
|
fullscreenAd,
|
||||||
generalAds,
|
generalAds,
|
||||||
merchandise,
|
merchandise,
|
||||||
viewProducts,
|
movieAds,
|
||||||
selfSponsor,
|
|
||||||
fullscreenAd,
|
|
||||||
channelProfile,
|
|
||||||
webLinkPanel,
|
|
||||||
shoppingLinks,
|
|
||||||
playerShoppingShelf,
|
playerShoppingShelf,
|
||||||
movieAds
|
selfSponsor,
|
||||||
|
shoppingLinks,
|
||||||
|
viewProducts,
|
||||||
|
webLinkPanel
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
boolean isFiltered(String identifier, String path, byte[] buffer,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
if (matchedGroup == playerShoppingShelf) {
|
if (matchedGroup == playerShoppingShelf) {
|
||||||
return contentIndex == 0 && playerShoppingShelfBuffer.check(protobufBufferArray).isFiltered();
|
return contentIndex == 0 && playerShoppingShelfBuffer.check(buffer).isFiltered();
|
||||||
}
|
|
||||||
|
|
||||||
// Check for the index because of likelihood of false positives.
|
|
||||||
if (contentIndex != 0 && matchedGroup == shoppingLinks) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exceptions.matches(path)) {
|
if (exceptions.matches(path)) {
|
||||||
@@ -192,10 +170,6 @@ public final class AdsFilter extends Filter {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matchedGroup == channelProfile) {
|
|
||||||
return visitStoreButton.check(protobufBufferArray).isFiltered();
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
package app.revanced.extension.youtube.patches.components;
|
package app.revanced.extension.youtube.patches.components;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import app.revanced.extension.youtube.patches.playback.quality.AdvancedVideoQualityMenuPatch;
|
import app.revanced.extension.youtube.patches.playback.quality.AdvancedVideoQualityMenuPatch;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abuse LithoFilter for {@link AdvancedVideoQualityMenuPatch}.
|
* LithoFilter for {@link AdvancedVideoQualityMenuPatch}.
|
||||||
*/
|
*/
|
||||||
public final class AdvancedVideoQualityMenuFilter extends Filter {
|
public final class AdvancedVideoQualityMenuFilter extends Filter {
|
||||||
// Must be volatile or synchronized, as litho filtering runs off main thread
|
// Must be volatile or synchronized, as litho filtering runs off main thread
|
||||||
@@ -21,7 +19,7 @@ public final class AdvancedVideoQualityMenuFilter extends Filter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
boolean isFiltered(String identifier, String path, byte[] buffer,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
isVideoQualityMenuVisible = true;
|
isVideoQualityMenuVisible = true;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package app.revanced.extension.youtube.patches.components;
|
package app.revanced.extension.youtube.patches.components;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
@@ -46,7 +44,7 @@ final class ButtonsFilter extends Filter {
|
|||||||
"|download_button.eml"
|
"|download_button.eml"
|
||||||
),
|
),
|
||||||
new StringFilterGroup(
|
new StringFilterGroup(
|
||||||
Settings.HIDE_PLAYLIST_BUTTON,
|
Settings.HIDE_SAVE_BUTTON,
|
||||||
"|save_to_playlist_button"
|
"|save_to_playlist_button"
|
||||||
),
|
),
|
||||||
new StringFilterGroup(
|
new StringFilterGroup(
|
||||||
@@ -76,11 +74,23 @@ final class ButtonsFilter extends Filter {
|
|||||||
Settings.HIDE_ASK_BUTTON,
|
Settings.HIDE_ASK_BUTTON,
|
||||||
"yt_fill_spark"
|
"yt_fill_spark"
|
||||||
),
|
),
|
||||||
|
new ByteArrayFilterGroup(
|
||||||
|
Settings.HIDE_STOP_ADS_BUTTON,
|
||||||
|
"yt_outline_slash_circle_left"
|
||||||
|
),
|
||||||
// Check for clip button both here and using a path filter,
|
// Check for clip button both here and using a path filter,
|
||||||
// as there's a chance the path is a generic action button and won't contain 'clip_button'
|
// as there's a chance the path is a generic action button and won't contain 'clip_button'
|
||||||
new ByteArrayFilterGroup(
|
new ByteArrayFilterGroup(
|
||||||
Settings.HIDE_CLIP_BUTTON,
|
Settings.HIDE_CLIP_BUTTON,
|
||||||
"yt_outline_scissors"
|
"yt_outline_scissors"
|
||||||
|
),
|
||||||
|
new ByteArrayFilterGroup(
|
||||||
|
Settings.HIDE_HYPE_BUTTON,
|
||||||
|
"yt_outline_star_shooting"
|
||||||
|
),
|
||||||
|
new ByteArrayFilterGroup(
|
||||||
|
Settings.HIDE_PROMOTE_BUTTON,
|
||||||
|
"yt_outline_megaphone"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -96,7 +106,7 @@ final class ButtonsFilter extends Filter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
boolean isFiltered(String identifier, String path, byte[] buffer,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
if (matchedGroup == likeSubscribeGlow) {
|
if (matchedGroup == likeSubscribeGlow) {
|
||||||
return (path.startsWith(VIDEO_ACTION_BAR_PATH_PREFIX) || path.startsWith(COMPACT_CHANNEL_BAR_PATH_PREFIX))
|
return (path.startsWith(VIDEO_ACTION_BAR_PATH_PREFIX) || path.startsWith(COMPACT_CHANNEL_BAR_PATH_PREFIX))
|
||||||
@@ -113,7 +123,7 @@ final class ButtonsFilter extends Filter {
|
|||||||
// Make sure the current path is the right one
|
// Make sure the current path is the right one
|
||||||
// to avoid false positives.
|
// to avoid false positives.
|
||||||
return path.startsWith(VIDEO_ACTION_BAR_PATH)
|
return path.startsWith(VIDEO_ACTION_BAR_PATH)
|
||||||
&& bufferButtonsGroupList.check(protobufBufferArray).isFiltered();
|
&& bufferButtonsGroupList.check(buffer).isFiltered();
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
package app.revanced.extension.youtube.patches.components;
|
package app.revanced.extension.youtube.patches.components;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
import app.revanced.extension.youtube.shared.PlayerType;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
final class CommentsFilter extends Filter {
|
final class CommentsFilter extends Filter {
|
||||||
|
|
||||||
private final StringFilterGroup filterChipBar;
|
private final StringFilterGroup chipBar;
|
||||||
private final ByteArrayFilterGroup aiCommentsSummary;
|
private final ByteArrayFilterGroup aiCommentsSummary;
|
||||||
|
|
||||||
public CommentsFilter() {
|
public CommentsFilter() {
|
||||||
@@ -16,6 +15,21 @@ final class CommentsFilter extends Filter {
|
|||||||
"live_chat_summary_banner.eml"
|
"live_chat_summary_banner.eml"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
chipBar = new StringFilterGroup(
|
||||||
|
Settings.HIDE_COMMENTS_AI_SUMMARY,
|
||||||
|
"chip_bar.eml"
|
||||||
|
);
|
||||||
|
|
||||||
|
aiCommentsSummary = new ByteArrayFilterGroup(
|
||||||
|
null,
|
||||||
|
"yt_fill_spark_"
|
||||||
|
);
|
||||||
|
|
||||||
|
var channelGuidelines = new StringFilterGroup(
|
||||||
|
Settings.HIDE_COMMENTS_CHANNEL_GUIDELINES,
|
||||||
|
"channel_guidelines_entry_banner"
|
||||||
|
);
|
||||||
|
|
||||||
var commentsByMembers = new StringFilterGroup(
|
var commentsByMembers = new StringFilterGroup(
|
||||||
Settings.HIDE_COMMENTS_BY_MEMBERS_HEADER,
|
Settings.HIDE_COMMENTS_BY_MEMBERS_HEADER,
|
||||||
"sponsorships_comments_header.eml",
|
"sponsorships_comments_header.eml",
|
||||||
@@ -28,6 +42,11 @@ final class CommentsFilter extends Filter {
|
|||||||
"_comments"
|
"_comments"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
var communityGuidelines = new StringFilterGroup(
|
||||||
|
Settings.HIDE_COMMENTS_COMMUNITY_GUIDELINES,
|
||||||
|
"community_guidelines"
|
||||||
|
);
|
||||||
|
|
||||||
var createAShort = new StringFilterGroup(
|
var createAShort = new StringFilterGroup(
|
||||||
Settings.HIDE_COMMENTS_CREATE_A_SHORT_BUTTON,
|
Settings.HIDE_COMMENTS_CREATE_A_SHORT_BUTTON,
|
||||||
"composer_short_creation_button.eml"
|
"composer_short_creation_button.eml"
|
||||||
@@ -50,33 +69,28 @@ final class CommentsFilter extends Filter {
|
|||||||
"composer_timestamp_button.eml"
|
"composer_timestamp_button.eml"
|
||||||
);
|
);
|
||||||
|
|
||||||
filterChipBar = new StringFilterGroup(
|
|
||||||
Settings.HIDE_COMMENTS_AI_SUMMARY,
|
|
||||||
"filter_chip_bar.eml"
|
|
||||||
);
|
|
||||||
|
|
||||||
aiCommentsSummary = new ByteArrayFilterGroup(
|
|
||||||
null,
|
|
||||||
"yt_fill_spark_"
|
|
||||||
);
|
|
||||||
|
|
||||||
addPathCallbacks(
|
addPathCallbacks(
|
||||||
|
channelGuidelines,
|
||||||
chatSummary,
|
chatSummary,
|
||||||
|
chipBar,
|
||||||
commentsByMembers,
|
commentsByMembers,
|
||||||
comments,
|
comments,
|
||||||
|
communityGuidelines,
|
||||||
createAShort,
|
createAShort,
|
||||||
previewComment,
|
previewComment,
|
||||||
thanksButton,
|
thanksButton,
|
||||||
timestampButton,
|
timestampButton
|
||||||
filterChipBar
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
boolean isFiltered(String identifier, String path, byte[] buffer,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
if (matchedGroup == filterChipBar) {
|
if (matchedGroup == chipBar) {
|
||||||
return aiCommentsSummary.check(protobufBufferArray).isFiltered();
|
// Playlist sort button uses same components and must only filter if the player is opened.
|
||||||
|
return PlayerType.getCurrent().isMaximizedOrFullscreen()
|
||||||
|
&& aiCommentsSummary.check(buffer).isFiltered();
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package app.revanced.extension.youtube.patches.components;
|
|||||||
import static app.revanced.extension.shared.StringRef.str;
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
@@ -146,7 +145,7 @@ final class CustomFilter extends Filter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
boolean isFiltered(String identifier, String path, byte[] buffer,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
// All callbacks are custom filter groups.
|
// All callbacks are custom filter groups.
|
||||||
CustomFilterGroup custom = (CustomFilterGroup) matchedGroup;
|
CustomFilterGroup custom = (CustomFilterGroup) matchedGroup;
|
||||||
@@ -158,6 +157,6 @@ final class CustomFilter extends Filter {
|
|||||||
return true; // No buffer filter, only path filtering.
|
return true; // No buffer filter, only path filtering.
|
||||||
}
|
}
|
||||||
|
|
||||||
return custom.bufferSearch.matches(protobufBufferArray);
|
return custom.bufferSearch.matches(buffer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
package app.revanced.extension.youtube.patches.components;
|
package app.revanced.extension.youtube.patches.components;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import app.revanced.extension.youtube.StringTrieSearch;
|
import app.revanced.extension.youtube.StringTrieSearch;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
import app.revanced.extension.youtube.shared.PlayerType;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
final class DescriptionComponentsFilter extends Filter {
|
final class DescriptionComponentsFilter extends Filter {
|
||||||
@@ -14,6 +13,11 @@ final class DescriptionComponentsFilter extends Filter {
|
|||||||
|
|
||||||
private final StringFilterGroup macroMarkersCarousel;
|
private final StringFilterGroup macroMarkersCarousel;
|
||||||
|
|
||||||
|
private final StringFilterGroup horizontalShelf;
|
||||||
|
private final ByteArrayFilterGroup cellVideoAttribute;
|
||||||
|
|
||||||
|
private final StringFilterGroup aiGeneratedVideoSummarySection;
|
||||||
|
|
||||||
public DescriptionComponentsFilter() {
|
public DescriptionComponentsFilter() {
|
||||||
exceptions.addPatterns(
|
exceptions.addPatterns(
|
||||||
"compact_channel",
|
"compact_channel",
|
||||||
@@ -23,7 +27,7 @@ final class DescriptionComponentsFilter extends Filter {
|
|||||||
"metadata"
|
"metadata"
|
||||||
);
|
);
|
||||||
|
|
||||||
final StringFilterGroup aiGeneratedVideoSummarySection = new StringFilterGroup(
|
aiGeneratedVideoSummarySection = new StringFilterGroup(
|
||||||
Settings.HIDE_AI_GENERATED_VIDEO_SUMMARY_SECTION,
|
Settings.HIDE_AI_GENERATED_VIDEO_SUMMARY_SECTION,
|
||||||
"cell_expandable_metadata.eml"
|
"cell_expandable_metadata.eml"
|
||||||
);
|
);
|
||||||
@@ -35,8 +39,7 @@ final class DescriptionComponentsFilter extends Filter {
|
|||||||
|
|
||||||
final StringFilterGroup attributesSection = new StringFilterGroup(
|
final StringFilterGroup attributesSection = new StringFilterGroup(
|
||||||
Settings.HIDE_ATTRIBUTES_SECTION,
|
Settings.HIDE_ATTRIBUTES_SECTION,
|
||||||
"gaming_section",
|
// "gaming_section", "music_section"
|
||||||
"music_section",
|
|
||||||
"video_attributes_section"
|
"video_attributes_section"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -76,25 +79,46 @@ final class DescriptionComponentsFilter extends Filter {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
horizontalShelf = new StringFilterGroup(
|
||||||
|
Settings.HIDE_ATTRIBUTES_SECTION,
|
||||||
|
"horizontal_shelf.eml"
|
||||||
|
);
|
||||||
|
|
||||||
|
cellVideoAttribute = new ByteArrayFilterGroup(
|
||||||
|
null,
|
||||||
|
"cell_video_attribute"
|
||||||
|
);
|
||||||
|
|
||||||
addPathCallbacks(
|
addPathCallbacks(
|
||||||
aiGeneratedVideoSummarySection,
|
aiGeneratedVideoSummarySection,
|
||||||
askSection,
|
askSection,
|
||||||
attributesSection,
|
attributesSection,
|
||||||
infoCardsSection,
|
infoCardsSection,
|
||||||
|
horizontalShelf,
|
||||||
howThisWasMadeSection,
|
howThisWasMadeSection,
|
||||||
|
macroMarkersCarousel,
|
||||||
podcastSection,
|
podcastSection,
|
||||||
transcriptSection,
|
transcriptSection
|
||||||
macroMarkersCarousel
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
boolean isFiltered(String identifier, String path, byte[] buffer,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
|
|
||||||
|
if (matchedGroup == aiGeneratedVideoSummarySection) {
|
||||||
|
// Only hide if player is open, in case this component is used somewhere else.
|
||||||
|
return PlayerType.getCurrent().isMaximizedOrFullscreen();
|
||||||
|
}
|
||||||
|
|
||||||
if (exceptions.matches(path)) return false;
|
if (exceptions.matches(path)) return false;
|
||||||
|
|
||||||
if (matchedGroup == macroMarkersCarousel) {
|
if (matchedGroup == macroMarkersCarousel) {
|
||||||
return contentIndex == 0 && macroMarkersCarouselGroupList.check(protobufBufferArray).isFiltered();
|
return contentIndex == 0 && macroMarkersCarouselGroupList.check(buffer).isFiltered();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchedGroup == horizontalShelf) {
|
||||||
|
return cellVideoAttribute.check(buffer).isFiltered();
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package app.revanced.extension.youtube.patches.components;
|
package app.revanced.extension.youtube.patches.components;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -59,7 +57,6 @@ abstract class Filter {
|
|||||||
* Called after an enabled filter has been matched.
|
* Called after an enabled filter has been matched.
|
||||||
* Default implementation is to always filter the matched component and log the action.
|
* Default implementation is to always filter the matched component and log the action.
|
||||||
* Subclasses can perform additional or different checks if needed.
|
* Subclasses can perform additional or different checks if needed.
|
||||||
*
|
|
||||||
* <p>
|
* <p>
|
||||||
* Method is called off the main thread.
|
* Method is called off the main thread.
|
||||||
*
|
*
|
||||||
@@ -68,7 +65,7 @@ abstract class Filter {
|
|||||||
* @param contentIndex Matched index of the identifier or path.
|
* @param contentIndex Matched index of the identifier or path.
|
||||||
* @return True if the litho component should be filtered out.
|
* @return True if the litho component should be filtered out.
|
||||||
*/
|
*/
|
||||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
boolean isFiltered(String identifier, String path, byte[] buffer,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ package app.revanced.extension.youtube.patches.components;
|
|||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public final class HideInfoCardsFilterPatch extends Filter {
|
public final class HideInfoCardsFilter extends Filter {
|
||||||
|
|
||||||
public HideInfoCardsFilterPatch() {
|
public HideInfoCardsFilter() {
|
||||||
addIdentifierCallbacks(
|
addIdentifierCallbacks(
|
||||||
new StringFilterGroup(
|
new StringFilterGroup(
|
||||||
Settings.HIDE_INFO_CARDS,
|
Settings.HIDE_INFO_CARDS,
|
||||||
@@ -554,7 +554,7 @@ final class KeywordContentFilter extends Filter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
boolean isFiltered(String identifier, String path, byte[] buffer,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
if (contentIndex != 0 && matchedGroup == startsWithFilter) {
|
if (contentIndex != 0 && matchedGroup == startsWithFilter) {
|
||||||
return false;
|
return false;
|
||||||
@@ -574,7 +574,7 @@ final class KeywordContentFilter extends Filter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MutableReference<String> matchRef = new MutableReference<>();
|
MutableReference<String> matchRef = new MutableReference<>();
|
||||||
if (bufferSearch.matches(protobufBufferArray, matchRef)) {
|
if (bufferSearch.matches(buffer, matchRef)) {
|
||||||
updateStats(true, matchRef.value);
|
updateStats(true, matchRef.value);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButt
|
|||||||
|
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.youtube.StringTrieSearch;
|
import app.revanced.extension.youtube.StringTrieSearch;
|
||||||
|
import app.revanced.extension.youtube.patches.ChangeHeaderPatch;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
import app.revanced.extension.youtube.shared.NavigationBar;
|
import app.revanced.extension.youtube.shared.NavigationBar;
|
||||||
import app.revanced.extension.youtube.shared.PlayerType;
|
import app.revanced.extension.youtube.shared.PlayerType;
|
||||||
@@ -30,7 +32,8 @@ public final class LayoutComponentsFilter extends Filter {
|
|||||||
);
|
);
|
||||||
|
|
||||||
private final StringTrieSearch exceptions = new StringTrieSearch();
|
private final StringTrieSearch exceptions = new StringTrieSearch();
|
||||||
private final StringFilterGroup inFeedSurvey;
|
private final StringFilterGroup communityPosts;
|
||||||
|
private final StringFilterGroup surveys;
|
||||||
private final StringFilterGroup notifyMe;
|
private final StringFilterGroup notifyMe;
|
||||||
private final StringFilterGroup singleItemInformationPanel;
|
private final StringFilterGroup singleItemInformationPanel;
|
||||||
private final StringFilterGroup expandableMetadata;
|
private final StringFilterGroup expandableMetadata;
|
||||||
@@ -39,6 +42,10 @@ public final class LayoutComponentsFilter extends Filter {
|
|||||||
private final ByteArrayFilterGroup joinMembershipButton;
|
private final ByteArrayFilterGroup joinMembershipButton;
|
||||||
private final StringFilterGroup horizontalShelves;
|
private final StringFilterGroup horizontalShelves;
|
||||||
private final ByteArrayFilterGroup ticketShelf;
|
private final ByteArrayFilterGroup ticketShelf;
|
||||||
|
private final StringFilterGroup chipBar;
|
||||||
|
private final StringFilterGroup channelProfile;
|
||||||
|
private final ByteArrayFilterGroupList channelProfileBuffer;
|
||||||
|
private final ByteArrayFilterGroup playablesBuffer;
|
||||||
|
|
||||||
public LayoutComponentsFilter() {
|
public LayoutComponentsFilter() {
|
||||||
exceptions.addPatterns(
|
exceptions.addPatterns(
|
||||||
@@ -63,7 +70,7 @@ public final class LayoutComponentsFilter extends Filter {
|
|||||||
|
|
||||||
// Paths.
|
// Paths.
|
||||||
|
|
||||||
final var communityPosts = new StringFilterGroup(
|
communityPosts = new StringFilterGroup(
|
||||||
Settings.HIDE_COMMUNITY_POSTS,
|
Settings.HIDE_COMMUNITY_POSTS,
|
||||||
"post_base_wrapper", // may be obsolete and no longer needed.
|
"post_base_wrapper", // may be obsolete and no longer needed.
|
||||||
"text_post_root.eml",
|
"text_post_root.eml",
|
||||||
@@ -80,18 +87,13 @@ public final class LayoutComponentsFilter extends Filter {
|
|||||||
"poll_post_responsive_root.eml"
|
"poll_post_responsive_root.eml"
|
||||||
);
|
);
|
||||||
|
|
||||||
final var communityGuidelines = new StringFilterGroup(
|
|
||||||
Settings.HIDE_COMMUNITY_GUIDELINES,
|
|
||||||
"community_guidelines"
|
|
||||||
);
|
|
||||||
|
|
||||||
final var subscribersCommunityGuidelines = new StringFilterGroup(
|
final var subscribersCommunityGuidelines = new StringFilterGroup(
|
||||||
Settings.HIDE_SUBSCRIBERS_COMMUNITY_GUIDELINES,
|
Settings.HIDE_SUBSCRIBERS_COMMUNITY_GUIDELINES,
|
||||||
"sponsorships_comments_upsell"
|
"sponsorships_comments_upsell"
|
||||||
);
|
);
|
||||||
|
|
||||||
final var channelMemberShelf = new StringFilterGroup(
|
final var channelMembersShelf = new StringFilterGroup(
|
||||||
Settings.HIDE_CHANNEL_MEMBER_SHELF,
|
Settings.HIDE_MEMBERS_SHELF,
|
||||||
"member_recognition_shelf"
|
"member_recognition_shelf"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -105,8 +107,13 @@ public final class LayoutComponentsFilter extends Filter {
|
|||||||
"subscriptions_chip_bar"
|
"subscriptions_chip_bar"
|
||||||
);
|
);
|
||||||
|
|
||||||
inFeedSurvey = new StringFilterGroup(
|
chipBar = new StringFilterGroup(
|
||||||
Settings.HIDE_FEED_SURVEY,
|
Settings.HIDE_FILTER_BAR_FEED_IN_HISTORY,
|
||||||
|
"chip_bar"
|
||||||
|
);
|
||||||
|
|
||||||
|
surveys = new StringFilterGroup(
|
||||||
|
Settings.HIDE_SURVEYS,
|
||||||
"in_feed_survey",
|
"in_feed_survey",
|
||||||
"slimline_survey",
|
"slimline_survey",
|
||||||
"feed_nudge"
|
"feed_nudge"
|
||||||
@@ -133,13 +140,13 @@ public final class LayoutComponentsFilter extends Filter {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final var latestPosts = new StringFilterGroup(
|
final var latestPosts = new StringFilterGroup(
|
||||||
Settings.HIDE_HIDE_LATEST_POSTS,
|
Settings.HIDE_LATEST_POSTS,
|
||||||
"post_shelf"
|
"post_shelf"
|
||||||
);
|
);
|
||||||
|
|
||||||
final var channelGuidelines = new StringFilterGroup(
|
final var channelLinksPreview = new StringFilterGroup(
|
||||||
Settings.HIDE_HIDE_CHANNEL_GUIDELINES,
|
Settings.HIDE_LINKS_PREVIEW,
|
||||||
"channel_guidelines_entry_banner"
|
"attribution.eml"
|
||||||
);
|
);
|
||||||
|
|
||||||
final var emergencyBox = new StringFilterGroup(
|
final var emergencyBox = new StringFilterGroup(
|
||||||
@@ -164,7 +171,7 @@ public final class LayoutComponentsFilter extends Filter {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expandableMetadata = new StringFilterGroup(
|
expandableMetadata = new StringFilterGroup(
|
||||||
Settings.HIDE_EXPANDABLE_CHIP,
|
Settings.HIDE_EXPANDABLE_CARD,
|
||||||
"inline_expander"
|
"inline_expander"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -184,6 +191,12 @@ public final class LayoutComponentsFilter extends Filter {
|
|||||||
"mini_game_card.eml"
|
"mini_game_card.eml"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Playable horizontal shelf header.
|
||||||
|
playablesBuffer = new ByteArrayFilterGroup(
|
||||||
|
Settings.HIDE_PLAYABLES,
|
||||||
|
"FEmini_app_destination"
|
||||||
|
);
|
||||||
|
|
||||||
final var quickActions = new StringFilterGroup(
|
final var quickActions = new StringFilterGroup(
|
||||||
Settings.HIDE_QUICK_ACTIONS,
|
Settings.HIDE_QUICK_ACTIONS,
|
||||||
"quick_actions"
|
"quick_actions"
|
||||||
@@ -194,7 +207,6 @@ public final class LayoutComponentsFilter extends Filter {
|
|||||||
"image_shelf"
|
"image_shelf"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
final var timedReactions = new StringFilterGroup(
|
final var timedReactions = new StringFilterGroup(
|
||||||
Settings.HIDE_TIMED_REACTIONS,
|
Settings.HIDE_TIMED_REACTIONS,
|
||||||
"emoji_control_panel",
|
"emoji_control_panel",
|
||||||
@@ -221,7 +233,6 @@ public final class LayoutComponentsFilter extends Filter {
|
|||||||
"sponsorships"
|
"sponsorships"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
final var channelWatermark = new StringFilterGroup(
|
final var channelWatermark = new StringFilterGroup(
|
||||||
Settings.HIDE_VIDEO_CHANNEL_WATERMARK,
|
Settings.HIDE_VIDEO_CHANNEL_WATERMARK,
|
||||||
"featured_channel_watermark_overlay"
|
"featured_channel_watermark_overlay"
|
||||||
@@ -232,11 +243,27 @@ public final class LayoutComponentsFilter extends Filter {
|
|||||||
"mixed_content_shelf"
|
"mixed_content_shelf"
|
||||||
);
|
);
|
||||||
|
|
||||||
final var searchResultRecommendationLabels = new StringFilterGroup(
|
final var videoRecommendationLabels = new StringFilterGroup(
|
||||||
Settings.HIDE_SEARCH_RESULT_RECOMMENDATION_LABELS,
|
Settings.HIDE_VIDEO_RECOMMENDATION_LABELS,
|
||||||
"endorsement_header_footer.eml"
|
"endorsement_header_footer.eml"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
channelProfile = new StringFilterGroup(
|
||||||
|
null,
|
||||||
|
"channel_profile.eml",
|
||||||
|
"page_header.eml"
|
||||||
|
);
|
||||||
|
channelProfileBuffer = new ByteArrayFilterGroupList();
|
||||||
|
channelProfileBuffer.addAll(new ByteArrayFilterGroup(
|
||||||
|
Settings.HIDE_VISIT_STORE_BUTTON,
|
||||||
|
"header_store_button"
|
||||||
|
),
|
||||||
|
new ByteArrayFilterGroup(
|
||||||
|
Settings.HIDE_VISIT_COMMUNITY_BUTTON,
|
||||||
|
"community_button"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
horizontalShelves = new StringFilterGroup(
|
horizontalShelves = new StringFilterGroup(
|
||||||
Settings.HIDE_HORIZONTAL_SHELVES,
|
Settings.HIDE_HORIZONTAL_SHELVES,
|
||||||
"horizontal_video_shelf.eml",
|
"horizontal_video_shelf.eml",
|
||||||
@@ -247,44 +274,45 @@ public final class LayoutComponentsFilter extends Filter {
|
|||||||
|
|
||||||
ticketShelf = new ByteArrayFilterGroup(
|
ticketShelf = new ByteArrayFilterGroup(
|
||||||
Settings.HIDE_TICKET_SHELF,
|
Settings.HIDE_TICKET_SHELF,
|
||||||
"ticket"
|
"ticket_item.eml"
|
||||||
);
|
);
|
||||||
|
|
||||||
addPathCallbacks(
|
addPathCallbacks(
|
||||||
expandableMetadata,
|
artistCard,
|
||||||
inFeedSurvey,
|
audioTrackButton,
|
||||||
notifyMe,
|
channelLinksPreview,
|
||||||
compactChannelBar,
|
channelMembersShelf,
|
||||||
communityPosts,
|
channelProfile,
|
||||||
paidPromotion,
|
|
||||||
searchResultRecommendationLabels,
|
|
||||||
latestPosts,
|
|
||||||
channelWatermark,
|
channelWatermark,
|
||||||
communityGuidelines,
|
chipBar,
|
||||||
|
compactBanner,
|
||||||
|
compactChannelBar,
|
||||||
|
compactChannelBarInner,
|
||||||
|
communityPosts,
|
||||||
|
emergencyBox,
|
||||||
|
expandableMetadata,
|
||||||
|
forYouShelf,
|
||||||
|
horizontalShelves,
|
||||||
|
imageShelf,
|
||||||
|
infoPanel,
|
||||||
|
latestPosts,
|
||||||
|
medicalPanel,
|
||||||
|
notifyMe,
|
||||||
|
paidPromotion,
|
||||||
playables,
|
playables,
|
||||||
quickActions,
|
quickActions,
|
||||||
relatedVideos,
|
relatedVideos,
|
||||||
compactBanner,
|
|
||||||
compactChannelBarInner,
|
|
||||||
medicalPanel,
|
|
||||||
infoPanel,
|
|
||||||
singleItemInformationPanel,
|
singleItemInformationPanel,
|
||||||
emergencyBox,
|
|
||||||
subscribersCommunityGuidelines,
|
subscribersCommunityGuidelines,
|
||||||
subscriptionsChipBar,
|
subscriptionsChipBar,
|
||||||
channelGuidelines,
|
surveys,
|
||||||
audioTrackButton,
|
|
||||||
artistCard,
|
|
||||||
timedReactions,
|
timedReactions,
|
||||||
imageShelf,
|
videoRecommendationLabels
|
||||||
channelMemberShelf,
|
|
||||||
forYouShelf,
|
|
||||||
horizontalShelves
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
boolean isFiltered(String identifier, String path, byte[] buffer,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
// This identifier is used not only in players but also in search results:
|
// This identifier is used not only in players but also in search results:
|
||||||
// https://github.com/ReVanced/revanced-patches/issues/3245
|
// https://github.com/ReVanced/revanced-patches/issues/3245
|
||||||
@@ -297,21 +325,37 @@ public final class LayoutComponentsFilter extends Filter {
|
|||||||
|
|
||||||
// The groups are excluded from the filter due to the exceptions list below.
|
// The groups are excluded from the filter due to the exceptions list below.
|
||||||
// Filter them separately here.
|
// Filter them separately here.
|
||||||
if (matchedGroup == notifyMe || matchedGroup == inFeedSurvey || matchedGroup == expandableMetadata) {
|
if (matchedGroup == notifyMe || matchedGroup == surveys || matchedGroup == expandableMetadata) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (matchedGroup == channelProfile) {
|
||||||
|
return channelProfileBuffer.check(buffer).isFiltered();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchedGroup == communityPosts && NavigationBar.isBackButtonVisible()) {
|
||||||
|
// Allow community posts on channel profile page,
|
||||||
|
// or if viewing an individual channel in the feed.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (exceptions.matches(path)) return false; // Exceptions are not filtered.
|
if (exceptions.matches(path)) return false; // Exceptions are not filtered.
|
||||||
|
|
||||||
if (matchedGroup == compactChannelBarInner) {
|
if (matchedGroup == compactChannelBarInner) {
|
||||||
return compactChannelBarInnerButton.check(path).isFiltered()
|
return compactChannelBarInnerButton.check(path).isFiltered()
|
||||||
// The filter may be broad, but in the context of a compactChannelBarInnerButton,
|
// The filter may be broad, but in the context of a compactChannelBarInnerButton,
|
||||||
// it's safe to assume that the button is the only thing that should be hidden.
|
// it's safe to assume that the button is the only thing that should be hidden.
|
||||||
&& joinMembershipButton.check(protobufBufferArray).isFiltered();
|
&& joinMembershipButton.check(buffer).isFiltered();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matchedGroup == horizontalShelves) {
|
if (matchedGroup == horizontalShelves) {
|
||||||
return contentIndex == 0 && (hideShelves() || ticketShelf.check(protobufBufferArray).isFiltered());
|
return contentIndex == 0 && (hideShelves()
|
||||||
|
|| ticketShelf.check(buffer).isFiltered()
|
||||||
|
|| playablesBuffer.check(buffer).isFiltered());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchedGroup == chipBar) {
|
||||||
|
return contentIndex == 0 && NavigationButton.getSelectedNavigationButton() == NavigationButton.LIBRARY;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -321,7 +365,7 @@ public final class LayoutComponentsFilter extends Filter {
|
|||||||
* Injection point.
|
* Injection point.
|
||||||
* Called from a different place then the other filters.
|
* Called from a different place then the other filters.
|
||||||
*/
|
*/
|
||||||
public static boolean filterMixPlaylists(final Object conversionContext, @Nullable final byte[] bytes) {
|
public static boolean filterMixPlaylists(Object conversionContext, @Nullable final byte[] bytes) {
|
||||||
try {
|
try {
|
||||||
if (!Settings.HIDE_MIX_PLAYLISTS.get()) {
|
if (!Settings.HIDE_MIX_PLAYLISTS.get()) {
|
||||||
return false;
|
return false;
|
||||||
@@ -411,13 +455,11 @@ public final class LayoutComponentsFilter extends Filter {
|
|||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
@Nullable
|
public static void setDoodleDrawable(ImageView imageView, Drawable original) {
|
||||||
public static Drawable hideYoodles(Drawable animatedYoodle) {
|
Drawable replacement = HIDE_DOODLES_ENABLED
|
||||||
if (HIDE_DOODLES_ENABLED) {
|
? ChangeHeaderPatch.getDrawable(original)
|
||||||
return null;
|
: original;
|
||||||
}
|
imageView.setImageDrawable(replacement);
|
||||||
|
|
||||||
return animatedYoodle;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final boolean HIDE_SHOW_MORE_BUTTON_ENABLED = Settings.HIDE_SHOW_MORE_BUTTON.get();
|
private static final boolean HIDE_SHOW_MORE_BUTTON_ENABLED = Settings.HIDE_SHOW_MORE_BUTTON.get();
|
||||||
@@ -448,7 +490,7 @@ public final class LayoutComponentsFilter extends Filter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Do not hide if the navigation back button is visible,
|
// Do not hide if the navigation back button is visible,
|
||||||
// otherwise the content shelves in the explore/music/courses pages are hidde.
|
// otherwise the content shelves in the explore/music/courses pages are hidden.
|
||||||
if (NavigationBar.isBackButtonVisible()) {
|
if (NavigationBar.isBackButtonVisible()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user