mirror of
https://github.com/revanced/revanced-patches.git
synced 2025-12-07 09:53:55 +01:00
Compare commits
633 Commits
v5.21.0-de
...
v5.41.0-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
191b9169ff | ||
|
|
212418b8db | ||
|
|
7dbc744be0 | ||
|
|
150a3e7c60 | ||
|
|
5027943470 | ||
|
|
fa9e590b3a | ||
|
|
5823f0e982 | ||
|
|
f506a67e4a | ||
|
|
ed6e1155f2 | ||
|
|
8c229954d7 | ||
|
|
c5eb88bbf6 | ||
|
|
ef514017f4 | ||
|
|
c72d99518c | ||
|
|
772df6eb73 | ||
|
|
dfb5407e67 | ||
|
|
6d5f6ecdd2 | ||
|
|
a0a62ddad2 | ||
|
|
512e50e892 | ||
|
|
a2304c3310 | ||
|
|
45c1ee8a12 | ||
|
|
74cdf550a5 | ||
|
|
c36ea22975 | ||
|
|
58d088ab30 | ||
|
|
ece8076f7c | ||
|
|
ebb446b22a | ||
|
|
b44a369f59 | ||
|
|
092a72c774 | ||
|
|
6330773bfc | ||
|
|
43dbb4710b | ||
|
|
966727ca2d | ||
|
|
1f371c8156 | ||
|
|
a8a410708d | ||
|
|
7651ef0881 | ||
|
|
f97d33206b | ||
|
|
3d986e6716 | ||
|
|
01c0f1bd1a | ||
|
|
4178e8a64f | ||
|
|
7e1bb8f3c7 | ||
|
|
f7f4a1b0f0 | ||
|
|
e89660d234 | ||
|
|
db796fb883 | ||
|
|
6bb8bad8d7 | ||
|
|
aa1fb41ad8 | ||
|
|
418f5945c2 | ||
|
|
e26c971067 | ||
|
|
eb1d07fd98 | ||
|
|
651d358096 | ||
|
|
0d15c5f338 | ||
|
|
5c7c8b5364 | ||
|
|
729997ec3e | ||
|
|
767f1e3695 | ||
|
|
7857876551 | ||
|
|
04057c6e56 | ||
|
|
8ba9a19ade | ||
|
|
6862200a28 | ||
|
|
dfff3d7c0a | ||
|
|
e6cce85541 | ||
|
|
8502eb8eac | ||
|
|
0652c56d0d | ||
|
|
b7026b7086 | ||
|
|
fa4f422a15 | ||
|
|
38e0cbd724 | ||
|
|
0bdebd927d | ||
|
|
3eac25cf7f | ||
|
|
c9f741e616 | ||
|
|
cba44ccfc8 | ||
|
|
a84db7be7f | ||
|
|
2520129ace | ||
|
|
7eeffd3392 | ||
|
|
6c3391164e | ||
|
|
0b8b46c73e | ||
|
|
cbe576bc38 | ||
|
|
3a29f2a805 | ||
|
|
50069c7e05 | ||
|
|
2e9c9dc244 | ||
|
|
56166896d9 | ||
|
|
b4c695b1d5 | ||
|
|
1475643f84 | ||
|
|
9a7179f9cf | ||
|
|
6fb94a7a41 | ||
|
|
3776dda710 | ||
|
|
f88b3a5162 | ||
|
|
0eeaf7ad67 | ||
|
|
2726231404 | ||
|
|
9f0558e494 | ||
|
|
01f7bc9f8d | ||
|
|
5e20bd80f1 | ||
|
|
f304c178e2 | ||
|
|
1d65887e01 | ||
|
|
6b6eea8414 | ||
|
|
1db131e90e | ||
|
|
abe3943f98 | ||
|
|
cb6d802de3 | ||
|
|
f11d1ef990 | ||
|
|
3d25da18bc | ||
|
|
fa04c8eecf | ||
|
|
105f6e0e97 | ||
|
|
7d59efe05d | ||
|
|
81ff5576b0 | ||
|
|
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 | ||
|
|
2257dd90aa | ||
|
|
4b8499ff2c | ||
|
|
bde3fda972 | ||
|
|
e2e07b5cb2 | ||
|
|
9d10ab6c00 | ||
|
|
d7644152fd | ||
|
|
9be21f4824 | ||
|
|
a2eae0bf04 | ||
|
|
679354b5b3 | ||
|
|
91dec21033 | ||
|
|
1d0c56819b | ||
|
|
4410816c22 | ||
|
|
7e4e48bc9f | ||
|
|
e435b33593 | ||
|
|
bf288b83ae | ||
|
|
7a53580380 | ||
|
|
6439efa2a9 | ||
|
|
bc45433dcb | ||
|
|
8871803e83 | ||
|
|
18954a0285 | ||
|
|
ce5385b28e | ||
|
|
3f4cdf6f83 | ||
|
|
094b4a1ea8 | ||
|
|
a320e35c32 | ||
|
|
5bf5a2d2db | ||
|
|
ff903ba9ac | ||
|
|
1079a54dbe | ||
|
|
2b0e3b4553 | ||
|
|
0265a7791b | ||
|
|
49ae0df224 | ||
|
|
e279491724 | ||
|
|
495260fe2b | ||
|
|
40f069fff7 | ||
|
|
de263c1061 | ||
|
|
bf1f26d8bb | ||
|
|
0ee2ed72d4 | ||
|
|
02373b0bd2 | ||
|
|
97c8e2489d | ||
|
|
08b2b2e104 | ||
|
|
6b386b67d2 | ||
|
|
f8343ae9f6 | ||
|
|
3ba791ac7d | ||
|
|
443b54bf09 | ||
|
|
53587f190d | ||
|
|
83c148addc | ||
|
|
5c8ed05727 | ||
|
|
33833d7a1e | ||
|
|
b712f38017 | ||
|
|
517368eda7 | ||
|
|
2093c0c175 | ||
|
|
a7cfd80bfe | ||
|
|
2990dc6d4e | ||
|
|
c0e52bb6b3 | ||
|
|
93fdd6f538 | ||
|
|
decd249f20 | ||
|
|
d79cb3eea8 | ||
|
|
584b00fd87 | ||
|
|
795016abce | ||
|
|
dc1dbd50a8 | ||
|
|
2984d7362d | ||
|
|
627aed4010 | ||
|
|
4ab1f0cfa9 | ||
|
|
86e8e61ab2 | ||
|
|
e286dab74e | ||
|
|
712a82439f | ||
|
|
4449546c85 | ||
|
|
8d61ba90c3 | ||
|
|
689be79f71 | ||
|
|
b6047fa6b3 | ||
|
|
82bbd603ac | ||
|
|
bc0c3c452d | ||
|
|
fe864d8331 | ||
|
|
4f686935c3 | ||
|
|
798596fd83 | ||
|
|
38b37f182a | ||
|
|
52b9dc5c9f | ||
|
|
dea7108c45 | ||
|
|
24b4579cb9 | ||
|
|
0b52f3d192 | ||
|
|
18c374a81e | ||
|
|
092303e431 | ||
|
|
6bf5bf9d45 | ||
|
|
b2b09a2025 | ||
|
|
4a3a7f1674 | ||
|
|
e59c9e9b3c | ||
|
|
dfb552b01a | ||
|
|
94999c56b1 | ||
|
|
c4fd1f0146 | ||
|
|
4cd0ae9b92 | ||
|
|
9548d581c1 | ||
|
|
a2fe3af6be | ||
|
|
6ef6504d41 | ||
|
|
e58290839f | ||
|
|
e18260bd65 | ||
|
|
b2fcd5a846 | ||
|
|
e68cd70f66 | ||
|
|
14a8f4fb96 | ||
|
|
2593c004f4 | ||
|
|
db68c41d5e | ||
|
|
a4f9cb3cef | ||
|
|
9aec1999bb | ||
|
|
26ecbe646e | ||
|
|
46ba0d8a2e | ||
|
|
f454183646 | ||
|
|
d2b440d800 | ||
|
|
494c5f04a4 | ||
|
|
48d5fdf7e1 | ||
|
|
887c9f0d75 | ||
|
|
7de4c9d41d | ||
|
|
7d3b8d9c42 | ||
|
|
25e1a965d6 | ||
|
|
b29c01cee1 | ||
|
|
639850471b | ||
|
|
796c118fe1 | ||
|
|
edf20e397d | ||
|
|
5f0541407c | ||
|
|
56b7ba9ba7 | ||
|
|
f8bdf744ab | ||
|
|
f4f36ff273 | ||
|
|
5028c1acb3 | ||
|
|
555c9a5823 | ||
|
|
777957e2d0 | ||
|
|
b3316a5915 | ||
|
|
2ca2bb7692 | ||
|
|
23fd720fa7 | ||
|
|
1f08586ae8 | ||
|
|
60fdf4c44c | ||
|
|
63f3342815 | ||
|
|
858c59d728 | ||
|
|
5debf9936d | ||
|
|
f1b85d20a1 | ||
|
|
37d0de5e93 | ||
|
|
96d08d5eb7 | ||
|
|
9b1013e1c2 | ||
|
|
75d6cd7c7b | ||
|
|
5a17f5e1c1 | ||
|
|
1d16de6617 | ||
|
|
aee7cba46d | ||
|
|
ec3faf30a8 | ||
|
|
45b5a51da3 | ||
|
|
8abf176bc9 | ||
|
|
ef35ed7335 | ||
|
|
4fd666b667 | ||
|
|
72e0c01922 | ||
|
|
f69eab3e3b | ||
|
|
7c5c2d95bc | ||
|
|
b2453fecfc | ||
|
|
0d54f8bd80 | ||
|
|
fda16fad1a | ||
|
|
ddd43acd73 | ||
|
|
3451318d53 | ||
|
|
2d94ba9df6 | ||
|
|
aaf3437a5a | ||
|
|
ec8bf06047 | ||
|
|
96512de6c9 | ||
|
|
6114807c43 | ||
|
|
6d69f01421 | ||
|
|
fd4218154d | ||
|
|
8bed8a6622 | ||
|
|
3174047223 | ||
|
|
15053e2b68 | ||
|
|
e5b6aac018 | ||
|
|
d7c9dd0f77 | ||
|
|
a0eb6d5fdb | ||
|
|
55c5eb3d14 | ||
|
|
896de8910a | ||
|
|
e2a7e25c66 | ||
|
|
77ea5c4033 | ||
|
|
6eea2354f5 |
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -72,6 +72,7 @@ body:
|
|||||||
|
|
||||||
- **Do not submit a duplicate bug report**: Search for existing bug reports [here](https://github.com/ReVanced/revanced-patches/issues?q=label%3A%22Bug+report%22).
|
- **Do not submit a duplicate bug report**: Search for existing bug reports [here](https://github.com/ReVanced/revanced-patches/issues?q=label%3A%22Bug+report%22).
|
||||||
- **Review the contribution guidelines**: Make sure your bug report adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-patches/blob/main/CONTRIBUTING.md).
|
- **Review the contribution guidelines**: Make sure your bug report adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-patches/blob/main/CONTRIBUTING.md).
|
||||||
|
- **Check the troubleshooting guide**: A solution to your issue might be found in the [FAQ](https://github.com/ReVanced/revanced-documentation/blob/main/docs/revanced-resources/questions.md) or the [troubleshooting guide](https://github.com/ReVanced/revanced-documentation/blob/main/docs/revanced-resources/troubleshooting.md).
|
||||||
- **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
|
- **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
1
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
1
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -72,6 +72,7 @@ body:
|
|||||||
|
|
||||||
- **Do not submit a duplicate feature request**: Search for existing feature requests [here](https://github.com/ReVanced/revanced-patches/issues?q=label%3A%22Feature+request%22).
|
- **Do not submit a duplicate feature request**: Search for existing feature requests [here](https://github.com/ReVanced/revanced-patches/issues?q=label%3A%22Feature+request%22).
|
||||||
- **Review the contribution guidelines**: Make sure your feature request adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-patches/blob/main/CONTRIBUTING.md).
|
- **Review the contribution guidelines**: Make sure your feature request adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-patches/blob/main/CONTRIBUTING.md).
|
||||||
|
- **Check the troubleshooting guide**: Information about your issue might be found in the [FAQ](https://github.com/ReVanced/revanced-documentation/blob/main/docs/revanced-resources/questions.md) or the [troubleshooting guide](https://github.com/ReVanced/revanced-documentation/blob/main/docs/revanced-resources/troubleshooting.md).
|
||||||
- **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
|
- **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
8
.github/workflows/build_pull_request.yml
vendored
8
.github/workflows/build_pull_request.yml
vendored
@@ -13,17 +13,15 @@ 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
|
||||||
with:
|
with:
|
||||||
distribution: "temurin"
|
distribution: 'temurin'
|
||||||
java-version: "17"
|
java-version: '17'
|
||||||
|
|
||||||
- name: Cache Gradle
|
- name: Cache Gradle
|
||||||
uses: burrunan/gradle-cache-action@v1
|
uses: burrunan/gradle-cache-action@v3
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
env:
|
env:
|
||||||
|
|||||||
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:
|
||||||
|
|||||||
25
.github/workflows/release.yml
vendored
25
.github/workflows/release.yml
vendored
@@ -13,24 +13,21 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
packages: write
|
packages: write
|
||||||
|
id-token: write
|
||||||
|
attestations: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
# Make sure the release step uses its own credentials:
|
|
||||||
# https://github.com/cycjimmy/semantic-release-action#private-packages
|
|
||||||
persist-credentials: false
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: "temurin"
|
distribution: 'temurin'
|
||||||
java-version: "17"
|
java-version: '17'
|
||||||
|
|
||||||
- name: Cache Gradle
|
- name: Cache Gradle
|
||||||
uses: burrunan/gradle-cache-action@v1
|
uses: burrunan/gradle-cache-action@v3
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
env:
|
env:
|
||||||
@@ -40,7 +37,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "lts/*"
|
node-version: 'lts/*'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@@ -54,6 +51,14 @@ jobs:
|
|||||||
fingerprint: ${{ vars.GPG_FINGERPRINT }}
|
fingerprint: ${{ vars.GPG_FINGERPRINT }}
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
|
uses: cycjimmy/semantic-release-action@v4
|
||||||
|
id: release
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: npm exec semantic-release
|
|
||||||
|
- name: Attest
|
||||||
|
if: steps.release.outputs.new_release_published == 'true'
|
||||||
|
uses: actions/attest-build-provenance@v2
|
||||||
|
with:
|
||||||
|
subject-name: 'ReVanced Patches ${{ steps.release.outputs.new_release_git_tag }}'
|
||||||
|
subject-path: patches/build/libs/patches-*.rvp
|
||||||
|
|||||||
10
.releaserc
10
.releaserc
@@ -22,7 +22,7 @@
|
|||||||
{
|
{
|
||||||
"assets": [
|
"assets": [
|
||||||
"CHANGELOG.md",
|
"CHANGELOG.md",
|
||||||
"gradle.properties",
|
"gradle.properties"
|
||||||
],
|
],
|
||||||
"message": "chore: Release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
"message": "chore: Release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
||||||
}
|
}
|
||||||
@@ -33,16 +33,16 @@
|
|||||||
"assets": [
|
"assets": [
|
||||||
{
|
{
|
||||||
"path": "patches/build/libs/patches-!(*sources*|*javadoc*).rvp?(.asc)"
|
"path": "patches/build/libs/patches-!(*sources*|*javadoc*).rvp?(.asc)"
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
successComment: false
|
"successComment": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"@saithodev/semantic-release-backmerge",
|
"@saithodev/semantic-release-backmerge",
|
||||||
{
|
{
|
||||||
backmergeBranches: [{"from": "main", "to": "dev"}],
|
"backmergeBranches": [{"from": "main", "to": "dev"}],
|
||||||
clearWorkspace: true
|
"clearWorkspace": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|||||||
1966
CHANGELOG.md
1966
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(); }
|
||||||
|
}
|
||||||
3
extensions/instagram/build.gradle.kts
Normal file
3
extensions/instagram/build.gradle.kts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
dependencies {
|
||||||
|
compileOnly(project(":extensions:shared:library"))
|
||||||
|
}
|
||||||
1
extensions/instagram/src/main/AndroidManifest.xml
Normal file
1
extensions/instagram/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest/>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package app.revanced.extension.instagram.feed;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class LimitFeedToFollowedProfiles {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static Map<String, String> setFollowingHeader(Map<String, String> requestHeaderMap) {
|
||||||
|
String paginationHeaderName = "pagination_source";
|
||||||
|
|
||||||
|
// Patch the header only if it's trying to fetch the default feed
|
||||||
|
String currentHeader = requestHeaderMap.get(paginationHeaderName);
|
||||||
|
if (currentHeader != null && !currentHeader.equals("feed_recs")) {
|
||||||
|
return requestHeaderMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new map as original is unmodifiable.
|
||||||
|
Map<String, String> patchedRequestHeaderMap = new HashMap<>(requestHeaderMap);
|
||||||
|
patchedRequestHeaderMap.put(paginationHeaderName, "following");
|
||||||
|
return patchedRequestHeaderMap;
|
||||||
|
}
|
||||||
|
}
|
||||||
3
extensions/messenger/build.gradle.kts
Normal file
3
extensions/messenger/build.gradle.kts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
dependencies {
|
||||||
|
compileOnly(project(":extensions:shared:library"))
|
||||||
|
}
|
||||||
1
extensions/messenger/src/main/AndroidManifest.xml
Normal file
1
extensions/messenger/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest/>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package app.revanced.extension.messenger.metaai;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class RemoveMetaAIPatch {
|
||||||
|
private static final Set<Long> loggedIDs = Collections.synchronizedSet(new HashSet<>());
|
||||||
|
|
||||||
|
public static boolean overrideBooleanFlag(long id, boolean value) {
|
||||||
|
try {
|
||||||
|
if (Long.toString(id).startsWith("REPLACED_BY_PATCH")) {
|
||||||
|
if (loggedIDs.add(id))
|
||||||
|
Logger.printInfo(() -> "Overriding " + id + " from " + value + " to false");
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "overrideBooleanFlag failure", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,24 @@
|
|||||||
|
package app.revanced.extension.music.patches;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.Utils.hideViewBy0dpUnderCondition;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
import app.revanced.extension.music.settings.Settings;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class HideCastButtonPatch {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point
|
||||||
|
*/
|
||||||
|
public static int hideCastButton(int original) {
|
||||||
|
return Settings.HIDE_CAST_BUTTON.get() ? View.GONE : original;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point
|
||||||
|
*/
|
||||||
|
public static void hideCastButton(View view) {
|
||||||
|
hideViewBy0dpUnderCondition(Settings.HIDE_CAST_BUTTON.get(), view);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package app.revanced.extension.music.patches;
|
||||||
|
|
||||||
|
import app.revanced.extension.music.settings.Settings;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class HideCategoryBarPatch {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point
|
||||||
|
*/
|
||||||
|
public static boolean hideCategoryBar() {
|
||||||
|
return Settings.HIDE_CATEGORY_BAR.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package app.revanced.extension.music.patches;
|
||||||
|
|
||||||
|
import app.revanced.extension.music.settings.Settings;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class HideGetPremiumPatch {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point
|
||||||
|
*/
|
||||||
|
public static boolean hideGetPremiumLabel() {
|
||||||
|
return Settings.HIDE_GET_PREMIUM_LABEL.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package app.revanced.extension.music.patches;
|
||||||
|
|
||||||
|
import app.revanced.extension.music.settings.Settings;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class HideVideoAdsPatch {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point
|
||||||
|
*/
|
||||||
|
public static boolean showVideoAds(boolean original) {
|
||||||
|
if (Settings.HIDE_VIDEO_ADS.get()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return original;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package app.revanced.extension.music.patches;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.Utils.hideViewUnderCondition;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import app.revanced.extension.music.settings.Settings;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class NavigationBarPatch {
|
||||||
|
@NonNull
|
||||||
|
private static String lastYTNavigationEnumName = "";
|
||||||
|
|
||||||
|
public static void setLastAppNavigationEnum(@Nullable Enum<?> ytNavigationEnumName) {
|
||||||
|
if (ytNavigationEnumName != null) {
|
||||||
|
lastYTNavigationEnumName = ytNavigationEnumName.name();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void hideNavigationLabel(TextView textview) {
|
||||||
|
hideViewUnderCondition(Settings.HIDE_NAVIGATION_BAR_LABEL.get(), textview);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void hideNavigationButton(@NonNull View view) {
|
||||||
|
// Hide entire navigation bar.
|
||||||
|
if (Settings.HIDE_NAVIGATION_BAR.get() && view.getParent() != null) {
|
||||||
|
hideViewUnderCondition(true, (View) view.getParent());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide navigation buttons based on their type.
|
||||||
|
for (NavigationButton button : NavigationButton.values()) {
|
||||||
|
if (button.ytEnumNames.equals(lastYTNavigationEnumName)) {
|
||||||
|
hideViewUnderCondition(button.hidden, view);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum NavigationButton {
|
||||||
|
HOME(
|
||||||
|
"TAB_HOME",
|
||||||
|
Settings.HIDE_NAVIGATION_BAR_HOME_BUTTON.get()
|
||||||
|
),
|
||||||
|
SAMPLES(
|
||||||
|
"TAB_SAMPLES",
|
||||||
|
Settings.HIDE_NAVIGATION_BAR_SAMPLES_BUTTON.get()
|
||||||
|
),
|
||||||
|
EXPLORE(
|
||||||
|
"TAB_EXPLORE",
|
||||||
|
Settings.HIDE_NAVIGATION_BAR_EXPLORE_BUTTON.get()
|
||||||
|
),
|
||||||
|
LIBRARY(
|
||||||
|
"LIBRARY_MUSIC",
|
||||||
|
Settings.HIDE_NAVIGATION_BAR_LIBRARY_BUTTON.get()
|
||||||
|
),
|
||||||
|
UPGRADE(
|
||||||
|
"TAB_MUSIC_PREMIUM",
|
||||||
|
Settings.HIDE_NAVIGATION_BAR_UPGRADE_BUTTON.get()
|
||||||
|
);
|
||||||
|
|
||||||
|
private final String ytEnumNames;
|
||||||
|
private final boolean hidden;
|
||||||
|
|
||||||
|
NavigationButton(@NonNull String ytEnumNames, boolean hidden) {
|
||||||
|
this.ytEnumNames = ytEnumNames;
|
||||||
|
this.hidden = hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package app.revanced.extension.music.patches;
|
||||||
|
|
||||||
|
import app.revanced.extension.music.settings.Settings;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class PermanentRepeatPatch {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point
|
||||||
|
*/
|
||||||
|
public static boolean permanentRepeat() {
|
||||||
|
return Settings.PERMANENT_REPEAT.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package app.revanced.extension.music.patches.spoof;
|
||||||
|
|
||||||
|
import static app.revanced.extension.music.settings.Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE;
|
||||||
|
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 static app.revanced.extension.shared.spoof.ClientType.VISIONOS;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.spoof.ClientType;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class SpoofVideoStreamsPatch {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static void setClientOrderToUse() {
|
||||||
|
List<ClientType> availableClients = List.of(
|
||||||
|
ANDROID_VR_1_43_32,
|
||||||
|
ANDROID_VR_1_61_48,
|
||||||
|
VISIONOS
|
||||||
|
);
|
||||||
|
|
||||||
|
app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch.setClientsToUse(
|
||||||
|
availableClients, SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package app.revanced.extension.music.settings;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.graphics.PorterDuff;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.preference.PreferenceFragment;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Toolbar;
|
||||||
|
|
||||||
|
import app.revanced.extension.music.settings.preference.MusicPreferenceFragment;
|
||||||
|
import app.revanced.extension.music.settings.search.MusicSearchViewController;
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.shared.settings.BaseActivityHook;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hooks GoogleApiActivity to inject a custom {@link MusicPreferenceFragment} with a toolbar and search.
|
||||||
|
*/
|
||||||
|
public class MusicActivityHook extends BaseActivityHook {
|
||||||
|
|
||||||
|
@SuppressLint("StaticFieldLeak")
|
||||||
|
public static MusicSearchViewController searchViewController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public static void initialize(Activity parentActivity) {
|
||||||
|
// Must touch the Music settings to ensure the class is loaded and
|
||||||
|
// the values can be found when setting the UI preferences.
|
||||||
|
// Logging anything under non debug ensures this is set.
|
||||||
|
Logger.printInfo(() -> "Permanent repeat enabled: " + Settings.PERMANENT_REPEAT.get());
|
||||||
|
|
||||||
|
// YT Music always uses dark mode.
|
||||||
|
Utils.setIsDarkModeEnabled(true);
|
||||||
|
|
||||||
|
BaseActivityHook.initialize(new MusicActivityHook(), parentActivity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the fixed theme for the activity.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void customizeActivityTheme(Activity activity) {
|
||||||
|
// Override the default YouTube Music theme to increase start padding of list items.
|
||||||
|
// Custom style located in resources/music/values/style.xml
|
||||||
|
activity.setTheme(Utils.getResourceIdentifierOrThrow(
|
||||||
|
"Theme.ReVanced.YouTubeMusic.Settings", "style"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the resource ID for the YouTube Music settings layout.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected int getContentViewResourceId() {
|
||||||
|
return LAYOUT_REVANCED_SETTINGS_WITH_TOOLBAR;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the fixed background color for the toolbar.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected int getToolbarBackgroundColor() {
|
||||||
|
return Utils.getResourceColor("ytm_color_black");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the navigation icon with a color filter applied.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected Drawable getNavigationIcon() {
|
||||||
|
Drawable navigationIcon = MusicPreferenceFragment.getBackButtonDrawable();
|
||||||
|
navigationIcon.setColorFilter(Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN);
|
||||||
|
return navigationIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the click listener that finishes the activity when the navigation icon is clicked.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected View.OnClickListener getNavigationClickListener(Activity activity) {
|
||||||
|
return view -> {
|
||||||
|
if (searchViewController != null && searchViewController.isSearchActive()) {
|
||||||
|
searchViewController.closeSearch();
|
||||||
|
} else {
|
||||||
|
activity.finish();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds search view components to the toolbar for {@link MusicPreferenceFragment}.
|
||||||
|
*
|
||||||
|
* @param activity The activity hosting the toolbar.
|
||||||
|
* @param toolbar The configured toolbar.
|
||||||
|
* @param fragment The PreferenceFragment associated with the activity.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void onPostToolbarSetup(Activity activity, Toolbar toolbar, PreferenceFragment fragment) {
|
||||||
|
if (fragment instanceof MusicPreferenceFragment) {
|
||||||
|
searchViewController = MusicSearchViewController.addSearchViewComponents(
|
||||||
|
activity, toolbar, (MusicPreferenceFragment) fragment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@link MusicPreferenceFragment} for the activity.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected PreferenceFragment createPreferenceFragment() {
|
||||||
|
return new MusicPreferenceFragment();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
* <p>
|
||||||
|
* Overrides {@link Activity#finish()} of the injection Activity.
|
||||||
|
*
|
||||||
|
* @return if the original activity finish method should be allowed to run.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public static boolean handleFinish() {
|
||||||
|
return MusicSearchViewController.handleFinish(searchViewController);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package app.revanced.extension.music.settings;
|
||||||
|
|
||||||
|
import static java.lang.Boolean.FALSE;
|
||||||
|
import static java.lang.Boolean.TRUE;
|
||||||
|
import static app.revanced.extension.shared.settings.Setting.parent;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
|
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||||
|
import app.revanced.extension.shared.settings.EnumSetting;
|
||||||
|
import app.revanced.extension.shared.spoof.ClientType;
|
||||||
|
|
||||||
|
public class Settings extends BaseSettings {
|
||||||
|
|
||||||
|
// Ads
|
||||||
|
public static final BooleanSetting HIDE_VIDEO_ADS = new BooleanSetting("revanced_music_hide_video_ads", TRUE, true);
|
||||||
|
public static final BooleanSetting HIDE_GET_PREMIUM_LABEL = new BooleanSetting("revanced_music_hide_get_premium_label", TRUE, true);
|
||||||
|
|
||||||
|
// General
|
||||||
|
public static final BooleanSetting HIDE_CAST_BUTTON = new BooleanSetting("revanced_music_hide_cast_button", TRUE, false);
|
||||||
|
public static final BooleanSetting HIDE_CATEGORY_BAR = new BooleanSetting("revanced_music_hide_category_bar", FALSE, true);
|
||||||
|
public static final BooleanSetting HIDE_NAVIGATION_BAR_HOME_BUTTON = new BooleanSetting("revanced_music_hide_navigation_bar_home_button", FALSE, true);
|
||||||
|
public static final BooleanSetting HIDE_NAVIGATION_BAR_SAMPLES_BUTTON = new BooleanSetting("revanced_music_hide_navigation_bar_samples_button", FALSE, true);
|
||||||
|
public static final BooleanSetting HIDE_NAVIGATION_BAR_EXPLORE_BUTTON = new BooleanSetting("revanced_music_hide_navigation_bar_explore_button", FALSE, true);
|
||||||
|
public static final BooleanSetting HIDE_NAVIGATION_BAR_LIBRARY_BUTTON = new BooleanSetting("revanced_music_hide_navigation_bar_library_button", FALSE, true);
|
||||||
|
public static final BooleanSetting HIDE_NAVIGATION_BAR_UPGRADE_BUTTON = new BooleanSetting("revanced_music_hide_navigation_bar_upgrade_button", TRUE, true);
|
||||||
|
public static final BooleanSetting HIDE_NAVIGATION_BAR = new BooleanSetting("revanced_music_hide_navigation_bar", FALSE, true);
|
||||||
|
public static final BooleanSetting HIDE_NAVIGATION_BAR_LABEL = new BooleanSetting("revanced_music_hide_navigation_bar_labels", FALSE, true);
|
||||||
|
|
||||||
|
// Player
|
||||||
|
public static final BooleanSetting PERMANENT_REPEAT = new BooleanSetting("revanced_music_play_permanent_repeat", FALSE, true);
|
||||||
|
|
||||||
|
// Miscellaneous
|
||||||
|
public static final EnumSetting<ClientType> SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_video_streams_client_type",
|
||||||
|
ClientType.ANDROID_VR_1_43_32, true, parent(SPOOF_VIDEO_STREAMS));
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package app.revanced.extension.music.settings.preference;
|
||||||
|
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.preference.PreferenceScreen;
|
||||||
|
import android.widget.Toolbar;
|
||||||
|
|
||||||
|
import app.revanced.extension.music.settings.MusicActivityHook;
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.shared.settings.preference.ToolbarPreferenceFragment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preference fragment for ReVanced settings.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
public class MusicPreferenceFragment extends ToolbarPreferenceFragment {
|
||||||
|
/**
|
||||||
|
* The main PreferenceScreen used to display the current set of preferences.
|
||||||
|
*/
|
||||||
|
private PreferenceScreen preferenceScreen;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the preference fragment.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void initialize() {
|
||||||
|
super.initialize();
|
||||||
|
|
||||||
|
try {
|
||||||
|
preferenceScreen = getPreferenceScreen();
|
||||||
|
Utils.sortPreferenceGroups(preferenceScreen);
|
||||||
|
setPreferenceScreenToolbar(preferenceScreen);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "initialize failure", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the fragment starts.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void onStart() {
|
||||||
|
super.onStart();
|
||||||
|
try {
|
||||||
|
// Initialize search controller if needed
|
||||||
|
if (MusicActivityHook.searchViewController != null) {
|
||||||
|
// Trigger search data collection after fragment is ready.
|
||||||
|
MusicActivityHook.searchViewController.initializeSearchData();
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "onStart failure", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets toolbar for all nested preference screens.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void customizeToolbar(Toolbar toolbar) {
|
||||||
|
MusicActivityHook.setToolbarLayoutParams(toolbar);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform actions after toolbar setup.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void onPostToolbarSetup(Toolbar toolbar, Dialog preferenceScreenDialog) {
|
||||||
|
if (MusicActivityHook.searchViewController != null
|
||||||
|
&& MusicActivityHook.searchViewController.isSearchActive()) {
|
||||||
|
toolbar.post(() -> MusicActivityHook.searchViewController.closeSearch());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the preference screen for external access by SearchViewController.
|
||||||
|
*/
|
||||||
|
public PreferenceScreen getPreferenceScreenForSearch() {
|
||||||
|
return preferenceScreen;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package app.revanced.extension.music.settings.search;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.preference.PreferenceScreen;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.settings.search.BaseSearchResultsAdapter;
|
||||||
|
import app.revanced.extension.shared.settings.search.BaseSearchViewController;
|
||||||
|
import app.revanced.extension.shared.settings.search.BaseSearchResultItem;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Music-specific search results adapter.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
public class MusicSearchResultsAdapter extends BaseSearchResultsAdapter {
|
||||||
|
|
||||||
|
public MusicSearchResultsAdapter(Context context, List<BaseSearchResultItem> items,
|
||||||
|
BaseSearchViewController.BasePreferenceFragment fragment,
|
||||||
|
BaseSearchViewController searchViewController) {
|
||||||
|
super(context, items, fragment, searchViewController);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PreferenceScreen getMainPreferenceScreen() {
|
||||||
|
return fragment.getPreferenceScreenForSearch();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package app.revanced.extension.music.settings.search;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.preference.Preference;
|
||||||
|
import android.preference.PreferenceScreen;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Toolbar;
|
||||||
|
|
||||||
|
import app.revanced.extension.music.settings.preference.MusicPreferenceFragment;
|
||||||
|
import app.revanced.extension.shared.settings.search.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Music-specific search view controller implementation.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
public class MusicSearchViewController extends BaseSearchViewController {
|
||||||
|
|
||||||
|
public static MusicSearchViewController addSearchViewComponents(Activity activity, Toolbar toolbar,
|
||||||
|
MusicPreferenceFragment fragment) {
|
||||||
|
return new MusicSearchViewController(activity, toolbar, fragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MusicSearchViewController(Activity activity, Toolbar toolbar, MusicPreferenceFragment fragment) {
|
||||||
|
super(activity, toolbar, new PreferenceFragmentAdapter(fragment));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected BaseSearchResultsAdapter createSearchResultsAdapter() {
|
||||||
|
return new MusicSearchResultsAdapter(activity, filteredSearchItems, fragment, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isSpecialPreferenceGroup(Preference preference) {
|
||||||
|
// Music doesn't have SponsorBlock, so no special groups.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setupSpecialPreferenceListeners(BaseSearchResultItem item) {
|
||||||
|
// Music doesn't have special preferences.
|
||||||
|
// This method can be empty or handle music-specific preferences if any.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Static method for handling Activity finish
|
||||||
|
public static boolean handleFinish(MusicSearchViewController searchViewController) {
|
||||||
|
if (searchViewController != null && searchViewController.isSearchActive()) {
|
||||||
|
searchViewController.closeSearch();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adapter to wrap MusicPreferenceFragment to BasePreferenceFragment interface.
|
||||||
|
private record PreferenceFragmentAdapter(MusicPreferenceFragment fragment) implements BasePreferenceFragment {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PreferenceScreen getPreferenceScreenForSearch() {
|
||||||
|
return fragment.getPreferenceScreenForSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View getView() {
|
||||||
|
return fragment.getView();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Activity getActivity() {
|
||||||
|
return fragment.getActivity();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -82,7 +82,7 @@ public class HideAdsPatch {
|
|||||||
|
|
||||||
// Filter HeaderBlock with known ads until next HeaderBlock.
|
// Filter HeaderBlock with known ads until next HeaderBlock.
|
||||||
if (currentBlock instanceof HeaderBlock headerBlock) {
|
if (currentBlock instanceof HeaderBlock headerBlock) {
|
||||||
StyledText headerText = headerBlock.component20();
|
StyledText headerText = headerBlock.getTitle();
|
||||||
if (headerText != null) {
|
if (headerText != null) {
|
||||||
skipFullHeader = false;
|
skipFullHeader = false;
|
||||||
for (String blockedHeaderBlock : blockedHeaderBlocks) {
|
for (String blockedHeaderBlock : blockedHeaderBlocks) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id(libs.plugins.android.library.get().pluginId)
|
alias(libs.plugins.android.library)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ package nl.nu.performance.api.client.objects;
|
|||||||
import nl.nu.performance.api.client.interfaces.Block;
|
import nl.nu.performance.api.client.interfaces.Block;
|
||||||
|
|
||||||
public class HeaderBlock extends Block {
|
public class HeaderBlock extends Block {
|
||||||
// returns title
|
public final StyledText getTitle() {
|
||||||
public final StyledText component20() {
|
|
||||||
throw new UnsupportedOperationException("Stub");
|
throw new UnsupportedOperationException("Stub");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4
extensions/primevideo/build.gradle.kts
Normal file
4
extensions/primevideo/build.gradle.kts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
dependencies {
|
||||||
|
compileOnly(project(":extensions:shared:library"))
|
||||||
|
compileOnly(project(":extensions:primevideo:stub"))
|
||||||
|
}
|
||||||
1
extensions/primevideo/src/main/AndroidManifest.xml
Normal file
1
extensions/primevideo/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest/>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package app.revanced.extension.primevideo.ads;
|
||||||
|
|
||||||
|
import com.amazon.avod.fsm.SimpleTrigger;
|
||||||
|
import com.amazon.avod.media.ads.AdBreak;
|
||||||
|
import com.amazon.avod.media.ads.internal.state.AdBreakTrigger;
|
||||||
|
import com.amazon.avod.media.ads.internal.state.AdEnabledPlayerTriggerType;
|
||||||
|
import com.amazon.avod.media.playback.VideoPlayer;
|
||||||
|
import com.amazon.avod.media.ads.internal.state.ServerInsertedAdBreakState;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public final class SkipAdsPatch {
|
||||||
|
public static void enterServerInsertedAdBreakState(ServerInsertedAdBreakState state, AdBreakTrigger trigger, VideoPlayer player) {
|
||||||
|
try {
|
||||||
|
AdBreak adBreak = trigger.getBreak();
|
||||||
|
|
||||||
|
// There are two scenarios when entering the original method:
|
||||||
|
// 1. Player naturally entered an ad break while watching a video.
|
||||||
|
// 2. User is skipped/scrubbed to a position on the timeline. If seek position is past an ad break,
|
||||||
|
// user is forced to watch an ad before continuing.
|
||||||
|
//
|
||||||
|
// Scenario 2 is indicated by trigger.getSeekStartPosition() != null, so skip directly to the scrubbing
|
||||||
|
// target. Otherwise, just calculate when the ad break should end and skip to there.
|
||||||
|
if (trigger.getSeekStartPosition() != null)
|
||||||
|
player.seekTo(trigger.getSeekTarget().getTotalMilliseconds());
|
||||||
|
else
|
||||||
|
player.seekTo(player.getCurrentPosition() + adBreak.getDurationExcludingAux().getTotalMilliseconds());
|
||||||
|
|
||||||
|
// Send "end of ads" trigger to state machine so everything doesn't get whacky.
|
||||||
|
state.doTrigger(new SimpleTrigger(AdEnabledPlayerTriggerType.NO_MORE_ADS_SKIP_TRANSITION));
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "Failed skipping ads", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
17
extensions/primevideo/stub/build.gradle.kts
Normal file
17
extensions/primevideo/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/primevideo/stub/src/main/AndroidManifest.xml
Normal file
1
extensions/primevideo/stub/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest/>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.amazon.avod.fsm;
|
||||||
|
|
||||||
|
public final class SimpleTrigger<T> implements Trigger<T> {
|
||||||
|
public SimpleTrigger(T triggerType) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.amazon.avod.fsm;
|
||||||
|
|
||||||
|
public abstract class StateBase<S, T> {
|
||||||
|
// This method orginally has protected access (modified in patch code).
|
||||||
|
public void doTrigger(Trigger<T> trigger) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package com.amazon.avod.fsm;
|
||||||
|
|
||||||
|
public interface Trigger<T> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.amazon.avod.media;
|
||||||
|
|
||||||
|
public final class TimeSpan {
|
||||||
|
public long getTotalMilliseconds() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.amazon.avod.media.ads;
|
||||||
|
|
||||||
|
import com.amazon.avod.media.TimeSpan;
|
||||||
|
|
||||||
|
public interface AdBreak {
|
||||||
|
TimeSpan getDurationExcludingAux();
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package com.amazon.avod.media.ads.internal.state;
|
||||||
|
|
||||||
|
public abstract class AdBreakState extends AdEnabledPlaybackState {
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.amazon.avod.media.ads.internal.state;
|
||||||
|
|
||||||
|
import com.amazon.avod.media.ads.AdBreak;
|
||||||
|
import com.amazon.avod.media.TimeSpan;
|
||||||
|
|
||||||
|
public class AdBreakTrigger {
|
||||||
|
public AdBreak getBreak() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public TimeSpan getSeekTarget() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public TimeSpan getSeekStartPosition() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.amazon.avod.media.ads.internal.state;
|
||||||
|
|
||||||
|
import com.amazon.avod.fsm.StateBase;
|
||||||
|
import com.amazon.avod.media.playback.state.PlayerStateType;
|
||||||
|
import com.amazon.avod.media.playback.state.trigger.PlayerTriggerType;
|
||||||
|
|
||||||
|
public class AdEnabledPlaybackState extends StateBase<PlayerStateType, PlayerTriggerType> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.amazon.avod.media.ads.internal.state;
|
||||||
|
|
||||||
|
public enum AdEnabledPlayerTriggerType {
|
||||||
|
NO_MORE_ADS_SKIP_TRANSITION
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package com.amazon.avod.media.ads.internal.state;
|
||||||
|
|
||||||
|
public class ServerInsertedAdBreakState extends AdBreakState {
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.amazon.avod.media.playback;
|
||||||
|
|
||||||
|
public interface VideoPlayer {
|
||||||
|
long getCurrentPosition();
|
||||||
|
|
||||||
|
void seekTo(long positionMs);
|
||||||
|
|
||||||
|
void pause();
|
||||||
|
|
||||||
|
void play();
|
||||||
|
|
||||||
|
boolean isPlaying();
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package com.amazon.avod.media.playback.state;
|
||||||
|
|
||||||
|
public interface PlayerStateType {
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package com.amazon.avod.media.playback.state.trigger;
|
||||||
|
|
||||||
|
public interface PlayerTriggerType {
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
package app.revanced.extension.youtube;
|
package app.revanced.extension.shared;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
@@ -39,7 +37,7 @@ public final class ByteTrieSearch extends TrieSearch<byte[]> {
|
|||||||
return replacement;
|
return replacement;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ByteTrieSearch(@NonNull byte[]... patterns) {
|
public ByteTrieSearch(byte[]... patterns) {
|
||||||
super(new ByteTrieNode(), patterns);
|
super(new ByteTrieNode(), patterns);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
package app.revanced.extension.shared;
|
package app.revanced.extension.shared;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
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;
|
||||||
@@ -14,15 +15,21 @@ 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.RequiresApi;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
/**
|
import app.revanced.extension.shared.requests.Requester;
|
||||||
* @noinspection unused
|
import app.revanced.extension.shared.requests.Route;
|
||||||
*/
|
import app.revanced.extension.shared.ui.CustomDialog;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
public class GmsCoreSupport {
|
public class GmsCoreSupport {
|
||||||
private static final String PACKAGE_NAME_YOUTUBE = "com.google.android.youtube";
|
private static final String PACKAGE_NAME_YOUTUBE = "com.google.android.youtube";
|
||||||
private static final String PACKAGE_NAME_YOUTUBE_MUSIC = "com.google.android.apps.youtube.music";
|
private static final String PACKAGE_NAME_YOUTUBE_MUSIC = "com.google.android.apps.youtube.music";
|
||||||
@@ -31,10 +38,24 @@ public class GmsCoreSupport {
|
|||||||
= getGmsCoreVendorGroupId() + ".android.gms";
|
= getGmsCoreVendorGroupId() + ".android.gms";
|
||||||
private static final Uri GMS_CORE_PROVIDER
|
private static final Uri GMS_CORE_PROVIDER
|
||||||
= Uri.parse("content://" + getGmsCoreVendorGroupId() + ".android.gsf.gservices/prefix");
|
= Uri.parse("content://" + getGmsCoreVendorGroupId() + ".android.gsf.gservices/prefix");
|
||||||
private static final String DONT_KILL_MY_APP_LINK
|
private static final String DONT_KILL_MY_APP_URL
|
||||||
= "https://dontkillmyapp.com";
|
= "https://dontkillmyapp.com/";
|
||||||
|
private static final Route DONT_KILL_MY_APP_MANUFACTURER_API
|
||||||
|
= new Route(GET, "/api/v2/{manufacturer}.json");
|
||||||
|
private static final String DONT_KILL_MY_APP_NAME_PARAMETER
|
||||||
|
= "?app=MicroG";
|
||||||
|
private static final String BUILD_MANUFACTURER
|
||||||
|
= Build.MANUFACTURER.toLowerCase(Locale.ROOT).replace(" ", "-");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If a manufacturer specific page exists on DontKillMyApp.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
private static volatile Boolean DONT_KILL_MY_APP_MANUFACTURER_SUPPORTED;
|
||||||
|
|
||||||
private static void open(String queryOrLink) {
|
private static void open(String queryOrLink) {
|
||||||
|
Logger.printInfo(() -> "Opening link: " + queryOrLink);
|
||||||
|
|
||||||
Intent intent;
|
Intent intent;
|
||||||
try {
|
try {
|
||||||
// Check if queryOrLink is a valid URL.
|
// Check if queryOrLink is a valid URL.
|
||||||
@@ -59,13 +80,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 = CustomDialog.create(
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
@@ -73,7 +108,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.
|
||||||
@@ -88,7 +122,7 @@ public class GmsCoreSupport {
|
|||||||
|
|
||||||
// Do not exit. If the app exits before launch completes (and without
|
// Do not exit. If the app exits before launch completes (and without
|
||||||
// opening another activity), then on some devices such as Pixel phone Android 10
|
// opening another activity), then on some devices such as Pixel phone Android 10
|
||||||
// no toast will be shown and the app will continually be relaunched
|
// no toast will be shown and the app will continually relaunch
|
||||||
// with the appearance of a hung app.
|
// with the appearance of a hung app.
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,15 +155,20 @@ 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();
|
||||||
|
|
||||||
showBatteryOptimizationDialog(context,
|
showBatteryOptimizationDialog(context,
|
||||||
"gms_core_dialog_not_whitelisted_not_allowed_in_background_message",
|
"gms_core_dialog_not_whitelisted_not_allowed_in_background_message",
|
||||||
"gms_core_dialog_open_website_text",
|
"gms_core_dialog_open_website_text",
|
||||||
(dialog, id) -> open(DONT_KILL_MY_APP_LINK));
|
(dialog, id) -> openDontKillMyApp());
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
if (client != null) client.close();
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "checkGmsCore failure", ex);
|
Logger.printException(() -> "checkGmsCore failure", ex);
|
||||||
@@ -143,10 +182,57 @@ public class GmsCoreSupport {
|
|||||||
activity.startActivityForResult(intent, 0);
|
activity.startActivityForResult(intent, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void checkIfDontKillMyAppSupportsManufacturer() {
|
||||||
|
Utils.runOnBackgroundThread(() -> {
|
||||||
|
try {
|
||||||
|
final long start = System.currentTimeMillis();
|
||||||
|
HttpURLConnection connection = Requester.getConnectionFromRoute(
|
||||||
|
DONT_KILL_MY_APP_URL, DONT_KILL_MY_APP_MANUFACTURER_API, BUILD_MANUFACTURER);
|
||||||
|
connection.setConnectTimeout(5000);
|
||||||
|
connection.setReadTimeout(5000);
|
||||||
|
|
||||||
|
final boolean supported = connection.getResponseCode() == 200;
|
||||||
|
Logger.printInfo(() -> "Manufacturer is " + (supported ? "" : "NOT ")
|
||||||
|
+ "listed on DontKillMyApp: " + BUILD_MANUFACTURER
|
||||||
|
+ " fetch took: " + (System.currentTimeMillis() - start) + "ms");
|
||||||
|
DONT_KILL_MY_APP_MANUFACTURER_SUPPORTED = supported;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printInfo(() -> "Could not check if manufacturer is listed on DontKillMyApp: "
|
||||||
|
+ BUILD_MANUFACTURER, ex);
|
||||||
|
DONT_KILL_MY_APP_MANUFACTURER_SUPPORTED = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void openDontKillMyApp() {
|
||||||
|
final Boolean manufacturerSupported = DONT_KILL_MY_APP_MANUFACTURER_SUPPORTED;
|
||||||
|
|
||||||
|
String manufacturerPageToOpen;
|
||||||
|
if (manufacturerSupported == null) {
|
||||||
|
// Fetch has not completed yet. Only happens on extremely slow internet connections
|
||||||
|
// and the user spends less than 1 second reading what's on screen.
|
||||||
|
// Instead of waiting for the fetch (which may timeout),
|
||||||
|
// open the website without a vendor.
|
||||||
|
manufacturerPageToOpen = "";
|
||||||
|
} else if (manufacturerSupported) {
|
||||||
|
manufacturerPageToOpen = BUILD_MANUFACTURER;
|
||||||
|
} else {
|
||||||
|
// No manufacturer specific page exists. Open the general page.
|
||||||
|
manufacturerPageToOpen = "general";
|
||||||
|
}
|
||||||
|
|
||||||
|
open(DONT_KILL_MY_APP_URL + manufacturerPageToOpen + DONT_KILL_MY_APP_NAME_PARAMETER);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return If GmsCore is not whitelisted from battery optimizations.
|
* @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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,27 @@
|
|||||||
package app.revanced.extension.shared;
|
package app.revanced.extension.shared;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.settings.BaseSettings.DEBUG;
|
||||||
|
import static app.revanced.extension.shared.settings.BaseSettings.DEBUG_STACKTRACE;
|
||||||
|
import static app.revanced.extension.shared.settings.BaseSettings.DEBUG_TOAST_ON_ERROR;
|
||||||
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
|
||||||
|
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
import java.io.StringWriter;
|
import java.io.StringWriter;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.settings.BaseSettings.*;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
|
import app.revanced.extension.shared.settings.preference.LogBufferManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ReVanced specific logger. Logging is done to standard device log (accessible thru ADB),
|
||||||
|
* and additionally accessible thru {@link LogBufferManager}.
|
||||||
|
*
|
||||||
|
* All methods are thread safe, and are safe to call even
|
||||||
|
* if {@link Utils#getContext()} is not available.
|
||||||
|
*/
|
||||||
public class Logger {
|
public class Logger {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -17,99 +29,173 @@ public class Logger {
|
|||||||
*/
|
*/
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface LogMessage {
|
public interface LogMessage {
|
||||||
|
/**
|
||||||
|
* @return Logger string message. This method is only called if logging is enabled.
|
||||||
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
String buildMessageString();
|
String buildMessageString();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
private enum LogLevel {
|
||||||
* @return For outer classes, this returns {@link Class#getSimpleName()}.
|
DEBUG,
|
||||||
* For static, inner, or anonymous classes, this returns the simple name of the enclosing class.
|
INFO,
|
||||||
* <br>
|
ERROR
|
||||||
* For example, each of these classes return 'SomethingView':
|
}
|
||||||
* <code>
|
|
||||||
* com.company.SomethingView
|
|
||||||
* com.company.SomethingView$StaticClass
|
|
||||||
* com.company.SomethingView$1
|
|
||||||
* </code>
|
|
||||||
*/
|
|
||||||
private String findOuterClassSimpleName() {
|
|
||||||
var selfClass = this.getClass();
|
|
||||||
|
|
||||||
String fullClassName = selfClass.getName();
|
/**
|
||||||
final int dollarSignIndex = fullClassName.indexOf('$');
|
* Log tag prefix. Only used for system logging.
|
||||||
if (dollarSignIndex < 0) {
|
*/
|
||||||
return selfClass.getSimpleName(); // Already an outer class.
|
private static final String REVANCED_LOG_TAG_PREFIX = "revanced: ";
|
||||||
|
|
||||||
|
private static final String LOGGER_CLASS_NAME = Logger.class.getName();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return For outer classes, this returns {@link Class#getSimpleName()}.
|
||||||
|
* For static, inner, or anonymous classes, this returns the simple name of the enclosing class.
|
||||||
|
* <br>
|
||||||
|
* For example, each of these classes returns 'SomethingView':
|
||||||
|
* <code>
|
||||||
|
* com.company.SomethingView
|
||||||
|
* com.company.SomethingView$StaticClass
|
||||||
|
* com.company.SomethingView$1
|
||||||
|
* </code>
|
||||||
|
*/
|
||||||
|
private static String getOuterClassSimpleName(Object obj) {
|
||||||
|
Class<?> logClass = obj.getClass();
|
||||||
|
String fullClassName = logClass.getName();
|
||||||
|
final int dollarSignIndex = fullClassName.indexOf('$');
|
||||||
|
if (dollarSignIndex < 0) {
|
||||||
|
return logClass.getSimpleName(); // Already an outer class.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Class is inner, static, or anonymous.
|
||||||
|
// Parse the simple name full name.
|
||||||
|
// A class with no package returns index of -1, but incrementing gives index zero which is correct.
|
||||||
|
final int simpleClassNameStartIndex = fullClassName.lastIndexOf('.') + 1;
|
||||||
|
return fullClassName.substring(simpleClassNameStartIndex, dollarSignIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal method to handle logging to Android Log and {@link LogBufferManager}.
|
||||||
|
* Appends the log message, stack trace (if enabled), and exception (if present) to logBuffer
|
||||||
|
* with class name but without 'revanced:' prefix.
|
||||||
|
*
|
||||||
|
* @param logLevel The log level.
|
||||||
|
* @param message Log message object.
|
||||||
|
* @param ex Optional exception.
|
||||||
|
* @param includeStackTrace If the current stack should be included.
|
||||||
|
* @param showToast If a toast is to be shown.
|
||||||
|
*/
|
||||||
|
private static void logInternal(LogLevel logLevel, LogMessage message, @Nullable Throwable ex,
|
||||||
|
boolean includeStackTrace, boolean showToast) {
|
||||||
|
// It's very important that no Settings are used in this method,
|
||||||
|
// as this code is used when a context is not set and thus referencing
|
||||||
|
// a setting will crash the app.
|
||||||
|
String messageString = message.buildMessageString();
|
||||||
|
String className = getOuterClassSimpleName(message);
|
||||||
|
|
||||||
|
String logText = messageString;
|
||||||
|
|
||||||
|
// Append exception message if present.
|
||||||
|
if (ex != null) {
|
||||||
|
var exceptionMessage = ex.getMessage();
|
||||||
|
if (exceptionMessage != null) {
|
||||||
|
logText += "\nException: " + exceptionMessage;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Class is inner, static, or anonymous.
|
if (includeStackTrace) {
|
||||||
// Parse the simple name full name.
|
var sw = new StringWriter();
|
||||||
// A class with no package returns index of -1, but incrementing gives index zero which is correct.
|
new Throwable().printStackTrace(new PrintWriter(sw));
|
||||||
final int simpleClassNameStartIndex = fullClassName.lastIndexOf('.') + 1;
|
String stackTrace = sw.toString();
|
||||||
return fullClassName.substring(simpleClassNameStartIndex, dollarSignIndex);
|
// Remove the stacktrace elements of this class.
|
||||||
|
final int loggerIndex = stackTrace.lastIndexOf(LOGGER_CLASS_NAME);
|
||||||
|
final int loggerBegins = stackTrace.indexOf('\n', loggerIndex);
|
||||||
|
logText += stackTrace.substring(loggerBegins);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not include "revanced:" prefix in clipboard logs.
|
||||||
|
String managerToastString = className + ": " + logText;
|
||||||
|
LogBufferManager.appendToLogBuffer(managerToastString);
|
||||||
|
|
||||||
|
String logTag = REVANCED_LOG_TAG_PREFIX + className;
|
||||||
|
switch (logLevel) {
|
||||||
|
case DEBUG:
|
||||||
|
if (ex == null) Log.d(logTag, logText);
|
||||||
|
else Log.d(logTag, logText, ex);
|
||||||
|
break;
|
||||||
|
case INFO:
|
||||||
|
if (ex == null) Log.i(logTag, logText);
|
||||||
|
else Log.i(logTag, logText, ex);
|
||||||
|
break;
|
||||||
|
case ERROR:
|
||||||
|
if (ex == null) Log.e(logTag, logText);
|
||||||
|
else Log.e(logTag, logText, ex);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showToast) {
|
||||||
|
Utils.showToastLong(managerToastString);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String REVANCED_LOG_PREFIX = "revanced: ";
|
private static boolean shouldLogDebug() {
|
||||||
|
// If the app is still starting up and the context is not yet set,
|
||||||
|
// then allow debug logging regardless what the debug setting actually is.
|
||||||
|
return Utils.context == null || DEBUG.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean shouldShowErrorToast() {
|
||||||
|
return Utils.context != null && DEBUG_TOAST_ON_ERROR.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean includeStackTrace() {
|
||||||
|
return Utils.context != null && DEBUG_STACKTRACE.get();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs debug messages under the outer class name of the code calling this method.
|
* Logs debug messages under the outer class name of the code calling this method.
|
||||||
* Whenever possible, the log string should be constructed entirely inside {@link LogMessage#buildMessageString()}
|
* <p>
|
||||||
* so the performance cost of building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
|
* Whenever possible, the log string should be constructed entirely inside
|
||||||
|
* {@link LogMessage#buildMessageString()} so the performance cost of
|
||||||
|
* building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
|
||||||
*/
|
*/
|
||||||
public static void printDebug(@NonNull LogMessage message) {
|
public static void printDebug(LogMessage message) {
|
||||||
printDebug(message, null);
|
printDebug(message, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
* Whenever possible, the log string should be constructed entirely inside {@link LogMessage#buildMessageString()}
|
* <p>
|
||||||
* so the performance cost of building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
|
* Whenever possible, the log string should be constructed entirely inside
|
||||||
|
* {@link LogMessage#buildMessageString()} so the performance cost of
|
||||||
|
* building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
|
||||||
*/
|
*/
|
||||||
public static void printDebug(@NonNull LogMessage message, @Nullable Exception ex) {
|
public static void printDebug(LogMessage message, @Nullable Exception ex) {
|
||||||
if (DEBUG.get()) {
|
if (shouldLogDebug()) {
|
||||||
String logMessage = message.buildMessageString();
|
logInternal(LogLevel.DEBUG, message, ex, includeStackTrace(), false);
|
||||||
String logTag = REVANCED_LOG_PREFIX + message.findOuterClassSimpleName();
|
|
||||||
|
|
||||||
if (DEBUG_STACKTRACE.get()) {
|
|
||||||
var builder = new StringBuilder(logMessage);
|
|
||||||
var sw = new StringWriter();
|
|
||||||
new Throwable().printStackTrace(new PrintWriter(sw));
|
|
||||||
|
|
||||||
builder.append('\n').append(sw);
|
|
||||||
logMessage = builder.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ex == null) {
|
|
||||||
Log.d(logTag, logMessage);
|
|
||||||
} else {
|
|
||||||
Log.d(logTag, logMessage, ex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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(@NonNull LogMessage message) {
|
public static void printInfo(LogMessage message) {
|
||||||
printInfo(message, null);
|
printInfo(message, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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(@NonNull LogMessage message, @Nullable Exception ex) {
|
public static void printInfo(LogMessage message, @Nullable Exception ex) {
|
||||||
String logTag = REVANCED_LOG_PREFIX + message.findOuterClassSimpleName();
|
logInternal(LogLevel.INFO, message, ex, includeStackTrace(), false);
|
||||||
String logMessage = message.buildMessageString();
|
|
||||||
if (ex == null) {
|
|
||||||
Log.i(logTag, logMessage);
|
|
||||||
} else {
|
|
||||||
Log.i(logTag, logMessage, ex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs exceptions under the outer class name of the code calling this method.
|
* Logs exceptions under the outer class name of the code calling this method.
|
||||||
|
* Appends the log message, exception (if present), and toast message (if enabled) to logBuffer.
|
||||||
*/
|
*/
|
||||||
public static void printException(@NonNull LogMessage message) {
|
public static void printException(LogMessage message) {
|
||||||
printException(message, null);
|
printException(message, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,35 +208,7 @@ public class Logger {
|
|||||||
* @param message log message
|
* @param message log message
|
||||||
* @param ex exception (optional)
|
* @param ex exception (optional)
|
||||||
*/
|
*/
|
||||||
public static void printException(@NonNull LogMessage message, @Nullable Throwable ex) {
|
public static void printException(LogMessage message, @Nullable Throwable ex) {
|
||||||
String messageString = message.buildMessageString();
|
logInternal(LogLevel.ERROR, message, ex, includeStackTrace(), shouldShowErrorToast());
|
||||||
String outerClassSimpleName = message.findOuterClassSimpleName();
|
|
||||||
String logMessage = REVANCED_LOG_PREFIX + outerClassSimpleName;
|
|
||||||
if (ex == null) {
|
|
||||||
Log.e(logMessage, messageString);
|
|
||||||
} else {
|
|
||||||
Log.e(logMessage, messageString, ex);
|
|
||||||
}
|
|
||||||
if (DEBUG_TOAST_ON_ERROR.get()) {
|
|
||||||
Utils.showToastLong(outerClassSimpleName + ": " + messageString);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
/**
|
|
||||||
* Logging to use if {@link BaseSettings#DEBUG} or {@link Utils#getContext()} may not be initialized.
|
|
||||||
* Normally this method should not be used.
|
|
||||||
*/
|
|
||||||
public static void initializationInfo(@NonNull Class<?> callingClass, @NonNull String message) {
|
|
||||||
Log.i(REVANCED_LOG_PREFIX + callingClass.getSimpleName(), message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logging to use if {@link BaseSettings#DEBUG} or {@link Utils#getContext()} may not be initialized.
|
|
||||||
* Normally this method should not be used.
|
|
||||||
*/
|
|
||||||
public static void initializationException(@NonNull Class<?> callingClass, @NonNull String message,
|
|
||||||
@Nullable Exception ex) {
|
|
||||||
Log.e(REVANCED_LOG_PREFIX + callingClass.getSimpleName(), message, ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
package app.revanced.extension.youtube;
|
package app.revanced.extension.shared;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Text pattern searching using a prefix tree (trie).
|
* Text pattern searching using a prefix tree (trie).
|
||||||
@@ -28,7 +26,7 @@ public final class StringTrieSearch extends TrieSearch<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public StringTrieSearch(@NonNull String... patterns) {
|
public StringTrieSearch(String... patterns) {
|
||||||
super(new StringTrieNode(), patterns);
|
super(new StringTrieNode(), patterns);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package app.revanced.extension.youtube;
|
package app.revanced.extension.shared;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -57,11 +56,13 @@ public abstract class TrieSearch<T> {
|
|||||||
if (searchTextLength - searchTextIndex < patternLength - patternStartIndex) {
|
if (searchTextLength - searchTextIndex < patternLength - patternStartIndex) {
|
||||||
return false; // Remaining search text is shorter than the remaining leaf pattern and they cannot match.
|
return false; // Remaining search text is shorter than the remaining leaf pattern and they cannot match.
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = searchTextIndex, j = patternStartIndex; j < patternLength; i++, j++) {
|
for (int i = searchTextIndex, j = patternStartIndex; j < patternLength; i++, j++) {
|
||||||
if (enclosingNode.getCharValue(searchText, i) != enclosingNode.getCharValue(pattern, j)) {
|
if (enclosingNode.getCharValue(searchText, i) != enclosingNode.getCharValue(pattern, j)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return callback == null || callback.patternMatched(searchText,
|
return callback == null || callback.patternMatched(searchText,
|
||||||
searchTextIndex - patternStartIndex, patternLength, callbackParameter);
|
searchTextIndex - patternStartIndex, patternLength, callbackParameter);
|
||||||
}
|
}
|
||||||
@@ -136,7 +137,7 @@ public abstract class TrieSearch<T> {
|
|||||||
* @param patternLength Length of the pattern.
|
* @param patternLength Length of the pattern.
|
||||||
* @param callback Callback, where a value of NULL indicates to always accept a pattern match.
|
* @param callback Callback, where a value of NULL indicates to always accept a pattern match.
|
||||||
*/
|
*/
|
||||||
private void addPattern(@NonNull T pattern, int patternIndex, int patternLength,
|
private void addPattern(T pattern, int patternIndex, int patternLength,
|
||||||
@Nullable TriePatternMatchedCallback<T> callback) {
|
@Nullable TriePatternMatchedCallback<T> callback) {
|
||||||
if (patternIndex == patternLength) { // Reached the end of the pattern.
|
if (patternIndex == patternLength) { // Reached the end of the pattern.
|
||||||
if (endOfPatternCallback == null) {
|
if (endOfPatternCallback == null) {
|
||||||
@@ -145,6 +146,7 @@ public abstract class TrieSearch<T> {
|
|||||||
endOfPatternCallback.add(callback);
|
endOfPatternCallback.add(callback);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (leaf != null) {
|
if (leaf != null) {
|
||||||
// Reached end of the graph and a leaf exist.
|
// Reached end of the graph and a leaf exist.
|
||||||
// Recursively call back into this method and push the existing leaf down 1 level.
|
// Recursively call back into this method and push the existing leaf down 1 level.
|
||||||
@@ -159,6 +161,7 @@ public abstract class TrieSearch<T> {
|
|||||||
leaf = new TrieCompressedPath<>(pattern, patternIndex, patternLength, callback);
|
leaf = new TrieCompressedPath<>(pattern, patternIndex, patternLength, callback);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final char character = getCharValue(pattern, patternIndex);
|
final char character = getCharValue(pattern, patternIndex);
|
||||||
final int arrayIndex = hashIndexForTableSize(children.length, character);
|
final int arrayIndex = hashIndexForTableSize(children.length, character);
|
||||||
TrieNode<T> child = children[arrayIndex];
|
TrieNode<T> child = children[arrayIndex];
|
||||||
@@ -183,6 +186,7 @@ public abstract class TrieSearch<T> {
|
|||||||
//noinspection unchecked
|
//noinspection unchecked
|
||||||
TrieNode<T>[] replacement = new TrieNode[replacementArraySize];
|
TrieNode<T>[] replacement = new TrieNode[replacementArraySize];
|
||||||
addNodeToArray(replacement, child);
|
addNodeToArray(replacement, child);
|
||||||
|
|
||||||
boolean collision = false;
|
boolean collision = false;
|
||||||
for (TrieNode<T> existingChild : children) {
|
for (TrieNode<T> existingChild : children) {
|
||||||
if (existingChild != null) {
|
if (existingChild != null) {
|
||||||
@@ -195,6 +199,7 @@ public abstract class TrieSearch<T> {
|
|||||||
if (collision) {
|
if (collision) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
children = replacement;
|
children = replacement;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -234,6 +239,7 @@ public abstract class TrieSearch<T> {
|
|||||||
if (leaf != null && leaf.matches(startNode, searchText, searchTextEndIndex, searchTextIndex, callbackParameter)) {
|
if (leaf != null && leaf.matches(startNode, searchText, searchTextEndIndex, searchTextIndex, callbackParameter)) {
|
||||||
return true; // Leaf exists and it matched the search text.
|
return true; // Leaf exists and it matched the search text.
|
||||||
}
|
}
|
||||||
|
|
||||||
List<TriePatternMatchedCallback<T>> endOfPatternCallback = node.endOfPatternCallback;
|
List<TriePatternMatchedCallback<T>> endOfPatternCallback = node.endOfPatternCallback;
|
||||||
if (endOfPatternCallback != null) {
|
if (endOfPatternCallback != null) {
|
||||||
final int matchStartIndex = searchTextIndex - currentMatchLength;
|
final int matchStartIndex = searchTextIndex - currentMatchLength;
|
||||||
@@ -246,6 +252,7 @@ public abstract class TrieSearch<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TrieNode<T>[] children = node.children;
|
TrieNode<T>[] children = node.children;
|
||||||
if (children == null) {
|
if (children == null) {
|
||||||
return false; // Reached a graph end point and there's no further patterns to search.
|
return false; // Reached a graph end point and there's no further patterns to search.
|
||||||
@@ -278,9 +285,11 @@ public abstract class TrieSearch<T> {
|
|||||||
if (leaf != null) {
|
if (leaf != null) {
|
||||||
numberOfPointers += 4; // Number of fields in leaf node.
|
numberOfPointers += 4; // Number of fields in leaf node.
|
||||||
}
|
}
|
||||||
|
|
||||||
if (endOfPatternCallback != null) {
|
if (endOfPatternCallback != null) {
|
||||||
numberOfPointers += endOfPatternCallback.size();
|
numberOfPointers += endOfPatternCallback.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (children != null) {
|
if (children != null) {
|
||||||
numberOfPointers += children.length;
|
numberOfPointers += children.length;
|
||||||
for (TrieNode<T> child : children) {
|
for (TrieNode<T> child : children) {
|
||||||
@@ -308,13 +317,13 @@ public abstract class TrieSearch<T> {
|
|||||||
private final List<T> patterns = new ArrayList<>();
|
private final List<T> patterns = new ArrayList<>();
|
||||||
|
|
||||||
@SafeVarargs
|
@SafeVarargs
|
||||||
TrieSearch(@NonNull TrieNode<T> root, @NonNull T... patterns) {
|
TrieSearch(TrieNode<T> root, T... patterns) {
|
||||||
this.root = Objects.requireNonNull(root);
|
this.root = Objects.requireNonNull(root);
|
||||||
addPatterns(patterns);
|
addPatterns(patterns);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SafeVarargs
|
@SafeVarargs
|
||||||
public final void addPatterns(@NonNull T... patterns) {
|
public final void addPatterns(T... patterns) {
|
||||||
for (T pattern : patterns) {
|
for (T pattern : patterns) {
|
||||||
addPattern(pattern);
|
addPattern(pattern);
|
||||||
}
|
}
|
||||||
@@ -325,7 +334,7 @@ public abstract class TrieSearch<T> {
|
|||||||
*
|
*
|
||||||
* @param pattern Pattern to add. Calling this with a zero length pattern does nothing.
|
* @param pattern Pattern to add. Calling this with a zero length pattern does nothing.
|
||||||
*/
|
*/
|
||||||
public void addPattern(@NonNull T pattern) {
|
public void addPattern(T pattern) {
|
||||||
addPattern(pattern, root.getTextLength(pattern), null);
|
addPattern(pattern, root.getTextLength(pattern), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,31 +342,31 @@ public abstract class TrieSearch<T> {
|
|||||||
* @param pattern Pattern to add. Calling this with a zero length pattern does nothing.
|
* @param pattern Pattern to add. Calling this with a zero length pattern does nothing.
|
||||||
* @param callback Callback to determine if searching should halt when a match is found.
|
* @param callback Callback to determine if searching should halt when a match is found.
|
||||||
*/
|
*/
|
||||||
public void addPattern(@NonNull T pattern, @NonNull TriePatternMatchedCallback<T> callback) {
|
public void addPattern(T pattern, TriePatternMatchedCallback<T> callback) {
|
||||||
addPattern(pattern, root.getTextLength(pattern), Objects.requireNonNull(callback));
|
addPattern(pattern, root.getTextLength(pattern), Objects.requireNonNull(callback));
|
||||||
}
|
}
|
||||||
|
|
||||||
void addPattern(@NonNull T pattern, int patternLength, @Nullable TriePatternMatchedCallback<T> callback) {
|
void addPattern(T pattern, int patternLength, @Nullable TriePatternMatchedCallback<T> callback) {
|
||||||
if (patternLength == 0) return; // Nothing to match
|
if (patternLength == 0) return; // Nothing to match
|
||||||
|
|
||||||
patterns.add(pattern);
|
patterns.add(pattern);
|
||||||
root.addPattern(pattern, 0, patternLength, callback);
|
root.addPattern(pattern, 0, patternLength, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
public final boolean matches(@NonNull T textToSearch) {
|
public final boolean matches(T textToSearch) {
|
||||||
return matches(textToSearch, 0);
|
return matches(textToSearch, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean matches(@NonNull T textToSearch, @NonNull Object callbackParameter) {
|
public boolean matches(T textToSearch, Object callbackParameter) {
|
||||||
return matches(textToSearch, 0, root.getTextLength(textToSearch),
|
return matches(textToSearch, 0, root.getTextLength(textToSearch),
|
||||||
Objects.requireNonNull(callbackParameter));
|
Objects.requireNonNull(callbackParameter));
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean matches(@NonNull T textToSearch, int startIndex) {
|
public boolean matches(T textToSearch, int startIndex) {
|
||||||
return matches(textToSearch, startIndex, root.getTextLength(textToSearch));
|
return matches(textToSearch, startIndex, root.getTextLength(textToSearch));
|
||||||
}
|
}
|
||||||
|
|
||||||
public final boolean matches(@NonNull T textToSearch, int startIndex, int endIndex) {
|
public final boolean matches(T textToSearch, int startIndex, int endIndex) {
|
||||||
return matches(textToSearch, startIndex, endIndex, null);
|
return matches(textToSearch, startIndex, endIndex, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,11 +379,11 @@ public abstract class TrieSearch<T> {
|
|||||||
* @param callbackParameter Optional parameter passed to the callbacks.
|
* @param callbackParameter Optional parameter passed to the callbacks.
|
||||||
* @return If any pattern matched, and it's callback halted searching.
|
* @return If any pattern matched, and it's callback halted searching.
|
||||||
*/
|
*/
|
||||||
public boolean matches(@NonNull T textToSearch, int startIndex, int endIndex, @Nullable Object callbackParameter) {
|
public boolean matches(T textToSearch, int startIndex, int endIndex, @Nullable Object callbackParameter) {
|
||||||
return matches(textToSearch, root.getTextLength(textToSearch), startIndex, endIndex, callbackParameter);
|
return matches(textToSearch, root.getTextLength(textToSearch), startIndex, endIndex, callbackParameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean matches(@NonNull T textToSearch, int textToSearchLength, int startIndex, int endIndex,
|
private boolean matches(T textToSearch, int textToSearchLength, int startIndex, int endIndex,
|
||||||
@Nullable Object callbackParameter) {
|
@Nullable Object callbackParameter) {
|
||||||
if (endIndex > textToSearchLength) {
|
if (endIndex > textToSearchLength) {
|
||||||
throw new IllegalArgumentException("endIndex: " + endIndex
|
throw new IllegalArgumentException("endIndex: " + endIndex
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
package app.revanced.extension.shared;
|
package app.revanced.extension.shared;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.*;
|
import android.app.Activity;
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.app.DialogFragment;
|
||||||
|
import android.content.ClipData;
|
||||||
|
import android.content.ClipboardManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.ApplicationInfo;
|
import android.content.pm.ApplicationInfo;
|
||||||
@@ -18,9 +22,15 @@ import android.os.Looper;
|
|||||||
import android.preference.Preference;
|
import android.preference.Preference;
|
||||||
import android.preference.PreferenceGroup;
|
import android.preference.PreferenceGroup;
|
||||||
import android.preference.PreferenceScreen;
|
import android.preference.PreferenceScreen;
|
||||||
|
import android.util.DisplayMetrics;
|
||||||
|
import android.util.Pair;
|
||||||
|
import android.util.TypedValue;
|
||||||
|
import android.view.Gravity;
|
||||||
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.view.Window;
|
||||||
|
import android.view.WindowManager;
|
||||||
import android.view.animation.Animation;
|
import android.view.animation.Animation;
|
||||||
import android.view.animation.AnimationUtils;
|
import android.view.animation.AnimationUtils;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
@@ -29,31 +39,45 @@ import android.widget.RelativeLayout;
|
|||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
import android.widget.Toolbar;
|
import android.widget.Toolbar;
|
||||||
|
|
||||||
|
import androidx.annotation.ColorInt;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import java.text.Bidi;
|
import java.text.Bidi;
|
||||||
import java.util.*;
|
import java.util.ArrayList;
|
||||||
import java.util.regex.Pattern;
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.concurrent.Callable;
|
import java.util.concurrent.Callable;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
import java.util.concurrent.SynchronousQueue;
|
import java.util.concurrent.SynchronousQueue;
|
||||||
import java.util.concurrent.ThreadPoolExecutor;
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import app.revanced.extension.shared.settings.AppLanguage;
|
import app.revanced.extension.shared.settings.AppLanguage;
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||||
import app.revanced.extension.shared.settings.preference.ReVancedAboutPreference;
|
import app.revanced.extension.shared.settings.preference.ReVancedAboutPreference;
|
||||||
|
|
||||||
|
@SuppressWarnings("NewApi")
|
||||||
public class Utils {
|
public class Utils {
|
||||||
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
@SuppressLint("StaticFieldLeak")
|
||||||
private static volatile Context context;
|
static volatile Context context;
|
||||||
|
|
||||||
private static String versionName;
|
private static String versionName;
|
||||||
private static String applicationLabel;
|
private static String applicationLabel;
|
||||||
|
|
||||||
|
@ColorInt
|
||||||
|
private static int darkColor = Color.BLACK;
|
||||||
|
@ColorInt
|
||||||
|
private static int lightColor = Color.WHITE;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static Boolean isDarkModeEnabled;
|
||||||
|
|
||||||
private Utils() {
|
private Utils() {
|
||||||
} // utility class
|
} // utility class
|
||||||
|
|
||||||
@@ -85,7 +109,7 @@ public class Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return The version name of the app, such as 19.11.43
|
* @return The version name of the app, such as 20.13.41
|
||||||
*/
|
*/
|
||||||
public static String getAppVersionName() {
|
public static String getAppVersionName() {
|
||||||
if (versionName == null) {
|
if (versionName == null) {
|
||||||
@@ -177,8 +201,8 @@ public class Utils {
|
|||||||
public static boolean hideViewByRemovingFromParentUnderCondition(boolean setting, View view) {
|
public static boolean hideViewByRemovingFromParentUnderCondition(boolean setting, View view) {
|
||||||
if (setting) {
|
if (setting) {
|
||||||
ViewParent parent = view.getParent();
|
ViewParent parent = view.getParent();
|
||||||
if (parent instanceof ViewGroup) {
|
if (parent instanceof ViewGroup parentGroup) {
|
||||||
((ViewGroup) parent).removeView(view);
|
parentGroup.removeView(view);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -191,23 +215,22 @@ public class Utils {
|
|||||||
* All tasks run at max thread priority.
|
* All tasks run at max thread priority.
|
||||||
*/
|
*/
|
||||||
private static final ThreadPoolExecutor backgroundThreadPool = new ThreadPoolExecutor(
|
private static final ThreadPoolExecutor backgroundThreadPool = new ThreadPoolExecutor(
|
||||||
3, // 3 threads always ready to go
|
3, // 3 threads always ready to go.
|
||||||
Integer.MAX_VALUE,
|
Integer.MAX_VALUE,
|
||||||
10, // For any threads over the minimum, keep them alive 10 seconds after they go idle
|
10, // For any threads over the minimum, keep them alive 10 seconds after they go idle.
|
||||||
TimeUnit.SECONDS,
|
TimeUnit.SECONDS,
|
||||||
new SynchronousQueue<>(),
|
new SynchronousQueue<>(),
|
||||||
r -> { // ThreadFactory
|
r -> { // ThreadFactory
|
||||||
Thread t = new Thread(r);
|
Thread t = new Thread(r);
|
||||||
t.setPriority(Thread.MAX_PRIORITY); // run at max priority
|
t.setPriority(Thread.MAX_PRIORITY); // Run at max priority.
|
||||||
return t;
|
return t;
|
||||||
});
|
});
|
||||||
|
|
||||||
public static void runOnBackgroundThread(@NonNull Runnable task) {
|
public static void runOnBackgroundThread(Runnable task) {
|
||||||
backgroundThreadPool.execute(task);
|
backgroundThreadPool.execute(task);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
public static <T> Future<T> submitOnBackgroundThread(Callable<T> call) {
|
||||||
public static <T> Future<T> submitOnBackgroundThread(@NonNull Callable<T> call) {
|
|
||||||
return backgroundThreadPool.submit(call);
|
return backgroundThreadPool.submit(call);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,20 +245,19 @@ public class Utils {
|
|||||||
|
|
||||||
long meaninglessValue = 0;
|
long meaninglessValue = 0;
|
||||||
while (System.currentTimeMillis() - timeCalculationStarted < amountOfTimeToWaste) {
|
while (System.currentTimeMillis() - timeCalculationStarted < amountOfTimeToWaste) {
|
||||||
// could do a thread sleep, but that will trigger an exception if the thread is interrupted
|
// Could do a thread sleep, but that will trigger an exception if the thread is interrupted.
|
||||||
meaninglessValue += Long.numberOfLeadingZeros((long) Math.exp(Math.random()));
|
meaninglessValue += Long.numberOfLeadingZeros((long) Math.exp(Math.random()));
|
||||||
}
|
}
|
||||||
// return the value, otherwise the compiler or VM might optimize and remove the meaningless time wasting work,
|
// Return the value, otherwise the compiler or VM might optimize and remove the meaningless time wasting work,
|
||||||
// leaving an empty loop that hammers on the System.currentTimeMillis native call
|
// leaving an empty loop that hammers on the System.currentTimeMillis native call.
|
||||||
return meaninglessValue;
|
return meaninglessValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean containsAny(String value, String... targets) {
|
||||||
public static boolean containsAny(@NonNull String value, @NonNull String... targets) {
|
|
||||||
return indexOfFirstFound(value, targets) >= 0;
|
return indexOfFirstFound(value, targets) >= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int indexOfFirstFound(@NonNull String value, @NonNull String... targets) {
|
public static int indexOfFirstFound(String value, String... targets) {
|
||||||
for (String string : targets) {
|
for (String string : targets) {
|
||||||
if (!string.isEmpty()) {
|
if (!string.isEmpty()) {
|
||||||
final int indexOf = value.indexOf(string);
|
final int indexOf = value.indexOf(string);
|
||||||
@@ -246,40 +268,66 @@ public class Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return zero, if the resource is not found
|
* @return zero, if the resource is not found.
|
||||||
*/
|
*/
|
||||||
@SuppressLint("DiscouragedApi")
|
@SuppressLint("DiscouragedApi")
|
||||||
public static int getResourceIdentifier(@NonNull Context context, @NonNull String resourceIdentifierName, @NonNull String type) {
|
public static int getResourceIdentifier(Context context, String resourceIdentifierName, @Nullable String type) {
|
||||||
return context.getResources().getIdentifier(resourceIdentifierName, type, context.getPackageName());
|
return context.getResources().getIdentifier(resourceIdentifierName, type, context.getPackageName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int getResourceIdentifierOrThrow(Context context, String resourceIdentifierName, @Nullable String type) {
|
||||||
|
final int resourceId = getResourceIdentifier(context, resourceIdentifierName, type);
|
||||||
|
if (resourceId == 0) {
|
||||||
|
throw new Resources.NotFoundException("No resource id exists with name: " + resourceIdentifierName
|
||||||
|
+ " type: " + type);
|
||||||
|
}
|
||||||
|
return resourceId;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return zero, if the resource is not found
|
* @return zero, if the resource is not found.
|
||||||
|
* @see #getResourceIdentifierOrThrow(String, String)
|
||||||
*/
|
*/
|
||||||
public static int getResourceIdentifier(@NonNull String resourceIdentifierName, @NonNull String type) {
|
public static int getResourceIdentifier(String resourceIdentifierName, @Nullable String type) {
|
||||||
return getResourceIdentifier(getContext(), resourceIdentifierName, type);
|
return getResourceIdentifier(getContext(), resourceIdentifierName, type);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int getResourceInteger(@NonNull String resourceIdentifierName) throws Resources.NotFoundException {
|
/**
|
||||||
return getContext().getResources().getInteger(getResourceIdentifier(resourceIdentifierName, "integer"));
|
* @return The resource identifier, or throws an exception if not found.
|
||||||
|
*/
|
||||||
|
public static int getResourceIdentifierOrThrow(String resourceIdentifierName, @Nullable String type) {
|
||||||
|
final int resourceId = getResourceIdentifier(getContext(), resourceIdentifierName, type);
|
||||||
|
if (resourceId == 0) {
|
||||||
|
throw new Resources.NotFoundException("No resource id exists with name: " + resourceIdentifierName
|
||||||
|
+ " type: " + type);
|
||||||
|
}
|
||||||
|
return resourceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
public static int getResourceInteger(String resourceIdentifierName) throws Resources.NotFoundException {
|
||||||
public static Animation getResourceAnimation(@NonNull String resourceIdentifierName) throws Resources.NotFoundException {
|
return getContext().getResources().getInteger(getResourceIdentifierOrThrow(resourceIdentifierName, "integer"));
|
||||||
return AnimationUtils.loadAnimation(getContext(), getResourceIdentifier(resourceIdentifierName, "anim"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int getResourceColor(@NonNull String resourceIdentifierName) throws Resources.NotFoundException {
|
public static Animation getResourceAnimation(String resourceIdentifierName) throws Resources.NotFoundException {
|
||||||
|
return AnimationUtils.loadAnimation(getContext(), getResourceIdentifierOrThrow(resourceIdentifierName, "anim"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ColorInt
|
||||||
|
public static int getResourceColor(String resourceIdentifierName) throws Resources.NotFoundException {
|
||||||
//noinspection deprecation
|
//noinspection deprecation
|
||||||
return getContext().getResources().getColor(getResourceIdentifier(resourceIdentifierName, "color"));
|
return getContext().getResources().getColor(getResourceIdentifierOrThrow(resourceIdentifierName, "color"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int getResourceDimensionPixelSize(@NonNull String resourceIdentifierName) throws Resources.NotFoundException {
|
public static int getResourceDimensionPixelSize(String resourceIdentifierName) throws Resources.NotFoundException {
|
||||||
return getContext().getResources().getDimensionPixelSize(getResourceIdentifier(resourceIdentifierName, "dimen"));
|
return getContext().getResources().getDimensionPixelSize(getResourceIdentifierOrThrow(resourceIdentifierName, "dimen"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static float getResourceDimension(@NonNull String resourceIdentifierName) throws Resources.NotFoundException {
|
public static float getResourceDimension(String resourceIdentifierName) throws Resources.NotFoundException {
|
||||||
return getContext().getResources().getDimension(getResourceIdentifier(resourceIdentifierName, "dimen"));
|
return getContext().getResources().getDimension(getResourceIdentifierOrThrow(resourceIdentifierName, "dimen"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String[] getResourceStringArray(String resourceIdentifierName) throws Resources.NotFoundException {
|
||||||
|
return getContext().getResources().getStringArray(getResourceIdentifierOrThrow(resourceIdentifierName, "array"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface MatchFilter<T> {
|
public interface MatchFilter<T> {
|
||||||
@@ -288,16 +336,11 @@ public class Utils {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Includes sub children.
|
* Includes sub children.
|
||||||
*
|
|
||||||
* @noinspection unchecked
|
|
||||||
*/
|
*/
|
||||||
public static <R extends View> R getChildViewByResourceName(@NonNull View view, @NonNull String str) {
|
public static <R extends View> R getChildViewByResourceName(View view, String str) {
|
||||||
var child = view.findViewById(Utils.getResourceIdentifier(str, "id"));
|
var child = view.findViewById(Utils.getResourceIdentifierOrThrow(str, "id"));
|
||||||
if (child != null) {
|
//noinspection unchecked
|
||||||
return (R) child;
|
return (R) child;
|
||||||
}
|
|
||||||
|
|
||||||
throw new IllegalArgumentException("View with resource name '" + str + "' not found");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -306,8 +349,8 @@ public class Utils {
|
|||||||
* @return The first child view that matches the filter.
|
* @return The first child view that matches the filter.
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
public static <T extends View> T getChildView(@NonNull ViewGroup viewGroup, boolean searchRecursively,
|
public static <T extends View> T getChildView(ViewGroup viewGroup, boolean searchRecursively,
|
||||||
@NonNull MatchFilter<View> filter) {
|
MatchFilter<View> filter) {
|
||||||
for (int i = 0, childCount = viewGroup.getChildCount(); i < childCount; i++) {
|
for (int i = 0, childCount = viewGroup.getChildCount(); i < childCount; i++) {
|
||||||
View childAt = viewGroup.getChildAt(i);
|
View childAt = viewGroup.getChildAt(i);
|
||||||
|
|
||||||
@@ -326,7 +369,7 @@ public class Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public static ViewParent getParentView(@NonNull View view, int nthParent) {
|
public static ViewParent getParentView(View view, int nthParent) {
|
||||||
ViewParent parent = view.getParent();
|
ViewParent parent = view.getParent();
|
||||||
|
|
||||||
int currentDepth = 0;
|
int currentDepth = 0;
|
||||||
@@ -344,7 +387,7 @@ public class Utils {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void restartApp(@NonNull Context context) {
|
public static void restartApp(Context context) {
|
||||||
String packageName = context.getPackageName();
|
String packageName = context.getPackageName();
|
||||||
Intent intent = Objects.requireNonNull(context.getPackageManager().getLaunchIntentForPackage(packageName));
|
Intent intent = Objects.requireNonNull(context.getPackageManager().getLaunchIntentForPackage(packageName));
|
||||||
Intent mainIntent = Intent.makeRestartActivityTask(intent.getComponent());
|
Intent mainIntent = Intent.makeRestartActivityTask(intent.getComponent());
|
||||||
@@ -357,29 +400,35 @@ public class Utils {
|
|||||||
|
|
||||||
public static Context getContext() {
|
public static Context getContext() {
|
||||||
if (context == null) {
|
if (context == null) {
|
||||||
Logger.initializationException(Utils.class, "Context is not set by extension hook, returning null", null);
|
Logger.printException(() -> "Context is not set by extension hook, returning null", null);
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void setContext(Context appContext) {
|
public static void setContext(Context appContext) {
|
||||||
|
// Intentionally use logger before context is set,
|
||||||
|
// to expose any bugs in the 'no context available' logger code.
|
||||||
|
Logger.printInfo(() -> "Set context: " + appContext);
|
||||||
// Must initially set context to check the app language.
|
// Must initially set context to check the app language.
|
||||||
context = appContext;
|
context = appContext;
|
||||||
Logger.initializationInfo(Utils.class, "Set context: " + appContext);
|
|
||||||
|
|
||||||
AppLanguage language = BaseSettings.REVANCED_LANGUAGE.get();
|
AppLanguage language = BaseSettings.REVANCED_LANGUAGE.get();
|
||||||
if (language != AppLanguage.DEFAULT) {
|
if (language != AppLanguage.DEFAULT) {
|
||||||
// Create a new context with the desired language.
|
// Create a new context with the desired language.
|
||||||
Logger.printDebug(() -> "Using app language: " + language);
|
Logger.printDebug(() -> "Using app language: " + language);
|
||||||
Configuration config = appContext.getResources().getConfiguration();
|
Configuration config = new Configuration(appContext.getResources().getConfiguration());
|
||||||
config.setLocale(language.getLocale());
|
config.setLocale(language.getLocale());
|
||||||
context = appContext.createConfigurationContext(config);
|
context = appContext.createConfigurationContext(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setThemeLightColor(getThemeColor(getThemeLightColorResourceName(), Color.WHITE));
|
||||||
|
setThemeDarkColor(getThemeColor(getThemeDarkColorResourceName(), Color.BLACK));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void setClipboard(@NonNull String text) {
|
public static void setClipboard(CharSequence text) {
|
||||||
android.content.ClipboardManager clipboard = (android.content.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
ClipboardManager clipboard = (ClipboardManager) context
|
||||||
android.content.ClipData clip = android.content.ClipData.newPlainText("ReVanced", text);
|
.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||||
|
ClipData clip = ClipData.newPlainText("ReVanced", text);
|
||||||
clipboard.setPrimaryClip(clip);
|
clipboard.setPrimaryClip(clip);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,22 +440,53 @@ public class Utils {
|
|||||||
private static Boolean isRightToLeftTextLayout;
|
private static Boolean isRightToLeftTextLayout;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the device language uses right to left text layout (hebrew, arabic, etc)
|
* @return If the device language uses right to left text layout (Hebrew, Arabic, etc).
|
||||||
|
* If this should match any ReVanced language override then instead use
|
||||||
|
* {@link #isRightToLeftLocale(Locale)} with {@link BaseSettings#REVANCED_LANGUAGE}.
|
||||||
|
* This is the default locale of the device, which may differ if
|
||||||
|
* {@link BaseSettings#REVANCED_LANGUAGE} is set to a different language.
|
||||||
*/
|
*/
|
||||||
public static boolean isRightToLeftTextLayout() {
|
public static boolean isRightToLeftLocale() {
|
||||||
if (isRightToLeftTextLayout == null) {
|
if (isRightToLeftTextLayout == null) {
|
||||||
String displayLanguage = Locale.getDefault().getDisplayLanguage();
|
isRightToLeftTextLayout = isRightToLeftLocale(Locale.getDefault());
|
||||||
isRightToLeftTextLayout = new Bidi(displayLanguage, Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT).isRightToLeft();
|
|
||||||
}
|
}
|
||||||
return isRightToLeftTextLayout;
|
return isRightToLeftTextLayout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return If the locale uses right to left text layout (Hebrew, Arabic, etc).
|
||||||
|
*/
|
||||||
|
public static boolean isRightToLeftLocale(Locale locale) {
|
||||||
|
String displayLanguage = locale.getDisplayLanguage();
|
||||||
|
return new Bidi(displayLanguage, Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT).isRightToLeft();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return A UTF8 string containing a left-to-right or right-to-left
|
||||||
|
* character of the device locale. If this should match any ReVanced language
|
||||||
|
* override then instead use {@link #getTextDirectionString(Locale)} with
|
||||||
|
* {@link BaseSettings#REVANCED_LANGUAGE}.
|
||||||
|
*/
|
||||||
|
public static String getTextDirectionString() {
|
||||||
|
return getTextDirectionString(isRightToLeftLocale());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getTextDirectionString(Locale locale) {
|
||||||
|
return getTextDirectionString(isRightToLeftLocale(locale));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getTextDirectionString(boolean isRightToLeft) {
|
||||||
|
return isRightToLeft
|
||||||
|
? "\u200F" // u200F = right to left character.
|
||||||
|
: "\u200E"; // u200E = left to right character.
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return if the text contains at least 1 number character,
|
* @return if the text contains at least 1 number character,
|
||||||
* including any unicode numbers such as Arabic.
|
* including any unicode numbers such as Arabic.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||||
public static boolean containsNumber(@NonNull CharSequence text) {
|
public static boolean containsNumber(CharSequence text) {
|
||||||
for (int index = 0, length = text.length(); index < length;) {
|
for (int index = 0, length = text.length(); index < length;) {
|
||||||
final int codePoint = Character.codePointAt(text, index);
|
final int codePoint = Character.codePointAt(text, index);
|
||||||
if (Character.isDigit(codePoint)) {
|
if (Character.isDigit(codePoint)) {
|
||||||
@@ -445,7 +525,7 @@ public class Utils {
|
|||||||
super.onStart();
|
super.onStart();
|
||||||
|
|
||||||
if (onStartAction != null) {
|
if (onStartAction != null) {
|
||||||
onStartAction.onStart((AlertDialog) getDialog());
|
onStartAction.onStart(dialog);
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "onStart failure: " + dialog.getClass().getSimpleName(), ex);
|
Logger.printException(() -> "onStart failure: " + dialog.getClass().getSimpleName(), ex);
|
||||||
@@ -454,34 +534,34 @@ public class Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for {@link #showDialog(Activity, AlertDialog, boolean, DialogFragmentOnStartAction)}.
|
* Interface for {@link #showDialog(Activity, Dialog, boolean, DialogFragmentOnStartAction)}.
|
||||||
*/
|
*/
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface DialogFragmentOnStartAction {
|
public interface DialogFragmentOnStartAction {
|
||||||
void onStart(AlertDialog dialog);
|
void onStart(Dialog dialog);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void showDialog(Activity activity, AlertDialog dialog) {
|
public static void showDialog(Activity activity, Dialog dialog) {
|
||||||
showDialog(activity, dialog, true, null);
|
showDialog(activity, dialog, true, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility method to allow showing an AlertDialog on top of other alert dialogs.
|
* Utility method to allow showing a Dialog on top of other dialogs.
|
||||||
* Calling this will always display the dialog on top of all other dialogs
|
* Calling this will always display the dialog on top of all other dialogs
|
||||||
* previously called using this method.
|
* previously called using this method.
|
||||||
* <br>
|
* <p>
|
||||||
* Be aware the on start action can be called multiple times for some situations,
|
* Be aware the on start action can be called multiple times for some situations,
|
||||||
* such as the user switching apps without dismissing the dialog then switching back to this app.
|
* such as the user switching apps without dismissing the dialog then switching back to this app.
|
||||||
*<br>
|
* <p>
|
||||||
* This method is only useful during app startup and multiple patches may show their own dialog,
|
* This method is only useful during app startup and multiple patches may show their own dialog,
|
||||||
* and the most important dialog can be called last (using a delay) so it's always on top.
|
* and the most important dialog can be called last (using a delay) so it's always on top.
|
||||||
*<br>
|
* <p>
|
||||||
* For all other situations it's better to not use this method and
|
* For all other situations it's better to not use this method and
|
||||||
* call {@link AlertDialog#show()} on the dialog.
|
* call {@link Dialog#show()} on the dialog.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
public static void showDialog(Activity activity,
|
public static void showDialog(Activity activity,
|
||||||
AlertDialog dialog,
|
Dialog dialog,
|
||||||
boolean isCancelable,
|
boolean isCancelable,
|
||||||
@Nullable DialogFragmentOnStartAction onStartAction) {
|
@Nullable DialogFragmentOnStartAction onStartAction) {
|
||||||
verifyOnMainThread();
|
verifyOnMainThread();
|
||||||
@@ -495,40 +575,64 @@ public class Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Safe to call from any thread
|
* Safe to call from any thread.
|
||||||
*/
|
*/
|
||||||
public static void showToastShort(@NonNull String messageToToast) {
|
public static void showToastShort(String messageToToast) {
|
||||||
showToast(messageToToast, Toast.LENGTH_SHORT);
|
showToast(messageToToast, Toast.LENGTH_SHORT);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Safe to call from any thread
|
* Safe to call from any thread.
|
||||||
*/
|
*/
|
||||||
public static void showToastLong(@NonNull String messageToToast) {
|
public static void showToastLong(String messageToToast) {
|
||||||
showToast(messageToToast, Toast.LENGTH_LONG);
|
showToast(messageToToast, Toast.LENGTH_LONG);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void showToast(@NonNull String messageToToast, int toastDuration) {
|
/**
|
||||||
|
* Safe to call from any thread.
|
||||||
|
*
|
||||||
|
* @param messageToToast Message to show.
|
||||||
|
* @param toastDuration Either {@link Toast#LENGTH_SHORT} or {@link Toast#LENGTH_LONG}.
|
||||||
|
*/
|
||||||
|
public static void showToast(String messageToToast, int toastDuration) {
|
||||||
Objects.requireNonNull(messageToToast);
|
Objects.requireNonNull(messageToToast);
|
||||||
runOnMainThreadNowOrLater(() -> {
|
runOnMainThreadNowOrLater(() -> {
|
||||||
if (context == null) {
|
Context currentContext = context;
|
||||||
Logger.initializationException(Utils.class, "Cannot show toast (context is null): " + messageToToast, null);
|
|
||||||
} else {
|
if (currentContext == null) {
|
||||||
Logger.printDebug(() -> "Showing toast: " + messageToToast);
|
Logger.printException(() -> "Cannot show toast (context is null): " + messageToToast);
|
||||||
Toast.makeText(context, messageToToast, toastDuration).show();
|
} else {
|
||||||
}
|
Logger.printDebug(() -> "Showing toast: " + messageToToast);
|
||||||
}
|
Toast.makeText(currentContext, messageToToast, toastDuration).show();
|
||||||
);
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isDarkModeEnabled(Context context) {
|
/**
|
||||||
Configuration config = context.getResources().getConfiguration();
|
* @return The current dark mode as set by any patch.
|
||||||
|
* Or if none is set, then the system dark mode status is returned.
|
||||||
|
*/
|
||||||
|
public static boolean isDarkModeEnabled() {
|
||||||
|
Boolean isDarkMode = isDarkModeEnabled;
|
||||||
|
if (isDarkMode != null) {
|
||||||
|
return isDarkMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
Configuration config = Resources.getSystem().getConfiguration();
|
||||||
final int currentNightMode = config.uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
final int currentNightMode = config.uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||||
return currentNightMode == Configuration.UI_MODE_NIGHT_YES;
|
return currentNightMode == Configuration.UI_MODE_NIGHT_YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrides dark mode status as returned by {@link #isDarkModeEnabled()}.
|
||||||
|
*/
|
||||||
|
public static void setIsDarkModeEnabled(boolean isDarkMode) {
|
||||||
|
isDarkModeEnabled = isDarkMode;
|
||||||
|
Logger.printDebug(() -> "Dark mode status: " + isDarkMode);
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean isLandscapeOrientation() {
|
public static boolean isLandscapeOrientation() {
|
||||||
final int orientation = context.getResources().getConfiguration().orientation;
|
final int orientation = Resources.getSystem().getConfiguration().orientation;
|
||||||
return orientation == Configuration.ORIENTATION_LANDSCAPE;
|
return orientation == Configuration.ORIENTATION_LANDSCAPE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -537,14 +641,14 @@ public class Utils {
|
|||||||
*
|
*
|
||||||
* @see #runOnMainThreadNowOrLater(Runnable)
|
* @see #runOnMainThreadNowOrLater(Runnable)
|
||||||
*/
|
*/
|
||||||
public static void runOnMainThread(@NonNull Runnable runnable) {
|
public static void runOnMainThread(Runnable runnable) {
|
||||||
runOnMainThreadDelayed(runnable, 0);
|
runOnMainThreadDelayed(runnable, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Automatically logs any exceptions the runnable throws
|
* Automatically logs any exceptions the runnable throws.
|
||||||
*/
|
*/
|
||||||
public static void runOnMainThreadDelayed(@NonNull Runnable runnable, long delayMillis) {
|
public static void runOnMainThreadDelayed(Runnable runnable, long delayMillis) {
|
||||||
Runnable loggingRunnable = () -> {
|
Runnable loggingRunnable = () -> {
|
||||||
try {
|
try {
|
||||||
runnable.run();
|
runnable.run();
|
||||||
@@ -556,10 +660,10 @@ public class Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If called from the main thread, the code is run immediately.<p>
|
* If called from the main thread, the code is run immediately.
|
||||||
* If called off the main thread, this is the same as {@link #runOnMainThread(Runnable)}.
|
* If called off the main thread, this is the same as {@link #runOnMainThread(Runnable)}.
|
||||||
*/
|
*/
|
||||||
public static void runOnMainThreadNowOrLater(@NonNull Runnable runnable) {
|
public static void runOnMainThreadNowOrLater(Runnable runnable) {
|
||||||
if (isCurrentlyOnMainThread()) {
|
if (isCurrentlyOnMainThread()) {
|
||||||
runnable.run();
|
runnable.run();
|
||||||
} else {
|
} else {
|
||||||
@@ -568,14 +672,14 @@ public class Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return if the calling thread is on the main thread
|
* @return if the calling thread is on the main thread.
|
||||||
*/
|
*/
|
||||||
public static boolean isCurrentlyOnMainThread() {
|
public static boolean isCurrentlyOnMainThread() {
|
||||||
return Looper.getMainLooper().isCurrentThread();
|
return Looper.getMainLooper().isCurrentThread();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws IllegalStateException if the calling thread is _off_ the main thread
|
* @throws IllegalStateException if the calling thread is _off_ the main thread.
|
||||||
*/
|
*/
|
||||||
public static void verifyOnMainThread() throws IllegalStateException {
|
public static void verifyOnMainThread() throws IllegalStateException {
|
||||||
if (!isCurrentlyOnMainThread()) {
|
if (!isCurrentlyOnMainThread()) {
|
||||||
@@ -584,7 +688,7 @@ public class Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws IllegalStateException if the calling thread is _on_ the main thread
|
* @throws IllegalStateException if the calling thread is _on_ the main thread.
|
||||||
*/
|
*/
|
||||||
public static void verifyOffMainThread() throws IllegalStateException {
|
public static void verifyOffMainThread() throws IllegalStateException {
|
||||||
if (isCurrentlyOnMainThread()) {
|
if (isCurrentlyOnMainThread()) {
|
||||||
@@ -598,13 +702,23 @@ public class Utils {
|
|||||||
OTHER,
|
OTHER,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calling extension code must ensure the un-patched app has the permission
|
||||||
|
* <code>android.permission.ACCESS_NETWORK_STATE</code>,
|
||||||
|
* otherwise the app will crash if this method is used.
|
||||||
|
*/
|
||||||
public static boolean isNetworkConnected() {
|
public static boolean isNetworkConnected() {
|
||||||
NetworkType networkType = getNetworkType();
|
NetworkType networkType = getNetworkType();
|
||||||
return networkType == NetworkType.MOBILE
|
return networkType == NetworkType.MOBILE
|
||||||
|| networkType == NetworkType.OTHER;
|
|| networkType == NetworkType.OTHER;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint({"MissingPermission", "deprecation"}) // Permission already included in YouTube.
|
/**
|
||||||
|
* Calling extension code must ensure the un-patched app has the permission
|
||||||
|
* <code>android.permission.ACCESS_NETWORK_STATE</code>,
|
||||||
|
* otherwise the app will crash if this method is used.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({"MissingPermission", "deprecation"})
|
||||||
public static NetworkType getNetworkType() {
|
public static NetworkType getNetworkType() {
|
||||||
Context networkContext = getContext();
|
Context networkContext = getContext();
|
||||||
if (networkContext == null) {
|
if (networkContext == null) {
|
||||||
@@ -649,6 +763,169 @@ public class Utils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures the parameters of a dialog window, including its width, gravity, vertical offset and background dimming.
|
||||||
|
* The width is calculated as a percentage of the screen's portrait width and the vertical offset is specified in DIP.
|
||||||
|
* The default dialog background is removed to allow for custom styling.
|
||||||
|
*
|
||||||
|
* @param window The {@link Window} object to configure.
|
||||||
|
* @param gravity The gravity for positioning the dialog (e.g., {@link Gravity#BOTTOM}).
|
||||||
|
* @param yOffsetDip The vertical offset from the gravity position in DIP.
|
||||||
|
* @param widthPercentage The width of the dialog as a percentage of the screen's portrait width (0-100).
|
||||||
|
* @param dimAmount If true, sets the background dim amount to 0 (no dimming); if false, leaves the default dim amount.
|
||||||
|
*/
|
||||||
|
public static void setDialogWindowParameters(Window window, int gravity, int yOffsetDip, int widthPercentage, boolean dimAmount) {
|
||||||
|
WindowManager.LayoutParams params = window.getAttributes();
|
||||||
|
|
||||||
|
DisplayMetrics displayMetrics = Resources.getSystem().getDisplayMetrics();
|
||||||
|
int portraitWidth = Math.min(displayMetrics.widthPixels, displayMetrics.heightPixels);
|
||||||
|
|
||||||
|
params.width = (int) (portraitWidth * (widthPercentage / 100.0f)); // Set width based on parameters.
|
||||||
|
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
|
||||||
|
params.gravity = gravity;
|
||||||
|
params.y = yOffsetDip > 0 ? dipToPixels(yOffsetDip) : 0;
|
||||||
|
if (dimAmount) {
|
||||||
|
params.dimAmount = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.setAttributes(params); // Apply window attributes.
|
||||||
|
window.setBackgroundDrawable(null); // Remove default dialog background
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an array of corner radii for a rounded rectangle shape.
|
||||||
|
*
|
||||||
|
* @param dp Radius in density-independent pixels (dip) to apply to all corners.
|
||||||
|
* @return An array of eight float values representing the corner radii
|
||||||
|
* (top-left, top-right, bottom-right, bottom-left).
|
||||||
|
*/
|
||||||
|
public static float[] createCornerRadii(float dp) {
|
||||||
|
final float radius = dipToPixels(dp);
|
||||||
|
return new float[]{radius, radius, radius, radius, radius, radius, radius, radius};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the theme light color used by the app.
|
||||||
|
*/
|
||||||
|
public static void setThemeLightColor(@ColorInt int color) {
|
||||||
|
Logger.printDebug(() -> "Setting theme light color: " + getColorHexString(color));
|
||||||
|
lightColor = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the theme dark used by the app.
|
||||||
|
*/
|
||||||
|
public static void setThemeDarkColor(@ColorInt int color) {
|
||||||
|
Logger.printDebug(() -> "Setting theme dark color: " + getColorHexString(color));
|
||||||
|
darkColor = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the themed light color, or {@link Color#WHITE} if no theme was set using
|
||||||
|
* {@link #setThemeLightColor(int).
|
||||||
|
*/
|
||||||
|
@ColorInt
|
||||||
|
public static int getThemeLightColor() {
|
||||||
|
return lightColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the themed dark color, or {@link Color#BLACK} if no theme was set using
|
||||||
|
* {@link #setThemeDarkColor(int)}.
|
||||||
|
*/
|
||||||
|
@ColorInt
|
||||||
|
public static int getThemeDarkColor() {
|
||||||
|
return darkColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("SameReturnValue")
|
||||||
|
private static String getThemeLightColorResourceName() {
|
||||||
|
// Value is changed by Settings patch.
|
||||||
|
return "#FFFFFFFF";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("SameReturnValue")
|
||||||
|
private static String getThemeDarkColorResourceName() {
|
||||||
|
// Value is changed by Settings patch.
|
||||||
|
return "#FF000000";
|
||||||
|
}
|
||||||
|
|
||||||
|
@ColorInt
|
||||||
|
private static int getThemeColor(String resourceName, int defaultColor) {
|
||||||
|
try {
|
||||||
|
return getColorFromString(resourceName);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
// This code can never be reached since a bad custom color will
|
||||||
|
// fail during resource compilation. So no localized strings are needed here.
|
||||||
|
Logger.printException(() -> "Invalid custom theme color: " + resourceName, ex);
|
||||||
|
return defaultColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ColorInt
|
||||||
|
public static int getDialogBackgroundColor() {
|
||||||
|
if (isDarkModeEnabled()) {
|
||||||
|
final int darkColor = getThemeDarkColor();
|
||||||
|
return darkColor == Color.BLACK
|
||||||
|
// Lighten the background a little if using AMOLED dark theme
|
||||||
|
// as the dialogs are almost invisible.
|
||||||
|
? 0xFF080808 // 3%
|
||||||
|
: darkColor;
|
||||||
|
}
|
||||||
|
return getThemeLightColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The current app background color.
|
||||||
|
*/
|
||||||
|
@ColorInt
|
||||||
|
public static int getAppBackgroundColor() {
|
||||||
|
return isDarkModeEnabled() ? getThemeDarkColor() : getThemeLightColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The current app foreground color.
|
||||||
|
*/
|
||||||
|
@ColorInt
|
||||||
|
public static int getAppForegroundColor() {
|
||||||
|
return isDarkModeEnabled()
|
||||||
|
? getThemeLightColor()
|
||||||
|
: getThemeDarkColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ColorInt
|
||||||
|
public static int getOkButtonBackgroundColor() {
|
||||||
|
return isDarkModeEnabled()
|
||||||
|
// Must be inverted color.
|
||||||
|
? Color.WHITE
|
||||||
|
: Color.BLACK;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ColorInt
|
||||||
|
public static int getCancelOrNeutralButtonBackgroundColor() {
|
||||||
|
return isDarkModeEnabled()
|
||||||
|
? adjustColorBrightness(getDialogBackgroundColor(), 1.10f)
|
||||||
|
: adjustColorBrightness(getThemeLightColor(), 0.95f);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ColorInt
|
||||||
|
public static int getEditTextBackground() {
|
||||||
|
return isDarkModeEnabled()
|
||||||
|
? adjustColorBrightness(getDialogBackgroundColor(), 1.05f)
|
||||||
|
: adjustColorBrightness(getThemeLightColor(), 0.97f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getColorHexString(@ColorInt int color) {
|
||||||
|
return String.format("#%06X", (0x00FFFFFF & color));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link PreferenceScreen} and {@link PreferenceGroup} sorting styles.
|
* {@link PreferenceScreen} and {@link PreferenceGroup} sorting styles.
|
||||||
*/
|
*/
|
||||||
@@ -674,8 +951,7 @@ public class Utils {
|
|||||||
this.keySuffix = keySuffix;
|
this.keySuffix = keySuffix;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
static Sort fromKey(@Nullable String key, Sort defaultSort) {
|
||||||
static Sort fromKey(@Nullable String key, @NonNull Sort defaultSort) {
|
|
||||||
if (key != null) {
|
if (key != null) {
|
||||||
for (Sort sort : values()) {
|
for (Sort sort : values()) {
|
||||||
if (key.endsWith(sort.keySuffix)) {
|
if (key.endsWith(sort.keySuffix)) {
|
||||||
@@ -692,23 +968,24 @@ public class Utils {
|
|||||||
/**
|
/**
|
||||||
* Strips all punctuation and converts to lower case. A null parameter returns an empty string.
|
* Strips all punctuation and converts to lower case. A null parameter returns an empty string.
|
||||||
*/
|
*/
|
||||||
public static String removePunctuationConvertToLowercase(@Nullable CharSequence original) {
|
public static String removePunctuationToLowercase(@Nullable CharSequence original) {
|
||||||
if (original == null) return "";
|
if (original == null) return "";
|
||||||
return punctuationPattern.matcher(original).replaceAll("").toLowerCase();
|
return punctuationPattern.matcher(original).replaceAll("")
|
||||||
|
.toLowerCase(BaseSettings.REVANCED_LANGUAGE.get().getLocale());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sort a PreferenceGroup and all it's sub groups by title or key.
|
* Sort a PreferenceGroup and all it's sub groups by title or key.
|
||||||
*
|
* <p>
|
||||||
* Sort order is determined by the preferences key {@link Sort} suffix.
|
* Sort order is determined by the preferences key {@link Sort} suffix.
|
||||||
*
|
* <p>
|
||||||
* If a preference has no key or no {@link Sort} suffix,
|
* If a preference has no key or no {@link Sort} suffix,
|
||||||
* then the preferences are left unsorted.
|
* then the preferences are left unsorted.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
public static void sortPreferenceGroups(@NonNull PreferenceGroup group) {
|
public static void sortPreferenceGroups(PreferenceGroup group) {
|
||||||
Sort groupSort = Sort.fromKey(group.getKey(), Sort.UNSORTED);
|
Sort groupSort = Sort.fromKey(group.getKey(), Sort.UNSORTED);
|
||||||
SortedMap<String, Preference> preferences = new TreeMap<>();
|
List<Pair<String, Preference>> preferences = new ArrayList<>();
|
||||||
|
|
||||||
for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) {
|
for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) {
|
||||||
Preference preference = group.getPreference(i);
|
Preference preference = group.getPreference(i);
|
||||||
@@ -726,7 +1003,7 @@ public class Utils {
|
|||||||
final String sortValue;
|
final String sortValue;
|
||||||
switch (preferenceSort) {
|
switch (preferenceSort) {
|
||||||
case BY_TITLE:
|
case BY_TITLE:
|
||||||
sortValue = removePunctuationConvertToLowercase(preference.getTitle());
|
sortValue = removePunctuationToLowercase(preference.getTitle());
|
||||||
break;
|
break;
|
||||||
case BY_KEY:
|
case BY_KEY:
|
||||||
sortValue = preference.getKey();
|
sortValue = preference.getKey();
|
||||||
@@ -737,17 +1014,22 @@ public class Utils {
|
|||||||
throw new IllegalStateException();
|
throw new IllegalStateException();
|
||||||
}
|
}
|
||||||
|
|
||||||
preferences.put(sortValue, preference);
|
preferences.add(new Pair<>(sortValue, preference));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//noinspection ComparatorCombinators
|
||||||
|
Collections.sort(preferences, (pair1, pair2)
|
||||||
|
-> pair1.first.compareTo(pair2.first));
|
||||||
|
|
||||||
int index = 0;
|
int index = 0;
|
||||||
for (Preference pref : preferences.values()) {
|
for (Pair<String, Preference> pair : preferences) {
|
||||||
int order = index++;
|
int order = index++;
|
||||||
|
Preference pref = pair.second;
|
||||||
|
|
||||||
// Move any screens, intents, and the one off About preference to the top.
|
// Move any screens, intents, and the one off About preference to the top.
|
||||||
if (pref instanceof PreferenceScreen || pref instanceof ReVancedAboutPreference
|
if (pref instanceof PreferenceScreen || pref instanceof ReVancedAboutPreference
|
||||||
|| pref.getIntent() != null) {
|
|| pref.getIntent() != null) {
|
||||||
// Arbitrary high number.
|
// Any arbitrary large number.
|
||||||
order -= 1000;
|
order -= 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -759,7 +1041,7 @@ public class Utils {
|
|||||||
* Set all preferences to multiline titles if the device is not using an English variant.
|
* Set all preferences to multiline titles if the device is not using an English variant.
|
||||||
* The English strings are heavily scrutinized and all titles fit on screen
|
* The English strings are heavily scrutinized and all titles fit on screen
|
||||||
* except 2 or 3 preference strings and those do not affect readability.
|
* except 2 or 3 preference strings and those do not affect readability.
|
||||||
*
|
* <p>
|
||||||
* Allowing multiline for those 2 or 3 English preferences looks weird and out of place,
|
* Allowing multiline for those 2 or 3 English preferences looks weird and out of place,
|
||||||
* and visually it looks better to clip the text and keep all titles 1 line.
|
* and visually it looks better to clip the text and keep all titles 1 line.
|
||||||
*/
|
*/
|
||||||
@@ -784,30 +1066,108 @@ public class Utils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* If {@link Fragment} uses [Android library] rather than [AndroidX library],
|
|
||||||
* the Dialog theme corresponding to [Android library] should be used.
|
|
||||||
* <p>
|
|
||||||
* If not, the following issues will occur:
|
|
||||||
* <a href="https://github.com/ReVanced/revanced-patches/issues/3061">ReVanced/revanced-patches#3061</a>
|
|
||||||
* <p>
|
|
||||||
* To prevent these issues, apply the Dialog theme corresponding to [Android library].
|
|
||||||
*/
|
|
||||||
public static void setEditTextDialogTheme(AlertDialog.Builder builder) {
|
|
||||||
final int editTextDialogStyle = getResourceIdentifier(
|
|
||||||
"revanced_edit_text_dialog_style", "style");
|
|
||||||
if (editTextDialogStyle != 0) {
|
|
||||||
builder.getContext().setTheme(editTextDialogStyle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a color resource or hex code to an int representation of the color.
|
* Parse a color resource or hex code to an int representation of the color.
|
||||||
*/
|
*/
|
||||||
|
@ColorInt
|
||||||
public static int getColorFromString(String colorString) throws IllegalArgumentException, Resources.NotFoundException {
|
public static int getColorFromString(String colorString) throws IllegalArgumentException, Resources.NotFoundException {
|
||||||
if (colorString.startsWith("#")) {
|
if (colorString.startsWith("#")) {
|
||||||
return Color.parseColor(colorString);
|
return Color.parseColor(colorString);
|
||||||
}
|
}
|
||||||
return getResourceColor(colorString);
|
return getResourceColor(colorString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts dip value to actual device pixels.
|
||||||
|
*
|
||||||
|
* @param dip The density-independent pixels value.
|
||||||
|
* @return The device pixel value.
|
||||||
|
*/
|
||||||
|
public static int dipToPixels(float dip) {
|
||||||
|
return (int) TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
dip,
|
||||||
|
Resources.getSystem().getDisplayMetrics()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a percentage of the screen height to actual device pixels.
|
||||||
|
*
|
||||||
|
* @param percentage The percentage of the screen height (e.g., 30 for 30%).
|
||||||
|
* @return The device pixel value corresponding to the percentage of screen height.
|
||||||
|
*/
|
||||||
|
public static int percentageHeightToPixels(int percentage) {
|
||||||
|
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
|
||||||
|
return (int) (metrics.heightPixels * (percentage / 100.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a percentage of the screen width to actual device pixels.
|
||||||
|
*
|
||||||
|
* @param percentage The percentage of the screen width (e.g., 30 for 30%).
|
||||||
|
* @return The device pixel value corresponding to the percentage of screen width.
|
||||||
|
*/
|
||||||
|
public static int percentageWidthToPixels(int percentage) {
|
||||||
|
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
|
||||||
|
return (int) (metrics.widthPixels * (percentage / 100.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uses {@link #adjustColorBrightness(int, float)} depending if light or dark mode is active.
|
||||||
|
*/
|
||||||
|
@ColorInt
|
||||||
|
public static int adjustColorBrightness(@ColorInt int baseColor, float lightThemeFactor, float darkThemeFactor) {
|
||||||
|
return isDarkModeEnabled()
|
||||||
|
? adjustColorBrightness(baseColor, darkThemeFactor)
|
||||||
|
: adjustColorBrightness(baseColor, lightThemeFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
@ColorInt
|
||||||
|
public static int adjustColorBrightness(@ColorInt 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 = Math.round(red * factor);
|
||||||
|
green = Math.round(green * factor);
|
||||||
|
blue = Math.round(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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int clamp(int value, int lower, int upper) {
|
||||||
|
return Math.max(lower, Math.min(value, upper));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float clamp(float value, float lower, float upper) {
|
||||||
|
return Math.max(lower, Math.min(value, upper));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,17 @@ import static app.revanced.extension.shared.Utils.DialogFragmentOnStartAction;
|
|||||||
|
|
||||||
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;
|
||||||
|
|
||||||
@@ -20,6 +25,7 @@ import java.util.Collection;
|
|||||||
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.BaseSettings;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
|
import app.revanced.extension.shared.ui.CustomDialog;
|
||||||
|
|
||||||
abstract class Check {
|
abstract class Check {
|
||||||
private static final int NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING = 2;
|
private static final int NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING = 2;
|
||||||
@@ -86,38 +92,59 @@ abstract class Check {
|
|||||||
);
|
);
|
||||||
|
|
||||||
Utils.runOnMainThreadDelayed(() -> {
|
Utils.runOnMainThreadDelayed(() -> {
|
||||||
AlertDialog alert = new AlertDialog.Builder(activity)
|
// Create the custom dialog.
|
||||||
.setCancelable(false)
|
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
|
||||||
.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) -> {
|
() -> {
|
||||||
final var intent = new Intent(Intent.ACTION_VIEW, GOOD_SOURCE);
|
// Action for the OK (website) button.
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
final var intent = new Intent(Intent.ACTION_VIEW, GOOD_SOURCE);
|
||||||
activity.startActivity(intent);
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
activity.startActivity(intent);
|
||||||
|
|
||||||
// Shutdown to prevent the user from navigating back to this app,
|
// Shutdown to prevent the user from navigating back to this app,
|
||||||
// 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) -> {
|
() -> {
|
||||||
// Cleanup data if the user incorrectly imported a huge negative number.
|
// Neutral button action.
|
||||||
final int current = Math.max(0, BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get());
|
// Cleanup data if the user incorrectly imported a huge negative number.
|
||||||
BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(current + 1);
|
final int current = Math.max(0, BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get());
|
||||||
|
BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(current + 1);
|
||||||
|
},
|
||||||
|
true // Dismiss dialog when onNeutralClick.
|
||||||
|
);
|
||||||
|
|
||||||
dialog.dismiss();
|
// 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.getResourceIdentifierOrThrow(
|
||||||
|
"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 +152,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 rowContainer) {
|
||||||
|
// 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 +197,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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package app.revanced.extension.youtube.patches;
|
package app.revanced.extension.shared.patches;
|
||||||
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.ConcurrentMap;
|
import java.util.concurrent.ConcurrentMap;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package app.revanced.extension.shared.patches;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* YouTube and YouTube Music.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public final class SanitizeSharingLinksPatch {
|
||||||
|
private static final String NEW_TRACKING_PARAMETER_REGEX = ".si=.+";
|
||||||
|
private static final String OLD_TRACKING_PARAMETER_REGEX = ".feature=.+";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static String sanitize(String url) {
|
||||||
|
if (BaseSettings.SANITIZE_SHARED_LINKS.get()) {
|
||||||
|
url = url
|
||||||
|
.replaceAll(NEW_TRACKING_PARAMETER_REGEX, "")
|
||||||
|
.replaceAll(OLD_TRACKING_PARAMETER_REGEX, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BaseSettings.REPLACE_MUSIC_LINKS_WITH_YOUTUBE.get()) {
|
||||||
|
url = url.replace("music.youtube.com", "youtube.com");
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,9 +89,11 @@ public enum AppLanguage {
|
|||||||
ZU;
|
ZU;
|
||||||
|
|
||||||
private final String language;
|
private final String language;
|
||||||
|
private final Locale locale;
|
||||||
|
|
||||||
AppLanguage() {
|
AppLanguage() {
|
||||||
language = name().toLowerCase(Locale.US);
|
language = name().toLowerCase(Locale.US);
|
||||||
|
locale = Locale.forLanguageTag(language);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -112,6 +114,6 @@ public enum AppLanguage {
|
|||||||
return Locale.getDefault();
|
return Locale.getDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Locale.forLanguageTag(language);
|
return locale;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
package app.revanced.extension.shared.settings;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.preference.PreferenceFragment;
|
||||||
|
import android.util.TypedValue;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toolbar;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.shared.settings.preference.ToolbarPreferenceFragment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for hooking activities to inject a custom PreferenceFragment with a toolbar.
|
||||||
|
* Provides common logic for initializing the activity and setting up the toolbar.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({"deprecation", "NewApi"})
|
||||||
|
public abstract class BaseActivityHook extends Activity {
|
||||||
|
|
||||||
|
private static final int ID_REVANCED_SETTINGS_FRAGMENTS =
|
||||||
|
getResourceIdentifierOrThrow("revanced_settings_fragments", "id");
|
||||||
|
private static final int ID_REVANCED_TOOLBAR_PARENT =
|
||||||
|
getResourceIdentifierOrThrow("revanced_toolbar_parent", "id");
|
||||||
|
public static final int LAYOUT_REVANCED_SETTINGS_WITH_TOOLBAR =
|
||||||
|
getResourceIdentifierOrThrow("revanced_settings_with_toolbar", "layout");
|
||||||
|
private static final int STRING_REVANCED_SETTINGS_TITLE =
|
||||||
|
getResourceIdentifierOrThrow("revanced_settings_title", "string");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Layout parameters for the toolbar, extracted from the dummy toolbar.
|
||||||
|
*/
|
||||||
|
protected static ViewGroup.LayoutParams toolbarLayoutParams;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the layout parameters for the toolbar.
|
||||||
|
*/
|
||||||
|
public static void setToolbarLayoutParams(Toolbar toolbar) {
|
||||||
|
if (toolbarLayoutParams != null) {
|
||||||
|
toolbar.setLayoutParams(toolbarLayoutParams);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the activity by setting the theme, content view and injecting a PreferenceFragment.
|
||||||
|
*/
|
||||||
|
public static void initialize(BaseActivityHook hook, Activity activity) {
|
||||||
|
try {
|
||||||
|
hook.customizeActivityTheme(activity);
|
||||||
|
activity.setContentView(hook.getContentViewResourceId());
|
||||||
|
|
||||||
|
// Sanity check.
|
||||||
|
String dataString = activity.getIntent().getDataString();
|
||||||
|
if (!"revanced_settings_intent".equals(dataString)) {
|
||||||
|
Logger.printException(() -> "Unknown intent: " + dataString);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PreferenceFragment fragment = hook.createPreferenceFragment();
|
||||||
|
hook.createToolbar(activity, fragment);
|
||||||
|
|
||||||
|
activity.getFragmentManager()
|
||||||
|
.beginTransaction()
|
||||||
|
.replace(ID_REVANCED_SETTINGS_FRAGMENTS, fragment)
|
||||||
|
.commit();
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "initialize failure", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
* Overrides the ReVanced settings language.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public static Context getAttachBaseContext(Context original) {
|
||||||
|
AppLanguage language = BaseSettings.REVANCED_LANGUAGE.get();
|
||||||
|
if (language == AppLanguage.DEFAULT) {
|
||||||
|
return original;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Utils.getContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and configures a toolbar for the activity, replacing a dummy placeholder.
|
||||||
|
*/
|
||||||
|
@SuppressLint("UseCompatLoadingForDrawables")
|
||||||
|
protected void createToolbar(Activity activity, PreferenceFragment fragment) {
|
||||||
|
// Replace dummy placeholder toolbar.
|
||||||
|
// This is required to fix submenu title alignment issue with Android ASOP 15+
|
||||||
|
ViewGroup toolBarParent = activity.findViewById(ID_REVANCED_TOOLBAR_PARENT);
|
||||||
|
ViewGroup dummyToolbar = Utils.getChildViewByResourceName(toolBarParent, "revanced_toolbar");
|
||||||
|
toolbarLayoutParams = dummyToolbar.getLayoutParams();
|
||||||
|
toolBarParent.removeView(dummyToolbar);
|
||||||
|
|
||||||
|
// Sets appropriate system navigation bar color for the activity.
|
||||||
|
ToolbarPreferenceFragment.setNavigationBarColor(activity.getWindow());
|
||||||
|
|
||||||
|
Toolbar toolbar = new Toolbar(toolBarParent.getContext());
|
||||||
|
toolbar.setBackgroundColor(getToolbarBackgroundColor());
|
||||||
|
toolbar.setNavigationIcon(getNavigationIcon());
|
||||||
|
toolbar.setNavigationOnClickListener(getNavigationClickListener(activity));
|
||||||
|
toolbar.setTitle(STRING_REVANCED_SETTINGS_TITLE);
|
||||||
|
|
||||||
|
final int margin = Utils.dipToPixels(16);
|
||||||
|
toolbar.setTitleMarginStart(margin);
|
||||||
|
toolbar.setTitleMarginEnd(margin);
|
||||||
|
TextView toolbarTextView = Utils.getChildView(toolbar, false, view -> view instanceof TextView);
|
||||||
|
if (toolbarTextView != null) {
|
||||||
|
toolbarTextView.setTextColor(Utils.getAppForegroundColor());
|
||||||
|
toolbarTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20);
|
||||||
|
}
|
||||||
|
setToolbarLayoutParams(toolbar);
|
||||||
|
|
||||||
|
onPostToolbarSetup(activity, toolbar, fragment);
|
||||||
|
|
||||||
|
toolBarParent.addView(toolbar, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Customizes the activity's theme.
|
||||||
|
*/
|
||||||
|
protected abstract void customizeActivityTheme(Activity activity);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the resource ID for the content view layout.
|
||||||
|
*/
|
||||||
|
protected abstract int getContentViewResourceId();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the background color for the toolbar.
|
||||||
|
*/
|
||||||
|
protected abstract int getToolbarBackgroundColor();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the navigation icon drawable for the toolbar.
|
||||||
|
*/
|
||||||
|
protected abstract Drawable getNavigationIcon();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the click listener for the toolbar's navigation icon.
|
||||||
|
*/
|
||||||
|
protected abstract View.OnClickListener getNavigationClickListener(Activity activity);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the PreferenceFragment to be injected into the activity.
|
||||||
|
*/
|
||||||
|
protected PreferenceFragment createPreferenceFragment() {
|
||||||
|
return new ToolbarPreferenceFragment();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs additional setup after the toolbar is configured.
|
||||||
|
*
|
||||||
|
* @param activity The activity hosting the toolbar.
|
||||||
|
* @param toolbar The configured toolbar.
|
||||||
|
* @param fragment The PreferenceFragment associated with the activity.
|
||||||
|
*/
|
||||||
|
protected void onPostToolbarSetup(Activity activity, Toolbar toolbar, PreferenceFragment fragment) {}
|
||||||
|
}
|
||||||
@@ -4,9 +4,6 @@ import static java.lang.Boolean.FALSE;
|
|||||||
import static java.lang.Boolean.TRUE;
|
import static java.lang.Boolean.TRUE;
|
||||||
import static app.revanced.extension.shared.settings.Setting.parent;
|
import static app.revanced.extension.shared.settings.Setting.parent;
|
||||||
import static app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch.AudioStreamLanguageOverrideAvailability;
|
import static app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch.AudioStreamLanguageOverrideAvailability;
|
||||||
import static app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch.SpoofiOSAvailability;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.spoof.ClientType;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Settings shared across multiple apps.
|
* Settings shared across multiple apps.
|
||||||
@@ -28,12 +25,13 @@ public class BaseSettings {
|
|||||||
*/
|
*/
|
||||||
public static final BooleanSetting SHOW_MENU_ICONS = new BooleanSetting("revanced_show_menu_icons", TRUE, true);
|
public static final BooleanSetting SHOW_MENU_ICONS = new BooleanSetting("revanced_show_menu_icons", TRUE, true);
|
||||||
|
|
||||||
|
public static final BooleanSetting SETTINGS_SEARCH_HISTORY = new BooleanSetting("revanced_settings_search_history", TRUE, true);
|
||||||
|
public static final StringSetting SETTINGS_SEARCH_ENTRIES = new StringSetting("revanced_settings_search_entries", "");
|
||||||
|
|
||||||
public static final BooleanSetting SPOOF_VIDEO_STREAMS = new BooleanSetting("revanced_spoof_video_streams", TRUE, true, "revanced_spoof_video_streams_user_dialog_message");
|
public static final BooleanSetting SPOOF_VIDEO_STREAMS = new BooleanSetting("revanced_spoof_video_streams", TRUE, true, "revanced_spoof_video_streams_user_dialog_message");
|
||||||
public static final EnumSetting<AppLanguage> SPOOF_VIDEO_STREAMS_LANGUAGE = new EnumSetting<>("revanced_spoof_video_streams_language", AppLanguage.DEFAULT, new AudioStreamLanguageOverrideAvailability());
|
public static final EnumSetting<AppLanguage> SPOOF_VIDEO_STREAMS_LANGUAGE = new EnumSetting<>("revanced_spoof_video_streams_language", AppLanguage.DEFAULT, new AudioStreamLanguageOverrideAvailability());
|
||||||
public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE, parent(SPOOF_VIDEO_STREAMS));
|
public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE, parent(SPOOF_VIDEO_STREAMS));
|
||||||
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());
|
|
||||||
// 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 BooleanSetting SANITIZE_SHARED_LINKS = new BooleanSetting("revanced_sanitize_sharing_links", TRUE);
|
||||||
|
public static final BooleanSetting REPLACE_MUSIC_LINKS_WITH_YOUTUBE = new BooleanSetting("revanced_replace_music_with_youtube", FALSE);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -1,18 +1,27 @@
|
|||||||
package app.revanced.extension.shared.settings;
|
package app.revanced.extension.shared.settings;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
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.StringRef;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.preference.SharedPrefCategory;
|
import app.revanced.extension.shared.settings.preference.SharedPrefCategory;
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
|
||||||
|
|
||||||
public abstract class Setting<T> {
|
public abstract class Setting<T> {
|
||||||
|
|
||||||
@@ -23,39 +32,69 @@ public abstract class Setting<T> {
|
|||||||
*/
|
*/
|
||||||
public interface Availability {
|
public interface Availability {
|
||||||
boolean isAvailable();
|
boolean isAvailable();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return parent settings (dependencies) of this availability.
|
||||||
|
*/
|
||||||
|
default List<Setting<?>> getParentSettings() {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 new Availability() {
|
||||||
return parent::get;
|
@Override
|
||||||
|
public boolean isAvailable() {
|
||||||
|
return parent.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Setting<?>> getParentSettings() {
|
||||||
|
return Collections.singletonList(parent);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 new Availability() {
|
||||||
return () -> {
|
@Override
|
||||||
for (BooleanSetting parent : parents) {
|
public boolean isAvailable() {
|
||||||
if (!parent.get()) return false;
|
for (BooleanSetting parent : parents) {
|
||||||
|
if (!parent.get()) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Setting<?>> getParentSettings() {
|
||||||
|
return Collections.unmodifiableList(Arrays.asList(parents));
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 new Availability() {
|
||||||
return () -> {
|
@Override
|
||||||
for (BooleanSetting parent : parents) {
|
public boolean isAvailable() {
|
||||||
if (parent.get()) return true;
|
for (BooleanSetting parent : parents) {
|
||||||
|
if (parent.get()) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Setting<?>> getParentSettings() {
|
||||||
|
return Collections.unmodifiableList(Arrays.asList(parents));
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +118,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 +139,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,8 +153,8 @@ 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() {
|
||||||
|
//noinspection ComparatorCombinators
|
||||||
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 +162,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 +197,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 +234,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,
|
||||||
@@ -215,9 +250,7 @@ public abstract class Setting<T> {
|
|||||||
|
|
||||||
SETTINGS.add(this);
|
SETTINGS.add(this);
|
||||||
if (PATH_TO_SETTINGS.put(key, this) != null) {
|
if (PATH_TO_SETTINGS.put(key, this) != null) {
|
||||||
// Debug setting may not be created yet so using Logger may cause an initialization crash.
|
Logger.printException(() -> this.getClass().getSimpleName()
|
||||||
// Show a toast instead.
|
|
||||||
Utils.showToastLong(this.getClass().getSimpleName()
|
|
||||||
+ " error: Duplicate Setting key found: " + key);
|
+ " error: Duplicate Setting key found: " + key);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,7 +260,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()) {
|
||||||
@@ -239,11 +272,11 @@ public abstract class Setting<T> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Migrate an old Setting value previously stored in a different SharedPreference.
|
* Migrate an old Setting value previously stored in a different SharedPreference.
|
||||||
*
|
* <p>
|
||||||
* This method will be deleted in the future.
|
* This method will be deleted in the future.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("rawtypes")
|
@SuppressWarnings({"rawtypes", "NewApi"})
|
||||||
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.
|
||||||
}
|
}
|
||||||
@@ -262,7 +295,7 @@ public abstract class Setting<T> {
|
|||||||
migratedValue = oldPrefs.getString(settingKey, (String) newValue);
|
migratedValue = oldPrefs.getString(settingKey, (String) newValue);
|
||||||
} else {
|
} else {
|
||||||
Logger.printException(() -> "Unknown setting: " + setting);
|
Logger.printException(() -> "Unknown setting: " + setting);
|
||||||
// Remove otherwise it'll show a toast on every launch
|
// Remove otherwise it'll show a toast on every launch.
|
||||||
oldPrefs.preferences.edit().remove(settingKey).apply();
|
oldPrefs.preferences.edit().remove(settingKey).apply();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -281,11 +314,11 @@ public abstract class Setting<T> {
|
|||||||
/**
|
/**
|
||||||
* Sets, but does _not_ persistently save the value.
|
* Sets, but does _not_ persistently save the value.
|
||||||
* This method is only to be used by the Settings preference code.
|
* This method is only to be used by the Settings preference code.
|
||||||
*
|
* <p>
|
||||||
* 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 +332,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 +342,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;
|
||||||
}
|
}
|
||||||
@@ -342,9 +375,12 @@ public abstract class Setting<T> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Identical to calling {@link #save(Object)} using {@link #defaultValue}.
|
* Identical to calling {@link #save(Object)} using {@link #defaultValue}.
|
||||||
|
*
|
||||||
|
* @return The newly saved default value.
|
||||||
*/
|
*/
|
||||||
public void resetToDefault() {
|
public T resetToDefault() {
|
||||||
save(defaultValue);
|
save(defaultValue);
|
||||||
|
return defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -354,6 +390,14 @@ public abstract class Setting<T> {
|
|||||||
return availability == null || availability.isAvailable();
|
return availability == null || availability.isAvailable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the parent Settings that this setting depends on.
|
||||||
|
* @return List of parent Settings (e.g., BooleanSetting or EnumSetting), or empty list if no dependencies exist.
|
||||||
|
*/
|
||||||
|
public List<Setting<?>> getParentSettings() {
|
||||||
|
return availability == null ? Collections.emptyList() : availability.getParentSettings();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return if the currently set value is the same as {@link #defaultValue}
|
* @return if the currently set value is the same as {@link #defaultValue}
|
||||||
*/
|
*/
|
||||||
@@ -403,7 +447,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();
|
||||||
@@ -442,7 +485,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
|
||||||
@@ -473,9 +516,12 @@ public abstract class Setting<T> {
|
|||||||
callback.settingsImported(alertDialogContext);
|
callback.settingsImported(alertDialogContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
Utils.showToastLong(numberOfSettingsImported == 0
|
// Use a delay, otherwise the toast can move about on screen from the dismissing dialog.
|
||||||
? str("revanced_settings_import_reset")
|
final int numberOfSettingsImportedFinal = numberOfSettingsImported;
|
||||||
: str("revanced_settings_import_success", numberOfSettingsImported));
|
Utils.runOnMainThreadDelayed(() -> Utils.showToastLong(numberOfSettingsImportedFinal == 0
|
||||||
|
? str("revanced_settings_import_reset")
|
||||||
|
: str("revanced_settings_import_success", numberOfSettingsImportedFinal)),
|
||||||
|
150);
|
||||||
|
|
||||||
return rebootSettingChanged;
|
return rebootSettingChanged;
|
||||||
} catch (JSONException | IllegalArgumentException ex) {
|
} catch (JSONException | IllegalArgumentException ex) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
@@ -19,6 +27,7 @@ import app.revanced.extension.shared.Utils;
|
|||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||||
import app.revanced.extension.shared.settings.Setting;
|
import app.revanced.extension.shared.settings.Setting;
|
||||||
|
import app.revanced.extension.shared.ui.CustomDialog;
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
||||||
@@ -44,7 +53,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 +85,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 +125,14 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
|||||||
|
|
||||||
showingUserDialogMessage = true;
|
showingUserDialogMessage = true;
|
||||||
|
|
||||||
new AlertDialog.Builder(context)
|
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
|
||||||
.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 +141,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -233,7 +249,8 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
|||||||
Setting.privateSetValueFromString(setting, listPref.getValue());
|
Setting.privateSetValueFromString(setting, listPref.getValue());
|
||||||
}
|
}
|
||||||
updateListPreferenceSummary(listPref, setting);
|
updateListPreferenceSummary(listPref, setting);
|
||||||
} else {
|
} else if (!pref.getClass().equals(Preference.class)) {
|
||||||
|
// Ignore root preference class because there is no data to sync.
|
||||||
Logger.printException(() -> "Setting cannot be handled: " + pref.getClass() + ": " + pref);
|
Logger.printException(() -> "Setting cannot be handled: " + pref.getClass() + ": " + pref);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -280,17 +297,28 @@ 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 = CustomDialog.create(
|
||||||
.setMessage(restartDialogTitle)
|
context,
|
||||||
.setPositiveButton(restartDialogButtonText, (dialog, id)
|
restartDialogTitle, // Title.
|
||||||
-> Utils.restartApp(context))
|
restartDialogMessage, // Message.
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
null, // No EditText.
|
||||||
.setCancelable(false)
|
restartDialogButtonText, // OK button text.
|
||||||
.show();
|
() -> Utils.restartApp(context), // OK button action.
|
||||||
|
() -> {}, // Cancel button action (dismiss only).
|
||||||
|
null, // No Neutral button text.
|
||||||
|
null, // No Neutral button action.
|
||||||
|
true // Dismiss dialog when onNeutralClick.
|
||||||
|
);
|
||||||
|
|
||||||
|
// Show the dialog.
|
||||||
|
dialogPair.first.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ResourceType")
|
@SuppressLint("ResourceType")
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package app.revanced.extension.shared.settings.preference;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.preference.Preference;
|
||||||
|
import android.text.SpannableStringBuilder;
|
||||||
|
import android.text.Spanned;
|
||||||
|
import android.text.SpannedString;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.text.style.BulletSpan;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats the summary text bullet points into Spanned text for better presentation.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({"unused", "deprecation"})
|
||||||
|
public class BulletPointPreference extends Preference {
|
||||||
|
|
||||||
|
public static SpannedString formatIntoBulletPoints(CharSequence source) {
|
||||||
|
SpannableStringBuilder builder = new SpannableStringBuilder(source);
|
||||||
|
|
||||||
|
int lineStart = 0;
|
||||||
|
int length = builder.length();
|
||||||
|
|
||||||
|
while (lineStart < length) {
|
||||||
|
int lineEnd = TextUtils.indexOf(builder, '\n', lineStart);
|
||||||
|
if (lineEnd < 0) lineEnd = length;
|
||||||
|
|
||||||
|
// Apply BulletSpan only if the line starts with the '•' character.
|
||||||
|
if (lineEnd > lineStart && builder.charAt(lineStart) == '•') {
|
||||||
|
int deleteEnd = lineStart + 1; // remove the bullet itself
|
||||||
|
|
||||||
|
// If there's a single space right after the bullet, remove that too.
|
||||||
|
if (deleteEnd < builder.length() && builder.charAt(deleteEnd) == ' ') {
|
||||||
|
deleteEnd++;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.delete(lineStart, deleteEnd);
|
||||||
|
|
||||||
|
// Apply the BulletSpan to the remainder of that line.
|
||||||
|
builder.setSpan(new BulletSpan(20),
|
||||||
|
lineStart,
|
||||||
|
lineEnd - (deleteEnd - lineStart), // adjust for deleted chars.
|
||||||
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update total length and lineEnd after deletion.
|
||||||
|
length = builder.length();
|
||||||
|
final int removed = deleteEnd - lineStart;
|
||||||
|
lineEnd -= removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
lineStart = lineEnd + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SpannedString(builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BulletPointPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BulletPointPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BulletPointPreference(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BulletPointPreference(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setSummary(CharSequence summary) {
|
||||||
|
super.setSummary(formatIntoBulletPoints(summary));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package app.revanced.extension.shared.settings.preference;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.settings.preference.BulletPointPreference.formatIntoBulletPoints;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.preference.SwitchPreference;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats the summary text bullet points into Spanned text for better presentation.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({"unused", "deprecation"})
|
||||||
|
public class BulletPointSwitchPreference extends SwitchPreference {
|
||||||
|
|
||||||
|
public BulletPointSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BulletPointSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BulletPointSwitchPreference(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BulletPointSwitchPreference(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setSummary(CharSequence summary) {
|
||||||
|
super.setSummary(formatIntoBulletPoints(summary));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setSummaryOn(CharSequence summaryOn) {
|
||||||
|
super.setSummaryOn(formatIntoBulletPoints(summaryOn));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setSummaryOff(CharSequence summaryOff) {
|
||||||
|
super.setSummaryOff(formatIntoBulletPoints(summaryOff));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package app.revanced.extension.shared.settings.preference;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.preference.Preference;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom preference that clears the ReVanced debug log buffer when clicked.
|
||||||
|
* Invokes the {@link LogBufferManager#clearLogBuffer} method.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class ClearLogBufferPreference extends Preference {
|
||||||
|
|
||||||
|
{
|
||||||
|
setOnPreferenceClickListener(pref -> {
|
||||||
|
LogBufferManager.clearLogBuffer();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClearLogBufferPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
}
|
||||||
|
public ClearLogBufferPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
public ClearLogBufferPreference(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
public ClearLogBufferPreference(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,478 @@
|
|||||||
|
package app.revanced.extension.shared.settings.preference;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||||
|
import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
|
||||||
|
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.preference.EditTextPreference;
|
||||||
|
import android.text.Editable;
|
||||||
|
import android.text.InputType;
|
||||||
|
import android.text.TextWatcher;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.util.Pair;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.ViewParent;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.ScrollView;
|
||||||
|
|
||||||
|
import androidx.annotation.ColorInt;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.shared.settings.Setting;
|
||||||
|
import app.revanced.extension.shared.settings.StringSetting;
|
||||||
|
import app.revanced.extension.shared.ui.ColorDot;
|
||||||
|
import app.revanced.extension.shared.ui.CustomDialog;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom preference for selecting a color via a hexadecimal code or a color picker dialog.
|
||||||
|
* Extends {@link EditTextPreference} to display a colored dot in the widget area,
|
||||||
|
* reflecting the currently selected color. The dot is dimmed when the preference is disabled.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({"unused", "deprecation"})
|
||||||
|
public class ColorPickerPreference extends EditTextPreference {
|
||||||
|
/** Length of a valid color string of format #RRGGBB (without alpha) or #AARRGGBB (with alpha). */
|
||||||
|
public static final int COLOR_STRING_LENGTH_WITHOUT_ALPHA = 7;
|
||||||
|
public static final int COLOR_STRING_LENGTH_WITH_ALPHA = 9;
|
||||||
|
|
||||||
|
/** Matches everything that is not a hex number/letter. */
|
||||||
|
private static final Pattern PATTERN_NOT_HEX = Pattern.compile("[^0-9A-Fa-f]");
|
||||||
|
|
||||||
|
/** Alpha for dimming when the preference is disabled. */
|
||||||
|
public static final float DISABLED_ALPHA = 0.5f; // 50%
|
||||||
|
|
||||||
|
/** View displaying a colored dot in the widget area. */
|
||||||
|
private View widgetColorDot;
|
||||||
|
|
||||||
|
/** Dialog View displaying a colored dot for the selected color preview in the dialog. */
|
||||||
|
private View dialogColorDot;
|
||||||
|
|
||||||
|
/** Current color, including alpha channel if opacity slider is enabled. */
|
||||||
|
@ColorInt
|
||||||
|
private int currentColor;
|
||||||
|
|
||||||
|
/** Associated setting for storing the color value. */
|
||||||
|
private StringSetting colorSetting;
|
||||||
|
|
||||||
|
/** Dialog TextWatcher for the EditText to monitor color input changes. */
|
||||||
|
private TextWatcher colorTextWatcher;
|
||||||
|
|
||||||
|
/** Dialog color picker view. */
|
||||||
|
protected ColorPickerView dialogColorPickerView;
|
||||||
|
|
||||||
|
/** Listener for color changes. */
|
||||||
|
protected OnColorChangeListener colorChangeListener;
|
||||||
|
|
||||||
|
/** Whether the opacity slider is enabled. */
|
||||||
|
private boolean opacitySliderEnabled = false;
|
||||||
|
|
||||||
|
public static final int ID_REVANCED_COLOR_PICKER_VIEW =
|
||||||
|
getResourceIdentifierOrThrow("revanced_color_picker_view", "id");
|
||||||
|
public static final int ID_PREFERENCE_COLOR_DOT =
|
||||||
|
getResourceIdentifierOrThrow("preference_color_dot", "id");
|
||||||
|
public static final int LAYOUT_REVANCED_COLOR_DOT_WIDGET =
|
||||||
|
getResourceIdentifierOrThrow("revanced_color_dot_widget", "layout");
|
||||||
|
public static final int LAYOUT_REVANCED_COLOR_PICKER =
|
||||||
|
getResourceIdentifierOrThrow("revanced_color_picker", "layout");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes non valid hex characters, converts to all uppercase,
|
||||||
|
* and adds # character to the start if not present.
|
||||||
|
*/
|
||||||
|
public static String cleanupColorCodeString(String colorString, boolean includeAlpha) {
|
||||||
|
String result = "#" + PATTERN_NOT_HEX.matcher(colorString)
|
||||||
|
.replaceAll("").toUpperCase(Locale.ROOT);
|
||||||
|
|
||||||
|
int maxLength = includeAlpha ? COLOR_STRING_LENGTH_WITH_ALPHA : COLOR_STRING_LENGTH_WITHOUT_ALPHA;
|
||||||
|
if (result.length() < maxLength) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.substring(0, maxLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param color Color, with or without alpha channel.
|
||||||
|
* @param includeAlpha Whether to include the alpha channel in the output string.
|
||||||
|
* @return #RRGGBB or #AARRGGBB hex color string
|
||||||
|
*/
|
||||||
|
public static String getColorString(@ColorInt int color, boolean includeAlpha) {
|
||||||
|
if (includeAlpha) {
|
||||||
|
return String.format("#%08X", color);
|
||||||
|
}
|
||||||
|
color = color & 0x00FFFFFF; // Mask to strip alpha.
|
||||||
|
return String.format("#%06X", color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for notifying color changes.
|
||||||
|
*/
|
||||||
|
public interface OnColorChangeListener {
|
||||||
|
void onColorChanged(String key, int newColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the listener for color changes.
|
||||||
|
*/
|
||||||
|
public void setOnColorChangeListener(OnColorChangeListener listener) {
|
||||||
|
this.colorChangeListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables or disables the opacity slider in the color picker dialog.
|
||||||
|
*/
|
||||||
|
public void setOpacitySliderEnabled(boolean enabled) {
|
||||||
|
this.opacitySliderEnabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ColorPickerPreference(Context context) {
|
||||||
|
super(context);
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ColorPickerPreference(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ColorPickerPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the preference by setting up the EditText, loading the color, and set the widget layout.
|
||||||
|
*/
|
||||||
|
private void init() {
|
||||||
|
if (getKey() != null) {
|
||||||
|
colorSetting = (StringSetting) Setting.getSettingFromPath(getKey());
|
||||||
|
if (colorSetting == null) {
|
||||||
|
Logger.printException(() -> "Could not find color setting for: " + getKey());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Logger.printDebug(() -> "initialized without key, settings will be loaded later");
|
||||||
|
}
|
||||||
|
|
||||||
|
EditText editText = getEditText();
|
||||||
|
editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
|
||||||
|
| InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
editText.setAutofillHints((String) null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the widget layout to a custom layout containing the colored dot.
|
||||||
|
setWidgetLayoutResource(LAYOUT_REVANCED_COLOR_DOT_WIDGET);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the selected color and updates the UI and settings.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void setText(String colorString) {
|
||||||
|
try {
|
||||||
|
Logger.printDebug(() -> "setText: " + colorString);
|
||||||
|
super.setText(colorString);
|
||||||
|
|
||||||
|
currentColor = Color.parseColor(colorString);
|
||||||
|
if (colorSetting != null) {
|
||||||
|
colorSetting.save(getColorString(currentColor, opacitySliderEnabled));
|
||||||
|
}
|
||||||
|
updateDialogColorDot();
|
||||||
|
updateWidgetColorDot();
|
||||||
|
|
||||||
|
// Notify the listener about the color change.
|
||||||
|
if (colorChangeListener != null) {
|
||||||
|
colorChangeListener.onColorChanged(getKey(), currentColor);
|
||||||
|
}
|
||||||
|
} catch (IllegalArgumentException ex) {
|
||||||
|
// This code is reached if the user pastes settings json with an invalid color
|
||||||
|
// since this preference is updated with the new setting text.
|
||||||
|
Logger.printDebug(() -> "Parse color error: " + colorString, ex);
|
||||||
|
Utils.showToastShort(str("revanced_settings_color_invalid"));
|
||||||
|
setText(colorSetting.resetToDefault());
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "setText failure: " + colorString, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a TextWatcher to monitor changes in the EditText for color input.
|
||||||
|
*/
|
||||||
|
private TextWatcher createColorTextWatcher(ColorPickerView colorPickerView) {
|
||||||
|
return new TextWatcher() {
|
||||||
|
@Override
|
||||||
|
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterTextChanged(Editable edit) {
|
||||||
|
try {
|
||||||
|
String colorString = edit.toString();
|
||||||
|
String sanitizedColorString = cleanupColorCodeString(colorString, opacitySliderEnabled);
|
||||||
|
if (!sanitizedColorString.equals(colorString)) {
|
||||||
|
edit.replace(0, colorString.length(), sanitizedColorString);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int expectedLength = opacitySliderEnabled
|
||||||
|
? COLOR_STRING_LENGTH_WITH_ALPHA
|
||||||
|
: COLOR_STRING_LENGTH_WITHOUT_ALPHA;
|
||||||
|
if (sanitizedColorString.length() != expectedLength) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int newColor = Color.parseColor(colorString);
|
||||||
|
if (currentColor != newColor) {
|
||||||
|
Logger.printDebug(() -> "afterTextChanged: " + sanitizedColorString);
|
||||||
|
currentColor = newColor;
|
||||||
|
updateDialogColorDot();
|
||||||
|
updateWidgetColorDot();
|
||||||
|
colorPickerView.setColor(newColor);
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
// Should never be reached since input is validated before using.
|
||||||
|
Logger.printException(() -> "afterTextChanged failure", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for subclasses to add a custom view to the top of the dialog.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
protected View createExtraDialogContentView(Context context) {
|
||||||
|
return null; // Default implementation returns no extra view.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for subclasses to handle the OK button click.
|
||||||
|
*/
|
||||||
|
protected void onDialogOkClicked() {
|
||||||
|
// Default implementation does nothing.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for subclasses to handle the Neutral button click.
|
||||||
|
*/
|
||||||
|
protected void onDialogNeutralClicked() {
|
||||||
|
// Default implementation.
|
||||||
|
try {
|
||||||
|
final int defaultColor = Color.parseColor(colorSetting.defaultValue);
|
||||||
|
dialogColorPickerView.setColor(defaultColor);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "Reset button failure", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void showDialog(Bundle state) {
|
||||||
|
Context context = getContext();
|
||||||
|
|
||||||
|
// Create content container for all dialog views.
|
||||||
|
LinearLayout contentContainer = new LinearLayout(context);
|
||||||
|
contentContainer.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
|
||||||
|
// Add extra view from subclass if it exists.
|
||||||
|
View extraView = createExtraDialogContentView(context);
|
||||||
|
if (extraView != null) {
|
||||||
|
contentContainer.addView(extraView);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inflate color picker view.
|
||||||
|
View colorPicker = LayoutInflater.from(context).inflate(LAYOUT_REVANCED_COLOR_PICKER, null);
|
||||||
|
dialogColorPickerView = colorPicker.findViewById(ID_REVANCED_COLOR_PICKER_VIEW);
|
||||||
|
dialogColorPickerView.setOpacitySliderEnabled(opacitySliderEnabled);
|
||||||
|
dialogColorPickerView.setColor(currentColor);
|
||||||
|
contentContainer.addView(colorPicker);
|
||||||
|
|
||||||
|
// Horizontal layout for preview and EditText.
|
||||||
|
LinearLayout inputLayout = new LinearLayout(context);
|
||||||
|
inputLayout.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
inputLayout.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
|
||||||
|
dialogColorDot = new View(context);
|
||||||
|
LinearLayout.LayoutParams previewParams = new LinearLayout.LayoutParams(
|
||||||
|
dipToPixels(20),
|
||||||
|
dipToPixels(20)
|
||||||
|
);
|
||||||
|
previewParams.setMargins(dipToPixels(16), 0, dipToPixels(10), 0);
|
||||||
|
dialogColorDot.setLayoutParams(previewParams);
|
||||||
|
inputLayout.addView(dialogColorDot);
|
||||||
|
updateDialogColorDot();
|
||||||
|
|
||||||
|
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, opacitySliderEnabled);
|
||||||
|
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);
|
||||||
|
|
||||||
|
contentContainer.addView(inputLayout);
|
||||||
|
|
||||||
|
// Create ScrollView to wrap the content container.
|
||||||
|
ScrollView contentScrollView = new ScrollView(context);
|
||||||
|
contentScrollView.setVerticalScrollBarEnabled(false);
|
||||||
|
contentScrollView.setOverScrollMode(View.OVER_SCROLL_NEVER);
|
||||||
|
LinearLayout.LayoutParams scrollViewParams = new LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
0,
|
||||||
|
1.0f
|
||||||
|
);
|
||||||
|
contentScrollView.setLayoutParams(scrollViewParams);
|
||||||
|
contentScrollView.addView(contentContainer);
|
||||||
|
|
||||||
|
final int originalColor = currentColor;
|
||||||
|
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
|
||||||
|
context,
|
||||||
|
getTitle() != null ? getTitle().toString() : str("revanced_settings_color_picker_title"),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
() -> { // OK button action.
|
||||||
|
try {
|
||||||
|
String colorString = editText.getText().toString();
|
||||||
|
int expectedLength = opacitySliderEnabled
|
||||||
|
? COLOR_STRING_LENGTH_WITH_ALPHA
|
||||||
|
: COLOR_STRING_LENGTH_WITHOUT_ALPHA;
|
||||||
|
if (colorString.length() != expectedLength) {
|
||||||
|
Utils.showToastShort(str("revanced_settings_color_invalid"));
|
||||||
|
setText(getColorString(originalColor, opacitySliderEnabled));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setText(colorString);
|
||||||
|
|
||||||
|
onDialogOkClicked();
|
||||||
|
} catch (Exception ex) {
|
||||||
|
// Should never happen due to a bad color string,
|
||||||
|
// since the text is validated and fixed while the user types.
|
||||||
|
Logger.printException(() -> "OK button failure", ex);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() -> { // Cancel button action.
|
||||||
|
try {
|
||||||
|
setText(getColorString(originalColor, opacitySliderEnabled));
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "Cancel button failure", ex);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
str("revanced_settings_reset_color"), // Neutral button text.
|
||||||
|
this::onDialogNeutralClicked, // Neutral button action.
|
||||||
|
false // Do not dismiss dialog.
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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, opacitySliderEnabled);
|
||||||
|
Logger.printDebug(() -> "onColorChanged: " + updatedColorString);
|
||||||
|
currentColor = color;
|
||||||
|
editText.setText(updatedColorString);
|
||||||
|
editText.setSelection(updatedColorString.length());
|
||||||
|
|
||||||
|
updateDialogColorDot();
|
||||||
|
updateWidgetColorDot();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configure and show the dialog.
|
||||||
|
Dialog dialog = dialogPair.first;
|
||||||
|
dialog.setCanceledOnTouchOutside(false);
|
||||||
|
dialog.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDialogClosed(boolean positiveResult) {
|
||||||
|
super.onDialogClosed(positiveResult);
|
||||||
|
|
||||||
|
if (colorTextWatcher != null) {
|
||||||
|
getEditText().removeTextChangedListener(colorTextWatcher);
|
||||||
|
colorTextWatcher = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogColorDot = null;
|
||||||
|
dialogColorPickerView = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setEnabled(boolean enabled) {
|
||||||
|
super.setEnabled(enabled);
|
||||||
|
updateWidgetColorDot();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onBindView(View view) {
|
||||||
|
super.onBindView(view);
|
||||||
|
|
||||||
|
widgetColorDot = view.findViewById(ID_PREFERENCE_COLOR_DOT);
|
||||||
|
updateWidgetColorDot();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateWidgetColorDot() {
|
||||||
|
if (widgetColorDot == null) return;
|
||||||
|
|
||||||
|
ColorDot.applyColorDot(
|
||||||
|
widgetColorDot,
|
||||||
|
currentColor,
|
||||||
|
widgetColorDot.isEnabled()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateDialogColorDot() {
|
||||||
|
if (dialogColorDot == null) return;
|
||||||
|
|
||||||
|
ColorDot.applyColorDot(
|
||||||
|
dialogColorDot,
|
||||||
|
currentColor,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,639 @@
|
|||||||
|
package app.revanced.extension.shared.settings.preference;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||||
|
import static app.revanced.extension.shared.settings.preference.ColorPickerPreference.getColorString;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.ComposeShader;
|
||||||
|
import android.graphics.LinearGradient;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.PorterDuff;
|
||||||
|
import android.graphics.RectF;
|
||||||
|
import android.graphics.Shader;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import androidx.annotation.ColorInt;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom color picker view that allows the user to select a color using a hue slider, a saturation-value selector
|
||||||
|
* and an optional opacity slider.
|
||||||
|
* This implementation is density-independent and responsive across different screen sizes and DPIs.
|
||||||
|
* <p>
|
||||||
|
* This view displays three main components for color selection:
|
||||||
|
* <ul>
|
||||||
|
* <li><b>Hue Bar:</b> A horizontal bar at the bottom that allows the user to select the hue component of the color.
|
||||||
|
* <li><b>Saturation-Value Selector:</b> A rectangular area above the hue bar that allows the user to select the
|
||||||
|
* saturation and value (brightness) components of the color based on the selected hue.
|
||||||
|
* <li><b>Opacity Slider:</b> An optional horizontal bar below the hue bar that allows the user to adjust
|
||||||
|
* the opacity (alpha channel) of the color.
|
||||||
|
* </ul>
|
||||||
|
* <p>
|
||||||
|
* The view uses {@link LinearGradient} and {@link ComposeShader} to create the color gradients for the hue bar,
|
||||||
|
* opacity slider, and the saturation-value selector. It also uses {@link Paint} to draw the selectors (draggable handles).
|
||||||
|
* <p>
|
||||||
|
* The selected color can be retrieved using {@link #getColor()} and can be set using {@link #setColor(int)}.
|
||||||
|
* An {@link OnColorChangedListener} can be registered to receive notifications when the selected color changes.
|
||||||
|
*/
|
||||||
|
public class ColorPickerView extends View {
|
||||||
|
/**
|
||||||
|
* Interface definition for a callback to be invoked when the selected color changes.
|
||||||
|
*/
|
||||||
|
public interface OnColorChangedListener {
|
||||||
|
/**
|
||||||
|
* Called when the selected color has changed.
|
||||||
|
*/
|
||||||
|
void onColorChanged(@ColorInt int color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Expanded touch area for the hue and opacity bars to increase the touch-sensitive area. */
|
||||||
|
public static final float TOUCH_EXPANSION = dipToPixels(20f);
|
||||||
|
|
||||||
|
/** Margin between different areas of the view (saturation-value selector, hue bar, and opacity slider). */
|
||||||
|
private static final float MARGIN_BETWEEN_AREAS = dipToPixels(24);
|
||||||
|
|
||||||
|
/** Padding around the view. */
|
||||||
|
private static final float VIEW_PADDING = dipToPixels(16);
|
||||||
|
|
||||||
|
/** Height of the hue bar. */
|
||||||
|
private static final float HUE_BAR_HEIGHT = dipToPixels(12);
|
||||||
|
|
||||||
|
/** Height of the opacity slider. */
|
||||||
|
private static final float OPACITY_BAR_HEIGHT = dipToPixels(12);
|
||||||
|
|
||||||
|
/** Corner radius for the hue bar. */
|
||||||
|
private static final float HUE_CORNER_RADIUS = dipToPixels(6);
|
||||||
|
|
||||||
|
/** Corner radius for the opacity slider. */
|
||||||
|
private static final float OPACITY_CORNER_RADIUS = dipToPixels(6);
|
||||||
|
|
||||||
|
/** Radius of the selector handles. */
|
||||||
|
private static final float SELECTOR_RADIUS = dipToPixels(12);
|
||||||
|
|
||||||
|
/** Stroke width for the selector handle outlines. */
|
||||||
|
private static final float SELECTOR_STROKE_WIDTH = 8;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hue and opacity fill radius. Use slightly smaller radius for the selector handle fill,
|
||||||
|
* otherwise the anti-aliasing causes the fill color to bleed past the selector outline.
|
||||||
|
*/
|
||||||
|
private static final float SELECTOR_FILL_RADIUS = SELECTOR_RADIUS - SELECTOR_STROKE_WIDTH / 2;
|
||||||
|
|
||||||
|
/** Thin dark outline stroke width for the selector rings. */
|
||||||
|
private static final float SELECTOR_EDGE_STROKE_WIDTH = 1;
|
||||||
|
|
||||||
|
/** Radius for the outer edge of the selector rings, including stroke width. */
|
||||||
|
public static final float SELECTOR_EDGE_RADIUS =
|
||||||
|
SELECTOR_RADIUS + SELECTOR_STROKE_WIDTH / 2 + SELECTOR_EDGE_STROKE_WIDTH / 2;
|
||||||
|
|
||||||
|
/** Selector outline inner color. */
|
||||||
|
@ColorInt
|
||||||
|
private static final int SELECTOR_OUTLINE_COLOR = Color.WHITE;
|
||||||
|
|
||||||
|
/** Dark edge color for the selector rings. */
|
||||||
|
@ColorInt
|
||||||
|
private static final int SELECTOR_EDGE_COLOR = Color.parseColor("#CFCFCF");
|
||||||
|
|
||||||
|
/** Precomputed array of hue colors for the hue bar (0-360 degrees). */
|
||||||
|
private static final int[] HUE_COLORS = new int[361];
|
||||||
|
static {
|
||||||
|
for (int i = 0; i < 361; i++) {
|
||||||
|
HUE_COLORS[i] = Color.HSVToColor(new float[]{i, 1, 1});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Paint for the hue bar. */
|
||||||
|
private final Paint huePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
|
||||||
|
/** Paint for the opacity slider. */
|
||||||
|
private final Paint opacityPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
|
||||||
|
/** Paint for the saturation-value selector. */
|
||||||
|
private final Paint saturationValuePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
|
||||||
|
/** Paint for the draggable selector handles. */
|
||||||
|
private final Paint selectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
{
|
||||||
|
selectorPaint.setStrokeWidth(SELECTOR_STROKE_WIDTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Bounds of the hue bar. */
|
||||||
|
private final RectF hueRect = new RectF();
|
||||||
|
|
||||||
|
/** Bounds of the opacity slider. */
|
||||||
|
private final RectF opacityRect = new RectF();
|
||||||
|
|
||||||
|
/** Bounds of the saturation-value selector. */
|
||||||
|
private final RectF saturationValueRect = new RectF();
|
||||||
|
|
||||||
|
/** HSV color calculations to avoid allocations during drawing. */
|
||||||
|
private final float[] hsvArray = {1, 1, 1};
|
||||||
|
|
||||||
|
/** Current hue value (0-360). */
|
||||||
|
private float hue = 0f;
|
||||||
|
|
||||||
|
/** Current saturation value (0-1). */
|
||||||
|
private float saturation = 1f;
|
||||||
|
|
||||||
|
/** Current value (brightness) value (0-1). */
|
||||||
|
private float value = 1f;
|
||||||
|
|
||||||
|
/** Current opacity value (0-1). */
|
||||||
|
private float opacity = 1f;
|
||||||
|
|
||||||
|
/** The currently selected color, including alpha channel if opacity slider is enabled. */
|
||||||
|
@ColorInt
|
||||||
|
private int selectedColor;
|
||||||
|
|
||||||
|
/** Listener for color change events. */
|
||||||
|
private OnColorChangedListener colorChangedListener;
|
||||||
|
|
||||||
|
/** Tracks if the hue selector is being dragged. */
|
||||||
|
private boolean isDraggingHue;
|
||||||
|
|
||||||
|
/** Tracks if the saturation-value selector is being dragged. */
|
||||||
|
private boolean isDraggingSaturation;
|
||||||
|
|
||||||
|
/** Tracks if the opacity selector is being dragged. */
|
||||||
|
private boolean isDraggingOpacity;
|
||||||
|
|
||||||
|
/** Flag to enable/disable the opacity slider. */
|
||||||
|
private boolean opacitySliderEnabled = false;
|
||||||
|
|
||||||
|
public ColorPickerView(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ColorPickerView(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ColorPickerView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables or disables the opacity slider.
|
||||||
|
*/
|
||||||
|
public void setOpacitySliderEnabled(boolean enabled) {
|
||||||
|
if (opacitySliderEnabled != enabled) {
|
||||||
|
opacitySliderEnabled = enabled;
|
||||||
|
if (!enabled) {
|
||||||
|
opacity = 1f; // Reset to fully opaque when disabled.
|
||||||
|
updateSelectedColor();
|
||||||
|
}
|
||||||
|
updateOpacityShader();
|
||||||
|
requestLayout(); // Trigger re-measure to account for opacity slider.
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Measures the view, ensuring a consistent aspect ratio and minimum dimensions.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||||
|
final float DESIRED_ASPECT_RATIO = 0.8f; // height = width * 0.8
|
||||||
|
|
||||||
|
final int minWidth = dipToPixels(250);
|
||||||
|
final int minHeight = (int) (minWidth * DESIRED_ASPECT_RATIO) + (int) (HUE_BAR_HEIGHT + MARGIN_BETWEEN_AREAS)
|
||||||
|
+ (opacitySliderEnabled ? (int) (OPACITY_BAR_HEIGHT + MARGIN_BETWEEN_AREAS) : 0);
|
||||||
|
|
||||||
|
int width = resolveSize(minWidth, widthMeasureSpec);
|
||||||
|
int height = resolveSize(minHeight, heightMeasureSpec);
|
||||||
|
|
||||||
|
// Ensure minimum dimensions for usability.
|
||||||
|
width = Math.max(width, minWidth);
|
||||||
|
height = Math.max(height, minHeight);
|
||||||
|
|
||||||
|
// Adjust height to maintain desired aspect ratio if possible.
|
||||||
|
final int desiredHeight = (int) (width * DESIRED_ASPECT_RATIO) + (int) (HUE_BAR_HEIGHT + MARGIN_BETWEEN_AREAS)
|
||||||
|
+ (opacitySliderEnabled ? (int) (OPACITY_BAR_HEIGHT + MARGIN_BETWEEN_AREAS) : 0);
|
||||||
|
if (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY) {
|
||||||
|
height = desiredHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
setMeasuredDimension(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the view's layout when its size changes, recalculating bounds and shaders.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
|
||||||
|
super.onSizeChanged(width, height, oldWidth, oldHeight);
|
||||||
|
|
||||||
|
// Calculate bounds with hue bar and optional opacity bar at the bottom.
|
||||||
|
final float effectiveWidth = width - (2 * VIEW_PADDING);
|
||||||
|
final float effectiveHeight = height - (2 * VIEW_PADDING) - HUE_BAR_HEIGHT - MARGIN_BETWEEN_AREAS
|
||||||
|
- (opacitySliderEnabled ? OPACITY_BAR_HEIGHT + MARGIN_BETWEEN_AREAS : 0);
|
||||||
|
|
||||||
|
// Adjust rectangles to account for padding and density-independent dimensions.
|
||||||
|
saturationValueRect.set(
|
||||||
|
VIEW_PADDING,
|
||||||
|
VIEW_PADDING,
|
||||||
|
VIEW_PADDING + effectiveWidth,
|
||||||
|
VIEW_PADDING + effectiveHeight
|
||||||
|
);
|
||||||
|
|
||||||
|
hueRect.set(
|
||||||
|
VIEW_PADDING,
|
||||||
|
height - VIEW_PADDING - HUE_BAR_HEIGHT - (opacitySliderEnabled ? OPACITY_BAR_HEIGHT + MARGIN_BETWEEN_AREAS : 0),
|
||||||
|
VIEW_PADDING + effectiveWidth,
|
||||||
|
height - VIEW_PADDING - (opacitySliderEnabled ? OPACITY_BAR_HEIGHT + MARGIN_BETWEEN_AREAS : 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (opacitySliderEnabled) {
|
||||||
|
opacityRect.set(
|
||||||
|
VIEW_PADDING,
|
||||||
|
height - VIEW_PADDING - OPACITY_BAR_HEIGHT,
|
||||||
|
VIEW_PADDING + effectiveWidth,
|
||||||
|
height - VIEW_PADDING
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the shaders.
|
||||||
|
updateHueShader();
|
||||||
|
updateSaturationValueShader();
|
||||||
|
updateOpacityShader();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the shader for the hue bar to reflect the color gradient.
|
||||||
|
*/
|
||||||
|
private void updateHueShader() {
|
||||||
|
LinearGradient hueShader = new LinearGradient(
|
||||||
|
hueRect.left, hueRect.top,
|
||||||
|
hueRect.right, hueRect.top,
|
||||||
|
HUE_COLORS,
|
||||||
|
null,
|
||||||
|
Shader.TileMode.CLAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
huePaint.setShader(hueShader);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the shader for the opacity slider to reflect the current RGB color with varying opacity.
|
||||||
|
*/
|
||||||
|
private void updateOpacityShader() {
|
||||||
|
if (!opacitySliderEnabled) {
|
||||||
|
opacityPaint.setShader(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a linear gradient for opacity from transparent to opaque, using the current RGB color.
|
||||||
|
int rgbColor = Color.HSVToColor(0, new float[]{hue, saturation, value});
|
||||||
|
LinearGradient opacityShader = new LinearGradient(
|
||||||
|
opacityRect.left, opacityRect.top,
|
||||||
|
opacityRect.right, opacityRect.top,
|
||||||
|
rgbColor & 0x00FFFFFF, // Fully transparent
|
||||||
|
rgbColor | 0xFF000000, // Fully opaque
|
||||||
|
Shader.TileMode.CLAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
opacityPaint.setShader(opacityShader);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the shader for the saturation-value selector to reflect the current hue.
|
||||||
|
*/
|
||||||
|
private void updateSaturationValueShader() {
|
||||||
|
// Create a saturation-value gradient based on the current hue.
|
||||||
|
// Calculate the start color (white with the selected hue) for the saturation gradient.
|
||||||
|
final int startColor = Color.HSVToColor(new float[]{hue, 0f, 1f});
|
||||||
|
|
||||||
|
// Calculate the middle color (fully saturated color with the selected hue) for the saturation gradient.
|
||||||
|
final int midColor = Color.HSVToColor(new float[]{hue, 1f, 1f});
|
||||||
|
|
||||||
|
// Create a linear gradient for the saturation from startColor to midColor (horizontal).
|
||||||
|
LinearGradient satShader = new LinearGradient(
|
||||||
|
saturationValueRect.left, saturationValueRect.top,
|
||||||
|
saturationValueRect.right, saturationValueRect.top,
|
||||||
|
startColor,
|
||||||
|
midColor,
|
||||||
|
Shader.TileMode.CLAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a linear gradient for the value (brightness) from white to black (vertical).
|
||||||
|
LinearGradient valShader = new LinearGradient(
|
||||||
|
saturationValueRect.left, saturationValueRect.top,
|
||||||
|
saturationValueRect.left, saturationValueRect.bottom,
|
||||||
|
Color.WHITE,
|
||||||
|
Color.BLACK,
|
||||||
|
Shader.TileMode.CLAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
// Combine the saturation and value shaders using PorterDuff.Mode.MULTIPLY to create the final color.
|
||||||
|
ComposeShader combinedShader = new ComposeShader(satShader, valShader, PorterDuff.Mode.MULTIPLY);
|
||||||
|
|
||||||
|
// Set the combined shader for the saturation-value paint.
|
||||||
|
saturationValuePaint.setShader(combinedShader);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the color picker components, including the saturation-value selector, hue bar, opacity slider, and their respective handles.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void onDraw(Canvas canvas) {
|
||||||
|
// Draw the saturation-value selector rectangle.
|
||||||
|
canvas.drawRect(saturationValueRect, saturationValuePaint);
|
||||||
|
|
||||||
|
// Draw the hue bar.
|
||||||
|
canvas.drawRoundRect(hueRect, HUE_CORNER_RADIUS, HUE_CORNER_RADIUS, huePaint);
|
||||||
|
|
||||||
|
// Draw the opacity bar if enabled.
|
||||||
|
if (opacitySliderEnabled) {
|
||||||
|
canvas.drawRoundRect(opacityRect, OPACITY_CORNER_RADIUS, OPACITY_CORNER_RADIUS, opacityPaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
final float hueSelectorX = hueRect.left + (hue / 360f) * hueRect.width();
|
||||||
|
final float hueSelectorY = hueRect.centerY();
|
||||||
|
|
||||||
|
final float satSelectorX = saturationValueRect.left + saturation * saturationValueRect.width();
|
||||||
|
final float satSelectorY = saturationValueRect.top + (1 - value) * saturationValueRect.height();
|
||||||
|
|
||||||
|
// Draw the saturation and hue selector handles filled with their respective colors (fully opaque).
|
||||||
|
hsvArray[0] = hue;
|
||||||
|
final int hueHandleColor = Color.HSVToColor(0xFF, hsvArray); // Force opaque for hue handle.
|
||||||
|
final int satHandleColor = Color.HSVToColor(0xFF, new float[]{hue, saturation, value}); // Force opaque for sat-val handle.
|
||||||
|
selectorPaint.setStyle(Paint.Style.FILL_AND_STROKE);
|
||||||
|
|
||||||
|
selectorPaint.setColor(hueHandleColor);
|
||||||
|
canvas.drawCircle(hueSelectorX, hueSelectorY, SELECTOR_FILL_RADIUS, selectorPaint);
|
||||||
|
|
||||||
|
selectorPaint.setColor(satHandleColor);
|
||||||
|
canvas.drawCircle(satSelectorX, satSelectorY, SELECTOR_FILL_RADIUS, selectorPaint);
|
||||||
|
|
||||||
|
if (opacitySliderEnabled) {
|
||||||
|
final float opacitySelectorX = opacityRect.left + opacity * opacityRect.width();
|
||||||
|
final float opacitySelectorY = opacityRect.centerY();
|
||||||
|
selectorPaint.setColor(selectedColor); // Use full ARGB color to show opacity.
|
||||||
|
canvas.drawCircle(opacitySelectorX, opacitySelectorY, SELECTOR_FILL_RADIUS, selectorPaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw white outlines for the handles.
|
||||||
|
selectorPaint.setColor(SELECTOR_OUTLINE_COLOR);
|
||||||
|
selectorPaint.setStyle(Paint.Style.STROKE);
|
||||||
|
selectorPaint.setStrokeWidth(SELECTOR_STROKE_WIDTH);
|
||||||
|
canvas.drawCircle(hueSelectorX, hueSelectorY, SELECTOR_RADIUS, selectorPaint);
|
||||||
|
canvas.drawCircle(satSelectorX, satSelectorY, SELECTOR_RADIUS, selectorPaint);
|
||||||
|
if (opacitySliderEnabled) {
|
||||||
|
final float opacitySelectorX = opacityRect.left + opacity * opacityRect.width();
|
||||||
|
final float opacitySelectorY = opacityRect.centerY();
|
||||||
|
canvas.drawCircle(opacitySelectorX, opacitySelectorY, SELECTOR_RADIUS, selectorPaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw thin dark outlines for the handles at the outer edge of the white outline.
|
||||||
|
selectorPaint.setColor(SELECTOR_EDGE_COLOR);
|
||||||
|
selectorPaint.setStrokeWidth(SELECTOR_EDGE_STROKE_WIDTH);
|
||||||
|
canvas.drawCircle(hueSelectorX, hueSelectorY, SELECTOR_EDGE_RADIUS, selectorPaint);
|
||||||
|
canvas.drawCircle(satSelectorX, satSelectorY, SELECTOR_EDGE_RADIUS, selectorPaint);
|
||||||
|
if (opacitySliderEnabled) {
|
||||||
|
final float opacitySelectorX = opacityRect.left + opacity * opacityRect.width();
|
||||||
|
final float opacitySelectorY = opacityRect.centerY();
|
||||||
|
canvas.drawCircle(opacitySelectorX, opacitySelectorY, SELECTOR_EDGE_RADIUS, selectorPaint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles touch events to allow dragging of the hue, saturation-value, and opacity selectors.
|
||||||
|
*
|
||||||
|
* @param event The motion event.
|
||||||
|
* @return True if the event was handled, false otherwise.
|
||||||
|
*/
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
@Override
|
||||||
|
public boolean onTouchEvent(MotionEvent event) {
|
||||||
|
try {
|
||||||
|
final float x = event.getX();
|
||||||
|
final float y = event.getY();
|
||||||
|
final int action = event.getAction();
|
||||||
|
Logger.printDebug(() -> "onTouchEvent action: " + action + " x: " + x + " y: " + y);
|
||||||
|
|
||||||
|
// Define touch expansion for the hue and opacity bars.
|
||||||
|
RectF expandedHueRect = new RectF(
|
||||||
|
hueRect.left,
|
||||||
|
hueRect.top - TOUCH_EXPANSION,
|
||||||
|
hueRect.right,
|
||||||
|
hueRect.bottom + TOUCH_EXPANSION
|
||||||
|
);
|
||||||
|
RectF expandedOpacityRect = opacitySliderEnabled ? new RectF(
|
||||||
|
opacityRect.left,
|
||||||
|
opacityRect.top - TOUCH_EXPANSION,
|
||||||
|
opacityRect.right,
|
||||||
|
opacityRect.bottom + TOUCH_EXPANSION
|
||||||
|
) : new RectF();
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case MotionEvent.ACTION_DOWN:
|
||||||
|
// Calculate current handle positions.
|
||||||
|
final float hueSelectorX = hueRect.left + (hue / 360f) * hueRect.width();
|
||||||
|
final float hueSelectorY = hueRect.centerY();
|
||||||
|
|
||||||
|
final float satSelectorX = saturationValueRect.left + saturation * saturationValueRect.width();
|
||||||
|
final float valSelectorY = saturationValueRect.top + (1 - value) * saturationValueRect.height();
|
||||||
|
|
||||||
|
final float opacitySelectorX = opacitySliderEnabled ? opacityRect.left + opacity * opacityRect.width() : 0;
|
||||||
|
final float opacitySelectorY = opacitySliderEnabled ? opacityRect.centerY() : 0;
|
||||||
|
|
||||||
|
// Create hit areas for all handles.
|
||||||
|
RectF hueHitRect = new RectF(
|
||||||
|
hueSelectorX - SELECTOR_RADIUS,
|
||||||
|
hueSelectorY - SELECTOR_RADIUS,
|
||||||
|
hueSelectorX + SELECTOR_RADIUS,
|
||||||
|
hueSelectorY + SELECTOR_RADIUS
|
||||||
|
);
|
||||||
|
RectF satValHitRect = new RectF(
|
||||||
|
satSelectorX - SELECTOR_RADIUS,
|
||||||
|
valSelectorY - SELECTOR_RADIUS,
|
||||||
|
satSelectorX + SELECTOR_RADIUS,
|
||||||
|
valSelectorY + SELECTOR_RADIUS
|
||||||
|
);
|
||||||
|
RectF opacityHitRect = opacitySliderEnabled ? new RectF(
|
||||||
|
opacitySelectorX - SELECTOR_RADIUS,
|
||||||
|
opacitySelectorY - SELECTOR_RADIUS,
|
||||||
|
opacitySelectorX + SELECTOR_RADIUS,
|
||||||
|
opacitySelectorY + SELECTOR_RADIUS
|
||||||
|
) : new RectF();
|
||||||
|
|
||||||
|
// Check if the touch started on a handle or within the expanded bar areas.
|
||||||
|
if (hueHitRect.contains(x, y)) {
|
||||||
|
isDraggingHue = true;
|
||||||
|
updateHueFromTouch(x);
|
||||||
|
} else if (satValHitRect.contains(x, y)) {
|
||||||
|
isDraggingSaturation = true;
|
||||||
|
updateSaturationValueFromTouch(x, y);
|
||||||
|
} else if (opacitySliderEnabled && opacityHitRect.contains(x, y)) {
|
||||||
|
isDraggingOpacity = true;
|
||||||
|
updateOpacityFromTouch(x);
|
||||||
|
} else if (expandedHueRect.contains(x, y)) {
|
||||||
|
// Handle touch within the expanded hue bar area.
|
||||||
|
isDraggingHue = true;
|
||||||
|
updateHueFromTouch(x);
|
||||||
|
} else if (saturationValueRect.contains(x, y)) {
|
||||||
|
isDraggingSaturation = true;
|
||||||
|
updateSaturationValueFromTouch(x, y);
|
||||||
|
} else if (opacitySliderEnabled && expandedOpacityRect.contains(x, y)) {
|
||||||
|
isDraggingOpacity = true;
|
||||||
|
updateOpacityFromTouch(x);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MotionEvent.ACTION_MOVE:
|
||||||
|
// Continue updating values even if touch moves outside the view.
|
||||||
|
if (isDraggingHue) {
|
||||||
|
updateHueFromTouch(x);
|
||||||
|
} else if (isDraggingSaturation) {
|
||||||
|
updateSaturationValueFromTouch(x, y);
|
||||||
|
} else if (isDraggingOpacity) {
|
||||||
|
updateOpacityFromTouch(x);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MotionEvent.ACTION_UP:
|
||||||
|
case MotionEvent.ACTION_CANCEL:
|
||||||
|
isDraggingHue = false;
|
||||||
|
isDraggingSaturation = false;
|
||||||
|
isDraggingOpacity = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "onTouchEvent failure", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the hue value based on a touch event.
|
||||||
|
*/
|
||||||
|
private void updateHueFromTouch(float x) {
|
||||||
|
// Clamp x to the hue rectangle bounds.
|
||||||
|
final float clampedX = Utils.clamp(x, hueRect.left, hueRect.right);
|
||||||
|
final float updatedHue = ((clampedX - hueRect.left) / hueRect.width()) * 360f;
|
||||||
|
if (hue == updatedHue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hue = updatedHue;
|
||||||
|
updateSaturationValueShader();
|
||||||
|
updateOpacityShader();
|
||||||
|
updateSelectedColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the saturation and value based on a touch event.
|
||||||
|
*/
|
||||||
|
private void updateSaturationValueFromTouch(float x, float y) {
|
||||||
|
// Clamp x and y to the saturation-value rectangle bounds.
|
||||||
|
final float clampedX = Utils.clamp(x, saturationValueRect.left, saturationValueRect.right);
|
||||||
|
final float clampedY = Utils.clamp(y, saturationValueRect.top, saturationValueRect.bottom);
|
||||||
|
|
||||||
|
final float updatedSaturation = (clampedX - saturationValueRect.left) / saturationValueRect.width();
|
||||||
|
final float updatedValue = 1 - ((clampedY - saturationValueRect.top) / saturationValueRect.height());
|
||||||
|
|
||||||
|
if (saturation == updatedSaturation && value == updatedValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
saturation = updatedSaturation;
|
||||||
|
value = updatedValue;
|
||||||
|
updateOpacityShader();
|
||||||
|
updateSelectedColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the opacity value based on a touch event.
|
||||||
|
*/
|
||||||
|
private void updateOpacityFromTouch(float x) {
|
||||||
|
if (!opacitySliderEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final float clampedX = Utils.clamp(x, opacityRect.left, opacityRect.right);
|
||||||
|
final float updatedOpacity = (clampedX - opacityRect.left) / opacityRect.width();
|
||||||
|
if (opacity == updatedOpacity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
opacity = updatedOpacity;
|
||||||
|
updateSelectedColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the selected color based on the current hue, saturation, value, and opacity.
|
||||||
|
*/
|
||||||
|
private void updateSelectedColor() {
|
||||||
|
final int rgbColor = Color.HSVToColor(0, new float[]{hue, saturation, value});
|
||||||
|
final int updatedColor = opacitySliderEnabled
|
||||||
|
? (rgbColor & 0x00FFFFFF) | (((int) (opacity * 255)) << 24)
|
||||||
|
: (rgbColor & 0x00FFFFFF) | 0xFF000000;
|
||||||
|
|
||||||
|
if (selectedColor != updatedColor) {
|
||||||
|
selectedColor = updatedColor;
|
||||||
|
|
||||||
|
if (colorChangedListener != null) {
|
||||||
|
colorChangedListener.onColorChanged(updatedColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must always redraw, otherwise if saturation is pure grey or black
|
||||||
|
// then the hue slider cannot be changed.
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the selected color, updating the hue, saturation, value and opacity sliders accordingly.
|
||||||
|
*/
|
||||||
|
public void setColor(@ColorInt int color) {
|
||||||
|
if (selectedColor == color) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the selected color.
|
||||||
|
selectedColor = color;
|
||||||
|
Logger.printDebug(() -> "setColor: " + getColorString(selectedColor, opacitySliderEnabled));
|
||||||
|
|
||||||
|
// Convert the ARGB color to HSV values.
|
||||||
|
float[] hsv = new float[3];
|
||||||
|
Color.colorToHSV(color, hsv);
|
||||||
|
|
||||||
|
// Update the hue, saturation, and value.
|
||||||
|
hue = hsv[0];
|
||||||
|
saturation = hsv[1];
|
||||||
|
value = hsv[2];
|
||||||
|
opacity = opacitySliderEnabled ? ((color >> 24) & 0xFF) / 255f : 1f;
|
||||||
|
|
||||||
|
// Update the saturation-value shader based on the new hue.
|
||||||
|
updateSaturationValueShader();
|
||||||
|
updateOpacityShader();
|
||||||
|
|
||||||
|
// Notify the listener if it's set.
|
||||||
|
if (colorChangedListener != null) {
|
||||||
|
colorChangedListener.onColorChanged(selectedColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalidate the view to trigger a redraw.
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the currently selected color.
|
||||||
|
*/
|
||||||
|
@ColorInt
|
||||||
|
public int getColor() {
|
||||||
|
return selectedColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a listener to be notified when the selected color changes.
|
||||||
|
*/
|
||||||
|
public void setOnColorChangedListener(OnColorChangedListener listener) {
|
||||||
|
colorChangedListener = listener;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package app.revanced.extension.shared.settings.preference;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extended ColorPickerPreference that enables the opacity slider for color selection.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class ColorPickerWithOpacitySliderPreference extends ColorPickerPreference {
|
||||||
|
|
||||||
|
public ColorPickerWithOpacitySliderPreference(Context context) {
|
||||||
|
super(context);
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ColorPickerWithOpacitySliderPreference(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ColorPickerWithOpacitySliderPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the preference with opacity slider enabled.
|
||||||
|
*/
|
||||||
|
private void init() {
|
||||||
|
// Enable the opacity slider for alpha channel support.
|
||||||
|
setOpacitySliderEnabled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
package app.revanced.extension.shared.settings.preference;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
|
||||||
|
|
||||||
|
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.ArrayAdapter;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.ListView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.shared.ui.CustomDialog;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom ListPreference that uses a styled custom dialog with a custom checkmark indicator,
|
||||||
|
* supports a static summary and highlighted entries for search functionality.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({"unused", "deprecation"})
|
||||||
|
public class CustomDialogListPreference extends ListPreference {
|
||||||
|
|
||||||
|
public static final int ID_REVANCED_CHECK_ICON =
|
||||||
|
getResourceIdentifierOrThrow("revanced_check_icon", "id");
|
||||||
|
public static final int ID_REVANCED_CHECK_ICON_PLACEHOLDER =
|
||||||
|
getResourceIdentifierOrThrow("revanced_check_icon_placeholder", "id");
|
||||||
|
public static final int ID_REVANCED_ITEM_TEXT =
|
||||||
|
getResourceIdentifierOrThrow("revanced_item_text", "id");
|
||||||
|
public static final int LAYOUT_REVANCED_CUSTOM_LIST_ITEM_CHECKED =
|
||||||
|
getResourceIdentifierOrThrow("revanced_custom_list_item_checked", "layout");
|
||||||
|
|
||||||
|
private String staticSummary = null;
|
||||||
|
private CharSequence[] highlightedEntriesForDialog = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a static summary that will not be overwritten by value changes.
|
||||||
|
*/
|
||||||
|
public void setStaticSummary(String summary) {
|
||||||
|
this.staticSummary = summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the static summary if set, otherwise null.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public String getStaticSummary() {
|
||||||
|
return staticSummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Always return static summary if set.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public CharSequence getSummary() {
|
||||||
|
if (staticSummary != null) {
|
||||||
|
return staticSummary;
|
||||||
|
}
|
||||||
|
return super.getSummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets highlighted entries for display in the dialog.
|
||||||
|
* These entries are used only for the current dialog and are automatically cleared.
|
||||||
|
*/
|
||||||
|
public void setHighlightedEntriesForDialog(CharSequence[] highlightedEntries) {
|
||||||
|
this.highlightedEntriesForDialog = highlightedEntries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears highlighted entries after the dialog is closed.
|
||||||
|
*/
|
||||||
|
public void clearHighlightedEntriesForDialog() {
|
||||||
|
this.highlightedEntriesForDialog = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns entries for display in the dialog.
|
||||||
|
* If highlighted entries exist, they are used; otherwise, the original entries are returned.
|
||||||
|
*/
|
||||||
|
private CharSequence[] getEntriesForDialog() {
|
||||||
|
return highlightedEntriesForDialog != null ? highlightedEntriesForDialog : getEntries();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(ID_REVANCED_CHECK_ICON);
|
||||||
|
holder.placeholder = view.findViewById(ID_REVANCED_CHECK_ICON_PLACEHOLDER);
|
||||||
|
holder.itemText = view.findViewById(ID_REVANCED_ITEM_TEXT);
|
||||||
|
view.setTag(holder);
|
||||||
|
} else {
|
||||||
|
holder = (SubViewDataContainer) view.getTag();
|
||||||
|
}
|
||||||
|
|
||||||
|
CharSequence itemText = getItem(position);
|
||||||
|
holder.itemText.setText(itemText);
|
||||||
|
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();
|
||||||
|
|
||||||
|
CharSequence[] entriesToShow = getEntriesForDialog();
|
||||||
|
CharSequence[] entryValues = getEntryValues();
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
LAYOUT_REVANCED_CUSTOM_LIST_ITEM_CHECKED,
|
||||||
|
entriesToShow,
|
||||||
|
entryValues,
|
||||||
|
getValue()
|
||||||
|
);
|
||||||
|
listView.setAdapter(adapter);
|
||||||
|
|
||||||
|
// Set checked item.
|
||||||
|
String currentValue = getValue();
|
||||||
|
if (currentValue != null) {
|
||||||
|
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 = CustomDialog.create(
|
||||||
|
context,
|
||||||
|
getTitle() != null ? getTitle().toString() : "",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
this::clearHighlightedEntriesForDialog, // Cancel button action.
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
Dialog dialog = dialogPair.first;
|
||||||
|
// Add a listener to clear when the dialog is closed in any way.
|
||||||
|
dialog.setOnDismissListener(dialogInterface -> clearHighlightedEntriesForDialog());
|
||||||
|
|
||||||
|
// 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 = entryValues[position].toString();
|
||||||
|
if (callChangeListener(selectedValue)) {
|
||||||
|
setValue(selectedValue);
|
||||||
|
|
||||||
|
// Update summaries from the original entries (without highlighting).
|
||||||
|
if (staticSummary == null) {
|
||||||
|
CharSequence[] originalEntries = getEntries();
|
||||||
|
if (originalEntries != null && position < originalEntries.length) {
|
||||||
|
setSummary(originalEntries[position]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter.setSelectedValue(selectedValue);
|
||||||
|
adapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear highlighted entries before closing.
|
||||||
|
clearHighlightedEntriesForDialog();
|
||||||
|
dialog.dismiss();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show the dialog.
|
||||||
|
dialog.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package app.revanced.extension.shared.settings.preference;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.preference.Preference;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom preference that triggers exporting ReVanced debug logs to the clipboard when clicked.
|
||||||
|
* Invokes the {@link LogBufferManager#exportToClipboard} method.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({"deprecation", "unused"})
|
||||||
|
public class ExportLogToClipboardPreference extends Preference {
|
||||||
|
|
||||||
|
{
|
||||||
|
setOnPreferenceClickListener(pref -> {
|
||||||
|
LogBufferManager.exportToClipboard();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExportLogToClipboardPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
}
|
||||||
|
public ExportLogToClipboardPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
public ExportLogToClipboardPreference(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
public ExportLogToClipboardPreference(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,25 @@
|
|||||||
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 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.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
import android.util.Pair;
|
||||||
import android.util.TypedValue;
|
import android.util.TypedValue;
|
||||||
|
import android.view.inputmethod.InputMethodManager;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import app.revanced.extension.shared.settings.Setting;
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
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;
|
import app.revanced.extension.shared.ui.CustomDialog;
|
||||||
|
|
||||||
@SuppressWarnings({"unused", "deprecation"})
|
@SuppressWarnings({"unused", "deprecation"})
|
||||||
public class ImportExportPreference extends EditTextPreference implements Preference.OnPreferenceClickListener {
|
public class ImportExportPreference extends EditTextPreference implements Preference.OnPreferenceClickListener {
|
||||||
@@ -54,7 +60,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 +71,46 @@ 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 = CustomDialog.create(
|
||||||
Utils.setClipboard(getEditText().getText().toString());
|
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.
|
||||||
|
);
|
||||||
|
|
||||||
|
// If there are no settings yet, then show the on screen keyboard and bring focus to
|
||||||
|
// the edit text. This makes it easier to paste saved settings after a reinstall.
|
||||||
|
dialogPair.first.setOnShowListener(dialogInterface -> {
|
||||||
|
if (existingSettings.isEmpty()) {
|
||||||
|
editText.postDelayed(() -> {
|
||||||
|
editText.requestFocus();
|
||||||
|
|
||||||
|
InputMethodManager inputMethodManager = (InputMethodManager)
|
||||||
|
editText.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||||
|
inputMethodManager.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show the dialog.
|
||||||
|
dialogPair.first.show();
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "onPrepareDialogBuilder failure", ex);
|
Logger.printException(() -> "showDialog failure", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +123,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 +131,4 @@ public class ImportExportPreference extends EditTextPreference implements Prefer
|
|||||||
AbstractPreferenceFragment.settingImportInProgress = false;
|
AbstractPreferenceFragment.settingImportInProgress = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
package app.revanced.extension.shared.settings.preference;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages a buffer for storing debug logs from {@link Logger}.
|
||||||
|
* Stores just under 1MB of the most recent log data.
|
||||||
|
*
|
||||||
|
* All methods are thread-safe.
|
||||||
|
*/
|
||||||
|
public final class LogBufferManager {
|
||||||
|
/** Maximum byte size of all buffer entries. Must be less than Android's 1 MB Binder transaction limit. */
|
||||||
|
private static final int BUFFER_MAX_BYTES = 900_000;
|
||||||
|
/** Limit number of log lines. */
|
||||||
|
private static final int BUFFER_MAX_SIZE = 10_000;
|
||||||
|
|
||||||
|
private static final Deque<String> logBuffer = new ConcurrentLinkedDeque<>();
|
||||||
|
private static final AtomicInteger logBufferByteSize = new AtomicInteger();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends a log message to the internal buffer if debugging is enabled.
|
||||||
|
* The buffer is limited to approximately {@link #BUFFER_MAX_BYTES} or {@link #BUFFER_MAX_SIZE}
|
||||||
|
* to prevent excessive memory usage.
|
||||||
|
*
|
||||||
|
* @param message The log message to append.
|
||||||
|
*/
|
||||||
|
public static void appendToLogBuffer(String message) {
|
||||||
|
Objects.requireNonNull(message);
|
||||||
|
|
||||||
|
// It's very important that no Settings are used in this method,
|
||||||
|
// as this code is used when a context is not set and thus referencing
|
||||||
|
// a setting will crash the app.
|
||||||
|
logBuffer.addLast(message);
|
||||||
|
int newSize = logBufferByteSize.addAndGet(message.length());
|
||||||
|
|
||||||
|
// Remove oldest entries if over the log size limits.
|
||||||
|
while (newSize > BUFFER_MAX_BYTES || logBuffer.size() > BUFFER_MAX_SIZE) {
|
||||||
|
String removed = logBuffer.pollFirst();
|
||||||
|
if (removed == null) {
|
||||||
|
// Thread race of two different calls to this method, and the other thread won.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
newSize = logBufferByteSize.addAndGet(-removed.length());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports all logs from the internal buffer to the clipboard.
|
||||||
|
* Displays a toast with the result.
|
||||||
|
*/
|
||||||
|
public static void exportToClipboard() {
|
||||||
|
try {
|
||||||
|
if (!BaseSettings.DEBUG.get()) {
|
||||||
|
Utils.showToastShort(str("revanced_debug_logs_disabled"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logBuffer.isEmpty()) {
|
||||||
|
Utils.showToastShort(str("revanced_debug_logs_none_found"));
|
||||||
|
clearLogBufferData(); // Clear toast log entry that was just created.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Most (but not all) Android 13+ devices always show a "copied to clipboard" toast
|
||||||
|
// and there is no way to programmatically detect if a toast will show or not.
|
||||||
|
// Show a toast even if using Android 13+, but show ReVanced toast first (before copying to clipboard).
|
||||||
|
Utils.showToastShort(str("revanced_debug_logs_copied_to_clipboard"));
|
||||||
|
|
||||||
|
Utils.setClipboard(String.join("\n", logBuffer));
|
||||||
|
} catch (Exception ex) {
|
||||||
|
// Handle security exception if clipboard access is denied.
|
||||||
|
String errorMessage = String.format(str("revanced_debug_logs_failed_to_export"), ex.getMessage());
|
||||||
|
Utils.showToastLong(errorMessage);
|
||||||
|
Logger.printDebug(() -> errorMessage, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void clearLogBufferData() {
|
||||||
|
// Cannot simply clear the log buffer because there is no
|
||||||
|
// write lock for both the deque and the atomic int.
|
||||||
|
// Instead pop off log entries and decrement the size one by one.
|
||||||
|
while (!logBuffer.isEmpty()) {
|
||||||
|
String removed = logBuffer.pollFirst();
|
||||||
|
if (removed != null) {
|
||||||
|
logBufferByteSize.addAndGet(-removed.length());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the internal log buffer and displays a toast with the result.
|
||||||
|
*/
|
||||||
|
public static void clearLogBuffer() {
|
||||||
|
if (!BaseSettings.DEBUG.get()) {
|
||||||
|
Utils.showToastShort(str("revanced_debug_logs_disabled"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show toast before clearing, otherwise toast log will still remain.
|
||||||
|
Utils.showToastShort(str("revanced_debug_logs_clear_toast"));
|
||||||
|
clearLogBufferData();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,10 @@ public class NoTitlePreferenceCategory extends PreferenceCategory {
|
|||||||
super(context, attrs, defStyleAttr);
|
super(context, attrs, defStyleAttr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public NoTitlePreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
}
|
||||||
|
|
||||||
public NoTitlePreferenceCategory(Context context) {
|
public NoTitlePreferenceCategory(Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,17 +9,20 @@ 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.content.res.Configuration;
|
import android.graphics.drawable.ShapeDrawable;
|
||||||
import android.graphics.Color;
|
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.Gravity;
|
||||||
|
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;
|
||||||
@@ -49,28 +53,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(getContext());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subclasses can override this and provide a themed color.
|
|
||||||
*/
|
|
||||||
protected int getLightColor() {
|
|
||||||
return Color.WHITE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subclasses can override this and provide a themed color.
|
|
||||||
*/
|
|
||||||
protected int getDarkColor() {
|
|
||||||
return Color.BLACK;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apps that do not support bundling resources must override this.
|
* Apps that do not support bundling resources must override this.
|
||||||
*
|
*
|
||||||
@@ -87,9 +69,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>",
|
||||||
@@ -221,14 +202,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, Gravity.CENTER, 0, 90, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class OpenLinksExternallyWebClient extends WebViewClient {
|
private class OpenLinksExternallyWebClient extends WebViewClient {
|
||||||
@@ -316,7 +321,7 @@ class AboutLinksRoutes {
|
|||||||
// Do not show an exception toast if the server is down
|
// Do not show an exception toast if the server is down
|
||||||
final int responseCode = connection.getResponseCode();
|
final int responseCode = connection.getResponseCode();
|
||||||
if (responseCode != 200) {
|
if (responseCode != 200) {
|
||||||
Logger.printDebug(() -> "Failed to get social links. Response code: " + responseCode);
|
Logger.printDebug(() -> "Failed to get social links. Response code: " + responseCode);
|
||||||
return NO_CONNECTION_STATIC_LINKS;
|
return NO_CONNECTION_STATIC_LINKS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,21 +2,22 @@ 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;
|
||||||
|
|
||||||
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.settings.Setting;
|
import app.revanced.extension.shared.settings.Setting;
|
||||||
|
import app.revanced.extension.shared.ui.CustomDialog;
|
||||||
|
|
||||||
@SuppressWarnings({"unused", "deprecation"})
|
@SuppressWarnings({"unused", "deprecation"})
|
||||||
public class ResettableEditTextPreference extends EditTextPreference {
|
public class ResettableEditTextPreference extends EditTextPreference {
|
||||||
@@ -44,41 +45,61 @@ public class ResettableEditTextPreference extends EditTextPreference {
|
|||||||
this.setting = setting;
|
this.setting = setting;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
|
||||||
super.onPrepareDialogBuilder(builder);
|
|
||||||
Utils.setEditTextDialogTheme(builder);
|
|
||||||
|
|
||||||
if (setting == null) {
|
|
||||||
String key = getKey();
|
|
||||||
if (key != null) {
|
|
||||||
setting = Setting.getSettingFromPath(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setting != null) {
|
|
||||||
builder.setNeutralButton(str("revanced_settings_reset"), null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void showDialog(Bundle state) {
|
protected void showDialog(Bundle state) {
|
||||||
super.showDialog(state);
|
try {
|
||||||
|
Context context = getContext();
|
||||||
|
EditText editText = getEditText();
|
||||||
|
|
||||||
// Override the button click listener to prevent dismissing the dialog.
|
// Resolve setting if not already set.
|
||||||
Button button = ((AlertDialog) getDialog()).getButton(AlertDialog.BUTTON_NEUTRAL);
|
if (setting == null) {
|
||||||
if (button == null) {
|
String key = getKey();
|
||||||
return;
|
if (key != null) {
|
||||||
}
|
setting = Setting.getSettingFromPath(key);
|
||||||
button.setOnClickListener(v -> {
|
}
|
||||||
try {
|
|
||||||
String defaultStringValue = Objects.requireNonNull(setting).defaultValue.toString();
|
|
||||||
EditText editText = getEditText();
|
|
||||||
editText.setText(defaultStringValue);
|
|
||||||
editText.setSelection(defaultStringValue.length()); // move cursor to end of text
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "reset failure", ex);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
// 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 = CustomDialog.create(
|
||||||
|
context,
|
||||||
|
getTitle() != null ? getTitle().toString() : "", // Title.
|
||||||
|
null, // Message is replaced by EditText.
|
||||||
|
editText, // Pass the EditText.
|
||||||
|
null, // OK button text.
|
||||||
|
() -> {
|
||||||
|
// OK button action. Persist the EditText value when OK is clicked.
|
||||||
|
String newValue = editText.getText().toString();
|
||||||
|
if (callChangeListener(newValue)) {
|
||||||
|
setText(newValue);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() -> {}, // Cancel button action (dismiss only).
|
||||||
|
neutralButtonText, // Neutral button text (Reset).
|
||||||
|
() -> {
|
||||||
|
// Neutral button action.
|
||||||
|
if (setting != null) {
|
||||||
|
try {
|
||||||
|
String defaultStringValue = Objects.requireNonNull(setting).defaultValue.toString();
|
||||||
|
editText.setText(defaultStringValue);
|
||||||
|
editText.setSelection(defaultStringValue.length()); // Move cursor to end of text.
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "reset failure", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
false // Do not dismiss dialog when onNeutralClick.
|
||||||
|
);
|
||||||
|
|
||||||
|
// Show the dialog.
|
||||||
|
dialogPair.first.show();
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "showDialog failure", ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package app.revanced.extension.shared.settings.preference;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.util.Pair;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PreferenceList that sorts itself.
|
||||||
|
* By default the first entry is preserved in its original position,
|
||||||
|
* and all other entries are sorted alphabetically.
|
||||||
|
*
|
||||||
|
* Ideally the 'keep first entries to preserve' is an xml parameter,
|
||||||
|
* but currently that's not so simple since Extensions code cannot use
|
||||||
|
* generated code from the Patches repo (which is required for custom xml parameters).
|
||||||
|
*
|
||||||
|
* If any class wants to use a different getFirstEntriesToPreserve value,
|
||||||
|
* it needs to subclass this preference and override {@link #getFirstEntriesToPreserve}.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({"unused", "deprecation"})
|
||||||
|
public class SortedListPreference extends CustomDialogListPreference {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sorts the current list entries.
|
||||||
|
*
|
||||||
|
* @param firstEntriesToPreserve The number of entries to preserve in their original position,
|
||||||
|
* or a negative value to not sort and leave entries
|
||||||
|
* as they current are.
|
||||||
|
*/
|
||||||
|
public void sortEntryAndValues(int firstEntriesToPreserve) {
|
||||||
|
CharSequence[] entries = getEntries();
|
||||||
|
CharSequence[] entryValues = getEntryValues();
|
||||||
|
if (entries == null || entryValues == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int entrySize = entries.length;
|
||||||
|
if (entrySize != entryValues.length) {
|
||||||
|
// Xml array declaration has a missing/extra entry.
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstEntriesToPreserve < 0) {
|
||||||
|
return; // Nothing to do.
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Pair<CharSequence, CharSequence>> firstEntries = new ArrayList<>(firstEntriesToPreserve);
|
||||||
|
|
||||||
|
// Android does not have a triple class like Kotlin, So instead use a nested pair.
|
||||||
|
// Cannot easily use a SortedMap, because if two entries incorrectly have
|
||||||
|
// identical names then the duplicates entries are not preserved.
|
||||||
|
List<Pair<String, Pair<CharSequence, CharSequence>>> lastEntries = new ArrayList<>();
|
||||||
|
|
||||||
|
for (int i = 0; i < entrySize; i++) {
|
||||||
|
Pair<CharSequence, CharSequence> pair = new Pair<>(entries[i], entryValues[i]);
|
||||||
|
if (i < firstEntriesToPreserve) {
|
||||||
|
firstEntries.add(pair);
|
||||||
|
} else {
|
||||||
|
lastEntries.add(new Pair<>(Utils.removePunctuationToLowercase(pair.first), pair));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//noinspection ComparatorCombinators
|
||||||
|
Collections.sort(lastEntries, (pair1, pair2)
|
||||||
|
-> pair1.first.compareTo(pair2.first));
|
||||||
|
|
||||||
|
CharSequence[] sortedEntries = new CharSequence[entrySize];
|
||||||
|
CharSequence[] sortedEntryValues = new CharSequence[entrySize];
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
for (Pair<CharSequence, CharSequence> pair : firstEntries) {
|
||||||
|
sortedEntries[i] = pair.first;
|
||||||
|
sortedEntryValues[i] = pair.second;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Pair<String, Pair<CharSequence, CharSequence>> outer : lastEntries) {
|
||||||
|
Pair<CharSequence, CharSequence> inner = outer.second;
|
||||||
|
sortedEntries[i] = inner.first;
|
||||||
|
sortedEntryValues[i] = inner.second;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
super.setEntries(sortedEntries);
|
||||||
|
super.setEntryValues(sortedEntryValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SortedListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
|
||||||
|
sortEntryAndValues(getFirstEntriesToPreserve());
|
||||||
|
}
|
||||||
|
|
||||||
|
public SortedListPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
|
||||||
|
sortEntryAndValues(getFirstEntriesToPreserve());
|
||||||
|
}
|
||||||
|
|
||||||
|
public SortedListPreference(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
|
||||||
|
sortEntryAndValues(getFirstEntriesToPreserve());
|
||||||
|
}
|
||||||
|
|
||||||
|
public SortedListPreference(Context context) {
|
||||||
|
super(context);
|
||||||
|
|
||||||
|
sortEntryAndValues(getFirstEntriesToPreserve());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The number of first entries to leave exactly where they are, and do not sort them.
|
||||||
|
* A negative value indicates do not sort any entries.
|
||||||
|
*/
|
||||||
|
protected int getFirstEntriesToPreserve() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user