mirror of
https://github.com/revanced/revanced-patches.git
synced 2025-04-30 06:34:28 +02:00
Compare commits
3550 Commits
v1.0.0-dev
...
main
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f925479a93 | ||
![]() |
6514420719 | ||
![]() |
0e064cd607 | ||
![]() |
a50edf06f6 | ||
![]() |
43bcf5a098 | ||
![]() |
f6f06dcd02 | ||
![]() |
9d30474318 | ||
![]() |
2bbcf9d82c | ||
![]() |
ba83c01700 | ||
![]() |
ebee07ec3a | ||
![]() |
81999d8cd5 | ||
![]() |
d21c1b2a94 | ||
![]() |
eaceab5ee5 | ||
![]() |
4db5d3c3d5 | ||
![]() |
9ab0338a68 | ||
![]() |
433dbc3bf8 | ||
![]() |
255cb5874c | ||
![]() |
fd214046f2 | ||
![]() |
5062e24433 | ||
![]() |
ee9039428c | ||
![]() |
0cb38f9f36 | ||
![]() |
3e64a4c18f | ||
![]() |
cc3b94a3cb | ||
![]() |
dc89be0e94 | ||
![]() |
84bc40cba0 | ||
![]() |
f62bcbf42a | ||
![]() |
11027ec551 | ||
![]() |
79a2c0db3d | ||
![]() |
5ecbe823ed | ||
![]() |
409f98cf65 | ||
![]() |
4c8bf7fbf7 | ||
![]() |
655b39043a | ||
![]() |
564cfd2e38 | ||
![]() |
1ea8047aef | ||
![]() |
4c619fb346 | ||
![]() |
703359f0c1 | ||
![]() |
e2b9d65bc6 | ||
![]() |
1050c77efd | ||
![]() |
f28efcf965 | ||
![]() |
03d0eb2f8c | ||
![]() |
ffc74be822 | ||
![]() |
c5d1702411 | ||
![]() |
42230b0291 | ||
![]() |
b02928cdc6 | ||
![]() |
7b58010076 | ||
![]() |
d639151641 | ||
![]() |
36e25966c8 | ||
![]() |
215fccbaf2 | ||
![]() |
780822f1bd | ||
![]() |
8957325d78 | ||
![]() |
aba6a6b5c5 | ||
![]() |
50f5b1ac54 | ||
![]() |
5b5449e07a | ||
![]() |
6837348c45 | ||
![]() |
95440273a6 | ||
![]() |
a6b5d043fd | ||
![]() |
0ee36939f4 | ||
![]() |
b99c2e5eb7 | ||
![]() |
dcf6178f19 | ||
![]() |
065cc2a015 | ||
![]() |
26b25ef479 | ||
![]() |
7f98474d37 | ||
![]() |
3e18e868bb | ||
![]() |
618e3276e4 | ||
![]() |
89d44da171 | ||
![]() |
4b84742210 | ||
![]() |
47ebec532e | ||
![]() |
48358ff45d | ||
![]() |
a22ba8e4d6 | ||
![]() |
6d7101cb2e | ||
![]() |
d42320d49f | ||
![]() |
56e48f4c89 | ||
![]() |
67fe151d77 | ||
![]() |
152bb7c3ee | ||
![]() |
9387aae7ba | ||
![]() |
e8e5a6776a | ||
![]() |
106202f9eb | ||
![]() |
e5ffd2c353 | ||
![]() |
a3fde874af | ||
![]() |
b91285ec20 | ||
![]() |
649a2c0616 | ||
![]() |
fe1f3ed169 | ||
![]() |
b6e568f719 | ||
![]() |
e3fad97484 | ||
![]() |
ebc5ebc0a9 | ||
![]() |
3864f35501 | ||
![]() |
8e91507b95 | ||
![]() |
b67bbb2996 | ||
![]() |
7d86856713 | ||
![]() |
ea92a2e36c | ||
![]() |
935226f8a4 | ||
![]() |
d1ed583cc1 | ||
![]() |
0bb3e32244 | ||
![]() |
4ecd7e0a23 | ||
![]() |
0b9a5e7f89 | ||
![]() |
eaee621831 | ||
![]() |
d7a7a0b982 | ||
![]() |
3c316fa329 | ||
![]() |
ddc6e4c34f | ||
![]() |
9f0efab3a1 | ||
![]() |
34c14c9b44 | ||
![]() |
ef1916113d | ||
![]() |
d0c85f0440 | ||
![]() |
4d02f6cf14 | ||
![]() |
ff846b0b7e | ||
![]() |
8e16483229 | ||
![]() |
87c86b53a9 | ||
![]() |
e2083159b1 | ||
![]() |
503b7eb8d4 | ||
![]() |
899c2fe228 | ||
![]() |
4a897fdb98 | ||
![]() |
d6cc665f71 | ||
![]() |
2ed675cdd0 | ||
![]() |
8503013f9d | ||
![]() |
bb30807784 | ||
![]() |
a233938a3d | ||
![]() |
aaa826607e | ||
![]() |
d269bd0f7c | ||
![]() |
84f585492e | ||
![]() |
58ffbf40a5 | ||
![]() |
2ad568318b | ||
![]() |
c1379f6e52 | ||
![]() |
f191ab63fd | ||
![]() |
f033b1dedb | ||
![]() |
d8af6f452e | ||
![]() |
f5797289f4 | ||
![]() |
6b19e19332 | ||
![]() |
1df51e5a96 | ||
![]() |
568b40da96 | ||
![]() |
5f4c42bfa9 | ||
![]() |
a4a0e6869e | ||
![]() |
560c7d4c5f | ||
![]() |
a4865228f8 | ||
![]() |
9a10ee4d22 | ||
![]() |
43b2a1e158 | ||
![]() |
46bd1c829a | ||
![]() |
115a53b178 | ||
![]() |
4d1f75e658 | ||
![]() |
398e63b47a | ||
![]() |
06be36cddf | ||
![]() |
6a3d64a3ac | ||
![]() |
d045ea01dd | ||
![]() |
a85639ae7b | ||
![]() |
32a21f5437 | ||
![]() |
f048c50e56 | ||
![]() |
bae80204c4 | ||
![]() |
521fd48602 | ||
![]() |
e9b7f263f7 | ||
![]() |
f7497be2c5 | ||
![]() |
185732c89f | ||
![]() |
3d6ccf48ec | ||
![]() |
05a55d7b5a | ||
![]() |
81a5f751dc | ||
![]() |
8e31c54f14 | ||
![]() |
5012439a8e | ||
![]() |
0ccbe0935c | ||
![]() |
f323567f4d | ||
![]() |
0c961158fa | ||
![]() |
49797fe8d0 | ||
![]() |
fabe37cb91 | ||
![]() |
85764c2e8d | ||
![]() |
10205d858b | ||
![]() |
9d74c1ae34 | ||
![]() |
883fbe7123 | ||
![]() |
2090f7ec8f | ||
![]() |
6e8ffbade9 | ||
![]() |
6e4882e74f | ||
![]() |
d70e5ab192 | ||
![]() |
acb0870faf | ||
![]() |
358b8374ca | ||
![]() |
d662c000d3 | ||
![]() |
3646c70556 | ||
![]() |
aa2b79fe7e | ||
![]() |
741c2d5940 | ||
![]() |
ef278d66bf | ||
![]() |
7e54260ac6 | ||
![]() |
6f3f8fdce0 | ||
![]() |
584692dd72 | ||
![]() |
2585c5554c | ||
![]() |
62a6164b88 | ||
![]() |
237ced48c6 | ||
![]() |
fd6bc6ec36 | ||
![]() |
274df58056 | ||
![]() |
cf9f959923 | ||
![]() |
a623abcd8c | ||
![]() |
23cec67462 | ||
![]() |
ee67b763d5 | ||
![]() |
49bf811252 | ||
![]() |
5d3c8175b3 | ||
![]() |
1ed6933224 | ||
![]() |
4387a7b131 | ||
![]() |
f1b57e84e7 | ||
![]() |
5237235888 | ||
![]() |
8e50cc70fb | ||
![]() |
88142ab464 | ||
![]() |
74ec83f964 | ||
![]() |
aa5c001968 | ||
![]() |
5bd2e86afe | ||
![]() |
cb5b302b03 | ||
![]() |
44ddc817f7 | ||
![]() |
29bccf7b22 | ||
![]() |
e2c82593e9 | ||
![]() |
d9de88e835 | ||
![]() |
c8a87ff334 | ||
![]() |
647e7642ef | ||
![]() |
52e4b9c43e | ||
![]() |
d74732b759 | ||
![]() |
880dc3d5aa | ||
![]() |
07325d063e | ||
![]() |
634d0ee12e | ||
![]() |
94fb5ecaa4 | ||
![]() |
0210c1d7d7 | ||
![]() |
f3268fb03c | ||
![]() |
eb1033a709 | ||
![]() |
6c4885a1d5 | ||
![]() |
3c20829c69 | ||
![]() |
744def6652 | ||
![]() |
6dc4bf75e0 | ||
![]() |
576a8f48ab | ||
![]() |
a0b2e053a0 | ||
![]() |
320c36545f | ||
![]() |
1f08047b48 | ||
![]() |
afeef0bbec | ||
![]() |
edf66f4e16 | ||
![]() |
4213b87492 | ||
![]() |
4cf7e63330 | ||
![]() |
18c0fc2a7f | ||
![]() |
e0de416ea5 | ||
![]() |
4fbe13aad4 | ||
![]() |
5e622ccf66 | ||
![]() |
016d61f385 | ||
![]() |
63fe870d48 | ||
![]() |
3d1dd06177 | ||
![]() |
45e7c46dd9 | ||
![]() |
33711f74f7 | ||
![]() |
14b5ab6fae | ||
![]() |
10d0e1a46b | ||
![]() |
64663983b8 | ||
![]() |
4b61b99217 | ||
![]() |
506d2414bb | ||
![]() |
6b54879282 | ||
![]() |
c14bc24455 | ||
![]() |
a0e000c886 | ||
![]() |
d7000768a5 | ||
![]() |
5811cb5a14 | ||
![]() |
bf8e7759f9 | ||
![]() |
048a4f306a | ||
![]() |
9ae8dc25df | ||
![]() |
29b265d8fd | ||
![]() |
95b138ed9b | ||
![]() |
ae8041e630 | ||
![]() |
4ac8854b99 | ||
![]() |
198e4d2a23 | ||
![]() |
2b34096af6 | ||
![]() |
798cc17cea | ||
![]() |
314e340a1d | ||
![]() |
21f3d1f5db | ||
![]() |
9d0bdff010 | ||
![]() |
038c3c64f4 | ||
![]() |
0412c7901d | ||
![]() |
128441e78b | ||
![]() |
a31ceea41b | ||
![]() |
088030cada | ||
![]() |
1bd7986823 | ||
![]() |
564c07c5a3 | ||
![]() |
a0067581d0 | ||
![]() |
1c0fd9b78b | ||
![]() |
cfbcdcdde8 | ||
![]() |
b31fed9890 | ||
![]() |
c631ce91a5 | ||
![]() |
0d13282265 | ||
![]() |
5976c8d267 | ||
![]() |
a62ee2004f | ||
![]() |
fce39a9ef5 | ||
![]() |
83d116e8fd | ||
![]() |
ff26554055 | ||
![]() |
0868e38ce6 | ||
![]() |
0594887e3b | ||
![]() |
9a88b4239f | ||
![]() |
9a6f5ef6b5 | ||
![]() |
8f4883fc00 | ||
![]() |
7567329859 | ||
![]() |
adfa33e287 | ||
![]() |
4653b55038 | ||
![]() |
6fdcf401ad | ||
![]() |
a0b5a0251d | ||
![]() |
cdfffa8b48 | ||
![]() |
1d52b7478d | ||
![]() |
a935f5c6a6 | ||
![]() |
2d7d3d9b2b | ||
![]() |
b434182df6 | ||
![]() |
ab7d0f2344 | ||
![]() |
555fb30261 | ||
![]() |
f67ab2baf2 | ||
![]() |
7f322fd7d9 | ||
![]() |
50358cddea | ||
![]() |
f9db6c2ec6 | ||
![]() |
1cba2948a6 | ||
![]() |
8891f98511 | ||
![]() |
ae432f8c36 | ||
![]() |
226f40d75a | ||
![]() |
81b6aa52b0 | ||
![]() |
b00b914bba | ||
![]() |
509139ef0f | ||
![]() |
f2a1640c01 | ||
![]() |
5e1809cacf | ||
![]() |
1172da23ff | ||
![]() |
d003fd2c37 | ||
![]() |
d0d4595887 | ||
![]() |
ceffac7a9c | ||
![]() |
f35016265a | ||
![]() |
2036bac167 | ||
![]() |
152e50770f | ||
![]() |
5ff4ee823d | ||
![]() |
b809d373d5 | ||
![]() |
574bcc8447 | ||
![]() |
70c8a12b83 | ||
![]() |
d8ed474b16 | ||
![]() |
1faa9c6a49 | ||
![]() |
ed3410033f | ||
![]() |
e5b3aa1cc6 | ||
![]() |
34bdbc0b86 | ||
![]() |
9d63ea9a10 | ||
![]() |
525bad2b34 | ||
![]() |
7e68390641 | ||
![]() |
9b6f78a7e8 | ||
![]() |
0f28c2b44c | ||
![]() |
71b00980d2 | ||
![]() |
68ec54ef85 | ||
![]() |
440b41ff9b | ||
![]() |
5505087802 | ||
![]() |
59db22c10d | ||
![]() |
0528f7cad8 | ||
![]() |
6f2e474428 | ||
![]() |
952b4fc4c9 | ||
![]() |
e69197c6e5 | ||
![]() |
02685c4567 | ||
![]() |
1feba63f85 | ||
![]() |
f5cf6f2a44 | ||
![]() |
c84eb82ae1 | ||
![]() |
5b47a5f0f6 | ||
![]() |
f4febac128 | ||
![]() |
f03da98305 | ||
![]() |
b4cc1493b2 | ||
![]() |
761940bd1c | ||
![]() |
64b5903e49 | ||
![]() |
e9be637f42 | ||
![]() |
5086aabaef | ||
![]() |
7917871f51 | ||
![]() |
e89fd80ec9 | ||
![]() |
bf6a42f693 | ||
![]() |
7b8a2a2721 | ||
![]() |
ad478d390e | ||
![]() |
46f6075a7a | ||
![]() |
02fb26e945 | ||
![]() |
220e9c78c1 | ||
![]() |
c770e03f38 | ||
![]() |
d47beb70d5 | ||
![]() |
e93e1c8ec3 | ||
![]() |
fae45f46c4 | ||
![]() |
7139e5fae6 | ||
![]() |
6c25136b85 | ||
![]() |
fb4dd3063b | ||
![]() |
b780ab33dc | ||
![]() |
ede8c0788e | ||
![]() |
0479dd265e | ||
![]() |
bacf32648f | ||
![]() |
ad16bef08a | ||
![]() |
023aab9d0c | ||
![]() |
5901bc393c | ||
![]() |
321eed027a | ||
![]() |
3932af397a | ||
![]() |
8aa25c31db | ||
![]() |
c3423bb9e5 | ||
![]() |
519d57c04f | ||
![]() |
be5cf2e834 | ||
![]() |
c5b3255f33 | ||
![]() |
b0b985dbdb | ||
![]() |
6531273ac0 | ||
![]() |
b61c8ec8da | ||
![]() |
fe5bdb882b | ||
![]() |
644ac5baa6 | ||
![]() |
da4aeac42b | ||
![]() |
e3b25b2e58 | ||
![]() |
bb5d03bd89 | ||
![]() |
119092fafa | ||
![]() |
d6e389cc43 | ||
![]() |
cff1153d24 | ||
![]() |
d8a816f868 | ||
![]() |
85e8b2781c | ||
![]() |
d32baa0dc4 | ||
![]() |
029aee8023 | ||
![]() |
56ce9dbe92 | ||
![]() |
4bc666c0f7 | ||
![]() |
8e534aac5e | ||
![]() |
f84e459d3d | ||
![]() |
b35212502b | ||
![]() |
966af33315 | ||
![]() |
19c2742aa3 | ||
![]() |
892f86ce3a | ||
![]() |
3226dea8cb | ||
![]() |
c0ee308dfe | ||
![]() |
0ae9bc5ac9 | ||
![]() |
926810ab43 | ||
![]() |
9af6412d92 | ||
![]() |
821524ec6e | ||
![]() |
ab29f808a9 | ||
![]() |
fea8cab737 | ||
![]() |
dfcce5c6b4 | ||
![]() |
d235beae88 | ||
![]() |
115c26b2d3 | ||
![]() |
33ff997200 | ||
![]() |
b87d995587 | ||
![]() |
dda788c58c | ||
![]() |
9ce33cc23a | ||
![]() |
e84ec30260 | ||
![]() |
134b189791 | ||
![]() |
ed83a2cb4c | ||
![]() |
0240efe33e | ||
![]() |
7870a150f0 | ||
![]() |
98773cc7d4 | ||
![]() |
f7d5701ad1 | ||
![]() |
55514d3f5a | ||
![]() |
2d2e0c4308 | ||
![]() |
6e7bd4b580 | ||
![]() |
c186b2bf06 | ||
![]() |
2089e613d3 | ||
![]() |
3ae2c24b5a | ||
![]() |
99f3f29c64 | ||
![]() |
7179f7e90d | ||
![]() |
a7eedcb4cc | ||
![]() |
69ee16c9f3 | ||
![]() |
86abfb2b0d | ||
![]() |
33d6b8e173 | ||
![]() |
084602be3a | ||
![]() |
8a09174def | ||
![]() |
979565e7b5 | ||
![]() |
17abab628d | ||
![]() |
cc73f82e80 | ||
![]() |
a11c3fdadc | ||
![]() |
df3aeed3b1 | ||
![]() |
3f04d93858 | ||
![]() |
171b4e7e40 | ||
![]() |
66f8b0c25d | ||
![]() |
0d2017133e | ||
![]() |
f342e26997 | ||
![]() |
ad7fab6731 | ||
![]() |
9194e15a4b | ||
![]() |
894e36665d | ||
![]() |
5eea86cbe7 | ||
![]() |
18809872f6 | ||
![]() |
c7c5e5b2b9 | ||
![]() |
a4db3f1df7 | ||
![]() |
f3f54bb216 | ||
![]() |
c8b4dbcf7c | ||
![]() |
08f68cb5d3 | ||
![]() |
e6efd53adb | ||
![]() |
587090636d | ||
![]() |
2694158c3c | ||
![]() |
f349de482f | ||
![]() |
aeca5c1e2c | ||
![]() |
99f4e4d9ab | ||
![]() |
d1bec2edb1 | ||
![]() |
b81f2863dd | ||
![]() |
52e04d340c | ||
![]() |
1903ea05ec | ||
![]() |
ccea384c1f | ||
![]() |
0b2a10fa1e | ||
![]() |
3592b6c4ad | ||
![]() |
a2d2141cec | ||
![]() |
db74270a8c | ||
![]() |
140f484b4b | ||
![]() |
1150babf87 | ||
![]() |
c71443a088 | ||
![]() |
87b09f95e9 | ||
![]() |
b0925088e8 | ||
![]() |
3b48f2e5ef | ||
![]() |
3506f69c28 | ||
![]() |
f4aa440608 | ||
![]() |
1e1863446a | ||
![]() |
fb32972f4d | ||
![]() |
ea90c57d23 | ||
![]() |
5baf3cca4c | ||
![]() |
28c915a0e2 | ||
![]() |
eecfbb7122 | ||
![]() |
b0cde785a1 | ||
![]() |
f810a03802 | ||
![]() |
13c7592b21 | ||
![]() |
6777f17e15 | ||
![]() |
a02f78c046 | ||
![]() |
5e76286ca7 | ||
![]() |
58be339972 | ||
![]() |
269493cd19 | ||
![]() |
c696b5fa9d | ||
![]() |
c9b1a3bf60 | ||
![]() |
b639476a22 | ||
![]() |
17a5a6c169 | ||
![]() |
5897a1cc2b | ||
![]() |
09ce384e4d | ||
![]() |
5d8fc1bcd4 | ||
![]() |
d6c649e6a9 | ||
![]() |
16bb9dfc29 | ||
![]() |
e450c6021c | ||
![]() |
839a4045f1 | ||
![]() |
0501ba6724 | ||
![]() |
a92c7b2fa4 | ||
![]() |
e30ca6839e | ||
![]() |
4c46cb27a0 | ||
![]() |
be72064b9c | ||
![]() |
be3630afaf | ||
![]() |
f5769ec4ae | ||
![]() |
d95720fa69 | ||
![]() |
6bd22ffa7e | ||
![]() |
834ae2dd6f | ||
![]() |
f7d0d05d5d | ||
![]() |
cb22f652ed | ||
![]() |
f4659a328e | ||
![]() |
044a8ea984 | ||
![]() |
75c740c6ba | ||
![]() |
02edf0ca9e | ||
![]() |
e88657c7eb | ||
![]() |
18011689dc | ||
![]() |
630633cf57 | ||
![]() |
e84704ed98 | ||
![]() |
cf9af9a7f9 | ||
![]() |
86d2bcafd2 | ||
![]() |
ede666b5cb | ||
![]() |
fd7db2fa9c | ||
![]() |
44936e71e8 | ||
![]() |
fbc6ab6a35 | ||
![]() |
296d63bd42 | ||
![]() |
0b2af47d41 | ||
![]() |
1fe8b164ea | ||
![]() |
4def6dd857 | ||
![]() |
7d537ddff4 | ||
![]() |
605c920c0c | ||
![]() |
70bdc6840d | ||
![]() |
5aedc09bd1 | ||
![]() |
cf3116a758 | ||
![]() |
6a44e75203 | ||
![]() |
0af156f189 | ||
![]() |
fe4d855dc7 | ||
![]() |
030093e913 | ||
![]() |
7df20df2de | ||
![]() |
8281cf6a3e | ||
![]() |
3ce5ceeef7 | ||
![]() |
4e682b7a3a | ||
![]() |
124698688d | ||
![]() |
38a4bad5b8 | ||
![]() |
c2da147046 | ||
![]() |
6359f725bf | ||
![]() |
35db935708 | ||
![]() |
4017185e76 | ||
![]() |
cc40246e60 | ||
![]() |
69ec47cbef | ||
![]() |
2f3b0f5808 | ||
![]() |
54fe947568 | ||
![]() |
6883a66b23 | ||
![]() |
43c04216c6 | ||
![]() |
7623b3ae18 | ||
![]() |
1080a68da2 | ||
![]() |
62df5965d7 | ||
![]() |
414e1dac33 | ||
![]() |
b63fdeb10b | ||
![]() |
2c65044f3a | ||
![]() |
8c44829d66 | ||
![]() |
d89ad6501a | ||
![]() |
95c7aec40b | ||
![]() |
3525b4008c | ||
![]() |
c8eced5470 | ||
![]() |
3b3c7680d7 | ||
![]() |
9738296f6b | ||
![]() |
787860605d | ||
![]() |
2a089bb7ba | ||
![]() |
7382a020b8 | ||
![]() |
0baedef7a1 | ||
![]() |
22a6853389 | ||
![]() |
122aac6aee | ||
![]() |
a356760315 | ||
![]() |
cc2ac4e4cd | ||
![]() |
12ea26b10d | ||
![]() |
c65f642074 | ||
![]() |
b1ac80248f | ||
![]() |
79a543a574 | ||
![]() |
d3a2fb17f1 | ||
![]() |
4fddb1930b | ||
![]() |
99567da897 | ||
![]() |
e61686c103 | ||
![]() |
4b98c899ca | ||
![]() |
2f9be1bac2 | ||
![]() |
03fa88cfc1 | ||
![]() |
98d57e28af | ||
![]() |
c67d0c042c | ||
![]() |
ac70c3e677 | ||
![]() |
65f62fcd5a | ||
![]() |
2bfa15aa49 | ||
![]() |
ba3bf69df0 | ||
![]() |
da7931291d | ||
![]() |
a0f22a592c | ||
![]() |
8266c37cfa | ||
![]() |
fac54841d1 | ||
![]() |
3b24e53cf9 | ||
![]() |
e565cdb583 | ||
![]() |
71af1f86e8 | ||
![]() |
52f4dbb2c7 | ||
![]() |
a0da377ba8 | ||
![]() |
1cb84507d0 | ||
![]() |
7c4e3fe97e | ||
![]() |
3691fe6189 | ||
![]() |
774a70784f | ||
![]() |
0d78815e33 | ||
![]() |
3429286c15 | ||
![]() |
6a8bf95ad9 | ||
![]() |
f5794c1f89 | ||
![]() |
083e4d7921 | ||
![]() |
497739e8ce | ||
![]() |
e435a9e250 | ||
![]() |
1ebe787519 | ||
![]() |
9e18ecab18 | ||
![]() |
31af587943 | ||
![]() |
612facb905 | ||
![]() |
6ccf11426e | ||
![]() |
c3d00ce846 | ||
![]() |
4d8ff41143 | ||
![]() |
874de60be3 | ||
![]() |
bb526bc00a | ||
![]() |
53cb19f4a9 | ||
![]() |
5776de3cfb | ||
![]() |
a0ad07ef31 | ||
![]() |
ac1f6a263d | ||
![]() |
91894b7e26 | ||
![]() |
938ba979ae | ||
![]() |
ecc0a69557 | ||
![]() |
c330e9d67d | ||
![]() |
2ff1af16d2 | ||
![]() |
05b9f87098 | ||
![]() |
5e34910151 | ||
![]() |
6ae0d124e1 | ||
![]() |
a0d9c25ffa | ||
![]() |
2ee13160d5 | ||
![]() |
0c75929a83 | ||
![]() |
a6a74e289d | ||
![]() |
309618b81b | ||
![]() |
c579dd5df3 | ||
![]() |
aa477f8bd6 | ||
![]() |
2f5a6e5146 | ||
![]() |
ddb73e857d | ||
![]() |
c225ef1987 | ||
![]() |
97f5240d53 | ||
![]() |
4363dc5b4b | ||
![]() |
5279f13d61 | ||
![]() |
084e0a527b | ||
![]() |
4b818e49a2 | ||
![]() |
88709e1222 | ||
![]() |
3e285e503d | ||
![]() |
91ab911893 | ||
![]() |
2ace07d7f8 | ||
![]() |
ae160a3798 | ||
![]() |
2e47903825 | ||
![]() |
b0ac6ce7c9 | ||
![]() |
054cebdb6e | ||
![]() |
8da05ab46d | ||
![]() |
e32b19e170 | ||
![]() |
e377b1e6ad | ||
![]() |
a37096bf4d | ||
![]() |
e3f25a03cd | ||
![]() |
c7d9ccd13c | ||
![]() |
f9fa526b04 | ||
![]() |
454281ac21 | ||
![]() |
85de5c7d96 | ||
![]() |
0f42574b7f | ||
![]() |
15a8848cca | ||
![]() |
a697701c5f | ||
![]() |
053ebe34f0 | ||
![]() |
fec90fd7f2 | ||
![]() |
02e9f78b88 | ||
![]() |
b91e932e65 | ||
![]() |
e6ecc0f454 | ||
![]() |
33aeba2a08 | ||
![]() |
8f1c3110a1 | ||
![]() |
4b77648607 | ||
![]() |
ff85d49088 | ||
![]() |
bc6700119f | ||
![]() |
ab1b828aed | ||
![]() |
7db1a7751d | ||
![]() |
7e1bdab520 | ||
![]() |
806b21093e | ||
![]() |
5988b75975 | ||
![]() |
beb643678d | ||
![]() |
cf8104bfb4 | ||
![]() |
a361bfb1da | ||
![]() |
eee1692277 | ||
![]() |
5848269c2e | ||
![]() |
c3a5e14a0a | ||
![]() |
c35091305e | ||
![]() |
df3be10717 | ||
![]() |
a553a13c03 | ||
![]() |
99851dd032 | ||
![]() |
fd57f581dd | ||
![]() |
1952f3b3c4 | ||
![]() |
a89853cf27 | ||
![]() |
95ca85a36c | ||
![]() |
4ba0300590 | ||
![]() |
bbcb57a32d | ||
![]() |
77568428a7 | ||
![]() |
b54592cf9c | ||
![]() |
e5f3849446 | ||
![]() |
1ed677f7b8 | ||
![]() |
96b5aede48 | ||
![]() |
3dedfbfd92 | ||
![]() |
14af4c07bf | ||
![]() |
99862facce | ||
![]() |
c69a41041b | ||
![]() |
a371631972 | ||
![]() |
021d8584a7 | ||
![]() |
4d39770602 | ||
![]() |
2c5d390fb1 | ||
![]() |
ece86f217b | ||
![]() |
fccc401a59 | ||
![]() |
54bfc4b012 | ||
![]() |
9ed6ca64e5 | ||
![]() |
828a634667 | ||
![]() |
ca0c8cd973 | ||
![]() |
049e7f0813 | ||
![]() |
c9c7f01a2f | ||
![]() |
214c72baeb | ||
![]() |
26a04b36a1 | ||
![]() |
a47ee38b1c | ||
![]() |
085db408df | ||
![]() |
9269a076b6 | ||
![]() |
979ad6f583 | ||
![]() |
5189122006 | ||
![]() |
5150a15ad4 | ||
![]() |
b4c3dac77f | ||
![]() |
f71c4068bc | ||
![]() |
68beb7d39a | ||
![]() |
a62b50691c | ||
![]() |
1498ca0dd6 | ||
![]() |
13998bbf95 | ||
![]() |
a20f8d5d8b | ||
![]() |
b8c89164cf | ||
![]() |
4c5631e73a | ||
![]() |
1fe3a523e9 | ||
![]() |
8f3acbe8c2 | ||
![]() |
00a99dd13b | ||
![]() |
d56e2163b0 | ||
![]() |
070388a42b | ||
![]() |
b81a551dc3 | ||
![]() |
f5f2bad308 | ||
![]() |
c02c47c762 | ||
![]() |
36a44ec6d8 | ||
![]() |
65cf690ab2 | ||
![]() |
0488121536 | ||
![]() |
a5eac62918 | ||
![]() |
bf9610894f | ||
![]() |
5e5a55f6af | ||
![]() |
7272ad91f3 | ||
![]() |
ca0550bb24 | ||
![]() |
8c99321df4 | ||
![]() |
02bc0949f1 | ||
![]() |
3fa8af9fe5 | ||
![]() |
84f6d12185 | ||
![]() |
099ac5ea2c | ||
![]() |
2d203bdab7 | ||
![]() |
f9e19ce6e9 | ||
![]() |
a684b1cddb | ||
![]() |
7dd80183a9 | ||
![]() |
838f1834a5 | ||
![]() |
b794236294 | ||
![]() |
0d4e1f5d03 | ||
![]() |
aa01578986 | ||
![]() |
c14e7b84ac | ||
![]() |
4b88c316ed | ||
![]() |
f27cbece71 | ||
![]() |
adafe85d77 | ||
![]() |
36104b73ca | ||
![]() |
6b12bc8ae2 | ||
![]() |
00f78373f6 | ||
![]() |
f31b03ce1d | ||
![]() |
e5dcb72597 | ||
![]() |
d99687517e | ||
![]() |
a3306f6717 | ||
![]() |
3bd0c4bdaa | ||
![]() |
b9955d5ff6 | ||
![]() |
8074032fad | ||
![]() |
82d53cbc3b | ||
![]() |
74c8637943 | ||
![]() |
bbbc071db0 | ||
![]() |
4a49f56910 | ||
![]() |
7e3f175267 | ||
![]() |
4413533065 | ||
![]() |
5dffd3df02 | ||
![]() |
53431bc4e7 | ||
![]() |
19c81e909d | ||
![]() |
6a27a4fd5e | ||
![]() |
dfa94d70f6 | ||
![]() |
17126df51c | ||
![]() |
80ac80c334 | ||
![]() |
12f6f1966a | ||
![]() |
7b12688c01 | ||
![]() |
33fc090614 | ||
![]() |
25638b5f06 | ||
![]() |
66e7e33efc | ||
![]() |
ff8d20f98f | ||
![]() |
98ead49338 | ||
![]() |
6e35209b61 | ||
![]() |
46d11f3530 | ||
![]() |
96e03caad1 | ||
![]() |
0684ab5f18 | ||
![]() |
65b6638609 | ||
![]() |
df80b9f92f | ||
![]() |
532169de34 | ||
![]() |
fbcbdafa49 | ||
![]() |
04e1375238 | ||
![]() |
5998029280 | ||
![]() |
26cc1bf288 | ||
![]() |
98956e8f1a | ||
![]() |
3a537c70db | ||
![]() |
005be82d71 | ||
![]() |
a45b66781c | ||
![]() |
f5fb3512cf | ||
![]() |
009c3e4170 | ||
![]() |
2d79b8c5d2 | ||
![]() |
d0a8599f76 | ||
![]() |
eb740abd39 | ||
![]() |
9539e26e70 | ||
![]() |
6b886a5b5a | ||
![]() |
1a49d1f3c2 | ||
![]() |
1872d81abd | ||
![]() |
37b3dd1e78 | ||
![]() |
94a985bdd4 | ||
![]() |
fd51bcf6e3 | ||
![]() |
6c01fb4710 | ||
![]() |
0f5a771a5c | ||
![]() |
19dfc08cbe | ||
![]() |
09e3b30448 | ||
![]() |
a18e4e57f4 | ||
![]() |
a48c2db53d | ||
![]() |
9b764f767d | ||
![]() |
ef86122f5c | ||
![]() |
8ad8ecd8c1 | ||
![]() |
76d51f0db9 | ||
![]() |
4043a319e8 | ||
![]() |
1edf98d846 | ||
![]() |
b2b8454aa9 | ||
![]() |
a15a237d65 | ||
![]() |
b2a8678cc9 | ||
![]() |
0b206e069d | ||
![]() |
6c598f084e | ||
![]() |
f87419e7b5 | ||
![]() |
ae089d050c | ||
![]() |
637a4d4d8d | ||
![]() |
2af142525c | ||
![]() |
c314744d0a | ||
![]() |
f4e23cbb8a | ||
![]() |
7c218cd168 | ||
![]() |
e4b7f78516 | ||
![]() |
19ddae2d15 | ||
![]() |
345e514a81 | ||
![]() |
2571d6f93b | ||
![]() |
ad64d089f3 | ||
![]() |
dcabcb4f86 | ||
![]() |
acf38cafae | ||
![]() |
ebbcac74fd | ||
![]() |
6207c314c6 | ||
![]() |
5e1d001056 | ||
![]() |
7ae9f8fa0a | ||
![]() |
5919035616 | ||
![]() |
af28f4b0ad | ||
![]() |
90d3288090 | ||
![]() |
becaad4a98 | ||
![]() |
b3268ece01 | ||
![]() |
15449819ff | ||
![]() |
ee021e37cc | ||
![]() |
ec3adb5bb1 | ||
![]() |
d75b64595a | ||
![]() |
36442a372a | ||
![]() |
b292c200bf | ||
![]() |
fd220337f4 | ||
![]() |
217d209844 | ||
![]() |
60492aea78 | ||
![]() |
d8b7b7edf8 | ||
![]() |
094ae59fc9 | ||
![]() |
328ce031b5 | ||
![]() |
0e6ae5fee7 | ||
![]() |
cf9f49b422 | ||
![]() |
69c1f16f7e | ||
![]() |
7572e31a31 | ||
![]() |
59364d235d | ||
![]() |
d74c366dbf | ||
![]() |
549731fc6f | ||
![]() |
63b6cede5f | ||
![]() |
1fa4d3f4e9 | ||
![]() |
e7829b41e7 | ||
![]() |
371c52e277 | ||
![]() |
d1dbd2ccff | ||
![]() |
5f81b40e7d | ||
![]() |
24528e0a6e | ||
![]() |
cece0fefb3 | ||
![]() |
ad59096227 | ||
![]() |
5bc925107f | ||
![]() |
4505fa4138 | ||
![]() |
bf604df0c5 | ||
![]() |
516eb3b054 | ||
![]() |
765fab2af2 | ||
![]() |
d37184d6bd | ||
![]() |
4de86c6407 | ||
![]() |
c3e014f7ba | ||
![]() |
92a93c5111 | ||
![]() |
bd41a1c782 | ||
![]() |
687c9f7eb0 | ||
![]() |
03be0a586f | ||
![]() |
225f0a2f50 | ||
![]() |
f211a497f3 | ||
![]() |
a13cdb6974 | ||
![]() |
ad8d3bb1c8 | ||
![]() |
102862775a | ||
![]() |
3c79f3d34d | ||
![]() |
d9395fdbca | ||
![]() |
7a63b73aa4 | ||
![]() |
7201ac45c1 | ||
![]() |
cf34a98aa0 | ||
![]() |
faed14ca01 | ||
![]() |
541f1e7026 | ||
![]() |
a896823330 | ||
![]() |
1d8459ac99 | ||
![]() |
68437198ed | ||
![]() |
e00ee359f4 | ||
![]() |
69ea6f3bc2 | ||
![]() |
b8e52e8063 | ||
![]() |
3d32cca9cf | ||
![]() |
8fbe7e3d38 | ||
![]() |
9414122816 | ||
![]() |
fbd0507ce5 | ||
![]() |
c6b2f8c017 | ||
![]() |
3569f4b550 | ||
![]() |
f7cf1052e8 | ||
![]() |
5f4e540618 | ||
![]() |
b43db98e84 | ||
![]() |
8d792d3f88 | ||
![]() |
eed708fa03 | ||
![]() |
6aa47ec105 | ||
![]() |
429e50c625 | ||
![]() |
9025b6271c | ||
![]() |
b8166b998e | ||
![]() |
b84494f4e2 | ||
![]() |
a85cdbabf8 | ||
![]() |
0198a436f9 | ||
![]() |
8890b1894c | ||
![]() |
be9e24420f | ||
![]() |
54ad0928b1 | ||
![]() |
685ef39119 | ||
![]() |
d8e44fcba2 | ||
![]() |
9c4c4f05a7 | ||
![]() |
a6d575fde7 | ||
![]() |
c05264af39 | ||
![]() |
89f6158149 | ||
![]() |
a9258d48d3 | ||
![]() |
561a202bff | ||
![]() |
5fa9fd2dfe | ||
![]() |
2f252aad38 | ||
![]() |
3c31e55b13 | ||
![]() |
37d415b53a | ||
![]() |
66ccdbddd4 | ||
![]() |
5511736b0c | ||
![]() |
64ac233782 | ||
![]() |
5116f5642d | ||
![]() |
124198e6e9 | ||
![]() |
792e045e45 | ||
![]() |
ecf6b748af | ||
![]() |
ad00305ff5 | ||
![]() |
8f2359febc | ||
![]() |
95f290f113 | ||
![]() |
48aaef5072 | ||
![]() |
99b07e0e18 | ||
![]() |
999cf733f7 | ||
![]() |
24e4ebd77a | ||
![]() |
3ad59fb84c | ||
![]() |
d1ceca3998 | ||
![]() |
fcd7e94c1b | ||
![]() |
5114900b1b | ||
![]() |
0a11584a7a | ||
![]() |
68d35eafc1 | ||
![]() |
45bf70c84c | ||
![]() |
956a9e9336 | ||
![]() |
68253c0df5 | ||
![]() |
45b6a08980 | ||
![]() |
28f6ac150e | ||
![]() |
495e6d65e7 | ||
![]() |
d8415c0bee | ||
![]() |
4fb3456e93 | ||
![]() |
f7cf9832b9 | ||
![]() |
333ed64b74 | ||
![]() |
06d8f55e9b | ||
![]() |
2b7aeb9eb2 | ||
![]() |
dd1a087f5a | ||
![]() |
0ce9416c33 | ||
![]() |
8d3d7a9c69 | ||
![]() |
b688923c7e | ||
![]() |
b72fe87cf9 | ||
![]() |
944d1fdc17 | ||
![]() |
da081040a4 | ||
![]() |
9b5f4ce2b2 | ||
![]() |
c519fdfa17 | ||
![]() |
b5ad889029 | ||
![]() |
4f3100e1fa | ||
![]() |
d42fbb1521 | ||
![]() |
e5967ada0f | ||
![]() |
f56e2128e0 | ||
![]() |
0ba8429ca5 | ||
![]() |
bec1eef10f | ||
![]() |
5990f119f1 | ||
![]() |
d953c6bdd4 | ||
![]() |
0a7245ce11 | ||
![]() |
3732b2ce6b | ||
![]() |
3787439e1c | ||
![]() |
f9dc705051 | ||
![]() |
8ab9e8f89d | ||
![]() |
0b72de6347 | ||
![]() |
e9bfb25dfe | ||
![]() |
8035039247 | ||
![]() |
89c154861c | ||
![]() |
5aa7b7451d | ||
![]() |
3ff20dee4a | ||
![]() |
15cbade984 | ||
![]() |
fb02b481e2 | ||
![]() |
2e41ce2157 | ||
![]() |
92fc8aaad8 | ||
![]() |
2808f39432 | ||
![]() |
c2b5bb7234 | ||
![]() |
d3b873369f | ||
![]() |
6cdf697e8e | ||
![]() |
4d61a9d4f9 | ||
![]() |
cc8b4c913e | ||
![]() |
5d99669917 | ||
![]() |
d627d44ad0 | ||
![]() |
d293fd0220 | ||
![]() |
b156cb1d89 | ||
![]() |
a5172509ec | ||
![]() |
54baf08f77 | ||
![]() |
f462ca3243 | ||
![]() |
056e2d7dd5 | ||
![]() |
5242cbde99 | ||
![]() |
ecc56d643a | ||
![]() |
7c6ecd4b7e | ||
![]() |
e1bbcb338d | ||
![]() |
3c46709ead | ||
![]() |
e2062ce5d4 | ||
![]() |
03d2cfafbf | ||
![]() |
e72a8cff49 | ||
![]() |
4d6e34b054 | ||
![]() |
59369f20ec | ||
![]() |
7efe5aefb2 | ||
![]() |
1863625daf | ||
![]() |
b7c108ee20 | ||
![]() |
f0f876803e | ||
![]() |
259c8b4e58 | ||
![]() |
3901b7d4bb | ||
![]() |
b5e34f3aab | ||
![]() |
12969853ad | ||
![]() |
4ca6886fcf | ||
![]() |
b89bd8d799 | ||
![]() |
3a229763d0 | ||
![]() |
381256a71a | ||
![]() |
34d850dfcc | ||
![]() |
f6c3bc4319 | ||
![]() |
e46f669091 | ||
![]() |
730f3e3a7e | ||
![]() |
da12508a46 | ||
![]() |
10e170a730 | ||
![]() |
8f156e4a15 | ||
![]() |
80de996665 | ||
![]() |
1f6f4080f4 | ||
![]() |
3c95aac838 | ||
![]() |
b1c19a7fdc | ||
![]() |
9d6f305b7c | ||
![]() |
c06481a95e | ||
![]() |
74ec705f5c | ||
![]() |
684939314b | ||
![]() |
5abf89444a | ||
![]() |
c00256dea6 | ||
![]() |
17e4ac978a | ||
![]() |
b4c090762d | ||
![]() |
e0d2fe5bd2 | ||
![]() |
ce5da9a1b3 | ||
![]() |
842351548b | ||
![]() |
5c22d7b115 | ||
![]() |
1e6811243f | ||
![]() |
5210ac431c | ||
![]() |
dd3535ee04 | ||
![]() |
e5848e99c4 | ||
![]() |
1a7fe5eede | ||
![]() |
186b887415 | ||
![]() |
a92b7fb43c | ||
![]() |
be1b5e1ab4 | ||
![]() |
897b4dbce9 | ||
![]() |
c72b1711be | ||
![]() |
a05bbfc8dc | ||
![]() |
c12237fb9b | ||
![]() |
80a5599168 | ||
![]() |
d10c4ea4da | ||
![]() |
9ae0650c00 | ||
![]() |
abbc372e53 | ||
![]() |
82acb84b5f | ||
![]() |
d9c5057d76 | ||
![]() |
2bb3141b12 | ||
![]() |
2baff3a09a | ||
![]() |
b007e8e06a | ||
![]() |
9479a9584f | ||
![]() |
4c6a4ddd1c | ||
![]() |
06e9f682ff | ||
![]() |
308de4a63c | ||
![]() |
ce28db3274 | ||
![]() |
c1c31540cc | ||
![]() |
8f2a03ac58 | ||
![]() |
302a7be52e | ||
![]() |
bbb53631a9 | ||
![]() |
c98cb5eec8 | ||
![]() |
2fde60eceb | ||
![]() |
d642eb8b92 | ||
![]() |
dc969422b5 | ||
![]() |
16ce28027c | ||
![]() |
28294fcf6e | ||
![]() |
2954ba78d2 | ||
![]() |
912a77a7f4 | ||
![]() |
32b17fd6b7 | ||
![]() |
2e8d16b086 | ||
![]() |
c0bef25590 | ||
![]() |
025f5427c4 | ||
![]() |
425ff5a973 | ||
![]() |
eef4ad5c46 | ||
![]() |
8549e1ba58 | ||
![]() |
3c1daa3030 | ||
![]() |
894d73f00e | ||
![]() |
54fe34314d | ||
![]() |
f6ba67d8ea | ||
![]() |
a680dbbb5e | ||
![]() |
8378c84816 | ||
![]() |
497c067e80 | ||
![]() |
e25a3033a5 | ||
![]() |
caf91309f7 | ||
![]() |
9f897d4fa1 | ||
![]() |
59162042b0 | ||
![]() |
9a7f941ae4 | ||
![]() |
ea7d1e0d08 | ||
![]() |
e7b64e154e | ||
![]() |
2df0892682 | ||
![]() |
a16eda8645 | ||
![]() |
4a5b0b815e | ||
![]() |
33ea12228c | ||
![]() |
6bd8d01856 | ||
![]() |
7a25791d53 | ||
![]() |
0785819dd5 | ||
![]() |
1c649c976d | ||
![]() |
9f50470bf6 | ||
![]() |
dfd80a9bcb | ||
![]() |
395ccda7b9 | ||
![]() |
22a6905e2e | ||
![]() |
10afc8cc71 | ||
![]() |
645d2883d9 | ||
![]() |
44a8a13998 | ||
![]() |
be42124d5c | ||
![]() |
a212f29bd3 | ||
![]() |
dff4a3fa2d | ||
![]() |
f044dde054 | ||
![]() |
10f5363335 | ||
![]() |
b4c7bf4808 | ||
![]() |
f69658d136 | ||
![]() |
6d88cb49ec | ||
![]() |
f24adf753e | ||
![]() |
d9000113a9 | ||
![]() |
c4379c0e67 | ||
![]() |
816724457c | ||
![]() |
87887e4163 | ||
![]() |
e8b54bd0ec | ||
![]() |
ff316c1ac1 | ||
![]() |
fa27861ce0 | ||
![]() |
931a6f81d6 | ||
![]() |
36132df4be | ||
![]() |
0342c79c17 | ||
![]() |
83a7bd8d69 | ||
![]() |
2d53037847 | ||
![]() |
bcd8b48e70 | ||
![]() |
c22604a726 | ||
![]() |
9466d973c6 | ||
![]() |
a22ed46ae0 | ||
![]() |
2a842a1e14 | ||
![]() |
020c642761 | ||
![]() |
96a3f35926 | ||
![]() |
323e63f84e | ||
![]() |
0d011b876e | ||
![]() |
517f1407f4 | ||
![]() |
7d7fa93b1e | ||
![]() |
6dd4a7c29e | ||
![]() |
ebae8cffb9 | ||
![]() |
d0f91c8550 | ||
![]() |
1a89dd9f8c | ||
![]() |
c8f4f6e884 | ||
![]() |
77ba124509 | ||
![]() |
17f2eb9f9b | ||
![]() |
91e673a4ad | ||
![]() |
fd27725699 | ||
![]() |
0b70cf8666 | ||
![]() |
8cc468702f | ||
![]() |
cb3d715b90 | ||
![]() |
6b0fda196c | ||
![]() |
083bd40092 | ||
![]() |
930dc297c1 | ||
![]() |
ba75a51b71 | ||
![]() |
f5df957866 | ||
![]() |
69ec28a7f2 | ||
![]() |
d789fd6b5a | ||
![]() |
bdc54ef318 | ||
![]() |
2c438e414d | ||
![]() |
e27f56c8a3 | ||
![]() |
3b8bc08d4e | ||
![]() |
e86389e543 | ||
![]() |
16d8d26f9a | ||
![]() |
8dc0133c0b | ||
![]() |
a430d3c5a1 | ||
![]() |
f80feb743e | ||
![]() |
de3f2910fc | ||
![]() |
b5caab6649 | ||
![]() |
f15ef3f634 | ||
![]() |
e0ec9c687b | ||
![]() |
cc1d9b7436 | ||
![]() |
1a10238db6 | ||
![]() |
efff31b338 | ||
![]() |
541a4cc5e3 | ||
![]() |
2c20844eaa | ||
![]() |
bf064ecc1d | ||
![]() |
884c5ea1b3 | ||
![]() |
7d76e2e43c | ||
![]() |
d7451579ee | ||
![]() |
501fc48c68 | ||
![]() |
656eb37ff2 | ||
![]() |
6e230da5a1 | ||
![]() |
c23e0233cf | ||
![]() |
544fc0f646 | ||
![]() |
872a5b6d89 | ||
![]() |
b9bdeca99b | ||
![]() |
bf19af99cb | ||
![]() |
82e226d124 | ||
![]() |
0e66cf3099 | ||
![]() |
c59ecfb7a0 | ||
![]() |
c7c9700d93 | ||
![]() |
3be749ee99 | ||
![]() |
708dfd2eac | ||
![]() |
9ca37ca528 | ||
![]() |
02abace127 | ||
![]() |
aaed3a970b | ||
![]() |
4b2b97a6e2 | ||
![]() |
1b63898413 | ||
![]() |
f632fc77ea | ||
![]() |
e5f621c442 | ||
![]() |
ad5dbd74c1 | ||
![]() |
1181b0bca2 | ||
![]() |
e362071505 | ||
![]() |
bd86ef6fc2 | ||
![]() |
fcf5fe632c | ||
![]() |
44e09063cd | ||
![]() |
db55c7bf28 | ||
![]() |
fbbecd33bb | ||
![]() |
d5dde1610f | ||
![]() |
dd108ff70f | ||
![]() |
1eb334a618 | ||
![]() |
060ab8fbfe | ||
![]() |
cb7ecb6443 | ||
![]() |
0125036596 | ||
![]() |
35344a0449 | ||
![]() |
ff08f58ac4 | ||
![]() |
ba6d1dcd29 | ||
![]() |
94e08b74ce | ||
![]() |
df8eadbca9 | ||
![]() |
a938e736fa | ||
![]() |
57908dbeef | ||
![]() |
2fdc4c23b5 | ||
![]() |
1722767eba | ||
![]() |
b2a5dd3efc | ||
![]() |
2e1410bc0f | ||
![]() |
4dcefe7ba1 | ||
![]() |
51e2f3b476 | ||
![]() |
66da935519 | ||
![]() |
88b348fa5f | ||
![]() |
d2970e54fb | ||
![]() |
6a5e1f3a6e | ||
![]() |
d93a356ec2 | ||
![]() |
43a5677397 | ||
![]() |
d43d666ec6 | ||
![]() |
fd4b3c79a8 | ||
![]() |
9aadcad892 | ||
![]() |
427b81a79a | ||
![]() |
36f86dbf32 | ||
![]() |
4874b03a16 | ||
![]() |
75f785d1ef | ||
![]() |
a2c4876a81 | ||
![]() |
a08457e406 | ||
![]() |
0974e0e1ea | ||
![]() |
e8d1389d33 | ||
![]() |
75becc8425 | ||
![]() |
1db33e4f93 | ||
![]() |
1b442afe7b | ||
![]() |
5b31408fa7 | ||
![]() |
4a479f8fdb | ||
![]() |
de2cb88616 | ||
![]() |
29756dcb88 | ||
![]() |
c5d1c36ea8 | ||
![]() |
fb0e52c7c3 | ||
![]() |
4e1dc0041d | ||
![]() |
a07f83fe89 | ||
![]() |
12467969ec | ||
![]() |
ebf599349c | ||
![]() |
4157000168 | ||
![]() |
bdc9a129ef | ||
![]() |
55cc7f1c77 | ||
![]() |
24d50045ac | ||
![]() |
8105463791 | ||
![]() |
bb386d0fdf | ||
![]() |
828abb0558 | ||
![]() |
a7c3cf2f5c | ||
![]() |
1dfbda7be4 | ||
![]() |
eada08272b | ||
![]() |
d7e44cb887 | ||
![]() |
c9efa27d4a | ||
![]() |
048bf592ef | ||
![]() |
d1b86f4bf3 | ||
![]() |
866bcebdd9 | ||
![]() |
3b84305a6b | ||
![]() |
bcf5e14b31 | ||
![]() |
f4e2257072 | ||
![]() |
de4823c139 | ||
![]() |
f8365b4e35 | ||
![]() |
7d6340ca5b | ||
![]() |
50d3195845 | ||
![]() |
6c4855b977 | ||
![]() |
41fd7a68e4 | ||
![]() |
3f5a5bf2ab | ||
![]() |
42a5de98be | ||
![]() |
d90786e26d | ||
![]() |
fb6ee8a897 | ||
![]() |
2918b62320 | ||
![]() |
81cd630029 | ||
![]() |
231e569329 | ||
![]() |
6150c9c41b | ||
![]() |
a928c8c441 | ||
![]() |
284a7f0b1a | ||
![]() |
b10bbf71e6 | ||
![]() |
ea099f1cd4 | ||
![]() |
3d66cbf5dd | ||
![]() |
96cdc102dd | ||
![]() |
b7e53fb170 | ||
![]() |
ad9ba37c05 | ||
![]() |
8133f3cbbe | ||
![]() |
dab8900e22 | ||
![]() |
b288e45021 | ||
![]() |
c4c6e05b4e | ||
![]() |
41217f61e6 | ||
![]() |
314f843002 | ||
![]() |
3db6615568 | ||
![]() |
f07605486b | ||
![]() |
062310dcc3 | ||
![]() |
34e4489bf9 | ||
![]() |
1d5f1f83be | ||
![]() |
f9a5dc6c91 | ||
![]() |
c992b74a15 | ||
![]() |
84607ff5f4 | ||
![]() |
395228f1f0 | ||
![]() |
e024409219 | ||
![]() |
593e261ce4 | ||
![]() |
f2b9df4e22 | ||
![]() |
7d4d1bd97d | ||
![]() |
161261cfea | ||
![]() |
4fc9845d4b | ||
![]() |
4b878eeeda | ||
![]() |
1f1cae12e4 | ||
![]() |
c003f40a65 | ||
![]() |
840b29e989 | ||
![]() |
ca42fd9365 | ||
![]() |
25ebf61b75 | ||
![]() |
b8547e83cd | ||
![]() |
33a4ec80ea | ||
![]() |
c187066303 | ||
![]() |
ff82a36e6c | ||
![]() |
2c3ce3b1db | ||
![]() |
cdbbe8f78d | ||
![]() |
d69eed8fd4 | ||
![]() |
1b9a90f975 | ||
![]() |
f9c09b5777 | ||
![]() |
3897647321 | ||
![]() |
c73df2527c | ||
![]() |
880091a96d | ||
![]() |
8309435011 | ||
![]() |
3df4930e8a | ||
![]() |
c44904642f | ||
![]() |
1f80ee0395 | ||
![]() |
c92fde7f89 | ||
![]() |
b87005de0c | ||
![]() |
96382cf498 | ||
![]() |
cc16db56d1 | ||
![]() |
108d2d7c3f | ||
![]() |
3ae97c0417 | ||
![]() |
b75bc1a440 | ||
![]() |
0c875a1063 | ||
![]() |
2bc7485cda | ||
![]() |
1e79886145 | ||
![]() |
b2c5babf3f | ||
![]() |
da81a19d74 | ||
![]() |
104ed4eedd | ||
![]() |
37cdbfa868 | ||
![]() |
d6f0cdb50b | ||
![]() |
29236e4a75 | ||
![]() |
1293495904 | ||
![]() |
8b92a097dd | ||
![]() |
b3a6a2d0d8 | ||
![]() |
975c3915da | ||
![]() |
17af2422a1 | ||
![]() |
aeea73dee2 | ||
![]() |
54f20c308c | ||
![]() |
13494cda4c | ||
![]() |
3cfeb8a65c | ||
![]() |
8847c8c2c2 | ||
![]() |
2442902dac | ||
![]() |
f760b204f7 | ||
![]() |
869ec26966 | ||
![]() |
028baf6781 | ||
![]() |
c70e4a66bd | ||
![]() |
65b55a5189 | ||
![]() |
71da4f29ca | ||
![]() |
7247922d55 | ||
![]() |
0740eef9e1 | ||
![]() |
81900fb8db | ||
![]() |
941c1dd5cf | ||
![]() |
a0121ae7b6 | ||
![]() |
acf26ea821 | ||
![]() |
c50ee8281f | ||
![]() |
7f8eec42e0 | ||
![]() |
93f5f2cc31 | ||
![]() |
9f45d19036 | ||
![]() |
b86bac759e | ||
![]() |
feffc57a3d | ||
![]() |
8fe9df75ef | ||
![]() |
90987d821f | ||
![]() |
630b067b18 | ||
![]() |
4704943c5c | ||
![]() |
cd18216879 | ||
![]() |
cc5605b431 | ||
![]() |
843a568544 | ||
![]() |
ea6de498e3 | ||
![]() |
cc854415e2 | ||
![]() |
9561db50a8 | ||
![]() |
14ea9674c4 | ||
![]() |
38b8e44ec4 | ||
![]() |
7c4f021f8c | ||
![]() |
f7a296e1f1 | ||
![]() |
a627510edf | ||
![]() |
70dee584ed | ||
![]() |
c7927e9e52 | ||
![]() |
b44da1c701 | ||
![]() |
1f6951c0e0 | ||
![]() |
74cd30cdbc | ||
![]() |
6c37e21a22 | ||
![]() |
c3329527db | ||
![]() |
554738d6fe | ||
![]() |
0c7490cbaf | ||
![]() |
ede2d67b92 | ||
![]() |
fb373bc9a0 | ||
![]() |
d88373bf43 | ||
![]() |
19f3faa1f2 | ||
![]() |
fcbebf90f3 | ||
![]() |
2745c4d5f2 | ||
![]() |
1408c2e628 | ||
![]() |
2eb5604287 | ||
![]() |
304a0d46bf | ||
![]() |
28ca814411 | ||
![]() |
384a0a67c5 | ||
![]() |
12acb2d561 | ||
![]() |
9f1986960c | ||
![]() |
fb173e18af | ||
![]() |
68d10d4779 | ||
![]() |
ddda3f6e8c | ||
![]() |
e205ac761d | ||
![]() |
eee51a863a | ||
![]() |
c5236f812e | ||
![]() |
e21286e4d7 | ||
![]() |
9180d4f5c3 | ||
![]() |
2ba78b8516 | ||
![]() |
9c103acb94 | ||
![]() |
1ea0674e63 | ||
![]() |
0cb41efa06 | ||
![]() |
28e5c03582 | ||
![]() |
6b7cb7bd38 | ||
![]() |
4d236cb520 | ||
![]() |
2cd2453658 | ||
![]() |
f72b80fc04 | ||
![]() |
96d24a3e2e | ||
![]() |
8da46a56d8 | ||
![]() |
7e3ffc8863 | ||
![]() |
e34b7d2ea4 | ||
![]() |
c84a94075e | ||
![]() |
68562321a8 | ||
![]() |
bd65594260 | ||
![]() |
ef27f6458a | ||
![]() |
4a7d18962d | ||
![]() |
ae91553ca3 | ||
![]() |
79a1571dd6 | ||
![]() |
c04c54eb26 | ||
![]() |
b5e63c11ab | ||
![]() |
beeb3bc123 | ||
![]() |
8fb945a461 | ||
![]() |
a5ff2b184c | ||
![]() |
2ba31f1301 | ||
![]() |
5b83a9da3d | ||
![]() |
9937b8fc7e | ||
![]() |
9bfe135577 | ||
![]() |
88cce592ad | ||
![]() |
e97e0e6631 | ||
![]() |
bb8e583764 | ||
![]() |
79a2416419 | ||
![]() |
ff077ef371 | ||
![]() |
09f168406c | ||
![]() |
2a5514a6b3 | ||
![]() |
71a00177d9 | ||
![]() |
f3726fefb9 | ||
![]() |
d046e72369 | ||
![]() |
e6fe646a7f | ||
![]() |
bea246a3fe | ||
![]() |
0f9e6e751f | ||
![]() |
44a4c0109b | ||
![]() |
5d3008a5af | ||
![]() |
cf6006cc83 | ||
![]() |
b42cfa3ccd | ||
![]() |
38f9f0f4fd | ||
![]() |
5a4a05f9af | ||
![]() |
0a0450075a | ||
![]() |
c087226796 | ||
![]() |
5408f1c105 | ||
![]() |
03bb7d3cff | ||
![]() |
17e414d32b | ||
![]() |
445c4a1cdb | ||
![]() |
4179b166bb | ||
![]() |
be0e0e40d6 | ||
![]() |
c2a484742b | ||
![]() |
7a075074af | ||
![]() |
8f341bbf7c | ||
![]() |
0b31857a56 | ||
![]() |
33806a391e | ||
![]() |
f97798391f | ||
![]() |
083837fc58 | ||
![]() |
1671d8d826 | ||
![]() |
c954502c4f | ||
![]() |
c8d409e1db | ||
![]() |
ae7c60e2bd | ||
![]() |
f70a7ad949 | ||
![]() |
c000715b22 | ||
![]() |
d4a9ea1f6c | ||
![]() |
70f93bfadc | ||
![]() |
03be8522c9 | ||
![]() |
90d5877950 | ||
![]() |
c4cb1e6f99 | ||
![]() |
5ace3bb568 | ||
![]() |
cbfd5691d3 | ||
![]() |
894b5c0ebd | ||
![]() |
447c8dea75 | ||
![]() |
be8164cc2a | ||
![]() |
d7b8149b0a | ||
![]() |
20cfa8a5cd | ||
![]() |
03d26283c6 | ||
![]() |
8202aafae0 | ||
![]() |
0e8a2868e8 | ||
![]() |
9353305926 | ||
![]() |
3d1c0c1a95 | ||
![]() |
9f86daa822 | ||
![]() |
57dab3fd30 | ||
![]() |
166bf5b1ae | ||
![]() |
e605c58d4d | ||
![]() |
416c255f65 | ||
![]() |
1342dace9c | ||
![]() |
266d869f6a | ||
![]() |
031d0f295f | ||
![]() |
a73868cb27 | ||
![]() |
7a78a1dcec | ||
![]() |
46e3c97d24 | ||
![]() |
6e77812382 | ||
![]() |
d851e04471 | ||
![]() |
02f767df3c | ||
![]() |
60122df809 | ||
![]() |
a710f05bb4 | ||
![]() |
2223951fa1 | ||
![]() |
a48c4a7cc1 | ||
![]() |
49a233d75f | ||
![]() |
fe11db70ea | ||
![]() |
90b4263a2f | ||
![]() |
6c9baf2261 | ||
![]() |
c1a943df39 | ||
![]() |
52d07ecd39 | ||
![]() |
c5bbc591e6 | ||
![]() |
9741e155f0 | ||
![]() |
a4551151b2 | ||
![]() |
d683bf63b3 | ||
![]() |
a0aa2be86d | ||
![]() |
a4145d20ff | ||
![]() |
066023ca14 | ||
![]() |
4a7fa3a650 | ||
![]() |
5c140ea38d | ||
![]() |
2f0453b64c | ||
![]() |
79bb3e164f | ||
![]() |
5229ed33f6 | ||
![]() |
b2e04a0dd0 | ||
![]() |
9f2fc7c3ea | ||
![]() |
56e45a60a4 | ||
![]() |
b5a4dfde5d | ||
![]() |
3c6b2d5c20 | ||
![]() |
9344c8a067 | ||
![]() |
ef644e4801 | ||
![]() |
e5d548c642 | ||
![]() |
46f135892a | ||
![]() |
4e5513e973 | ||
![]() |
60f54a47a2 | ||
![]() |
703db6ed14 | ||
![]() |
5655067f28 | ||
![]() |
848c40f7da | ||
![]() |
094f57b601 | ||
![]() |
f4eda8c8d1 | ||
![]() |
f6582991da | ||
![]() |
516e8a14c0 | ||
![]() |
c3725900cd | ||
![]() |
2de51e65f0 | ||
![]() |
569c3cde98 | ||
![]() |
4ca3b5b310 | ||
![]() |
5e8076b330 | ||
![]() |
07ffd586fa | ||
![]() |
246cf2cc92 | ||
![]() |
9b30c4bc50 | ||
![]() |
a07981dc59 | ||
![]() |
1b71f893bb | ||
![]() |
3027c15757 | ||
![]() |
d0a775d685 | ||
![]() |
10ae31aa44 | ||
![]() |
b4b9746361 | ||
![]() |
a3c77f82d1 | ||
![]() |
481bf583af | ||
![]() |
13d0a13086 | ||
![]() |
b33ed75737 | ||
![]() |
9e43670b44 | ||
![]() |
37610732da | ||
![]() |
0037dea0d0 | ||
![]() |
f6f226ba28 | ||
![]() |
d156833a61 | ||
![]() |
93e9f0bd14 | ||
![]() |
125cac5928 | ||
![]() |
34ef556a74 | ||
![]() |
d5ab35a444 | ||
![]() |
d511771bdf | ||
![]() |
eda28e507e | ||
![]() |
04cb04bdb4 | ||
![]() |
cb38637e6b | ||
![]() |
ec37be2b7e | ||
![]() |
3ec96b9e24 | ||
![]() |
1d1222e1b3 | ||
![]() |
59a2e9617f | ||
![]() |
f5ff92e1e8 | ||
![]() |
1a793007c9 | ||
![]() |
6d548af064 | ||
![]() |
15e27bf93e | ||
![]() |
ae03c602f2 | ||
![]() |
5e8a2d3fe7 | ||
![]() |
7f1a1fc9fa | ||
![]() |
f7cf70b5d3 | ||
![]() |
837aae73ac | ||
![]() |
bf4a1159ff | ||
![]() |
e4b4012024 | ||
![]() |
af0217594d | ||
![]() |
ad15549fb1 | ||
![]() |
5cd0e366b3 | ||
![]() |
96e01f7a7b | ||
![]() |
f355dbf1d2 | ||
![]() |
00dd90f185 | ||
![]() |
5146de872a | ||
![]() |
0e0b73521c | ||
![]() |
5ccbf1bf8e | ||
![]() |
8e5494f03f | ||
![]() |
12ba3f21b3 | ||
![]() |
7ecb1b6022 | ||
![]() |
1593b1b767 | ||
![]() |
a1bad27ede | ||
![]() |
6df02311c4 | ||
![]() |
f736db4f5d | ||
![]() |
b7e6d6e658 | ||
![]() |
f14c5e7979 | ||
![]() |
5ce826c227 | ||
![]() |
bf1f9dc799 | ||
![]() |
fbaf3cce03 | ||
![]() |
aeb5299ca5 | ||
![]() |
487e7f2fa6 | ||
![]() |
238bed1251 | ||
![]() |
50733f5dd5 | ||
![]() |
c2bae15e72 | ||
![]() |
0fe115e8f9 | ||
![]() |
d7b9098925 | ||
![]() |
9a18326aeb | ||
![]() |
a4d6b4e5ce | ||
![]() |
29f19b9378 | ||
![]() |
bb6983bf66 | ||
![]() |
099b279c63 | ||
![]() |
af963c49e6 | ||
![]() |
ceaa512f31 | ||
![]() |
41fb43fb6b | ||
![]() |
07c267ad20 | ||
![]() |
f6ac4daa81 | ||
![]() |
910b0323f8 | ||
![]() |
ecb2e32b1e | ||
![]() |
4e6fc02b34 | ||
![]() |
ee975dea84 | ||
![]() |
da4b92e610 | ||
![]() |
ca632bd2cc | ||
![]() |
9acc733522 | ||
![]() |
688d8fa7e8 | ||
![]() |
fbf72b7677 | ||
![]() |
0d49450096 | ||
![]() |
9cd0f786b0 | ||
![]() |
d9834a9abb | ||
![]() |
d5f8a3688d | ||
![]() |
8a5311b1e6 | ||
![]() |
cae754d66c | ||
![]() |
5a2cad077f | ||
![]() |
0eb51f7302 | ||
![]() |
cafb485d83 | ||
![]() |
b1d2769d33 | ||
![]() |
2a4e5096ac | ||
![]() |
f7352feb6e | ||
![]() |
ab08bc92d9 | ||
![]() |
0a28d92e6d | ||
![]() |
7bf82e3666 | ||
![]() |
ed24a201a9 | ||
![]() |
933e88e5fa | ||
![]() |
4a3ad1a7d0 | ||
![]() |
5620632787 | ||
![]() |
c89b2aa261 | ||
![]() |
11e8eb8309 | ||
![]() |
0861991cfb | ||
![]() |
35a1a778a7 | ||
![]() |
d7237776ba | ||
![]() |
7b503e2336 | ||
![]() |
5afb63e052 | ||
![]() |
2622b008ab | ||
![]() |
4f01690cac | ||
![]() |
d6de957f4e | ||
![]() |
1d04f7c022 | ||
![]() |
387eb29e7e | ||
![]() |
84fca03cda | ||
![]() |
20aff26784 | ||
![]() |
47eac14f03 | ||
![]() |
d975eeafb7 | ||
![]() |
61a7533136 | ||
![]() |
0c9bf8ae1d | ||
![]() |
d9f0d08498 | ||
![]() |
70478f6ce1 | ||
![]() |
01c617d94e | ||
![]() |
fab6e9fe85 | ||
![]() |
dd1c368c1b | ||
![]() |
dba0d0c1ae | ||
![]() |
c0a13bbb78 | ||
![]() |
c1b9eefa28 | ||
![]() |
134e66aa79 | ||
![]() |
8a4277c486 | ||
![]() |
c92ed0170f | ||
![]() |
f21843bf55 | ||
![]() |
fd3813f66e | ||
![]() |
597e15160a | ||
![]() |
ca504e5421 | ||
![]() |
9140aa25b8 | ||
![]() |
fe94013a22 | ||
![]() |
d8ef855d5d | ||
![]() |
082e067338 | ||
![]() |
91833ac57c | ||
![]() |
94ed738515 | ||
![]() |
4e81888daf | ||
![]() |
9c69f87690 | ||
![]() |
2f78c387db | ||
![]() |
8354a879cf | ||
![]() |
45cc71f4d5 | ||
![]() |
220f694b12 | ||
![]() |
5a811962c3 | ||
![]() |
e83e62fc24 | ||
![]() |
51a2bf2f02 | ||
![]() |
189f719720 | ||
![]() |
872ce78ff0 | ||
![]() |
f71d893766 | ||
![]() |
243eb20f61 | ||
![]() |
506d49c82a | ||
![]() |
c5928b2b17 | ||
![]() |
95407be197 | ||
![]() |
26f9b0514f | ||
![]() |
896a71308f | ||
![]() |
86a271cd7c | ||
![]() |
e18a9bcb50 | ||
![]() |
08413bdc97 | ||
![]() |
a519919779 | ||
![]() |
545388b3b2 | ||
![]() |
343f186ef7 | ||
![]() |
236a18f935 | ||
![]() |
6bc51cd906 | ||
![]() |
bbde91cf9d | ||
![]() |
76bab10919 | ||
![]() |
20f24837d3 | ||
![]() |
22ef8ea417 | ||
![]() |
b9aaf610ad | ||
![]() |
c6c24b706c | ||
![]() |
8dbb0e212e | ||
![]() |
79cc28776b | ||
![]() |
ad7e147712 | ||
![]() |
cd103dd9b8 | ||
![]() |
1bac47df88 | ||
![]() |
2ef62e5363 | ||
![]() |
eb5aed99f6 | ||
![]() |
aea0af0597 | ||
![]() |
74fd7b6265 | ||
![]() |
d6ad3e61d7 | ||
![]() |
5bc95a5466 | ||
![]() |
fddd25a5fe | ||
![]() |
dd249e6224 | ||
![]() |
e71fb3b485 | ||
![]() |
7a828ea882 | ||
![]() |
d95b82fd40 | ||
![]() |
ee80acf4d2 | ||
![]() |
210108bd8f | ||
![]() |
5174225178 | ||
![]() |
8e6058b623 | ||
![]() |
3ef9f4a070 | ||
![]() |
56b535b2a1 | ||
![]() |
ffce3b8fce | ||
![]() |
f058db4ba4 | ||
![]() |
a808eca796 | ||
![]() |
f38982d575 | ||
![]() |
25f1f34b90 | ||
![]() |
8f6ff694ad | ||
![]() |
40ff6a0c08 | ||
![]() |
8a4e77a290 | ||
![]() |
dfb7d7b3dd | ||
![]() |
307442e654 | ||
![]() |
2f7be976d1 | ||
![]() |
dafe24f4e3 | ||
![]() |
b34e45b6da | ||
![]() |
c9f7831f1b | ||
![]() |
1f6fe3da42 | ||
![]() |
5c39985888 | ||
![]() |
f0f8c3630e | ||
![]() |
f0d38001b3 | ||
![]() |
ae4216776d | ||
![]() |
ce420b076e | ||
![]() |
ac508c46ce | ||
![]() |
9546d12643 | ||
![]() |
1f25b62008 | ||
![]() |
6641356d41 | ||
![]() |
2870d2619a | ||
![]() |
324bbde92a | ||
![]() |
969a1a462f | ||
![]() |
f2742f1ba1 | ||
![]() |
2edb923548 | ||
![]() |
1456577f11 | ||
![]() |
6c013143a5 | ||
![]() |
7c64d1cd6f | ||
![]() |
fc8660b740 | ||
![]() |
1dfd10793b | ||
![]() |
77e8639b71 | ||
![]() |
3fb8431aa8 | ||
![]() |
3175431187 | ||
![]() |
5c63fa26dc | ||
![]() |
5bfae3f4eb | ||
![]() |
c04be545f2 | ||
![]() |
7b6ff02cbb | ||
![]() |
e2187f33ff | ||
![]() |
82daa0e755 | ||
![]() |
ce6125e752 | ||
![]() |
434bd4247c | ||
![]() |
7e562f3fb3 | ||
![]() |
3d8856b29a | ||
![]() |
4b20b2dbe0 | ||
![]() |
cf589c9ecd | ||
![]() |
d009f96be0 | ||
![]() |
2dacc45f74 | ||
![]() |
8d26cc6ecb | ||
![]() |
acfda46bce | ||
![]() |
3e78b8380e | ||
![]() |
de233e2824 | ||
![]() |
bee081ff4f | ||
![]() |
e466b86c3e | ||
![]() |
a2b37be9bf | ||
![]() |
33a87bd6ea | ||
![]() |
23c95b0940 | ||
![]() |
d20fde1e57 | ||
![]() |
eeb3d80525 | ||
![]() |
4a30022eed | ||
![]() |
862a7ec5b0 | ||
![]() |
861c4ab57a | ||
![]() |
267dcacb99 | ||
![]() |
8cd60eea36 | ||
![]() |
ca1065f646 | ||
![]() |
7e9f0b2d02 | ||
![]() |
8c50de6308 | ||
![]() |
6065d6d1fd | ||
![]() |
97bbdefcb1 | ||
![]() |
737be9815b | ||
![]() |
f90d1a64ce | ||
![]() |
4d9317fef0 | ||
![]() |
c2211d458d | ||
![]() |
857b8683cc | ||
![]() |
f1ba16ebfe | ||
![]() |
8c2c0a58a0 | ||
![]() |
8eacb5b5ac | ||
![]() |
58abb9e63b | ||
![]() |
b8df8fb997 | ||
![]() |
f27a6a4ab6 | ||
![]() |
2a2897dc9e | ||
![]() |
a9060954c7 | ||
![]() |
4d1b0b4427 | ||
![]() |
22940a3d91 | ||
![]() |
9aba10398e | ||
![]() |
f3c5bcebf2 | ||
![]() |
ff3ca30e31 | ||
![]() |
a7c18ca198 | ||
![]() |
1503df2b04 | ||
![]() |
37f831a212 | ||
![]() |
6f5e007a78 | ||
![]() |
cade228a6c | ||
![]() |
a3195aa911 | ||
![]() |
9cc798a32f | ||
![]() |
e739d2fa16 | ||
![]() |
f310eca462 | ||
![]() |
14809cd451 | ||
![]() |
7685e99ac0 | ||
![]() |
473e051231 | ||
![]() |
1b66069c02 | ||
![]() |
77bb761b0b | ||
![]() |
e9fb849d5f | ||
![]() |
eb69a8813f | ||
![]() |
7c7294b750 | ||
![]() |
478d49e312 | ||
![]() |
4befb44146 | ||
![]() |
a62f9d75a7 | ||
![]() |
8449785e8b | ||
![]() |
f34c6437ba | ||
![]() |
3e25f5f8df | ||
![]() |
4392f108ed | ||
![]() |
fe66f4089d | ||
![]() |
b15ee60c1b | ||
![]() |
f9770cf1ba | ||
![]() |
9efd7904ac | ||
![]() |
8a6c5c3a31 | ||
![]() |
6756f80e3e | ||
![]() |
1686cb7b30 | ||
![]() |
29b7d26f9f | ||
![]() |
92c073e6d0 | ||
![]() |
826ed49c7c | ||
![]() |
becb033dc9 | ||
![]() |
ec39732a05 | ||
![]() |
edd67b8748 | ||
![]() |
276af1415a | ||
![]() |
e860920d8a | ||
![]() |
0dbffaae7d | ||
![]() |
d053014d6b | ||
![]() |
3fb1ce9f9a | ||
![]() |
49f5fe6611 | ||
![]() |
3dbc4bd49d | ||
![]() |
af7eecea3e | ||
![]() |
cab04b3a56 | ||
![]() |
eb908d120d | ||
![]() |
227f705122 | ||
![]() |
d5efe26f89 | ||
![]() |
46d2dee63f | ||
![]() |
ef96ed124e | ||
![]() |
2fac865b3b | ||
![]() |
7e0417f672 | ||
![]() |
46006dcd56 | ||
![]() |
fc69491dfe | ||
![]() |
1fe3e54f95 | ||
![]() |
420d6cf733 | ||
![]() |
baf8ba43a0 | ||
![]() |
ad2a8585b2 | ||
![]() |
18ec8f46dd | ||
![]() |
8f673da7c9 | ||
![]() |
d34288b6e8 | ||
![]() |
cb064f2037 | ||
![]() |
2afea71557 | ||
![]() |
306f996017 | ||
![]() |
9b5af77a22 | ||
![]() |
e099ae90b0 | ||
![]() |
dcfb774fe7 | ||
![]() |
375cff8d0d | ||
![]() |
4db2eba6d6 | ||
![]() |
7e8e504b41 | ||
![]() |
db3dda0b0f | ||
![]() |
bd131cd34b | ||
![]() |
a39b2fd982 | ||
![]() |
39a64ea039 | ||
![]() |
9a0a5c6710 | ||
![]() |
02f0d346d2 | ||
![]() |
c3fd36cfba | ||
![]() |
c522301d29 | ||
![]() |
c93f0c6a99 | ||
![]() |
bc97c10a53 | ||
![]() |
947bf42b7c | ||
![]() |
41779305d5 | ||
![]() |
9b6e730395 | ||
![]() |
1b77f9633c | ||
![]() |
bde9053f04 | ||
![]() |
cc66b1fc62 | ||
![]() |
d1823297ce | ||
![]() |
6ee836c587 | ||
![]() |
144ae5a787 | ||
![]() |
f5224c4980 | ||
![]() |
a9d9649d8b | ||
![]() |
98c7a8418d | ||
![]() |
17a5d049c0 | ||
![]() |
8adc05a174 | ||
![]() |
fbc48a7b4b | ||
![]() |
ee9945bcce | ||
![]() |
e79e64be67 | ||
![]() |
dbb38eddfd | ||
![]() |
c66f93537d | ||
![]() |
3d03471b1c | ||
![]() |
5e4ce32ab5 | ||
![]() |
a01e6cad69 | ||
![]() |
562fecd950 | ||
![]() |
76fb700884 | ||
![]() |
41b4eef72c | ||
![]() |
a99cef87b4 | ||
![]() |
6dc9916bd6 | ||
![]() |
a12c4bb161 | ||
![]() |
28594f3eea | ||
![]() |
a919b943a8 | ||
![]() |
2743a95b41 | ||
![]() |
876b271dca | ||
![]() |
1dffbaf0aa | ||
![]() |
2669f881fc | ||
![]() |
53d91e3218 | ||
![]() |
857cae3e39 | ||
![]() |
5d7772be94 | ||
![]() |
749c83d068 | ||
![]() |
0c22f3f4de | ||
![]() |
0f07bf467a | ||
![]() |
94f31f8491 | ||
![]() |
a5b323d1d9 | ||
![]() |
38687bd74d | ||
![]() |
49ce47c3ee | ||
![]() |
65702909ea | ||
![]() |
3e6d0528b2 | ||
![]() |
aa99a7b79c | ||
![]() |
9789ad30ff | ||
![]() |
f8206c7ca1 | ||
![]() |
4480911e0b | ||
![]() |
e0b1b35fb3 | ||
![]() |
91d1aabd32 | ||
![]() |
47580a0367 | ||
![]() |
72773ac569 | ||
![]() |
d27c39114f | ||
![]() |
5195dd8936 | ||
![]() |
acccfd9f92 | ||
![]() |
8732a84422 | ||
![]() |
44366398eb | ||
![]() |
1ac380e85c | ||
![]() |
5cd865c630 | ||
![]() |
9155d305a2 | ||
![]() |
83a490575c | ||
![]() |
4b333d4d70 | ||
![]() |
ab9f5998ab | ||
![]() |
a4d528e519 | ||
![]() |
4da268edc0 | ||
![]() |
34f4d7ce0d | ||
![]() |
25e3b075ee | ||
![]() |
f1288e4bb8 | ||
![]() |
7b089d0a16 | ||
![]() |
1e8dcae6f5 | ||
![]() |
633703ddea | ||
![]() |
9593e4b5db | ||
![]() |
afd761855e | ||
![]() |
c655416a91 | ||
![]() |
2f799b7b73 | ||
![]() |
46da83430f | ||
![]() |
95bbf46e77 | ||
![]() |
89ef0fcd42 | ||
![]() |
fec442ca99 | ||
![]() |
491f292182 | ||
![]() |
19798bf31c | ||
![]() |
a64f33e385 | ||
![]() |
758b300591 | ||
![]() |
a561a9afc2 | ||
![]() |
24f376a4b8 | ||
![]() |
64868f41be | ||
![]() |
6347146900 | ||
![]() |
b738b6bf35 | ||
![]() |
67d237c23f | ||
![]() |
7ffd1a09a7 | ||
![]() |
ca59f65da8 | ||
![]() |
4ccb1ee0b9 | ||
![]() |
c6b5b2cae3 | ||
![]() |
c6c23ff0d9 | ||
![]() |
99916aefaa | ||
![]() |
b8ab180f0b | ||
![]() |
7159f86780 | ||
![]() |
5d1de4f4ea | ||
![]() |
4633fcd4d6 | ||
![]() |
2bba5f72fa | ||
![]() |
623a520362 | ||
![]() |
e0f64520d0 | ||
![]() |
e212c807fc | ||
![]() |
41cb2878d5 | ||
![]() |
b59cb3ed60 | ||
![]() |
78803f8ea8 | ||
![]() |
54a8b7b572 | ||
![]() |
ffa2e5d7eb | ||
![]() |
6f45b32fe3 | ||
![]() |
e0877e3381 | ||
![]() |
c3f6bbe3ab | ||
![]() |
d10b9ba79c | ||
![]() |
24c57b80dd | ||
![]() |
eafe5b54a8 | ||
![]() |
76a3bf23b5 | ||
![]() |
61e6bb248f | ||
![]() |
758ef42f9c | ||
![]() |
6f196007c3 | ||
![]() |
f4b918075a | ||
![]() |
1bbb05d6e0 | ||
![]() |
80fb6701b5 | ||
![]() |
12a635794c | ||
![]() |
6cc5f61e07 | ||
![]() |
4dbce43cf7 | ||
![]() |
d97815af18 | ||
![]() |
6a2b6b6e03 | ||
![]() |
d4970273ad | ||
![]() |
1b76da8559 | ||
![]() |
669e3db719 | ||
![]() |
f68a41ce9f | ||
![]() |
fd08a6b506 | ||
![]() |
2e6debc2a0 | ||
![]() |
e5bb63c7ab | ||
![]() |
81d86b5fe6 | ||
![]() |
ac5532a65c | ||
![]() |
58dd60a52e | ||
![]() |
f693d55caf | ||
![]() |
3ec25bb543 | ||
![]() |
5be25cde4b | ||
![]() |
deda92fcff | ||
![]() |
a54c464522 | ||
![]() |
82e7d3bd2c | ||
![]() |
bbd27eb036 | ||
![]() |
7bb25729e4 | ||
![]() |
ba094790ac | ||
![]() |
17942d4126 | ||
![]() |
42a5a387da | ||
![]() |
4e74a800c3 | ||
![]() |
bb4819a46b | ||
![]() |
fb94a1cb48 | ||
![]() |
4347f88af0 | ||
![]() |
8a43d75e2d | ||
![]() |
ac4f49191d | ||
![]() |
c1de5d6e43 | ||
![]() |
35dcdcb223 | ||
![]() |
71708022a0 | ||
![]() |
470edfa320 | ||
![]() |
e48f1278da | ||
![]() |
58632205e3 | ||
![]() |
9c65573bc3 | ||
![]() |
19b22f5179 | ||
![]() |
a95344879c | ||
![]() |
279b193b68 | ||
![]() |
23759b009b | ||
![]() |
729d088719 | ||
![]() |
aada4d9e9b | ||
![]() |
1abc929583 | ||
![]() |
2bf610875b | ||
![]() |
f86836d629 | ||
![]() |
bcfdc7897f | ||
![]() |
ede765ae3c | ||
![]() |
334948d61e | ||
![]() |
30a954cac8 | ||
![]() |
43d22014c7 | ||
![]() |
4fdb2df061 | ||
![]() |
7895383d88 | ||
![]() |
4bece31f56 | ||
![]() |
b3a356d7f8 | ||
![]() |
385ceda61f | ||
![]() |
4559b69ec5 | ||
![]() |
691a231d99 | ||
![]() |
784e5fb0c7 | ||
![]() |
9b465d9588 | ||
![]() |
feceb1c056 | ||
![]() |
315c7b0945 | ||
![]() |
fe3fdd5c6c | ||
![]() |
8beb5ea860 | ||
![]() |
795f7d37e6 | ||
![]() |
a9bb284297 | ||
![]() |
1b2ad98714 | ||
![]() |
8d7f305aa2 | ||
![]() |
ae43b42888 | ||
![]() |
8128e6ba57 | ||
![]() |
77de880dfb | ||
![]() |
c82ccb5995 | ||
![]() |
1135c032e0 | ||
![]() |
05904a2569 | ||
![]() |
08f570bde8 | ||
![]() |
a16cb0d32f | ||
![]() |
0b3cbb255e | ||
![]() |
1c6fb941f5 | ||
![]() |
2151036d04 | ||
![]() |
dc6e9b2268 | ||
![]() |
1dba11cde6 | ||
![]() |
89b1484d1d | ||
![]() |
47d8d678d2 | ||
![]() |
fd6641747b | ||
![]() |
10589404f8 | ||
![]() |
df9270d9e3 | ||
![]() |
f2f0b1fa7a | ||
![]() |
1b2627fec6 | ||
![]() |
ae56718e75 | ||
![]() |
e0391e256a | ||
![]() |
e717e260fd | ||
![]() |
d9e8bd6177 | ||
![]() |
b91d18d24f | ||
![]() |
db67c54cca | ||
![]() |
c3c8ae6b43 | ||
![]() |
81997f0dc7 | ||
![]() |
d233d96faf | ||
![]() |
fcb46d5dff | ||
![]() |
94ffd5a26f | ||
![]() |
62512f0525 | ||
![]() |
08584e680f | ||
![]() |
b37db5d494 | ||
![]() |
6c9741fdc3 | ||
![]() |
a239ad8ab7 | ||
![]() |
c91db24b90 | ||
![]() |
ad82fcdb63 | ||
![]() |
c0c10dec34 | ||
![]() |
c3614ab1d1 | ||
![]() |
bb5885eca4 | ||
![]() |
d23d19b2a6 | ||
![]() |
6ae0f0b466 | ||
![]() |
642eef39e8 | ||
![]() |
0b60c2d1c0 | ||
![]() |
d168ede78b | ||
![]() |
6138c2ac24 | ||
![]() |
1a62194a20 | ||
![]() |
3ab5842ebb | ||
![]() |
dcf41a7759 | ||
![]() |
b2856f7f71 | ||
![]() |
e695792641 | ||
![]() |
b58842a5f6 | ||
![]() |
d9d0fe7e23 | ||
![]() |
ec306e3e72 | ||
![]() |
f5e58f6091 | ||
![]() |
cfa9da5a2c | ||
![]() |
bc05e4494d | ||
![]() |
0d47375092 | ||
![]() |
32fcd258c6 | ||
![]() |
091a25d461 | ||
![]() |
3e0c45c2df | ||
![]() |
9d62f7cd6c | ||
![]() |
beb8d9cbf2 | ||
![]() |
fc89c865f9 | ||
![]() |
85675b8000 | ||
![]() |
7d30541781 | ||
![]() |
5b987e14e8 | ||
![]() |
83747b7aea | ||
![]() |
fbb17636d8 | ||
![]() |
bd224d90de | ||
![]() |
2cfc9829e1 | ||
![]() |
43464fd6ff | ||
![]() |
d86b6a4a65 | ||
![]() |
5e148d9384 | ||
![]() |
f9a6672122 | ||
![]() |
e58038f480 | ||
![]() |
e3666e68ed | ||
![]() |
05023bab1d | ||
![]() |
33f795350d | ||
![]() |
7403fc86ae | ||
![]() |
29561eca10 | ||
![]() |
4f4ceab2cc | ||
![]() |
b0834faa69 | ||
![]() |
b5d712a332 | ||
![]() |
04a2accfe9 | ||
![]() |
47ff447f8e | ||
![]() |
ef8f26fb97 | ||
![]() |
f48e794eeb | ||
![]() |
3012df4775 | ||
![]() |
15ddcaf873 | ||
![]() |
23eb096e54 | ||
![]() |
dd609408fe | ||
![]() |
0dbfa6247e | ||
![]() |
0a858ecef3 | ||
![]() |
eae3cbc563 | ||
![]() |
129701e9f0 | ||
![]() |
fdaa7f1435 | ||
![]() |
b8bf804835 | ||
![]() |
a9c9452857 | ||
![]() |
049f7fd400 | ||
![]() |
f95ab6e13e | ||
![]() |
5eb8704596 | ||
![]() |
7620ea1752 | ||
![]() |
aad6e05538 | ||
![]() |
dddf150f92 | ||
![]() |
49ec3e83f1 | ||
![]() |
4d107567e6 | ||
![]() |
0e34fa657b | ||
![]() |
af4e765ca8 | ||
![]() |
dde2365103 | ||
![]() |
f700579255 | ||
![]() |
f1395f49fa | ||
![]() |
289ef56f5c | ||
![]() |
7dd067b0e9 | ||
![]() |
83c74684f4 | ||
![]() |
9753ea93db | ||
![]() |
72a985bb09 | ||
![]() |
1090388395 | ||
![]() |
b6ca3b4491 | ||
![]() |
29f952d73e | ||
![]() |
675c970041 | ||
![]() |
7d4294f691 | ||
![]() |
3ecea46857 | ||
![]() |
c8997e8ca5 | ||
![]() |
77fe23b53e | ||
![]() |
f68c87b0ed | ||
![]() |
65e5c26390 | ||
![]() |
19047b6963 | ||
![]() |
df718e3fdb | ||
![]() |
e56cd5dcc6 | ||
![]() |
4f248387fb | ||
![]() |
743e215fc1 | ||
![]() |
8f33b110fa | ||
![]() |
98d00848c9 | ||
![]() |
7680838829 | ||
![]() |
d56d3bd94b | ||
![]() |
71506625ac | ||
![]() |
b803e3e1a7 | ||
![]() |
89820c60bb | ||
![]() |
e4c2450eba | ||
![]() |
cda05697b9 | ||
![]() |
13090eeb47 | ||
![]() |
79589a3c8d | ||
![]() |
4306f25d3f | ||
![]() |
c33514f85e | ||
![]() |
d776d1ec77 | ||
![]() |
278b7c5aab | ||
![]() |
967c1cbd4b | ||
![]() |
3278f5bc33 | ||
![]() |
d23c31a9ec | ||
![]() |
fff9670a81 | ||
![]() |
6e418ce643 | ||
![]() |
6041e2c910 | ||
![]() |
79291a0d34 | ||
![]() |
a673514f84 | ||
![]() |
7f312f088f | ||
![]() |
12537a43c2 | ||
![]() |
ea1cb4eede | ||
![]() |
179ea1638e | ||
![]() |
931aa0fba6 | ||
![]() |
b1bea85ce5 | ||
![]() |
539ccf43a8 | ||
![]() |
4443ef663c | ||
![]() |
4fc90e39a8 | ||
![]() |
84fe9c3646 | ||
![]() |
626dcf9517 | ||
![]() |
e8c9a91a92 | ||
![]() |
3dbc5ff272 | ||
![]() |
e14893ec89 | ||
![]() |
099ef1dde8 | ||
![]() |
fb070d8e95 | ||
![]() |
c74c77d125 | ||
![]() |
169e2ba670 | ||
![]() |
9003977c00 | ||
![]() |
4c824876b4 | ||
![]() |
1c6f30d061 | ||
![]() |
f261e64b10 | ||
![]() |
0d4b9c766b | ||
![]() |
d984bdc8b1 | ||
![]() |
6a1701e80a | ||
![]() |
2b580e0af4 | ||
![]() |
9ce4d1267e | ||
![]() |
f155e261d4 | ||
![]() |
8e37e0e39d | ||
![]() |
6a2fd88d40 | ||
![]() |
04c2202a3d | ||
![]() |
160b71644d | ||
![]() |
44b74ae268 | ||
![]() |
fe64881c83 | ||
![]() |
57f2f96191 | ||
![]() |
e4ea40c970 | ||
![]() |
5421c60e7f | ||
![]() |
9975981094 | ||
![]() |
35f1cb6a3a | ||
![]() |
731aac5820 | ||
![]() |
103fdd746c | ||
![]() |
1ba4497682 | ||
![]() |
21d6e6e5bd | ||
![]() |
3f90b8c437 | ||
![]() |
d33f959752 | ||
![]() |
c953d155ab | ||
![]() |
71e15945c1 | ||
![]() |
f8ba61ccc6 | ||
![]() |
f10a529e67 | ||
![]() |
dc35b8e053 | ||
![]() |
cd48030cad | ||
![]() |
4033e4da85 | ||
![]() |
7bad5b0184 | ||
![]() |
2e6d115043 | ||
![]() |
3d4646ae7e | ||
![]() |
5fc06d67aa | ||
![]() |
7b1bc1ad50 | ||
![]() |
767f474dc2 | ||
![]() |
750ed80dd8 | ||
![]() |
e777c8ba79 | ||
![]() |
0a72fa10bf | ||
![]() |
13a1381228 | ||
![]() |
1bb1afcf2e | ||
![]() |
85c57aaf19 | ||
![]() |
61668e6708 | ||
![]() |
1d55380169 | ||
![]() |
4c71c06dc8 | ||
![]() |
db699ab7d4 | ||
![]() |
3a5d48e6c9 | ||
![]() |
73132e37cf | ||
![]() |
f63347297d | ||
![]() |
c988b4fafe | ||
![]() |
3469d37bce | ||
![]() |
c1fdc85c9e | ||
![]() |
8ba59f407d | ||
![]() |
adce206d66 | ||
![]() |
f8738a7292 | ||
![]() |
491c4138f0 | ||
![]() |
bed9ae695d | ||
![]() |
0b1024ab75 | ||
![]() |
9f0de4f567 | ||
![]() |
8070256523 | ||
![]() |
c5d3d36599 | ||
![]() |
22245de7d5 | ||
![]() |
d74f90c91a | ||
![]() |
9943a520d2 | ||
![]() |
184b403df3 | ||
![]() |
aafdb891b2 | ||
![]() |
76f32a4d49 | ||
![]() |
3d1cce5b4c | ||
![]() |
cd10e9f95d | ||
![]() |
2c7f2587da | ||
![]() |
864d91109f | ||
![]() |
7b8f0db2c1 | ||
![]() |
300f30be1c | ||
![]() |
5817e4d27f | ||
![]() |
b23299f198 | ||
![]() |
f46fcadd85 | ||
![]() |
0d14c46a1b | ||
![]() |
5d232c2018 | ||
![]() |
661f143f0c | ||
![]() |
1598306eb5 | ||
![]() |
c4c9e5bb37 | ||
![]() |
d5cc730d3d | ||
![]() |
8e98605a74 | ||
![]() |
b5a407d5fe | ||
![]() |
13143cb526 | ||
![]() |
e29be7c6d4 | ||
![]() |
439343ab9d | ||
![]() |
66cd88f4d8 | ||
![]() |
81934efb39 | ||
![]() |
caf3d70c30 | ||
![]() |
1d46d63fdc | ||
![]() |
39521386c2 | ||
![]() |
bf45817677 | ||
![]() |
89c18acf9e | ||
![]() |
d0ab4da50f | ||
![]() |
96f17cea41 | ||
![]() |
3a9e4d89b3 | ||
![]() |
cf710b2774 | ||
![]() |
3cf77cdb4e | ||
![]() |
355a847b1c | ||
![]() |
f938ba81ec | ||
![]() |
4cb1973003 | ||
![]() |
2a3bedd560 | ||
![]() |
1e6ef87f17 | ||
![]() |
86dbfdf83f | ||
![]() |
a5d16d7a22 | ||
![]() |
4d04ff5d79 | ||
![]() |
edcb6cc949 | ||
![]() |
ef030eee95 | ||
![]() |
2874bbef15 | ||
![]() |
d3c9e03ff2 | ||
![]() |
3871ef5e28 | ||
![]() |
c759df5ad6 | ||
![]() |
c4757b7762 | ||
![]() |
a6b5efcc0c | ||
![]() |
e72b3bd4e2 | ||
![]() |
7bbfc2ca0c | ||
![]() |
81d65273be | ||
![]() |
995c43bef8 | ||
![]() |
f0fd05444d | ||
![]() |
76856185dc | ||
![]() |
4b3f78e533 | ||
![]() |
d84934bb5d | ||
![]() |
c5cd54c2a8 | ||
![]() |
bd4d3b5706 | ||
![]() |
fd782aa0a3 | ||
![]() |
e6dcb55382 | ||
![]() |
a16ab7969d | ||
![]() |
c7d116afd7 | ||
![]() |
6abb732be4 | ||
![]() |
66cf6e9b45 | ||
![]() |
67e18b6fde | ||
![]() |
d1a0e14e08 | ||
![]() |
9bd42ec1a1 | ||
![]() |
177e908dba | ||
![]() |
b23b570043 | ||
![]() |
cde45fca76 | ||
![]() |
03f28404ab | ||
![]() |
87206c5028 | ||
![]() |
81e1eb32f7 | ||
![]() |
e89e54c316 | ||
![]() |
2fe282b7a0 | ||
![]() |
faf0079c45 | ||
![]() |
2c26a5c8d8 | ||
![]() |
e56219e036 | ||
![]() |
44dd198ba6 | ||
![]() |
9799182409 | ||
![]() |
24405877dd | ||
![]() |
bf982e8d77 | ||
![]() |
52faa5c047 | ||
![]() |
5b5e4ef97e | ||
![]() |
fd0fe1c86c | ||
![]() |
76c19324f1 | ||
![]() |
e135485250 | ||
![]() |
12571dda75 | ||
![]() |
7190066a8d | ||
![]() |
7960d7f8b1 | ||
![]() |
9deb312199 | ||
![]() |
b65facc132 | ||
![]() |
ce2b104f50 | ||
![]() |
1afc5355e2 | ||
![]() |
92d78576f0 | ||
![]() |
4993302210 | ||
![]() |
a1abb834b4 | ||
![]() |
26473a3cc7 | ||
![]() |
adf3e309da | ||
![]() |
dde92cd1ee | ||
![]() |
53b91fe2b5 | ||
![]() |
c363997568 | ||
![]() |
47fc9fe74d | ||
![]() |
38a987744f | ||
![]() |
931f927584 | ||
![]() |
2f04a06e3b | ||
![]() |
36666f8c47 | ||
![]() |
003a400ce4 | ||
![]() |
52226797be | ||
![]() |
2d1093251d | ||
![]() |
29ebd5d688 | ||
![]() |
78a43b8abd | ||
![]() |
2308cd6e3f | ||
![]() |
e0e8ef6c3d | ||
![]() |
f5a4460df8 | ||
![]() |
30273a9bbd | ||
![]() |
26784d3755 | ||
![]() |
2f988e1321 | ||
![]() |
74aecf4720 | ||
![]() |
b551717d80 | ||
![]() |
e8cdfb9c83 | ||
![]() |
f598a1c179 | ||
![]() |
fa6204a2bd | ||
![]() |
83ebc73113 | ||
![]() |
176d34b2ff | ||
![]() |
1e5b007c85 | ||
![]() |
0eca2af1a4 | ||
![]() |
567f193010 | ||
![]() |
7c56dc7b88 | ||
![]() |
155e9131d0 | ||
![]() |
c88740dc2b | ||
![]() |
2ac15460f5 | ||
![]() |
c4a3099c1c | ||
![]() |
d0c4fa8c50 | ||
![]() |
f887de03d5 | ||
![]() |
80e7ed3357 | ||
![]() |
d2237f89d7 | ||
![]() |
143854de19 | ||
![]() |
659be16214 | ||
![]() |
97892e0104 | ||
![]() |
92ac8b2379 | ||
![]() |
42ec0218d8 | ||
![]() |
4ae7f55de1 | ||
![]() |
402b2c461b | ||
![]() |
a08827fcd0 | ||
![]() |
a12cbf5f75 | ||
![]() |
c00e771705 | ||
![]() |
c76cd0d5a6 | ||
![]() |
3041628180 | ||
![]() |
c6ab87ca01 | ||
![]() |
414f503e49 | ||
![]() |
278abba136 | ||
![]() |
d242946246 | ||
![]() |
90f658b13a | ||
![]() |
11b59f767c | ||
![]() |
bb56037cda | ||
![]() |
018606e983 | ||
![]() |
b327619069 | ||
![]() |
23ed547adb | ||
![]() |
b6ea5a43b3 | ||
![]() |
514b5da9a7 | ||
![]() |
01afde2772 | ||
![]() |
4b8876c0fd | ||
![]() |
a6726037ef | ||
![]() |
6080d9b42c | ||
![]() |
8d195a7de3 | ||
![]() |
f3dcde6842 | ||
![]() |
4a61c3864d | ||
![]() |
55cf0a8ba6 | ||
![]() |
f0afd8ebfd | ||
![]() |
9fdf7a40f9 | ||
![]() |
ab7193e8d4 | ||
![]() |
3dc7b3616e | ||
![]() |
47fa865244 | ||
![]() |
bc6e8bff6c | ||
![]() |
65f56a432f | ||
![]() |
cfe765e991 | ||
![]() |
6a90efcf0d | ||
![]() |
f888b0b380 | ||
![]() |
cb6f25a0ce | ||
![]() |
81c7095d1a | ||
![]() |
635b2f6593 | ||
![]() |
839000ae0e | ||
![]() |
d0f2464c10 | ||
![]() |
31079f8544 | ||
![]() |
40ef51c3c4 | ||
![]() |
a43d149660 | ||
![]() |
d93b480057 | ||
![]() |
ae7bdfdf9f | ||
![]() |
cd51c7195f | ||
![]() |
47cf235abc | ||
![]() |
8c4e53f2f9 | ||
![]() |
1ca09ff63e | ||
![]() |
18ce2cb08a | ||
![]() |
ccab3b1e9e | ||
![]() |
5c8fa2c2bf | ||
![]() |
6badb3f83f | ||
![]() |
4b6ec32d41 | ||
![]() |
1348b589b3 | ||
![]() |
2b97849156 | ||
![]() |
de1382fd99 | ||
![]() |
464a9e9020 | ||
![]() |
dc23cf3b1a | ||
![]() |
52309463e4 | ||
![]() |
58c3f5ade2 | ||
![]() |
96f4ed6fe1 | ||
![]() |
3068c7e0da | ||
![]() |
4b49e24ea7 | ||
![]() |
d9d5e32740 | ||
![]() |
536ada48f8 | ||
![]() |
6159b26614 | ||
![]() |
a5d65a074a | ||
![]() |
7fe0a5b912 | ||
![]() |
b29459eb48 | ||
![]() |
3ae72b54d2 | ||
![]() |
4d29ce5792 | ||
![]() |
e7978f34aa | ||
![]() |
abe3914cd3 | ||
![]() |
b30ba328a4 | ||
![]() |
fa9c291cb1 | ||
![]() |
4416a85fc7 | ||
![]() |
5179e7a0f8 | ||
![]() |
77ba6305f6 | ||
![]() |
f0f4748c23 | ||
![]() |
af2a22dd87 | ||
![]() |
e87297934f | ||
![]() |
a81e67a966 | ||
![]() |
444ace625b | ||
![]() |
3f994ff18e | ||
![]() |
46df8ac7e7 | ||
![]() |
3709c36ca1 | ||
![]() |
e733cdf251 | ||
![]() |
7269014762 | ||
![]() |
e442c0c0a6 | ||
![]() |
53c2111673 | ||
![]() |
2b3be9ba75 | ||
![]() |
b4deb8af3d | ||
![]() |
3d5666fc08 | ||
![]() |
0dfb39b13d | ||
![]() |
b8256f8727 | ||
![]() |
fb3d3f5cb2 | ||
![]() |
1721109fef | ||
![]() |
6753d78567 | ||
![]() |
a438a47927 | ||
![]() |
55f9967b23 | ||
![]() |
268b3cd93c | ||
![]() |
8fa8a25a56 | ||
![]() |
54cea3d5d0 | ||
![]() |
50977c01a2 | ||
![]() |
041adc981b | ||
![]() |
cc88759435 | ||
![]() |
fd6b3b77c5 | ||
![]() |
5cc5cc4ec9 | ||
![]() |
07c78a9128 | ||
![]() |
91c4b8f1cd | ||
![]() |
77190fcb70 | ||
![]() |
b0eeb59191 | ||
![]() |
e46fa2c6a7 | ||
![]() |
8ec2184edb | ||
![]() |
0d602d2a83 | ||
![]() |
f0c25eed3c | ||
![]() |
616fe64634 | ||
![]() |
df71b94849 | ||
![]() |
b8515c91d7 | ||
![]() |
826f6d0786 | ||
![]() |
2531ca67d7 | ||
![]() |
5b6c8b58e9 | ||
![]() |
6d7c10b413 | ||
![]() |
5bae5ca423 | ||
![]() |
b3eec46ee4 | ||
![]() |
c2538f3ff9 | ||
![]() |
3865add799 | ||
![]() |
965f79bd7f | ||
![]() |
0ac1dc5e3f | ||
![]() |
1ce0625a43 | ||
![]() |
4e18b12852 | ||
![]() |
ff59f15e59 | ||
![]() |
3ababe02a6 | ||
![]() |
ade22636da | ||
![]() |
34c73b762f | ||
![]() |
ead676d3a7 | ||
![]() |
c8f2e8ed9c | ||
![]() |
1a5686c8d5 | ||
![]() |
eae9359ebd | ||
![]() |
88eca1e146 | ||
![]() |
167b4c70a9 | ||
![]() |
97217929af | ||
![]() |
12ab08f207 | ||
![]() |
239c668435 | ||
![]() |
07343eca74 | ||
![]() |
9de797bf05 | ||
![]() |
02afb5643c | ||
![]() |
d4771f7c9a | ||
![]() |
fb18e9c2ed | ||
![]() |
3e0df76545 | ||
![]() |
c2686ac1f9 | ||
![]() |
6eb7e2081b | ||
![]() |
cb7c9c932e | ||
![]() |
506b1345ab | ||
![]() |
d8b849f9c0 | ||
![]() |
bc30e39ae5 | ||
![]() |
dd12dcecf4 | ||
![]() |
11143f47a8 | ||
![]() |
08ecf1f287 | ||
![]() |
39148c82a0 | ||
![]() |
e9fe01baf8 | ||
![]() |
961ebfbd8f | ||
![]() |
500ad3ca38 | ||
![]() |
be07271bb7 | ||
![]() |
3bfcf36297 | ||
![]() |
64618772b2 | ||
![]() |
f1e70d8dd1 | ||
![]() |
98964e8e49 | ||
![]() |
406141c86e | ||
![]() |
99464dc50f | ||
![]() |
a75c731eba | ||
![]() |
f45ce5938b | ||
![]() |
6f8b1d83cb | ||
![]() |
40b15d0acb | ||
![]() |
abbf2aa055 | ||
![]() |
0d8d180fbf | ||
![]() |
c7a9b1b41a | ||
![]() |
81db6145eb | ||
![]() |
45ce4a2162 | ||
![]() |
f96b674f49 | ||
![]() |
bb89e25c3a | ||
![]() |
323e385ae9 | ||
![]() |
e7ef5bac66 | ||
![]() |
211fe9a249 | ||
![]() |
c3395b6b20 | ||
![]() |
7ef568bc87 | ||
![]() |
d1cde6ae54 | ||
![]() |
e40857fe8a | ||
![]() |
5b3c12539d | ||
![]() |
0e04efb774 | ||
![]() |
e67d4d3d18 | ||
![]() |
2f4f96ecdc | ||
![]() |
d874cc6182 | ||
![]() |
2da508689c | ||
![]() |
96fc24e352 | ||
![]() |
ff2edc076a | ||
![]() |
2fffa4e17c | ||
![]() |
bd128ebf08 | ||
![]() |
f0f6c52828 | ||
![]() |
4f99a293d0 | ||
![]() |
618de17a40 | ||
![]() |
66c4fa2833 | ||
![]() |
eade139811 | ||
![]() |
522e3ccbbf | ||
![]() |
e327a0eb2f | ||
![]() |
e35bdf0193 | ||
![]() |
24e0146964 | ||
![]() |
ea1f86d312 | ||
![]() |
0abd2667a7 | ||
![]() |
800b82ed74 | ||
![]() |
682ce6150f | ||
![]() |
ba9cbaf127 | ||
![]() |
8d73183a6c | ||
![]() |
0d4028fa8f | ||
![]() |
49d9bbb632 | ||
![]() |
f70b2bd77d | ||
![]() |
a7c6ca148d | ||
![]() |
965572a28b | ||
![]() |
1428c0c3c5 | ||
![]() |
2280d86bc5 | ||
![]() |
80f46ef77e | ||
![]() |
ddfc18a811 | ||
![]() |
dbfa2223f6 | ||
![]() |
8943905373 | ||
![]() |
a6eed119d8 | ||
![]() |
d5b39b3adc | ||
![]() |
0dfa8b829a | ||
![]() |
525128614a | ||
![]() |
c75ffe1152 | ||
![]() |
e346f7f070 | ||
![]() |
bfd80ddd22 | ||
![]() |
b3549a0fc4 | ||
![]() |
3a800928bb | ||
![]() |
baf87c8d7f | ||
![]() |
c8803b9951 | ||
![]() |
1eb0c5ce0a | ||
![]() |
ee8cda5a4c | ||
![]() |
0299a87e67 | ||
![]() |
fdea41ba94 | ||
![]() |
22479585bb | ||
![]() |
d4b3ae4ec1 | ||
![]() |
49b4ca9fd2 | ||
![]() |
b0d559bff5 | ||
![]() |
d9dcedd9f4 | ||
![]() |
309b07a9fe | ||
![]() |
1159406c05 | ||
![]() |
7752b962ef | ||
![]() |
403c8583b0 | ||
![]() |
d2a7f1c078 | ||
![]() |
30e8554729 | ||
![]() |
748ee0442c | ||
![]() |
b86be41784 | ||
![]() |
7038720fd9 | ||
![]() |
f983d33521 | ||
![]() |
0be90ded03 | ||
![]() |
5147404c9e | ||
![]() |
dccb2ab910 | ||
![]() |
147398ba50 | ||
![]() |
2471f0363d | ||
![]() |
06e1b44ba2 | ||
![]() |
e992c667cf | ||
![]() |
cc09659e0a | ||
![]() |
e938ba99b5 | ||
![]() |
37f48e2b99 | ||
![]() |
5b904dc5d3 | ||
![]() |
c1ce12ed12 | ||
![]() |
1a1331b908 | ||
![]() |
81dd5961a4 | ||
![]() |
798ac1062b | ||
![]() |
47fb33b570 | ||
![]() |
63f398c33a | ||
![]() |
e5f66f6aca | ||
![]() |
259e0181aa | ||
![]() |
1ee464fddb | ||
![]() |
404efb4c39 | ||
![]() |
e864c6eef9 | ||
![]() |
6f5b20858f | ||
![]() |
fa42fb3269 | ||
![]() |
811f5c9038 | ||
![]() |
5e74f61a74 | ||
![]() |
80adcde10f | ||
![]() |
b696b3e9d6 | ||
![]() |
03c007b4cc | ||
![]() |
1ca9619278 | ||
![]() |
0d6c503fb6 | ||
![]() |
a7a9050528 | ||
![]() |
95cb6e8836 | ||
![]() |
b1bc6d4dd2 | ||
![]() |
11094248f0 | ||
![]() |
33ac8cf325 | ||
![]() |
daea026a2c | ||
![]() |
fde7714f6c | ||
![]() |
a23ba9aefa | ||
![]() |
c033b95f77 | ||
![]() |
801a25dab1 | ||
![]() |
ce2d18cf4b | ||
![]() |
edd9d79a36 | ||
![]() |
4804d7ec29 | ||
![]() |
9746b50055 | ||
![]() |
1a9d942a8c | ||
![]() |
93d47cc9c7 | ||
![]() |
6f054c75b3 | ||
![]() |
d4c166cd60 | ||
![]() |
f1ec0a4c0c | ||
![]() |
346b9bdaae | ||
![]() |
33df7e2c81 | ||
![]() |
c282c3a82f | ||
![]() |
239cd34087 | ||
![]() |
12af4b1478 | ||
![]() |
ec1eb2ef9c | ||
![]() |
e15f515a02 | ||
![]() |
355724a09d | ||
![]() |
e7dcd033b4 | ||
![]() |
3e067ff1a3 | ||
![]() |
d451910e80 | ||
![]() |
642c332dfa | ||
![]() |
a1c1be7cd0 | ||
![]() |
0ce5e10e72 | ||
![]() |
11f063390b | ||
![]() |
726de151d5 | ||
![]() |
0e20a17904 | ||
![]() |
a151deb608 | ||
![]() |
797376a395 | ||
![]() |
4a4f8836c5 | ||
![]() |
8276282511 | ||
![]() |
d49c662a33 | ||
![]() |
af2c5ed667 | ||
![]() |
368b36cb48 | ||
![]() |
301e72e816 | ||
![]() |
fcbe2dafbc | ||
![]() |
ee68dfa4a3 | ||
![]() |
92751c66cd | ||
![]() |
c38717f79e | ||
![]() |
cb78f8e6d0 | ||
![]() |
da4e6fe606 | ||
![]() |
3504912eba | ||
![]() |
dc043d6fe1 | ||
![]() |
383a473bb7 | ||
![]() |
7301151873 | ||
![]() |
a57c255ac5 | ||
![]() |
28fccf4a9a | ||
![]() |
58e94788b7 | ||
![]() |
627955742f | ||
![]() |
1981d84f01 | ||
![]() |
cbc3aa6c38 | ||
![]() |
8a9ec137cc | ||
![]() |
193635d497 | ||
![]() |
a928fc1ffd | ||
![]() |
ef5e28ff51 | ||
![]() |
75fe2d2d01 | ||
![]() |
72bb3d91a1 | ||
![]() |
69c9b58cb8 | ||
![]() |
d23570be0b | ||
![]() |
9575f633e9 | ||
![]() |
96d2fac22e | ||
![]() |
9b2185103b | ||
![]() |
841f223fbe | ||
![]() |
f9922518cc | ||
![]() |
2f2c0e2992 | ||
![]() |
661d8c9430 | ||
![]() |
254dca7862 | ||
![]() |
f2da074492 | ||
![]() |
5e2737d687 | ||
![]() |
7fd29c1f1d | ||
![]() |
b63707e52f | ||
![]() |
f883e72dce | ||
![]() |
136e1b7124 | ||
![]() |
99800ac558 | ||
![]() |
e3b32800ff | ||
![]() |
68d4142bab | ||
![]() |
c258643a28 | ||
![]() |
08b48b2bb4 | ||
![]() |
29b8f9ee53 | ||
![]() |
6e49b0b9b3 | ||
![]() |
7bcac4fb65 | ||
![]() |
9514264b29 | ||
![]() |
781f8949af | ||
![]() |
e8fd0693a7 | ||
![]() |
ae77709ea7 | ||
![]() |
fd299656dd | ||
![]() |
2f046f9cbe | ||
![]() |
21c74cdac6 | ||
![]() |
26453368c9 | ||
![]() |
ded9d76508 | ||
![]() |
5857cde652 | ||
![]() |
28816fdce7 | ||
![]() |
56ad0f02e5 | ||
![]() |
b961ce804f | ||
![]() |
a050405249 | ||
![]() |
e28819b297 | ||
![]() |
88c1450fa4 | ||
![]() |
835a3b82c2 | ||
![]() |
4b917b21b7 | ||
![]() |
ecc9086940 | ||
![]() |
46310a8207 | ||
![]() |
161256afd3 | ||
![]() |
1e58834cae | ||
![]() |
6e1e8f432b | ||
![]() |
a234b971fa | ||
![]() |
7dbe4f54c7 | ||
![]() |
5cfb618022 | ||
![]() |
b36566aa69 | ||
![]() |
62c90f916b | ||
![]() |
8a76cafd37 | ||
![]() |
7ac8a0a897 | ||
![]() |
ae145ab3b9 | ||
![]() |
2431d65381 | ||
![]() |
04f0c3d467 | ||
![]() |
fa91de8d39 | ||
![]() |
b89e884078 | ||
![]() |
3c4126dd0f | ||
![]() |
fa8fd0eddb | ||
![]() |
6e84c95a7f | ||
![]() |
6c420f92b6 | ||
![]() |
86f0b930b3 | ||
![]() |
c8b3a0942a | ||
![]() |
f667b76c39 | ||
![]() |
6d5761bbad | ||
![]() |
d3f86689a6 | ||
![]() |
bc2e787644 | ||
![]() |
f82500b921 | ||
![]() |
81bb2781c3 | ||
![]() |
a3b549b09e | ||
![]() |
0da4998c13 | ||
![]() |
3a6348e1fc | ||
![]() |
e678115b24 | ||
![]() |
bcef48c8e2 | ||
![]() |
761eb4eab3 | ||
![]() |
c228187615 | ||
![]() |
2c9228f3e5 | ||
![]() |
46949f0907 | ||
![]() |
b8664d84f7 | ||
![]() |
82335dc0be | ||
![]() |
a6a6704d2c | ||
![]() |
dfff01a277 | ||
![]() |
85868db150 | ||
![]() |
7004b30ef9 | ||
![]() |
9aa5484b4c | ||
![]() |
24a8178592 | ||
![]() |
83272d6c5c | ||
![]() |
250e00c087 | ||
![]() |
27cc7ddd9b | ||
![]() |
7cd1ed8eef | ||
![]() |
33ece5bdb4 | ||
![]() |
fd09a3cfa9 | ||
![]() |
ea50c6f7f1 | ||
![]() |
e941c0a314 | ||
![]() |
ee4a684422 | ||
![]() |
60886bcdc2 | ||
![]() |
193d222442 | ||
![]() |
87405b8759 | ||
![]() |
aafe5dfad8 | ||
![]() |
cf871ffad9 | ||
![]() |
91356e156d | ||
![]() |
5b5e671320 | ||
![]() |
0454982041 | ||
![]() |
3cd1c90a29 | ||
![]() |
13157fd7d0 | ||
![]() |
e78749d4b6 | ||
![]() |
6eb19b5364 | ||
![]() |
e9ab125b04 | ||
![]() |
7ec0f15e2d | ||
![]() |
e40bff50c2 | ||
![]() |
97000c67ae | ||
![]() |
5c808b0329 | ||
![]() |
a0722e04ff | ||
![]() |
1c630eb647 | ||
![]() |
fb342b2689 | ||
![]() |
9025844a88 | ||
![]() |
bd017045eb | ||
![]() |
6d24d2f441 | ||
![]() |
b82114bddf | ||
![]() |
c97611f2b1 | ||
![]() |
77db73d0e9 | ||
![]() |
e2f4488248 | ||
![]() |
6f5104d934 | ||
![]() |
5ee4651569 | ||
![]() |
8af8cdbfd6 | ||
![]() |
059913c239 | ||
![]() |
040c937117 | ||
![]() |
31a0a66842 | ||
![]() |
1d084917ae | ||
![]() |
b679fcf6a2 | ||
![]() |
bf19565900 | ||
![]() |
aefd2ddb0b | ||
![]() |
4c8578b7b3 | ||
![]() |
e87666a228 | ||
![]() |
86eaba8248 | ||
![]() |
945ce7490a | ||
![]() |
b824d35960 | ||
![]() |
dcc8d560b2 | ||
![]() |
69465f3a99 | ||
![]() |
6f14542c18 | ||
![]() |
81bef57742 | ||
![]() |
256f98f5f6 | ||
![]() |
5267a0f357 | ||
![]() |
dae41f1c59 | ||
![]() |
53f5d5db69 | ||
![]() |
f1edfdf974 | ||
![]() |
38a586f645 | ||
![]() |
6f16bb0925 | ||
![]() |
a2f3f58371 | ||
![]() |
c81d61f685 | ||
![]() |
046fb0d3f0 | ||
![]() |
c303589bd8 | ||
![]() |
a8524e6020 | ||
![]() |
b0d9449fa6 | ||
![]() |
0ab3e97241 | ||
![]() |
a0e777bd77 | ||
![]() |
d5df2e68f4 | ||
![]() |
ebe986b67f | ||
![]() |
e42897801b | ||
![]() |
df9718c2d8 | ||
![]() |
f43446ed18 | ||
![]() |
bec8785c23 | ||
![]() |
70d850cf29 | ||
![]() |
36a2ae886c | ||
![]() |
baf4aa29ba | ||
![]() |
f8c62ae16b | ||
![]() |
aafe2bd19c | ||
![]() |
1748d1e5ba | ||
![]() |
48e4ad7a1d | ||
![]() |
8dafe05b2c | ||
![]() |
adcf4065ac | ||
![]() |
6d73591d59 | ||
![]() |
de61c38a23 | ||
![]() |
e58b96a6f4 | ||
![]() |
125bfd294c | ||
![]() |
0039f59a1a | ||
![]() |
553fad3fe1 | ||
![]() |
6791be02bd | ||
![]() |
2d34231172 | ||
![]() |
6ced6df8ed | ||
![]() |
86f50a41a9 | ||
![]() |
9f117c74cd | ||
![]() |
71b8d7bc12 | ||
![]() |
0e229a46cb | ||
![]() |
b3d8dc2219 | ||
![]() |
e0e11447a7 | ||
![]() |
99d043d466 | ||
![]() |
5c62d0a2e0 | ||
![]() |
f99406eac4 | ||
![]() |
50a34d587b | ||
![]() |
7f6cdfd7c2 | ||
![]() |
27c1c4938e | ||
![]() |
e1dfa793a8 | ||
![]() |
385678eaa7 | ||
![]() |
379327a6b2 | ||
![]() |
2b774ac3f5 | ||
![]() |
c235e43ccc | ||
![]() |
d63c016d38 | ||
![]() |
9fdd5226b7 | ||
![]() |
9264c24fac | ||
![]() |
50863e97cc | ||
![]() |
5fc46dd429 | ||
![]() |
4fa2a127d7 | ||
![]() |
e3ac28a042 | ||
![]() |
49d902853d | ||
![]() |
be6494a809 | ||
![]() |
0c579473bb | ||
![]() |
913866677c | ||
![]() |
e8d516af89 | ||
![]() |
c6a0842a68 | ||
![]() |
609ad7dee7 | ||
![]() |
8f396b9354 | ||
![]() |
3606015d71 | ||
![]() |
27cf708bd7 | ||
![]() |
738cb6af17 | ||
![]() |
5ad7a9446c | ||
![]() |
76166bb35f | ||
![]() |
13b3626044 | ||
![]() |
f16e67fc75 | ||
![]() |
4329b14af3 | ||
![]() |
bdeb8e0460 | ||
![]() |
b6d3141d31 | ||
![]() |
3e285bf307 | ||
![]() |
4e676725c1 | ||
![]() |
72b1390ba6 | ||
![]() |
2e241b2c10 | ||
![]() |
7a35e5c985 | ||
![]() |
791755fb90 | ||
![]() |
a2a1ee8eb5 | ||
![]() |
795ca4f341 | ||
![]() |
5372105e72 | ||
![]() |
7b70b8555b | ||
![]() |
78dcfb92b4 | ||
![]() |
51c8ea699b | ||
![]() |
83fb66e87f | ||
![]() |
586eed515f | ||
![]() |
a7f86cd94b | ||
![]() |
4f961298f6 | ||
![]() |
5ff9d67e9d | ||
![]() |
b9bb9fa30d | ||
![]() |
d4fd18bc74 | ||
![]() |
4eb8b12eee | ||
![]() |
83753bacf8 | ||
![]() |
f0bb6544ed | ||
![]() |
523fd8627b | ||
![]() |
bb40de567f | ||
![]() |
2fdc50776a | ||
![]() |
7e1d82f116 | ||
![]() |
b527114f6f | ||
![]() |
31e3b42c6b | ||
![]() |
beed3ec782 | ||
![]() |
639aab411e | ||
![]() |
ac53f835b6 | ||
![]() |
2d2ed870da | ||
![]() |
36af4cc14f | ||
![]() |
8f06ea9115 | ||
![]() |
b93401a391 | ||
![]() |
9312fdac78 | ||
![]() |
07806a16e5 | ||
![]() |
e8e471e6a3 | ||
![]() |
2497425c9f | ||
![]() |
f090ff1229 | ||
![]() |
5824c2cdfb | ||
![]() |
2f2e19eec9 | ||
![]() |
620fefae74 | ||
![]() |
69490d7e83 | ||
![]() |
e01915cdee | ||
![]() |
dd481bae83 | ||
![]() |
18a66d8454 | ||
![]() |
23da1455b3 | ||
![]() |
2437d3070f | ||
![]() |
1a3ef3c090 | ||
![]() |
cbb71b5faf | ||
![]() |
811aa8c7ba | ||
![]() |
04e3eba77d | ||
![]() |
cc3ce9aaad | ||
![]() |
93b66c7163 | ||
![]() |
ff5094cdc4 | ||
![]() |
000ec6d8f6 | ||
![]() |
57e9c0a645 | ||
![]() |
add7232619 | ||
![]() |
d1c7edfc87 | ||
![]() |
a5ab78188d | ||
![]() |
e6029d6716 | ||
![]() |
c06e9b09d7 | ||
![]() |
9beff9567f | ||
![]() |
d7ef58f66e | ||
![]() |
5f2e9ba30b | ||
![]() |
b65d7af704 | ||
![]() |
ff207a57af | ||
![]() |
ba794ce0cb | ||
![]() |
b7dba09927 | ||
![]() |
ffe68a108e | ||
![]() |
fe628ba909 | ||
![]() |
1fbc7b4337 | ||
![]() |
95c2bbdd1d | ||
![]() |
c78dc4094a | ||
![]() |
0689957087 | ||
![]() |
f505dd2a2a | ||
![]() |
acc0e0abfe | ||
![]() |
26bf1d818f | ||
![]() |
152b2c90cf | ||
![]() |
10c53f720d | ||
![]() |
4779defeb5 | ||
![]() |
48b22d5b1e | ||
![]() |
8081a20285 | ||
![]() |
dca6e6e73f | ||
![]() |
b04112c856 | ||
![]() |
ba64d9efc3 | ||
![]() |
b82b0aad88 | ||
![]() |
d337fc4e8f | ||
![]() |
e0ac9f385f | ||
![]() |
0c095ae963 | ||
![]() |
be8bd1b2a4 | ||
![]() |
9f75d66e83 | ||
![]() |
ac1b645ae3 | ||
![]() |
b40ba5cbca | ||
![]() |
da5896dde0 | ||
![]() |
277526eeea | ||
![]() |
332f4d12d0 | ||
![]() |
04f0342726 | ||
![]() |
4d84c1914f | ||
![]() |
c0d5d49100 | ||
![]() |
7f2a2b2ee4 | ||
![]() |
54d67a45d5 | ||
![]() |
66e1f3384a | ||
![]() |
587937b876 | ||
![]() |
cd5e911f4e | ||
![]() |
a40af7026b | ||
![]() |
1d0a7dcc0c | ||
![]() |
24f452f9b5 | ||
![]() |
6e1a538d34 | ||
![]() |
6146ef081d | ||
![]() |
44161ecd47 | ||
![]() |
1cddf8d906 | ||
![]() |
c17c74aa82 | ||
![]() |
bb355e7b7e | ||
![]() |
0ef7efbc03 | ||
![]() |
131057366a | ||
![]() |
31a767adbb | ||
![]() |
5cb9fb3661 | ||
![]() |
f063c326d0 | ||
![]() |
d614eaae3d | ||
![]() |
05e98f8d05 | ||
![]() |
6c24ebef2f | ||
![]() |
7c563c00c9 | ||
![]() |
9feded3b41 | ||
![]() |
d9147cd60c | ||
![]() |
14e4dfbf37 | ||
![]() |
df435475cd | ||
![]() |
f5d4f6c341 | ||
![]() |
6f63a3efd9 | ||
![]() |
0cdb65bbb3 | ||
![]() |
a579f8a7cc | ||
![]() |
889c9d564d | ||
![]() |
070992be0d | ||
![]() |
36d6ea2e2e | ||
![]() |
4dba323ddf | ||
![]() |
17162044e4 | ||
![]() |
c1a0f2c035 | ||
![]() |
ab9213641c | ||
![]() |
0d6fb51e02 | ||
![]() |
066d48f6de | ||
![]() |
25c1b19533 | ||
![]() |
188491a707 | ||
![]() |
7519895348 | ||
![]() |
224254bcce | ||
![]() |
81af14f5db | ||
![]() |
ba1f3af99b | ||
![]() |
74a39bf074 | ||
![]() |
feb09c56f4 | ||
![]() |
a7ee89d497 | ||
![]() |
9329b7323a | ||
![]() |
db32ffe56a | ||
![]() |
95b8d60404 | ||
![]() |
f08b53b07d | ||
![]() |
9420e8c932 | ||
![]() |
084078e7f1 | ||
![]() |
e8be311486 | ||
![]() |
ec626ccfa2 | ||
![]() |
adc60a6fa0 | ||
![]() |
3606943e5d | ||
![]() |
d26c423303 | ||
![]() |
9d2614d60c | ||
![]() |
8936c8aaed | ||
![]() |
a8f17948ac | ||
![]() |
e1f37bf7c7 | ||
![]() |
6ab821e377 | ||
![]() |
7642945108 | ||
![]() |
78be64accc | ||
![]() |
f62816cd83 | ||
![]() |
980c486732 | ||
![]() |
e3d0bb7ee1 | ||
![]() |
314e955a3d | ||
![]() |
d34c6905cb | ||
![]() |
1812bc39e0 | ||
![]() |
b319f7bc82 | ||
![]() |
5f54bc9aa8 | ||
![]() |
a61bcec09f | ||
![]() |
c2b299336a | ||
![]() |
1053d576ac | ||
![]() |
65ddd522dc | ||
![]() |
2c0a4196fe | ||
![]() |
8e86d9e6cb | ||
![]() |
5bf310dfee | ||
![]() |
ff7a5602f6 | ||
![]() |
e80e9f0467 | ||
![]() |
3748d0533e | ||
![]() |
e233fae63b | ||
![]() |
44082bba88 | ||
![]() |
80a746b456 | ||
![]() |
726abee0f1 | ||
![]() |
c7efa986ad | ||
![]() |
8d9eb85b80 | ||
![]() |
3bf0561635 | ||
![]() |
8a6815c077 | ||
![]() |
1a379dfd97 | ||
![]() |
e7bfa5aa9e | ||
![]() |
170fbbb99e | ||
![]() |
0a4fd8427c | ||
![]() |
91483a8fbf | ||
![]() |
0d3829e5b8 | ||
![]() |
4d07961c8a | ||
![]() |
a234c6c2da | ||
![]() |
28e3f554ea | ||
![]() |
c34c1be21f | ||
![]() |
2c336e11b7 | ||
![]() |
c9d6a19380 | ||
![]() |
7102a25dc6 | ||
![]() |
ffe5778710 | ||
![]() |
f6a8911906 | ||
![]() |
30d5c9a67c | ||
![]() |
bad6250388 | ||
![]() |
70a48c5f35 | ||
![]() |
dfd0b202a8 | ||
![]() |
6170e3689d | ||
![]() |
2cd531eb5a | ||
![]() |
22bdd23cac | ||
![]() |
78913bf1e8 | ||
![]() |
f81872b8e4 | ||
![]() |
0a8725e77b | ||
![]() |
efb6d4c2be | ||
![]() |
ae6e945bfa | ||
![]() |
0d65ea8cdb | ||
![]() |
6046f077ab | ||
![]() |
16ef5215d1 | ||
![]() |
0da15fb0ef | ||
![]() |
cf1b568f75 | ||
![]() |
1dc768f20f | ||
![]() |
f6adda48f1 | ||
![]() |
8897f18ff0 | ||
![]() |
dd8b01a5c5 | ||
![]() |
aeafe6973e | ||
![]() |
2cc94708e1 | ||
![]() |
9a063d4175 | ||
![]() |
d4ed67ce4f | ||
![]() |
bfa0671640 | ||
![]() |
e6f53553a9 | ||
![]() |
e12dc11b67 | ||
![]() |
110bbf143a | ||
![]() |
48bbd574a5 | ||
![]() |
6419816b47 | ||
![]() |
ee50ac01a4 | ||
![]() |
3ed4924a4a | ||
![]() |
763f78a549 | ||
![]() |
a0a4e55705 | ||
![]() |
799401ddf9 | ||
![]() |
e3e8a40831 | ||
![]() |
a817522bc3 | ||
![]() |
d12e92aead | ||
![]() |
f8bc3cc501 | ||
![]() |
494cb47a47 | ||
![]() |
f99bbef4c9 | ||
![]() |
85806bb355 | ||
![]() |
52f9147ee8 | ||
![]() |
7e2fd7e4eb | ||
![]() |
e9c946008e | ||
![]() |
8a0ee03a71 | ||
![]() |
603680f10f | ||
![]() |
a0fdee81a6 | ||
![]() |
509ebcda79 | ||
![]() |
03ec37fdb3 | ||
![]() |
320a083991 | ||
![]() |
25b86dfcb1 | ||
![]() |
a43b33bdbb | ||
![]() |
7bc60943cb | ||
![]() |
44e6145474 | ||
![]() |
159f7f3c3d | ||
![]() |
32b8ac5fca | ||
![]() |
3e55831960 | ||
![]() |
c24f8063a0 | ||
![]() |
77a164015a | ||
![]() |
9a48e929fc | ||
![]() |
27917eb4dc | ||
![]() |
c1099d7d0a | ||
![]() |
bad25dec1d | ||
![]() |
c4c86b65fd | ||
![]() |
d61bac4f82 | ||
![]() |
a173f6e5a7 | ||
![]() |
e12e484e37 | ||
![]() |
145a8c72f7 | ||
![]() |
7e485b4ffe | ||
![]() |
0c5264192b | ||
![]() |
50f9cc52ac | ||
![]() |
cb3c8e875e | ||
![]() |
16f2de8057 | ||
![]() |
3bceed8359 | ||
![]() |
a2df3fbc97 | ||
![]() |
3fcbf53405 | ||
![]() |
83916f96d2 | ||
![]() |
49616a62af | ||
![]() |
fe4a439cb2 | ||
![]() |
48f56603c7 | ||
![]() |
0f9a3a4dd7 | ||
![]() |
4a0c028d8e | ||
![]() |
08156c967a | ||
![]() |
e8c44bc493 | ||
![]() |
045bd44325 | ||
![]() |
e89c1dbee1 | ||
![]() |
91b8ec81f3 | ||
![]() |
0c52ea1544 | ||
![]() |
0f1a08ed84 | ||
![]() |
102793f24f | ||
![]() |
8a76df320c | ||
![]() |
2af29e9049 | ||
![]() |
7ffdc7cc05 | ||
![]() |
3ad344433d | ||
![]() |
e3b3a57650 | ||
![]() |
26ed7f0400 | ||
![]() |
cbb9e2cd1f | ||
![]() |
aeacdf5bfb | ||
![]() |
efd9a17b73 | ||
![]() |
581d1b0ca7 | ||
![]() |
86e0728e15 | ||
![]() |
ec9618ed55 | ||
![]() |
8ea34036e5 | ||
![]() |
a6aeca31bd | ||
![]() |
b299205aa7 | ||
![]() |
518229031c | ||
![]() |
e5fd7cece9 | ||
![]() |
b60c9d33b6 | ||
![]() |
14a668ae91 | ||
![]() |
62f1801e9c | ||
![]() |
6f1cabe972 | ||
![]() |
8b8ebb9f39 | ||
![]() |
af0b16482f | ||
![]() |
994d41ecd1 | ||
![]() |
88352ee6ec | ||
![]() |
91474ba073 | ||
![]() |
1a49bbdbc4 | ||
![]() |
50b767314e | ||
![]() |
55d56cf7f8 | ||
![]() |
2357bc09a3 | ||
![]() |
17bcf786a8 | ||
![]() |
736a71fac2 | ||
![]() |
e46ef02302 | ||
![]() |
b63a4af4d5 | ||
![]() |
36cddd1488 | ||
![]() |
424788edd7 | ||
![]() |
5f6a6d2b7d | ||
![]() |
c369899493 | ||
![]() |
e82459d377 | ||
![]() |
f4d8a8525b | ||
![]() |
a112b22ce6 | ||
![]() |
f32e4747b5 | ||
![]() |
823e503d84 | ||
![]() |
e65c6f240e | ||
![]() |
2d9ff2af0a | ||
![]() |
1d833957ed | ||
![]() |
0924ca2ad3 | ||
![]() |
9e29aeeeff | ||
![]() |
e5a609f716 | ||
![]() |
0077e26d23 | ||
![]() |
32146506f1 | ||
![]() |
6d4158af85 | ||
![]() |
a0dcea3a13 | ||
![]() |
0a398ef364 | ||
![]() |
ae4c7b29f2 | ||
![]() |
80276c7e5b | ||
![]() |
b45d175c6f | ||
![]() |
93c97c183f | ||
![]() |
8034fee8ea | ||
![]() |
f55d917db4 | ||
![]() |
0acdf89ae9 | ||
![]() |
2d3d72009e | ||
![]() |
76d3c71b67 | ||
![]() |
535aee0840 | ||
![]() |
9fbb89d053 | ||
![]() |
2f9360f57c | ||
![]() |
5119905955 | ||
![]() |
582ce70ce2 | ||
![]() |
36a2780ab1 | ||
![]() |
bc56555d9e | ||
![]() |
99099ea0bc | ||
![]() |
52fd726d9b | ||
![]() |
55677a44ff | ||
![]() |
f28f20974f | ||
![]() |
19c0b0d194 | ||
![]() |
ca3a70f2a4 | ||
![]() |
1eb85e8f4e | ||
![]() |
e37201d0ce | ||
![]() |
f6d07d0abd | ||
![]() |
3395d69747 | ||
![]() |
e088c67108 | ||
![]() |
dc4ec57441 | ||
![]() |
2fda5925dc | ||
![]() |
08af6e54af | ||
![]() |
43da3130e9 | ||
![]() |
91aa019f8d | ||
![]() |
76d1c7b7a8 | ||
![]() |
04a7cfff20 | ||
![]() |
795cef129c | ||
![]() |
68ea89f15e |
3
.editorconfig
Normal file
3
.editorconfig
Normal file
@ -0,0 +1,3 @@
|
||||
[*.{kt,kts}]
|
||||
ktlint_code_style = intellij_idea
|
||||
ktlint_standard_no-wildcard-imports = disabled
|
9
.gitattributes
vendored
Normal file
9
.gitattributes
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
#
|
||||
# https://help.github.com/articles/dealing-with-line-endings/
|
||||
#
|
||||
# Linux start script should use lf
|
||||
/gradlew text eol=lf
|
||||
|
||||
# These are Windows script files and should use crlf
|
||||
*.bat text eol=crlf
|
||||
|
110
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
110
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@ -0,0 +1,110 @@
|
||||
name: 🐞 Bug report
|
||||
description: Report a bug or an issue.
|
||||
title: 'bug: '
|
||||
labels: ['Bug report']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source
|
||||
width="256px"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcset="https://raw.githubusercontent.com/revanced/revanced-patches/main/assets/revanced-headline/revanced-headline-vertical-dark.svg"
|
||||
>
|
||||
<img
|
||||
width="256px"
|
||||
src="https://raw.githubusercontent.com/revanced/revanced-patches/main/assets/revanced-headline/revanced-headline-vertical-light.svg"
|
||||
>
|
||||
</picture>
|
||||
<br>
|
||||
<a href="https://revanced.app/">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/revanced/revanced-patches/main/assets/revanced-logo/revanced-logo.svg" />
|
||||
<img height="24px" src="https://raw.githubusercontent.com/revanced/revanced-patches/main/assets/revanced-logo/revanced-logo.svg" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://github.com/ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
|
||||
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="http://revanced.app/discord">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://reddit.com/r/revancedapp">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://t.me/app_revanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://x.com/revancedapp">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.youtube.com/@ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<br>
|
||||
<br>
|
||||
Continuing the legacy of Vanced
|
||||
</p>
|
||||
|
||||
# ReVanced Patches bug report
|
||||
|
||||
Before creating a new bug report, please keep the following in mind:
|
||||
|
||||
- **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).
|
||||
- **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
|
||||
attributes:
|
||||
label: Bug description
|
||||
description: |
|
||||
- Describe your bug in detail
|
||||
- Add steps to reproduce the bug if possible (Step 1. ... Step 2. ...)
|
||||
- Add images and videos if possible
|
||||
- List used patches if applicable
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Error logs
|
||||
description: Exceptions can be captured by running `logcat | grep AndroidRuntime` in a shell.
|
||||
render: shell
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Solution
|
||||
description: If applicable, add a possible solution to the bug.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add additional context here.
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
description: Your bug report will be closed if you don't follow the checklist below.
|
||||
options:
|
||||
- label: I have checked all open and closed bug reports and this is not a duplicate.
|
||||
required: true
|
||||
- label: I have chosen an appropriate title.
|
||||
required: true
|
||||
- label: All requested information has been provided properly.
|
||||
required: true
|
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 🗨 Discussions
|
||||
url: https://github.com/revanced/revanced-suggestions/discussions
|
||||
about: Have something unspecific to ReVanced Patches in mind? Search for or start a new discussion!
|
106
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
106
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@ -0,0 +1,106 @@
|
||||
name: ⭐ Feature request
|
||||
description: Create a detailed request for a new feature.
|
||||
title: 'feat: '
|
||||
labels: ['Feature request']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source
|
||||
width="256px"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcset="https://raw.githubusercontent.com/revanced/revanced-patches/main/assets/revanced-headline/revanced-headline-vertical-dark.svg"
|
||||
>
|
||||
<img
|
||||
width="256px"
|
||||
src="https://raw.githubusercontent.com/revanced/revanced-patches/main/assets/revanced-headline/revanced-headline-vertical-light.svg"
|
||||
>
|
||||
</picture>
|
||||
<br>
|
||||
<a href="https://revanced.app/">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/revanced/revanced-patches/main/assets/revanced-logo/revanced-logo.svg" />
|
||||
<img height="24px" src="https://raw.githubusercontent.com/revanced/revanced-patches/main/assets/revanced-logo/revanced-logo.svg" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://github.com/ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
|
||||
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="http://revanced.app/discord">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://reddit.com/r/revancedapp">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://t.me/app_revanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://x.com/revancedapp">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.youtube.com/@ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<br>
|
||||
<br>
|
||||
Continuing the legacy of Vanced
|
||||
</p>
|
||||
|
||||
# ReVanced Patches feature request
|
||||
|
||||
Before creating a new feature request, please keep the following in mind:
|
||||
|
||||
- **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).
|
||||
- **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
|
||||
attributes:
|
||||
label: Feature description
|
||||
description: |
|
||||
- Describe your feature in detail
|
||||
- Add images, videos, links, examples, references, etc. if possible
|
||||
- Add the target application name in case you request a new patch
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Motivation
|
||||
description: |
|
||||
A strong motivation is necessary for a feature request to be considered.
|
||||
|
||||
- Why should this feature be implemented?
|
||||
- What is the explicit use case?
|
||||
- What are the benefits?
|
||||
- What makes this feature important?
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
description: Your feature request will be closed if you don't follow the checklist below.
|
||||
options:
|
||||
- label: I have checked all open and closed feature requests and this is not a duplicate
|
||||
required: true
|
||||
- label: I have chosen an appropriate title.
|
||||
required: true
|
||||
- label: All requested information has been provided properly.
|
||||
required: true
|
2
.github/config.yml
vendored
Normal file
2
.github/config.yml
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
firstPRMergeComment: >
|
||||
Thank you for contributing to ReVanced. Join us on [Discord](https://revanced.app/discord) to receive a role for your contribution.
|
22
.github/dependabot.yml
vendored
Normal file
22
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
labels: []
|
||||
directory: /
|
||||
target-branch: dev
|
||||
schedule:
|
||||
interval: monthly
|
||||
|
||||
- package-ecosystem: npm
|
||||
labels: []
|
||||
directory: /
|
||||
target-branch: dev
|
||||
schedule:
|
||||
interval: monthly
|
||||
|
||||
- package-ecosystem: gradle
|
||||
labels: []
|
||||
directory: /
|
||||
target-branch: dev
|
||||
schedule:
|
||||
interval: monthly
|
37
.github/workflows/build_pull_request.yml
vendored
Normal file
37
.github/workflows/build_pull_request.yml
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
name: Build pull request
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: "17"
|
||||
|
||||
- name: Cache Gradle
|
||||
uses: burrunan/gradle-cache-action@v1
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./gradlew :patches:buildAndroid --no-daemon
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: revanced-patches
|
||||
path: patches/build/libs
|
31
.github/workflows/open_pull_request.yml
vendored
Normal file
31
.github/workflows/open_pull_request.yml
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
name: Open a PR to main
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
MESSAGE: Merge branch `${{ github.head_ref || github.ref_name }}` to `main`
|
||||
|
||||
jobs:
|
||||
pull-request:
|
||||
name: Open pull request
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Open pull request
|
||||
uses: repo-sync/pull-request@v2
|
||||
with:
|
||||
destination_branch: main
|
||||
pr_title: 'chore: ${{ env.MESSAGE }}'
|
||||
pr_body: |
|
||||
This pull request will ${{ env.MESSAGE }}.
|
||||
|
||||
## Before merging this PR
|
||||
|
||||
- [ ] Pull translations from Crowdin
|
||||
pr_draft: true
|
44
.github/workflows/pull_strings.yml
vendored
Normal file
44
.github/workflows/pull_strings.yml
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
name: Pull strings
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 */12 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
pull:
|
||||
name: Pull strings
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: dev
|
||||
fetch-depth: 0
|
||||
clean: true
|
||||
|
||||
- name: Pull strings
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
config: crowdin.yml
|
||||
upload_sources: false
|
||||
download_translations: true
|
||||
skip_ref_checkout: true
|
||||
localization_branch_name: feat/translations
|
||||
create_pull_request: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
|
||||
- name: Open pull request
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
uses: repo-sync/pull-request@v2
|
||||
with:
|
||||
source_branch: feat/translations
|
||||
destination_branch: dev
|
||||
pr_title: "chore: Sync translations"
|
||||
pr_body: "Sync translations from [crowdin.com/project/revanced](https://crowdin.com/project/revanced)"
|
33
.github/workflows/push_strings.yml
vendored
Normal file
33
.github/workflows/push_strings.yml
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
name: Push strings
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- patches/src/main/resources/addresources/values/strings.xml
|
||||
|
||||
jobs:
|
||||
push:
|
||||
name: Push strings
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Preprocess strings
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./gradlew clean preprocessCrowdinStrings
|
||||
|
||||
- name: Push strings
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
config: crowdin.yml
|
||||
upload_sources: true
|
||||
env:
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
60
.github/workflows/release.yml
vendored
60
.github/workflows/release.yml
vendored
@ -1,41 +1,59 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
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 JDK
|
||||
uses: actions/setup-java@v2
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '8'
|
||||
distribution: 'adopt'
|
||||
cache: gradle
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
- name: Make gradlew executable
|
||||
run: chmod +x gradlew
|
||||
- name: Build with Gradle
|
||||
distribution: "temurin"
|
||||
java-version: "17"
|
||||
|
||||
- name: Cache Gradle
|
||||
uses: burrunan/gradle-cache-action@v1
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./gradlew build
|
||||
- name: Setup semantic-release
|
||||
run: npm install -g semantic-release @semantic-release/git @semantic-release/changelog gradle-semantic-release-plugin -D
|
||||
run: ./gradlew :patches:buildAndroid clean
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Import GPG key
|
||||
uses: crazy-max/ghaction-import-gpg@v6
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
||||
fingerprint: ${{ vars.GPG_FINGERPRINT }}
|
||||
|
||||
- name: Release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: npx semantic-release
|
||||
run: npm exec semantic-release
|
||||
|
18
.github/workflows/update-gradle-wrapper.yml
vendored
Normal file
18
.github/workflows/update-gradle-wrapper.yml
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
name: Update Gradle wrapper
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 1 * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
update:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Update Gradle Wrapper
|
||||
uses: gradle-update/update-gradle-wrapper-action@v1
|
||||
with:
|
||||
target-branch: dev
|
15
.gitignore
vendored
15
.gitignore
vendored
@ -112,3 +112,18 @@ gradle-app.setting
|
||||
|
||||
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
|
||||
# gradle/wrapper/gradle-wrapper.properties
|
||||
|
||||
# Potentially copyrighted test APK
|
||||
*.apk
|
||||
|
||||
# Ignore vscode config
|
||||
.vscode/
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
|
||||
# Gradle properties, due to Github token
|
||||
./gradle.properties
|
||||
|
||||
# One package is called the same as the Gradle build folder
|
||||
!**/src/**/build/
|
1
.idea/.gitignore
generated
vendored
1
.idea/.gitignore
generated
vendored
@ -6,3 +6,4 @@
|
||||
/dataSources.local.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
/kotlinc.xml
|
||||
|
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@ -4,5 +4,5 @@
|
||||
<component name="FrameworkDetectionExcludesConfiguration">
|
||||
<file type="web" url="file://$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="azul-17" project-jdk-type="JavaSDK" />
|
||||
</project>
|
32
.releaserc
32
.releaserc
@ -7,7 +7,13 @@
|
||||
}
|
||||
],
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer",
|
||||
[
|
||||
"@semantic-release/commit-analyzer", {
|
||||
"releaseRules": [
|
||||
{ "type": "build", "scope": "Needs bump", "release": "patch" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"@semantic-release/release-notes-generator",
|
||||
"@semantic-release/changelog",
|
||||
"gradle-semantic-release-plugin",
|
||||
@ -16,10 +22,28 @@
|
||||
{
|
||||
"assets": [
|
||||
"CHANGELOG.md",
|
||||
"gradle.properties"
|
||||
]
|
||||
"gradle.properties",
|
||||
],
|
||||
"message": "chore: Release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
||||
}
|
||||
],
|
||||
"@semantic-release/github"
|
||||
[
|
||||
"@semantic-release/github",
|
||||
{
|
||||
"assets": [
|
||||
{
|
||||
"path": "patches/build/libs/patches-!(*sources*|*javadoc*).rvp?(.asc)"
|
||||
},
|
||||
],
|
||||
successComment: false
|
||||
}
|
||||
],
|
||||
[
|
||||
"@saithodev/semantic-release-backmerge",
|
||||
{
|
||||
backmergeBranches: [{"from": "main", "to": "dev"}],
|
||||
clearWorkspace: true
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
|
11164
CHANGELOG.md
11164
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
128
CONTRIBUTING.md
Normal file
128
CONTRIBUTING.md
Normal file
@ -0,0 +1,128 @@
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source
|
||||
width="256px"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcset="assets/revanced-headline/revanced-headline-vertical-dark.svg"
|
||||
>
|
||||
<img
|
||||
width="256px"
|
||||
src="assets/revanced-headline/revanced-headline-vertical-light.svg"
|
||||
>
|
||||
</picture>
|
||||
<br>
|
||||
<a href="https://revanced.app/">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="assets/revanced-logo/revanced-logo.svg" />
|
||||
<img height="24px" src="assets/revanced-logo/revanced-logo.svg" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://github.com/ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
|
||||
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="http://revanced.app/discord">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://reddit.com/r/revancedapp">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://t.me/app_revanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://x.com/revancedapp">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.youtube.com/@ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<br>
|
||||
<br>
|
||||
Continuing the legacy of Vanced
|
||||
</p>
|
||||
|
||||
# 👋 Contribution guidelines
|
||||
|
||||
This document describes how to contribute to ReVanced Patches.
|
||||
|
||||
## 📖 Resources to help you get started
|
||||
|
||||
* The [documentation](https://github.com/ReVanced/revanced-patcher/tree/main/docs) contains the fundamentals
|
||||
of ReVanced Patcher and how to use ReVanced Patcher to create patches
|
||||
* [Our backlog](https://github.com/orgs/ReVanced/projects/12) is where we keep track of what we're working on
|
||||
* [Issues](https://github.com/ReVanced/revanced-patches/issues) are where we keep track of bugs and feature requests
|
||||
|
||||
## 🙏 Submitting a feature request
|
||||
|
||||
Features can be requested by opening an issue using the
|
||||
[Feature request issue template](https://github.com/ReVanced/revanced-patches/issues/new?assignees=&labels=Feature+request&projects=&template=feature_request.yml&title=feat%3A+).
|
||||
|
||||
> **Note**
|
||||
> Requests can be accepted or rejected at the discretion of maintainers of ReVanced Patches.
|
||||
> Good motivation has to be provided for a request to be accepted.
|
||||
|
||||
## 🐞 Submitting a bug report
|
||||
|
||||
If you encounter a bug while using ReVanced Patches, open an issue using the
|
||||
[Bug report issue template](https://github.com/ReVanced/revanced-patches/issues/new?assignees=&labels=Bug+report&projects=&template=bug_report.yml&title=bug%3A+).
|
||||
|
||||
## 🌐 Submitting translations
|
||||
|
||||
You can contribute translations at [translate.revanced.app](https://translate.revanced.app).
|
||||
|
||||
## 🧑⚖️ Guidelines for requesting or contributing patches
|
||||
|
||||
To maintain a high-quality and ethical collection of patches, the following guidelines for requesting
|
||||
or contributing patches are effective as of September 14, 2023. Any patches present prior to this date
|
||||
are unaffected by this change.
|
||||
|
||||
> **Note**
|
||||
> We generally adhere to the guidelines outlined below. However, we may make exceptions
|
||||
> in specific cases based on our discretion. Pull requests for patches that deviate from the guidelines
|
||||
> will be evaluated individually. While a patch may not align with our general guidelines,
|
||||
> we will consider its acceptance on a case-by-case basis, taking into account its impact on user experience
|
||||
> and ethical considerations. We reserve the right to make exceptions for patches that provide significant value.
|
||||
|
||||
✅ Examples for acceptable patches include:
|
||||
|
||||
* Customizations: Feel free to contribute patches that allow users to personalize their experience
|
||||
* Ad-Blocking: Patches aimed at enhancing user privacy and blocking intrusive advertisements are appreciated
|
||||
* Feature additions: Patches that add new features or change behaviour to the app are welcome
|
||||
|
||||
❌ Examples for unacceptable patches include:
|
||||
|
||||
* Payment circumvention: We do not accept patches that exist solely to bypass payment for the app or any of its features
|
||||
* Malicious patches: Patches that are malicious in nature are not allowed
|
||||
|
||||
## 📝 How to contribute
|
||||
|
||||
1. Before contributing, it is recommended to open an issue to discuss your change
|
||||
with the maintainers of ReVanced Patches. This will help you determine whether your change is acceptable
|
||||
and whether it is worth your time to implement it
|
||||
2. Development happens on the `dev` branch. Fork the repository and create your branch from `dev`
|
||||
3. Commit your changes. In case you are contributing a new patch, make sure to follow the conventions for patches
|
||||
described in the [ReVanced Patcher documentation](https://github.com/ReVanced/revanced-patcher/tree/main/docs)
|
||||
4. Submit a pull request to the `dev` branch of the repository and reference issues
|
||||
that your pull request closes in the description of your pull request
|
||||
5. Our team will review your pull request and provide feedback. Once your pull request is approved,
|
||||
it will be merged into the `dev` branch and will be included in the next release of ReVanced Patches
|
||||
|
||||
❤️ Thank you for considering contributing to ReVanced Patches,
|
||||
ReVanced
|
107
README.md
107
README.md
@ -1,2 +1,105 @@
|
||||
# revanced-patches
|
||||
Repo for all ReVanced patches
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source
|
||||
width="256px"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcset="assets/revanced-headline/revanced-headline-vertical-dark.svg"
|
||||
>
|
||||
<img
|
||||
width="256px"
|
||||
src="assets/revanced-headline/revanced-headline-vertical-light.svg"
|
||||
>
|
||||
</picture>
|
||||
<br>
|
||||
<a href="https://revanced.app/">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="assets/revanced-logo/revanced-logo.svg" />
|
||||
<img height="24px" src="assets/revanced-logo/revanced-logo.svg" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://github.com/ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
|
||||
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="http://revanced.app/discord">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://reddit.com/r/revancedapp">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://t.me/app_revanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://x.com/revancedapp">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.youtube.com/@ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<br>
|
||||
<br>
|
||||
Continuing the legacy of Vanced
|
||||
</p>
|
||||
|
||||
# 🧩 ReVanced Patches
|
||||
|
||||

|
||||

|
||||
|
||||
This repository contains a collection of ReVanced Patches.
|
||||
|
||||
## ❓ About
|
||||
|
||||
Patches are small modifications to Android apps that allow you to change the behavior of or add new features,
|
||||
block ads, customize the appearance, and much more.
|
||||
|
||||
## 💪 Features
|
||||
|
||||
Some of the features the patches provide are:
|
||||
|
||||
* 🚫 **Block ads**: Say goodbye to ads
|
||||
* ⭐ **Customize your app**: Personalize the appearance of apps with various layouts and themes
|
||||
* 🪄 **Add new features**: Extend the functionality of apps with lots of new features
|
||||
* ⚙️ **Miscellaneous and general purpose**: Rename packages, enable debugging, disable screen capture restrictions,
|
||||
export activities, etc.
|
||||
* ✨ **And much more!**
|
||||
|
||||
For a complete list of all available patches, visit [revanced.app/patches](https://revanced.app/patches).
|
||||
|
||||
## 🚀 How to get started
|
||||
|
||||
You can use [ReVanced CLI](https://github.com/ReVanced/revanced-cli) or [ReVanced Manager](https://github.com/ReVanced/revanced-manager) to use ReVanced Patches.
|
||||
|
||||
## 📚 Everything else
|
||||
|
||||
### 📙 Contributing
|
||||
|
||||
Thank you for considering contributing to ReVanced Patches. You can find the contribution guidelines [here](CONTRIBUTING.md).
|
||||
|
||||
### 🛠️ Building
|
||||
|
||||
To build ReVanced Patches, you can follow the [ReVanced documentation](https://github.com/ReVanced/revanced-documentation).
|
||||
|
||||
## 📜 Licence
|
||||
|
||||
ReVanced Patches is licensed under the GPLv3 license. Please see the [license file](LICENSE) for more information.
|
||||
[tl;dr](https://www.tldrlegal.com/license/gnu-general-public-license-v3-gpl-3) you may copy, distribute and modify ReVanced Patches as long as you track changes/dates in source files.
|
||||
Any modifications to ReVanced Patches must also be made available under the GPL,
|
||||
along with build & install instructions.
|
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 11 KiB |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 11 KiB |
1
assets/revanced-logo/revanced-logo.svg
Normal file
1
assets/revanced-logo/revanced-logo.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 800 800" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><g id="Logo"><g id="Ring"><circle id="Ring-Background" serif:id="Ring Background" cx="400" cy="400" r="400" style="fill:#1b1b1b;"/><path id="Ring1" serif:id="Ring" d="M400,0c220.766,0 400,179.234 400,400c-0,220.766 -179.234,400 -400,400c-220.766,-0 -400,-179.234 -400,-400c0,-220.766 179.234,-400 400,-400Zm-0,36c200.897,-0 364,163.103 364,364c0,200.897 -163.103,364 -364,364c-200.897,0 -364,-163.103 -364,-364c-0,-200.897 163.103,-364 364,-364Z" style="fill:url(#_Linear1);"/></g><g id="Shape"><path id="V-Shape" serif:id="V Shape" d="M538.74,269.872c1.481,-3.382 1.157,-7.283 -0.863,-10.373c-2.021,-3.091 -5.464,-4.954 -9.156,-4.954c-5.148,0 -10.435,0 -14.165,0c-3.1,0 -5.907,1.834 -7.153,4.672c-12.468,28.396 -78.273,178.273 -100.25,228.328c-1.246,2.838 -4.053,4.671 -7.154,4.671c-3.1,0 -5.907,-1.833 -7.153,-4.671c-21.977,-50.055 -87.782,-199.932 -100.25,-228.328c-1.246,-2.838 -4.053,-4.672 -7.153,-4.672c-3.73,0 -9.017,0 -14.164,0c-3.693,0 -7.135,1.863 -9.156,4.954c-2.02,3.09 -2.344,6.991 -0.863,10.373c23.557,53.766 101.872,232.519 117.871,269.034c1.743,3.979 5.674,6.549 10.018,6.549c6.293,-0 15.408,-0 21.701,-0c4.344,-0 8.275,-2.57 10.018,-6.549c15.999,-36.515 94.315,-215.268 117.872,-269.034Z" style="fill:#fff;"/><path id="Diamond" d="M408.119,395.312c-1.675,2.901 -4.77,4.688 -8.119,4.688c-3.349,-0 -6.444,-1.787 -8.119,-4.688c-16.997,-29.44 -56.156,-97.264 -73.153,-126.704c-1.675,-2.901 -1.675,-6.474 0,-9.375c1.675,-2.901 4.77,-4.688 8.119,-4.688c33.995,0 112.311,0 146.306,0c3.349,0 6.444,1.787 8.119,4.688c1.675,2.901 1.675,6.474 -0,9.375c-16.997,29.44 -56.156,97.264 -73.153,126.704Z" style="fill:url(#_Linear2);"/></g></g><defs><linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(4.89859e-14,800,-800,4.89859e-14,400.001,3.31681e-10)"><stop offset="0" style="stop-color:#f04e98;stop-opacity:1"/><stop offset="0.5" style="stop-color:#5f65d4;stop-opacity:1"/><stop offset="1" style="stop-color:#4e98f0;stop-opacity:1"/></linearGradient><linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.77155e-14,289.317,-282.535,1.73003e-14,400,254.545)"><stop offset="0" style="stop-color:#f04e98;stop-opacity:1"/><stop offset="0.5" style="stop-color:#5f65d4;stop-opacity:1"/><stop offset="1" style="stop-color:#4e98f0;stop-opacity:1"/></linearGradient></defs></svg>
|
After Width: | Height: | Size: 2.8 KiB |
@ -1,56 +0,0 @@
|
||||
plugins {
|
||||
kotlin("jvm") version "1.6.10"
|
||||
java
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
group = "net.revanced"
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven {
|
||||
url = uri("https://maven.pkg.github.com/ReVancedTeam/revanced-patcher") // note the "r"!
|
||||
credentials {
|
||||
// DO NOT set these variables in the project's gradle.properties.
|
||||
// Instead, you should set them in:
|
||||
// Windows: %homepath%\.gradle\gradle.properties
|
||||
// Linux: ~/.gradle/gradle.properties
|
||||
username = project.findProperty("gpr.user") as String? ?: System.getenv("GITHUB_ACTOR") // DO NOT CHANGE!
|
||||
password = project.findProperty("gpr.key") as String? ?: System.getenv("GITHUB_TOKEN") // DO NOT CHANGE!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib:1.6.10")
|
||||
|
||||
implementation("org.ow2.asm:asm:9.2")
|
||||
implementation("org.ow2.asm:asm-util:9.2")
|
||||
implementation("org.ow2.asm:asm-tree:9.2")
|
||||
implementation("org.ow2.asm:asm-commons:9.2")
|
||||
|
||||
implementation("net.revanced:revanced-patcher:1.+") // use latest version.
|
||||
}
|
||||
|
||||
java {
|
||||
withSourcesJar()
|
||||
withJavadocJar()
|
||||
}
|
||||
|
||||
publishing {
|
||||
repositories {
|
||||
maven {
|
||||
name = "GitHubPackages"
|
||||
url = uri("https://maven.pkg.github.com/ReVancedTeam/revanced-patches") // note the "s"!
|
||||
credentials {
|
||||
username = System.getenv("GITHUB_ACTOR")
|
||||
password = System.getenv("GITHUB_TOKEN")
|
||||
}
|
||||
}
|
||||
}
|
||||
publications {
|
||||
register<MavenPublication>("gpr") {
|
||||
from(components["java"])
|
||||
}
|
||||
}
|
||||
}
|
8
crowdin.yml
Normal file
8
crowdin.yml
Normal file
@ -0,0 +1,8 @@
|
||||
project_id_env: "CROWDIN_PROJECT_ID"
|
||||
api_token_env: "CROWDIN_PERSONAL_TOKEN"
|
||||
|
||||
preserve_hierarchy: false
|
||||
files:
|
||||
- source: patches/src/main/resources/addresources/values/strings.xml
|
||||
translation: patches/src/main/resources/addresources/values-%android_code%/strings.xml
|
||||
skip_untranslated_strings: true
|
16
extensions/all/misc/adb/hide-adb/build.gradle.kts
Normal file
16
extensions/all/misc/adb/hide-adb/build.gradle.kts
Normal file
@ -0,0 +1,16 @@
|
||||
android {
|
||||
namespace = "app.revanced.extension"
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 21
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(libs.annotation)
|
||||
}
|
@ -0,0 +1 @@
|
||||
<manifest/>
|
@ -0,0 +1,28 @@
|
||||
package app.revanced.extension.all.misc.hide.adb;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.provider.Settings;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class HideAdbPatch {
|
||||
private static final List<String> SPOOF_SETTINGS = Arrays.asList("adb_enabled", "adb_wifi_enabled", "development_settings_enabled");
|
||||
|
||||
public static int getInt(ContentResolver cr, String name) throws Settings.SettingNotFoundException {
|
||||
if (SPOOF_SETTINGS.contains(name)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Settings.Global.getInt(cr, name);
|
||||
}
|
||||
|
||||
public static int getInt(ContentResolver cr, String name, int def) {
|
||||
if (SPOOF_SETTINGS.contains(name)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Settings.Global.getInt(cr, name, def);
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
android {
|
||||
namespace = "app.revanced.extension"
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(libs.annotation)
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
</manifest>
|
@ -0,0 +1,424 @@
|
||||
package app.revanced.extension.all.misc.connectivity.wifi.spoof;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.Network;
|
||||
import android.net.NetworkCapabilities;
|
||||
import android.net.NetworkInfo;
|
||||
import android.net.NetworkRequest;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
@SuppressWarnings({"deprecation", "unused"})
|
||||
public class SpoofWifiPatch {
|
||||
|
||||
// Used to check what the (real or fake) active network is (take a look at `hasTransport`).
|
||||
private static ConnectivityManager CONNECTIVITY_MANAGER;
|
||||
|
||||
// If Wifi is not enabled, these are types that would pretend to be Wifi for android.net.Network (lower index = higher priority).
|
||||
// This does not apply to android.net.NetworkInfo, because we can pretend that Wifi is always active there.
|
||||
//
|
||||
// VPN should be a fallback, because Reverse Tethering uses VPN.
|
||||
private static final int[] FAKE_FALLBACK_NETWORKS = { NetworkCapabilities.TRANSPORT_ETHERNET, NetworkCapabilities.TRANSPORT_VPN };
|
||||
|
||||
// In order to initialize our own ConnectivityManager, if it isn't initialized yet.
|
||||
public static Object getSystemService(Context context, String name) {
|
||||
Object result = context.getSystemService(name);
|
||||
if (CONNECTIVITY_MANAGER == null) {
|
||||
if (Context.CONNECTIVITY_SERVICE.equals(name)) {
|
||||
CONNECTIVITY_MANAGER = (ConnectivityManager) result;
|
||||
} else {
|
||||
CONNECTIVITY_MANAGER = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// In order to initialize our own ConnectivityManager, if it isn't initialized yet.
|
||||
public static Object getSystemService(Context context, Class<?> serviceClass) {
|
||||
Object result = context.getSystemService(serviceClass);
|
||||
if (CONNECTIVITY_MANAGER == null) {
|
||||
if (serviceClass == ConnectivityManager.class) {
|
||||
CONNECTIVITY_MANAGER = (ConnectivityManager) result;
|
||||
} else {
|
||||
CONNECTIVITY_MANAGER = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Simply always return Wifi as active network.
|
||||
public static NetworkInfo getActiveNetworkInfo(ConnectivityManager connectivityManager) {
|
||||
for (NetworkInfo networkInfo : connectivityManager.getAllNetworkInfo()) {
|
||||
if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
|
||||
return networkInfo;
|
||||
}
|
||||
}
|
||||
return connectivityManager.getActiveNetworkInfo();
|
||||
}
|
||||
|
||||
// Pretend Wifi is always connected.
|
||||
public static boolean isConnected(NetworkInfo networkInfo) {
|
||||
if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
|
||||
return true;
|
||||
}
|
||||
return networkInfo.isConnected();
|
||||
}
|
||||
|
||||
// Pretend Wifi is always connected.
|
||||
public static boolean isConnectedOrConnecting(NetworkInfo networkInfo) {
|
||||
if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
|
||||
return true;
|
||||
}
|
||||
return networkInfo.isConnectedOrConnecting();
|
||||
}
|
||||
|
||||
// Pretend Wifi is always available.
|
||||
public static boolean isAvailable(NetworkInfo networkInfo) {
|
||||
if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
|
||||
return true;
|
||||
}
|
||||
return networkInfo.isAvailable();
|
||||
}
|
||||
|
||||
// Pretend Wifi is always connected.
|
||||
public static NetworkInfo.State getState(NetworkInfo networkInfo) {
|
||||
if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
|
||||
return NetworkInfo.State.CONNECTED;
|
||||
}
|
||||
return networkInfo.getState();
|
||||
}
|
||||
|
||||
// Pretend Wifi is always connected.
|
||||
public static NetworkInfo.DetailedState getDetailedState(NetworkInfo networkInfo) {
|
||||
if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
|
||||
return NetworkInfo.DetailedState.CONNECTED;
|
||||
}
|
||||
return networkInfo.getDetailedState();
|
||||
}
|
||||
|
||||
// Pretend Wifi is enabled, so connection isn't metered.
|
||||
public static boolean isActiveNetworkMetered(ConnectivityManager connectivityManager) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Returns the Wifi network, if Wifi is enabled.
|
||||
// Otherwise if one of our fallbacks has a connection, return them.
|
||||
// And as a last resort, return the default active network.
|
||||
public static Network getActiveNetwork(ConnectivityManager connectivityManager) {
|
||||
Network[] prioritizedNetworks = new Network[FAKE_FALLBACK_NETWORKS.length];
|
||||
for (Network network : connectivityManager.getAllNetworks()) {
|
||||
NetworkCapabilities networkCapabilities = connectivityManager.getNetworkCapabilities(network);
|
||||
if (networkCapabilities == null) {
|
||||
continue;
|
||||
}
|
||||
if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
|
||||
return network;
|
||||
}
|
||||
if (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
|
||||
for (int i = 0; i < FAKE_FALLBACK_NETWORKS.length; i++) {
|
||||
int transportType = FAKE_FALLBACK_NETWORKS[i];
|
||||
if (networkCapabilities.hasTransport(transportType)) {
|
||||
prioritizedNetworks[i] = network;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (Network network : prioritizedNetworks) {
|
||||
if (network != null) {
|
||||
return network;
|
||||
}
|
||||
}
|
||||
return connectivityManager.getActiveNetwork();
|
||||
}
|
||||
|
||||
// If the given network is a real or fake Wifi connection, return a Wifi network.
|
||||
// Otherwise fallback to default implementation.
|
||||
public static NetworkInfo getNetworkInfo(ConnectivityManager connectivityManager, Network network) {
|
||||
NetworkCapabilities networkCapabilities = connectivityManager.getNetworkCapabilities(network);
|
||||
if (networkCapabilities != null && hasTransport(networkCapabilities, NetworkCapabilities.TRANSPORT_WIFI)) {
|
||||
for (NetworkInfo networkInfo : connectivityManager.getAllNetworkInfo()) {
|
||||
if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
|
||||
return networkInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
return connectivityManager.getNetworkInfo(network);
|
||||
}
|
||||
|
||||
// If we are checking if the NetworkCapabilities use Wifi, return yes if
|
||||
// - it is a real Wifi connection,
|
||||
// - or the NetworkCapabilities are from a network pretending being a Wifi network.
|
||||
// Otherwise fallback to default implementation.
|
||||
public static boolean hasTransport(NetworkCapabilities networkCapabilities, int transportType) {
|
||||
if (transportType == NetworkCapabilities.TRANSPORT_WIFI) {
|
||||
if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
|
||||
return true;
|
||||
}
|
||||
if (CONNECTIVITY_MANAGER != null) {
|
||||
Network activeNetwork = getActiveNetwork(CONNECTIVITY_MANAGER);
|
||||
NetworkCapabilities activeNetworkCapabilities = CONNECTIVITY_MANAGER.getNetworkCapabilities(activeNetwork);
|
||||
if (activeNetworkCapabilities != null) {
|
||||
for (int fallbackTransportType : FAKE_FALLBACK_NETWORKS) {
|
||||
if (activeNetworkCapabilities.hasTransport(fallbackTransportType) && networkCapabilities.hasTransport(fallbackTransportType)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return networkCapabilities.hasTransport(transportType);
|
||||
}
|
||||
|
||||
// If the given network is a real or fake Wifi connection, pretend it has a connection (and some other things).
|
||||
public static boolean hasCapability(NetworkCapabilities networkCapabilities, int capability) {
|
||||
if (hasTransport(networkCapabilities, NetworkCapabilities.TRANSPORT_WIFI) && (
|
||||
capability == NetworkCapabilities.NET_CAPABILITY_INTERNET
|
||||
|| capability == NetworkCapabilities.NET_CAPABILITY_FOREGROUND
|
||||
|| capability == NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED
|
||||
|| capability == NetworkCapabilities.NET_CAPABILITY_NOT_METERED
|
||||
|| capability == NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED
|
||||
|| capability == NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
|
||||
|| capability == NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
|
||||
|| capability == NetworkCapabilities.NET_CAPABILITY_NOT_VPN
|
||||
|| capability == NetworkCapabilities.NET_CAPABILITY_TRUSTED
|
||||
|| capability == NetworkCapabilities.NET_CAPABILITY_VALIDATED)) {
|
||||
return true;
|
||||
}
|
||||
return networkCapabilities.hasCapability(capability);
|
||||
}
|
||||
|
||||
// If it waits for Wifi connectivity, pretend it is fulfilled immediately if we have an active network.
|
||||
@RequiresApi(api = Build.VERSION_CODES.S)
|
||||
public static void registerBestMatchingNetworkCallback(ConnectivityManager connectivityManager, NetworkRequest request, ConnectivityManager.NetworkCallback networkCallback, Handler handler) {
|
||||
Utils.networkCallback(
|
||||
connectivityManager,
|
||||
Utils.Option.of(request),
|
||||
Utils.Option.of(networkCallback),
|
||||
Utils.Option.empty(),
|
||||
Utils.Option.of(handler),
|
||||
() -> connectivityManager.registerBestMatchingNetworkCallback(request, networkCallback, handler)
|
||||
);
|
||||
}
|
||||
|
||||
// If it waits for Wifi connectivity, pretend it is fulfilled immediately if we have an active network.
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
public static void registerDefaultNetworkCallback(ConnectivityManager connectivityManager, ConnectivityManager.NetworkCallback networkCallback) {
|
||||
Utils.networkCallback(
|
||||
connectivityManager,
|
||||
Utils.Option.empty(),
|
||||
Utils.Option.of(networkCallback),
|
||||
Utils.Option.empty(),
|
||||
Utils.Option.empty(),
|
||||
() -> connectivityManager.registerDefaultNetworkCallback(networkCallback)
|
||||
);
|
||||
}
|
||||
|
||||
// If it waits for Wifi connectivity, pretend it is fulfilled immediately if we have an active network.
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
public static void registerDefaultNetworkCallback(ConnectivityManager connectivityManager, ConnectivityManager.NetworkCallback networkCallback, Handler handler) {
|
||||
Utils.networkCallback(
|
||||
connectivityManager,
|
||||
Utils.Option.empty(),
|
||||
Utils.Option.of(networkCallback),
|
||||
Utils.Option.empty(),
|
||||
Utils.Option.of(handler),
|
||||
() -> connectivityManager.registerDefaultNetworkCallback(networkCallback, handler)
|
||||
);
|
||||
}
|
||||
|
||||
// If it waits for Wifi connectivity, pretend it is fulfilled immediately if we have an active network.
|
||||
public static void registerNetworkCallback(ConnectivityManager connectivityManager, NetworkRequest request, ConnectivityManager.NetworkCallback networkCallback) {
|
||||
Utils.networkCallback(
|
||||
connectivityManager,
|
||||
Utils.Option.of(request),
|
||||
Utils.Option.of(networkCallback),
|
||||
Utils.Option.empty(),
|
||||
Utils.Option.empty(),
|
||||
() -> connectivityManager.registerNetworkCallback(request, networkCallback)
|
||||
);
|
||||
}
|
||||
|
||||
// If it waits for Wifi connectivity, pretend it is fulfilled immediately.
|
||||
public static void registerNetworkCallback(ConnectivityManager connectivityManager, NetworkRequest request, PendingIntent operation) {
|
||||
Utils.networkCallback(
|
||||
connectivityManager,
|
||||
Utils.Option.of(request),
|
||||
Utils.Option.empty(),
|
||||
Utils.Option.of(operation),
|
||||
Utils.Option.empty(),
|
||||
() -> connectivityManager.registerNetworkCallback(request, operation)
|
||||
);
|
||||
}
|
||||
|
||||
// If it waits for Wifi connectivity, pretend it is fulfilled immediately if we have an active network.
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
public static void registerNetworkCallback(ConnectivityManager connectivityManager, NetworkRequest request, ConnectivityManager.NetworkCallback networkCallback, Handler handler) {
|
||||
Utils.networkCallback(
|
||||
connectivityManager,
|
||||
Utils.Option.of(request),
|
||||
Utils.Option.of(networkCallback),
|
||||
Utils.Option.empty(),
|
||||
Utils.Option.of(handler),
|
||||
() -> connectivityManager.registerNetworkCallback(request, networkCallback, handler)
|
||||
);
|
||||
}
|
||||
|
||||
// If it requests Wifi connectivity, pretend it is fulfilled immediately if we have an active network.
|
||||
public static void requestNetwork(ConnectivityManager connectivityManager, NetworkRequest request, ConnectivityManager.NetworkCallback networkCallback) {
|
||||
Utils.networkCallback(
|
||||
connectivityManager,
|
||||
Utils.Option.of(request),
|
||||
Utils.Option.of(networkCallback),
|
||||
Utils.Option.empty(),
|
||||
Utils.Option.empty(),
|
||||
() -> connectivityManager.requestNetwork(request, networkCallback)
|
||||
);
|
||||
}
|
||||
|
||||
// If it requests Wifi connectivity, pretend it is fulfilled immediately if we have an active network.
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
public static void requestNetwork(ConnectivityManager connectivityManager, NetworkRequest request, ConnectivityManager.NetworkCallback networkCallback, int timeoutMs) {
|
||||
Utils.networkCallback(
|
||||
connectivityManager,
|
||||
Utils.Option.of(request),
|
||||
Utils.Option.of(networkCallback),
|
||||
Utils.Option.empty(),
|
||||
Utils.Option.empty(),
|
||||
() -> connectivityManager.requestNetwork(request, networkCallback, timeoutMs)
|
||||
);
|
||||
}
|
||||
|
||||
// If it requests Wifi connectivity, pretend it is fulfilled immediately if we have an active network.
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
public static void requestNetwork(ConnectivityManager connectivityManager, NetworkRequest request, ConnectivityManager.NetworkCallback networkCallback, Handler handler) {
|
||||
Utils.networkCallback(
|
||||
connectivityManager,
|
||||
Utils.Option.of(request),
|
||||
Utils.Option.of(networkCallback),
|
||||
Utils.Option.empty(),
|
||||
Utils.Option.of(handler),
|
||||
() -> connectivityManager.requestNetwork(request, networkCallback, handler)
|
||||
);
|
||||
}
|
||||
|
||||
// If it requests Wifi connectivity, pretend it is fulfilled immediately.
|
||||
public static void requestNetwork(ConnectivityManager connectivityManager, NetworkRequest request, PendingIntent operation) {
|
||||
Utils.networkCallback(
|
||||
connectivityManager,
|
||||
Utils.Option.of(request),
|
||||
Utils.Option.empty(),
|
||||
Utils.Option.of(operation),
|
||||
Utils.Option.empty(),
|
||||
() -> connectivityManager.requestNetwork(request, operation)
|
||||
);
|
||||
}
|
||||
|
||||
// If it requests Wifi connectivity, pretend it is fulfilled immediately if we have an active network.
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
public static void requestNetwork(ConnectivityManager connectivityManager, NetworkRequest request, ConnectivityManager.NetworkCallback networkCallback, Handler handler, int timeoutMs) {
|
||||
Utils.networkCallback(
|
||||
connectivityManager,
|
||||
Utils.Option.of(request),
|
||||
Utils.Option.of(networkCallback),
|
||||
Utils.Option.empty(),
|
||||
Utils.Option.of(handler),
|
||||
() -> connectivityManager.requestNetwork(request, networkCallback, handler, timeoutMs)
|
||||
);
|
||||
}
|
||||
|
||||
public static void unregisterNetworkCallback(ConnectivityManager connectivityManager, ConnectivityManager.NetworkCallback networkCallback) {
|
||||
try {
|
||||
connectivityManager.unregisterNetworkCallback(networkCallback);
|
||||
} catch (IllegalArgumentException ignore) {
|
||||
// ignore: NetworkCallback was not registered
|
||||
}
|
||||
}
|
||||
|
||||
public static void unregisterNetworkCallback(ConnectivityManager connectivityManager, PendingIntent operation) {
|
||||
try {
|
||||
connectivityManager.unregisterNetworkCallback(operation);
|
||||
} catch (IllegalArgumentException ignore) {
|
||||
// ignore: PendingIntent was not registered
|
||||
}
|
||||
}
|
||||
|
||||
private static class Utils {
|
||||
private static class Option<T> {
|
||||
private final T value;
|
||||
private final boolean isPresent;
|
||||
|
||||
private Option(T value, boolean isPresent) {
|
||||
this.value = value;
|
||||
this.isPresent = isPresent;
|
||||
}
|
||||
|
||||
private static <T> Option<T> of(T value) {
|
||||
return new Option<>(value, true);
|
||||
}
|
||||
|
||||
private static <T> Option<T> empty() {
|
||||
return new Option<>(null, false);
|
||||
}
|
||||
}
|
||||
|
||||
private static void networkCallback(
|
||||
ConnectivityManager connectivityManager,
|
||||
Option<NetworkRequest> request,
|
||||
Option<ConnectivityManager.NetworkCallback> networkCallback,
|
||||
Option<PendingIntent> operation,
|
||||
Option<Handler> handler,
|
||||
Runnable fallback
|
||||
) {
|
||||
if(!request.isPresent || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && request.value != null && requestsWifiNetwork(request.value))) {
|
||||
Runnable runnable = null;
|
||||
if (networkCallback.isPresent && networkCallback.value != null) {
|
||||
Network network = activeWifiNetwork(connectivityManager);
|
||||
if (network != null) {
|
||||
runnable = () -> networkCallback.value.onAvailable(network);
|
||||
}
|
||||
} else if (operation.isPresent && operation.value != null) {
|
||||
runnable = () -> {
|
||||
try {
|
||||
operation.value.send();
|
||||
} catch (PendingIntent.CanceledException ignore) {}
|
||||
};
|
||||
}
|
||||
if (runnable != null) {
|
||||
if (handler.isPresent) {
|
||||
if (handler.value != null) {
|
||||
handler.value.post(runnable);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
runnable.run();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
fallback.run();
|
||||
}
|
||||
|
||||
// Returns an active (maybe fake) Wifi network if there is one, otherwise null.
|
||||
private static Network activeWifiNetwork(ConnectivityManager connectivityManager) {
|
||||
Network network = getActiveNetwork(connectivityManager);
|
||||
NetworkCapabilities networkCapabilities = connectivityManager.getNetworkCapabilities(network);
|
||||
if (networkCapabilities != null && hasTransport(networkCapabilities, NetworkCapabilities.TRANSPORT_WIFI)) {
|
||||
return network;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Whether a Wifi network with connection is requested.
|
||||
@RequiresApi(api = Build.VERSION_CODES.P)
|
||||
private static boolean requestsWifiNetwork(NetworkRequest request) {
|
||||
return request.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
&& (request.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
|| request.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
android {
|
||||
namespace = "app.revanced.extension"
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 21
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(libs.annotation)
|
||||
}
|
@ -0,0 +1 @@
|
||||
<manifest/>
|
@ -0,0 +1,336 @@
|
||||
package app.revanced.extension.all.misc.directory.documentsprovider;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.ProviderInfo;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.os.CancellationSignal;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.DocumentsProvider;
|
||||
import android.system.ErrnoException;
|
||||
import android.system.Os;
|
||||
import android.system.StructStat;
|
||||
import android.util.Log;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A DocumentsProvider that allows access to the app's internal data directory.
|
||||
*/
|
||||
@SuppressLint("LongLogTag")
|
||||
public class InternalDataDocumentsProvider extends DocumentsProvider {
|
||||
private static final String[] rootColumns =
|
||||
{"root_id", "mime_types", "flags", "icon", "title", "summary", "document_id"};
|
||||
private static final String[] directoryColumns =
|
||||
{"document_id", "mime_type", "_display_name", "last_modified", "flags",
|
||||
"_size", "full_path", "lstat_info"};
|
||||
private static final int S_IFLNK = 0x8000;
|
||||
|
||||
private String packageName;
|
||||
private File dataDirectory;
|
||||
|
||||
/**
|
||||
* Recursively delete a file or directory and all its children.
|
||||
*
|
||||
* @param root The file or directory to delete.
|
||||
* @return True if the file or directory and all its children were successfully deleted.
|
||||
*/
|
||||
private static boolean deleteRecursively(File root) {
|
||||
// If root is a directory, delete all children first
|
||||
if (root.isDirectory()) {
|
||||
try {
|
||||
// Only delete recursively if the directory is not a symlink
|
||||
if ((Os.lstat(root.getPath()).st_mode & S_IFLNK) != S_IFLNK) {
|
||||
File[] files = root.listFiles();
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
if (!deleteRecursively(file)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (ErrnoException e) {
|
||||
Log.e("InternalDocumentsProvider", "Failed to lstat " + root.getPath(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete file or empty directory
|
||||
return root.delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the MIME type of a file based on its extension.
|
||||
*
|
||||
* @param file The file to resolve the MIME type for.
|
||||
* @return The MIME type of the file.
|
||||
*/
|
||||
private static String resolveMimeType(File file) {
|
||||
if (file.isDirectory()) {
|
||||
return DocumentsContract.Document.MIME_TYPE_DIR;
|
||||
}
|
||||
|
||||
String name = file.getName();
|
||||
int indexOfExtDot = name.lastIndexOf('.');
|
||||
if (indexOfExtDot < 0) {
|
||||
// No extension
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
String extension = name.substring(indexOfExtDot + 1).toLowerCase();
|
||||
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
||||
return mimeType != null ? mimeType : "application/octet-stream";
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean onCreate() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void attachInfo(Context context, ProviderInfo providerInfo) {
|
||||
super.attachInfo(context, providerInfo);
|
||||
|
||||
this.packageName = context.getPackageName();
|
||||
this.dataDirectory = context.getFilesDir().getParentFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String createDocument(String parentDocumentId, String mimeType, String displayName) throws FileNotFoundException {
|
||||
File directory = resolveDocumentId(parentDocumentId);
|
||||
File file = new File(directory, displayName);
|
||||
|
||||
// If file already exists, append a number to the name
|
||||
int i = 2;
|
||||
while (file.exists()) {
|
||||
file = new File(directory, displayName + " (" + i + ")");
|
||||
i++;
|
||||
}
|
||||
|
||||
try {
|
||||
// Create the file or directory
|
||||
if (mimeType.equals(DocumentsContract.Document.MIME_TYPE_DIR) ? file.mkdir() : file.createNewFile()) {
|
||||
// Return the document ID of the new entity
|
||||
if (!parentDocumentId.endsWith("/")) {
|
||||
parentDocumentId = parentDocumentId + "/";
|
||||
}
|
||||
return parentDocumentId + file.getName();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Do nothing. We are throwing a FileNotFoundException later if the file could not be created.
|
||||
}
|
||||
throw new FileNotFoundException("Failed to create document in " + parentDocumentId + " with name " + displayName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void deleteDocument(String documentId) throws FileNotFoundException {
|
||||
File file = resolveDocumentId(documentId);
|
||||
if (!deleteRecursively(file)) {
|
||||
throw new FileNotFoundException("Failed to delete document " + documentId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String getDocumentType(String documentId) throws FileNotFoundException {
|
||||
return resolveMimeType(resolveDocumentId(documentId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean isChildDocument(String parentDocumentId, String documentId) {
|
||||
return documentId.startsWith(parentDocumentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String moveDocument(String sourceDocumentId, String sourceParentDocumentId, String targetParentDocumentId) throws FileNotFoundException {
|
||||
File source = resolveDocumentId(sourceDocumentId);
|
||||
File dest = resolveDocumentId(targetParentDocumentId);
|
||||
|
||||
File file = new File(dest, source.getName());
|
||||
if (!file.exists() && source.renameTo(file)) {
|
||||
// Return the new document ID
|
||||
if (targetParentDocumentId.endsWith("/")) {
|
||||
return targetParentDocumentId + file.getName();
|
||||
}
|
||||
return targetParentDocumentId + "/" + file.getName();
|
||||
}
|
||||
|
||||
throw new FileNotFoundException("Failed to move document from " + sourceDocumentId + " to " + targetParentDocumentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final ParcelFileDescriptor openDocument(String documentId, String mode, CancellationSignal signal) throws FileNotFoundException {
|
||||
File file = resolveDocumentId(documentId);
|
||||
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.parseMode(mode));
|
||||
}
|
||||
|
||||
@Override
|
||||
public final Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException {
|
||||
if (parentDocumentId.endsWith("/")) {
|
||||
parentDocumentId = parentDocumentId.substring(0, parentDocumentId.length() - 1);
|
||||
}
|
||||
|
||||
if (projection == null) {
|
||||
projection = directoryColumns;
|
||||
}
|
||||
|
||||
MatrixCursor cursor = new MatrixCursor(projection);
|
||||
File children = resolveDocumentId(parentDocumentId);
|
||||
|
||||
// Collect all children
|
||||
File[] files = children.listFiles();
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
addRowForDocument(cursor, parentDocumentId + "/" + file.getName(), file);
|
||||
}
|
||||
}
|
||||
return cursor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException {
|
||||
if (projection == null) {
|
||||
projection = directoryColumns;
|
||||
}
|
||||
|
||||
MatrixCursor cursor = new MatrixCursor(projection);
|
||||
addRowForDocument(cursor, documentId, null);
|
||||
return cursor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final Cursor queryRoots(String[] projection) {
|
||||
ApplicationInfo info = Objects.requireNonNull(getContext()).getApplicationInfo();
|
||||
String appName = info.loadLabel(getContext().getPackageManager()).toString();
|
||||
|
||||
if (projection == null) {
|
||||
projection = rootColumns;
|
||||
}
|
||||
|
||||
MatrixCursor cursor = new MatrixCursor(projection);
|
||||
MatrixCursor.RowBuilder row = cursor.newRow();
|
||||
row.add(DocumentsContract.Root.COLUMN_ROOT_ID, this.packageName);
|
||||
row.add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, this.packageName);
|
||||
row.add(DocumentsContract.Root.COLUMN_SUMMARY, this.packageName);
|
||||
row.add(DocumentsContract.Root.COLUMN_FLAGS,
|
||||
DocumentsContract.Root.FLAG_LOCAL_ONLY |
|
||||
DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD);
|
||||
row.add(DocumentsContract.Root.COLUMN_TITLE, appName);
|
||||
row.add(DocumentsContract.Root.COLUMN_MIME_TYPES, "*/*");
|
||||
row.add(DocumentsContract.Root.COLUMN_ICON, info.icon);
|
||||
return cursor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void removeDocument(String documentId, String parentDocumentId) throws FileNotFoundException {
|
||||
deleteDocument(documentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String renameDocument(String documentId, String displayName) throws FileNotFoundException {
|
||||
File file = resolveDocumentId(documentId);
|
||||
if (!file.renameTo(new File(file.getParentFile(), displayName))) {
|
||||
throw new FileNotFoundException("Failed to rename document from " + documentId + " to " + displayName);
|
||||
}
|
||||
|
||||
// Return the new document ID
|
||||
return documentId.substring(0, documentId.lastIndexOf('/', documentId.length() - 2)) + "/" + displayName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a file instance for a given document ID.
|
||||
*
|
||||
* @param fullContentPath The document ID to resolve.
|
||||
* @return File object for the given document ID.
|
||||
* @throws FileNotFoundException If the document ID is invalid or the file does not exist.
|
||||
*/
|
||||
private File resolveDocumentId(String fullContentPath) throws FileNotFoundException {
|
||||
if (!fullContentPath.startsWith(this.packageName)) {
|
||||
throw new FileNotFoundException(fullContentPath + " not found");
|
||||
}
|
||||
String path = fullContentPath.substring(this.packageName.length());
|
||||
|
||||
// Resolve the relative path within /data/data/{PKG}
|
||||
File file;
|
||||
if (path.equals("/") || path.isEmpty()) {
|
||||
file = this.dataDirectory;
|
||||
} else {
|
||||
// Remove leading slash
|
||||
String relativePath = path.substring(1);
|
||||
file = new File(this.dataDirectory, relativePath);
|
||||
}
|
||||
|
||||
if (!file.exists()) {
|
||||
throw new FileNotFoundException(fullContentPath + " not found");
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a row containing all file properties to a MatrixCursor for a given document ID.
|
||||
*
|
||||
* @param cursor The cursor to add the row to.
|
||||
* @param documentId The document ID to add the row for.
|
||||
* @param file The file to add the row for. If null, the file will be resolved from the document ID.
|
||||
* @throws FileNotFoundException If the file does not exist.
|
||||
*/
|
||||
private void addRowForDocument(MatrixCursor cursor, String documentId, File file) throws FileNotFoundException {
|
||||
if (file == null) {
|
||||
file = resolveDocumentId(documentId);
|
||||
}
|
||||
|
||||
int flags = 0;
|
||||
if (file.isDirectory()) {
|
||||
// Prefer list view for directories
|
||||
flags = flags | DocumentsContract.Document.FLAG_DIR_PREFERS_LAST_MODIFIED;
|
||||
}
|
||||
|
||||
if (file.canWrite()) {
|
||||
if (file.isDirectory()) {
|
||||
flags = flags | DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE;
|
||||
}
|
||||
|
||||
flags = flags | DocumentsContract.Document.FLAG_SUPPORTS_WRITE |
|
||||
DocumentsContract.Document.FLAG_SUPPORTS_DELETE |
|
||||
DocumentsContract.Document.FLAG_SUPPORTS_RENAME |
|
||||
DocumentsContract.Document.FLAG_SUPPORTS_MOVE;
|
||||
}
|
||||
|
||||
MatrixCursor.RowBuilder row = cursor.newRow();
|
||||
row.add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, documentId);
|
||||
row.add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, file.getName());
|
||||
row.add(DocumentsContract.Document.COLUMN_SIZE, file.length());
|
||||
row.add(DocumentsContract.Document.COLUMN_MIME_TYPE, resolveMimeType(file));
|
||||
row.add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, file.lastModified());
|
||||
row.add(DocumentsContract.Document.COLUMN_FLAGS, flags);
|
||||
|
||||
// Custom columns
|
||||
row.add("full_path", file.getAbsolutePath());
|
||||
|
||||
// Add lstat column
|
||||
String path = file.getPath();
|
||||
try {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
StructStat lstat = Os.lstat(path);
|
||||
sb.append(lstat.st_mode);
|
||||
sb.append(";");
|
||||
sb.append(lstat.st_uid);
|
||||
sb.append(";");
|
||||
sb.append(lstat.st_gid);
|
||||
// Append symlink target if it is a symlink
|
||||
if ((lstat.st_mode & S_IFLNK) == S_IFLNK) {
|
||||
sb.append(";");
|
||||
sb.append(Os.readlink(path));
|
||||
}
|
||||
row.add("lstat_info", sb.toString());
|
||||
} catch (Exception ex) {
|
||||
Log.e("InternalDocumentsProvider", "Failed to get lstat info for " + path, ex);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
android {
|
||||
namespace = "app.revanced.extension"
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 21
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(libs.annotation)
|
||||
}
|
@ -0,0 +1 @@
|
||||
<manifest/>
|
@ -0,0 +1,22 @@
|
||||
package app.revanced.extension.all.misc.screencapture.removerestriction;
|
||||
|
||||
import android.media.AudioAttributes;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class RemoveScreenCaptureRestrictionPatch {
|
||||
// Member of AudioAttributes.Builder
|
||||
@RequiresApi(api = Build.VERSION_CODES.Q)
|
||||
public static AudioAttributes.Builder setAllowedCapturePolicy(final AudioAttributes.Builder builder, final int capturePolicy) {
|
||||
builder.setAllowedCapturePolicy(AudioAttributes.ALLOW_CAPTURE_BY_ALL);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
// Member of AudioManager static class
|
||||
public static void setAllowedCapturePolicy(final int capturePolicy) {
|
||||
// Ignore request
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
android {
|
||||
namespace = "app.revanced.extension"
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 21
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(libs.annotation)
|
||||
}
|
@ -0,0 +1 @@
|
||||
<manifest/>
|
@ -0,0 +1,16 @@
|
||||
package app.revanced.extension.all.misc.screenshot.removerestriction;
|
||||
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class RemoveScreenshotRestrictionPatch {
|
||||
|
||||
public static void addFlags(Window window, int flags) {
|
||||
window.addFlags(flags & ~WindowManager.LayoutParams.FLAG_SECURE);
|
||||
}
|
||||
|
||||
public static void setFlags(Window window, int flags, int mask) {
|
||||
window.setFlags(flags & ~WindowManager.LayoutParams.FLAG_SECURE, mask & ~WindowManager.LayoutParams.FLAG_SECURE);
|
||||
}
|
||||
}
|
4
extensions/boostforreddit/build.gradle.kts
Normal file
4
extensions/boostforreddit/build.gradle.kts
Normal file
@ -0,0 +1,4 @@
|
||||
dependencies {
|
||||
compileOnly(project(":extensions:shared:library"))
|
||||
compileOnly(project(":extensions:boostforreddit:stub"))
|
||||
}
|
1
extensions/boostforreddit/src/main/AndroidManifest.xml
Normal file
1
extensions/boostforreddit/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
||||
<manifest/>
|
@ -0,0 +1,26 @@
|
||||
package app.revanced.extension.boostforreddit;
|
||||
|
||||
import com.rubenmayayo.reddit.ui.activities.WebViewActivity;
|
||||
|
||||
import app.revanced.extension.shared.fixes.slink.BaseFixSLinksPatch;
|
||||
|
||||
/**
|
||||
* @noinspection unused
|
||||
*/
|
||||
public class FixSLinksPatch extends BaseFixSLinksPatch {
|
||||
static {
|
||||
INSTANCE = new FixSLinksPatch();
|
||||
}
|
||||
|
||||
private FixSLinksPatch() {
|
||||
webViewActivityClass = WebViewActivity.class;
|
||||
}
|
||||
|
||||
public static boolean patchResolveSLink(String link) {
|
||||
return INSTANCE.resolveSLink(link);
|
||||
}
|
||||
|
||||
public static void patchSetAccessToken(String accessToken) {
|
||||
INSTANCE.setAccessToken(accessToken);
|
||||
}
|
||||
}
|
17
extensions/boostforreddit/stub/build.gradle.kts
Normal file
17
extensions/boostforreddit/stub/build.gradle.kts
Normal file
@ -0,0 +1,17 @@
|
||||
plugins {
|
||||
id(libs.plugins.android.library.get().pluginId)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "app.revanced.extension"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
<manifest/>
|
@ -0,0 +1,6 @@
|
||||
package com.rubenmayayo.reddit.ui.activities;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
public class WebViewActivity extends Activity {
|
||||
}
|
5
extensions/music/build.gradle.kts
Normal file
5
extensions/music/build.gradle.kts
Normal file
@ -0,0 +1,5 @@
|
||||
android {
|
||||
defaultConfig {
|
||||
minSdk = 26
|
||||
}
|
||||
}
|
1
extensions/music/src/main/AndroidManifest.xml
Normal file
1
extensions/music/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
||||
<manifest/>
|
@ -0,0 +1,27 @@
|
||||
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;
|
||||
}
|
||||
}
|
4
extensions/nunl/build.gradle.kts
Normal file
4
extensions/nunl/build.gradle.kts
Normal file
@ -0,0 +1,4 @@
|
||||
dependencies {
|
||||
compileOnly(project(":extensions:shared:library"))
|
||||
compileOnly(project(":extensions:nunl:stub"))
|
||||
}
|
1
extensions/nunl/src/main/AndroidManifest.xml
Normal file
1
extensions/nunl/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
||||
<manifest/>
|
@ -0,0 +1,114 @@
|
||||
package app.revanced.extension.nunl.ads;
|
||||
|
||||
import nl.nu.performance.api.client.interfaces.Block;
|
||||
import nl.nu.performance.api.client.unions.SmallArticleLinkFlavor;
|
||||
import nl.nu.performance.api.client.objects.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class HideAdsPatch {
|
||||
private static final String[] blockedHeaderBlocks = {
|
||||
"Aanbiedingen (Adverteerders)",
|
||||
"Aangeboden door NUshop"
|
||||
};
|
||||
|
||||
// "Rubrieken" menu links to ads.
|
||||
private static final String[] blockedLinkBlocks = {
|
||||
"Van onze adverteerders"
|
||||
};
|
||||
|
||||
public static void filterAds(List<Block> blocks) {
|
||||
try {
|
||||
ArrayList<Block> cleanedList = new ArrayList<>();
|
||||
|
||||
boolean skipFullHeader = false;
|
||||
boolean skipUntilDivider = false;
|
||||
|
||||
int index = 0;
|
||||
while (index < blocks.size()) {
|
||||
Block currentBlock = blocks.get(index);
|
||||
|
||||
// Because of pagination, we might not see the Divider in front of it.
|
||||
// Just remove it as is and leave potential extra spacing visible on the screen.
|
||||
if (currentBlock instanceof DpgBannerBlock) {
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (index + 1 < blocks.size()) {
|
||||
// Filter Divider -> DpgMediaBanner -> Divider.
|
||||
if (currentBlock instanceof DividerBlock
|
||||
&& blocks.get(index + 1) instanceof DpgBannerBlock) {
|
||||
index += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Filter Divider -> LinkBlock (... -> LinkBlock -> LinkBlock-> LinkBlock -> Divider).
|
||||
if (currentBlock instanceof DividerBlock
|
||||
&& blocks.get(index + 1) instanceof LinkBlock linkBlock) {
|
||||
Link link = linkBlock.getLink();
|
||||
if (link != null && link.getTitle() != null) {
|
||||
for (String blockedLinkBlock : blockedLinkBlocks) {
|
||||
if (blockedLinkBlock.equals(link.getTitle().getText())) {
|
||||
skipUntilDivider = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (skipUntilDivider) {
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Skip LinkBlocks with a "flavor" claiming to be "isPartner" (sponsored inline ads).
|
||||
if (currentBlock instanceof LinkBlock linkBlock
|
||||
&& linkBlock.getLink() != null
|
||||
&& linkBlock.getLink().getLinkFlavor() instanceof SmallArticleLinkFlavor smallArticleLinkFlavor
|
||||
&& smallArticleLinkFlavor.isPartner() != null
|
||||
&& smallArticleLinkFlavor.isPartner()) {
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentBlock instanceof DividerBlock) {
|
||||
skipUntilDivider = false;
|
||||
}
|
||||
|
||||
// Filter HeaderBlock with known ads until next HeaderBlock.
|
||||
if (currentBlock instanceof HeaderBlock headerBlock) {
|
||||
StyledText headerText = headerBlock.component20();
|
||||
if (headerText != null) {
|
||||
skipFullHeader = false;
|
||||
for (String blockedHeaderBlock : blockedHeaderBlocks) {
|
||||
if (blockedHeaderBlock.equals(headerText.getText())) {
|
||||
skipFullHeader = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (skipFullHeader) {
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!skipFullHeader && !skipUntilDivider) {
|
||||
cleanedList.add(currentBlock);
|
||||
}
|
||||
index++;
|
||||
}
|
||||
|
||||
// Replace list in-place to not deal with moving the result to the correct register in smali.
|
||||
blocks.clear();
|
||||
blocks.addAll(cleanedList);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "filterAds failure", ex);
|
||||
}
|
||||
}
|
||||
}
|
17
extensions/nunl/stub/build.gradle.kts
Normal file
17
extensions/nunl/stub/build.gradle.kts
Normal file
@ -0,0 +1,17 @@
|
||||
plugins {
|
||||
id(libs.plugins.android.library.get().pluginId)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "app.revanced.extension"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 26
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
}
|
1
extensions/nunl/stub/src/main/AndroidManifest.xml
Normal file
1
extensions/nunl/stub/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
||||
<manifest/>
|
@ -0,0 +1,5 @@
|
||||
package nl.nu.performance.api.client.interfaces;
|
||||
|
||||
public class Block {
|
||||
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package nl.nu.performance.api.client.objects;
|
||||
|
||||
import nl.nu.performance.api.client.interfaces.Block;
|
||||
|
||||
public class DividerBlock extends Block {
|
||||
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package nl.nu.performance.api.client.objects;
|
||||
|
||||
import nl.nu.performance.api.client.interfaces.Block;
|
||||
|
||||
public class DpgBannerBlock extends Block {
|
||||
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package nl.nu.performance.api.client.objects;
|
||||
|
||||
import nl.nu.performance.api.client.interfaces.Block;
|
||||
|
||||
public class HeaderBlock extends Block {
|
||||
// returns title
|
||||
public final StyledText component20() {
|
||||
throw new UnsupportedOperationException("Stub");
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package nl.nu.performance.api.client.objects;
|
||||
|
||||
import nl.nu.performance.api.client.unions.LinkFlavor;
|
||||
|
||||
public class Link {
|
||||
public final StyledText getTitle() {
|
||||
throw new UnsupportedOperationException("Stub");
|
||||
}
|
||||
|
||||
public final LinkFlavor getLinkFlavor() {
|
||||
throw new UnsupportedOperationException("Stub");
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package nl.nu.performance.api.client.objects;
|
||||
|
||||
import android.os.Parcelable;
|
||||
import nl.nu.performance.api.client.interfaces.Block;
|
||||
|
||||
public abstract class LinkBlock extends Block implements Parcelable {
|
||||
public final Link getLink() {
|
||||
throw new UnsupportedOperationException("Stub");
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package nl.nu.performance.api.client.objects;
|
||||
|
||||
public class StyledText {
|
||||
public final String getText() {
|
||||
throw new UnsupportedOperationException("Stub");
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
package nl.nu.performance.api.client.unions;
|
||||
|
||||
public interface LinkFlavor {
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package nl.nu.performance.api.client.unions;
|
||||
|
||||
public class SmallArticleLinkFlavor implements LinkFlavor {
|
||||
public final Boolean isPartner() {
|
||||
throw new UnsupportedOperationException("Stub");
|
||||
}
|
||||
}
|
9
extensions/proguard-rules.pro
vendored
Normal file
9
extensions/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
-dontobfuscate
|
||||
-dontoptimize
|
||||
-keepattributes *
|
||||
-keep class app.revanced.** {
|
||||
*;
|
||||
}
|
||||
-keep class com.google.** {
|
||||
*;
|
||||
}
|
3
extensions/reddit/build.gradle.kts
Normal file
3
extensions/reddit/build.gradle.kts
Normal file
@ -0,0 +1,3 @@
|
||||
dependencies {
|
||||
compileOnly(project(":extensions:reddit:stub"))
|
||||
}
|
1
extensions/reddit/src/main/AndroidManifest.xml
Normal file
1
extensions/reddit/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
||||
<manifest/>
|
@ -0,0 +1,27 @@
|
||||
package app.revanced.extension.reddit.patches;
|
||||
|
||||
import com.reddit.domain.model.ILink;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class FilterPromotedLinksPatch {
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* Filters list from promoted links.
|
||||
**/
|
||||
public static List<?> filterChildren(final Iterable<?> links) {
|
||||
final List<Object> filteredList = new ArrayList<>();
|
||||
|
||||
for (Object item : links) {
|
||||
if (item instanceof ILink && ((ILink) item).getPromoted()) continue;
|
||||
|
||||
filteredList.add(item);
|
||||
}
|
||||
|
||||
return filteredList;
|
||||
}
|
||||
}
|
17
extensions/reddit/stub/build.gradle.kts
Normal file
17
extensions/reddit/stub/build.gradle.kts
Normal file
@ -0,0 +1,17 @@
|
||||
plugins {
|
||||
id(libs.plugins.android.library.get().pluginId)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "app.revanced.extension"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
}
|
1
extensions/reddit/stub/src/main/AndroidManifest.xml
Normal file
1
extensions/reddit/stub/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
||||
<manifest/>
|
@ -0,0 +1,7 @@
|
||||
package com.reddit.domain.model;
|
||||
|
||||
public class ILink {
|
||||
public boolean getPromoted() {
|
||||
throw new UnsupportedOperationException("Stub");
|
||||
}
|
||||
}
|
3
extensions/shared/build.gradle.kts
Normal file
3
extensions/shared/build.gradle.kts
Normal file
@ -0,0 +1,3 @@
|
||||
dependencies {
|
||||
implementation(project(":extensions:shared:library"))
|
||||
}
|
21
extensions/shared/library/build.gradle.kts
Normal file
21
extensions/shared/library/build.gradle.kts
Normal file
@ -0,0 +1,21 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "app.revanced.extension"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 23
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(libs.annotation)
|
||||
}
|
@ -0,0 +1,171 @@
|
||||
package app.revanced.extension.shared;
|
||||
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.SearchManager;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.PowerManager;
|
||||
import android.provider.Settings;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
|
||||
/**
|
||||
* @noinspection unused
|
||||
*/
|
||||
public class GmsCoreSupport {
|
||||
private static final String PACKAGE_NAME_YOUTUBE = "com.google.android.youtube";
|
||||
private static final String PACKAGE_NAME_YOUTUBE_MUSIC = "com.google.android.apps.youtube.music";
|
||||
|
||||
private static final String GMS_CORE_PACKAGE_NAME
|
||||
= getGmsCoreVendorGroupId() + ".android.gms";
|
||||
private static final Uri GMS_CORE_PROVIDER
|
||||
= Uri.parse("content://" + getGmsCoreVendorGroupId() + ".android.gsf.gservices/prefix");
|
||||
private static final String DONT_KILL_MY_APP_LINK
|
||||
= "https://dontkillmyapp.com";
|
||||
|
||||
private static void open(String queryOrLink) {
|
||||
Intent intent;
|
||||
try {
|
||||
// Check if queryOrLink is a valid URL.
|
||||
new URL(queryOrLink);
|
||||
|
||||
intent = new Intent(Intent.ACTION_VIEW, Uri.parse(queryOrLink));
|
||||
} catch (MalformedURLException e) {
|
||||
intent = new Intent(Intent.ACTION_WEB_SEARCH);
|
||||
intent.putExtra(SearchManager.QUERY, queryOrLink);
|
||||
}
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
Utils.getContext().startActivity(intent);
|
||||
|
||||
// Gracefully exit, otherwise the broken app will continue to run.
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
private static void showBatteryOptimizationDialog(Activity context,
|
||||
String dialogMessageRef,
|
||||
String positiveButtonTextRef,
|
||||
DialogInterface.OnClickListener onPositiveClickListener) {
|
||||
// Use a delay to allow the activity to finish initializing.
|
||||
// Otherwise, if device is in dark mode the dialog is shown with wrong color scheme.
|
||||
Utils.runOnMainThreadDelayed(() -> {
|
||||
// Do not set cancelable to false, to allow using back button to skip the action,
|
||||
// just in case the battery change can never be satisfied.
|
||||
var dialog = new AlertDialog.Builder(context)
|
||||
.setTitle(str("gms_core_dialog_title"))
|
||||
.setMessage(str(dialogMessageRef))
|
||||
.setPositiveButton(str(positiveButtonTextRef), onPositiveClickListener)
|
||||
.create();
|
||||
Utils.showDialog(context, dialog);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
public static void checkGmsCore(Activity context) {
|
||||
try {
|
||||
// Verify the user has not included GmsCore for a root installation.
|
||||
// GmsCore Support changes the package name, but with a mounted installation
|
||||
// all manifest changes are ignored and the original package name is used.
|
||||
String packageName = context.getPackageName();
|
||||
if (packageName.equals(PACKAGE_NAME_YOUTUBE) || packageName.equals(PACKAGE_NAME_YOUTUBE_MUSIC)) {
|
||||
Logger.printInfo(() -> "App is mounted with root, but GmsCore patch was included");
|
||||
// Cannot use localize text here, since the app will load
|
||||
// resources from the unpatched app and all patch strings are missing.
|
||||
Utils.showToastLong("The 'GmsCore support' patch breaks mount installations");
|
||||
|
||||
// Do not exit. If the app exits before launch completes (and without
|
||||
// opening another activity), then on some devices such as Pixel phone Android 10
|
||||
// no toast will be shown and the app will continually be relaunched
|
||||
// with the appearance of a hung app.
|
||||
}
|
||||
|
||||
// Verify GmsCore is installed.
|
||||
try {
|
||||
PackageManager manager = context.getPackageManager();
|
||||
manager.getPackageInfo(GMS_CORE_PACKAGE_NAME, PackageManager.GET_ACTIVITIES);
|
||||
} catch (PackageManager.NameNotFoundException exception) {
|
||||
Logger.printInfo(() -> "GmsCore was not found");
|
||||
// Cannot show a dialog and must show a toast,
|
||||
// because on some installations the app crashes before a dialog can be displayed.
|
||||
Utils.showToastLong(str("gms_core_toast_not_installed_message"));
|
||||
open(getGmsCoreDownload());
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if GmsCore is whitelisted from battery optimizations.
|
||||
if (isAndroidAutomotive(context)) {
|
||||
// Ignore Android Automotive devices (Google built-in),
|
||||
// as there is no way to disable battery optimizations.
|
||||
Logger.printDebug(() -> "Device is Android Automotive");
|
||||
} else if (batteryOptimizationsEnabled(context)) {
|
||||
Logger.printInfo(() -> "GmsCore is not whitelisted from battery optimizations");
|
||||
|
||||
showBatteryOptimizationDialog(context,
|
||||
"gms_core_dialog_not_whitelisted_using_battery_optimizations_message",
|
||||
"gms_core_dialog_continue_text",
|
||||
(dialog, id) -> openGmsCoreDisableBatteryOptimizationsIntent(context));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if GmsCore is currently running in the background.
|
||||
try (var client = context.getContentResolver().acquireContentProviderClient(GMS_CORE_PROVIDER)) {
|
||||
if (client == null) {
|
||||
Logger.printInfo(() -> "GmsCore is not running in the background");
|
||||
|
||||
showBatteryOptimizationDialog(context,
|
||||
"gms_core_dialog_not_whitelisted_not_allowed_in_background_message",
|
||||
"gms_core_dialog_open_website_text",
|
||||
(dialog, id) -> open(DONT_KILL_MY_APP_LINK));
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "checkGmsCore failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("BatteryLife") // Permission is part of GmsCore
|
||||
private static void openGmsCoreDisableBatteryOptimizationsIntent(Activity activity) {
|
||||
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
|
||||
intent.setData(Uri.fromParts("package", GMS_CORE_PACKAGE_NAME, null));
|
||||
activity.startActivityForResult(intent, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return If GmsCore is not whitelisted from battery optimizations.
|
||||
*/
|
||||
private static boolean batteryOptimizationsEnabled(Context context) {
|
||||
var powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
|
||||
return !powerManager.isIgnoringBatteryOptimizations(GMS_CORE_PACKAGE_NAME);
|
||||
}
|
||||
|
||||
private static boolean isAndroidAutomotive(Context context) {
|
||||
return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
|
||||
}
|
||||
|
||||
private static String getGmsCoreDownload() {
|
||||
final var vendorGroupId = getGmsCoreVendorGroupId();
|
||||
//noinspection SwitchStatementWithTooFewBranches
|
||||
return switch (vendorGroupId) {
|
||||
case "app.revanced" -> "https://github.com/revanced/gmscore/releases/latest";
|
||||
default -> vendorGroupId + ".android.gms";
|
||||
};
|
||||
}
|
||||
|
||||
// Modified by a patch. Do not touch.
|
||||
private static String getGmsCoreVendorGroupId() {
|
||||
return "app.revanced";
|
||||
}
|
||||
}
|
@ -0,0 +1,156 @@
|
||||
package app.revanced.extension.shared;
|
||||
|
||||
import android.util.Log;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
|
||||
import static app.revanced.extension.shared.settings.BaseSettings.*;
|
||||
|
||||
public class Logger {
|
||||
|
||||
/**
|
||||
* Log messages using lambdas.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface LogMessage {
|
||||
@NonNull
|
||||
String buildMessageString();
|
||||
|
||||
/**
|
||||
* @return For outer classes, this returns {@link Class#getSimpleName()}.
|
||||
* For static, inner, or anonymous classes, this returns the simple name of the enclosing class.
|
||||
* <br>
|
||||
* For example, each of these classes return 'SomethingView':
|
||||
* <code>
|
||||
* com.company.SomethingView
|
||||
* com.company.SomethingView$StaticClass
|
||||
* com.company.SomethingView$1
|
||||
* </code>
|
||||
*/
|
||||
private String findOuterClassSimpleName() {
|
||||
var selfClass = this.getClass();
|
||||
|
||||
String fullClassName = selfClass.getName();
|
||||
final int dollarSignIndex = fullClassName.indexOf('$');
|
||||
if (dollarSignIndex < 0) {
|
||||
return selfClass.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);
|
||||
}
|
||||
}
|
||||
|
||||
private static final String REVANCED_LOG_PREFIX = "revanced: ";
|
||||
|
||||
/**
|
||||
* Logs debug messages under the outer class name of the code calling this method.
|
||||
* Whenever possible, the log string should be constructed entirely inside {@link LogMessage#buildMessageString()}
|
||||
* so the performance cost of building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
|
||||
*/
|
||||
public static void printDebug(@NonNull LogMessage message) {
|
||||
printDebug(message, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs debug messages under the outer class name of the code calling this method.
|
||||
* Whenever possible, the log string should be constructed entirely inside {@link LogMessage#buildMessageString()}
|
||||
* so the performance cost of building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
|
||||
*/
|
||||
public static void printDebug(@NonNull LogMessage message, @Nullable Exception ex) {
|
||||
if (DEBUG.get()) {
|
||||
String logMessage = message.buildMessageString();
|
||||
String logTag = REVANCED_LOG_PREFIX + message.findOuterClassSimpleName();
|
||||
|
||||
if (DEBUG_STACKTRACE.get()) {
|
||||
var builder = new StringBuilder(logMessage);
|
||||
var sw = new StringWriter();
|
||||
new Throwable().printStackTrace(new PrintWriter(sw));
|
||||
|
||||
builder.append('\n').append(sw);
|
||||
logMessage = builder.toString();
|
||||
}
|
||||
|
||||
if (ex == null) {
|
||||
Log.d(logTag, logMessage);
|
||||
} else {
|
||||
Log.d(logTag, logMessage, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs information messages using the outer class name of the code calling this method.
|
||||
*/
|
||||
public static void printInfo(@NonNull LogMessage message) {
|
||||
printInfo(message, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs information messages using the outer class name of the code calling this method.
|
||||
*/
|
||||
public static void printInfo(@NonNull LogMessage message, @Nullable Exception ex) {
|
||||
String logTag = REVANCED_LOG_PREFIX + message.findOuterClassSimpleName();
|
||||
String logMessage = message.buildMessageString();
|
||||
if (ex == null) {
|
||||
Log.i(logTag, logMessage);
|
||||
} else {
|
||||
Log.i(logTag, logMessage, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs exceptions under the outer class name of the code calling this method.
|
||||
*/
|
||||
public static void printException(@NonNull LogMessage message) {
|
||||
printException(message, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs exceptions under the outer class name of the code calling this method.
|
||||
* <p>
|
||||
* If the calling code is showing it's own error toast,
|
||||
* instead use {@link #printInfo(LogMessage, Exception)}
|
||||
*
|
||||
* @param message log message
|
||||
* @param ex exception (optional)
|
||||
*/
|
||||
public static void printException(@NonNull LogMessage message, @Nullable Throwable ex) {
|
||||
String messageString = message.buildMessageString();
|
||||
String outerClassSimpleName = message.findOuterClassSimpleName();
|
||||
String logMessage = REVANCED_LOG_PREFIX + outerClassSimpleName;
|
||||
if (ex == null) {
|
||||
Log.e(logMessage, messageString);
|
||||
} else {
|
||||
Log.e(logMessage, messageString, ex);
|
||||
}
|
||||
if (DEBUG_TOAST_ON_ERROR.get()) {
|
||||
Utils.showToastLong(outerClassSimpleName + ": " + messageString);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,122 @@
|
||||
package app.revanced.extension.shared;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class StringRef {
|
||||
private static Resources resources;
|
||||
private static String packageName;
|
||||
|
||||
// must use a thread safe map, as this class is used both on and off the main thread
|
||||
private static final Map<String, StringRef> strings = Collections.synchronizedMap(new HashMap<>());
|
||||
|
||||
/**
|
||||
* Returns a cached instance.
|
||||
* Should be used if the same String could be loaded more than once.
|
||||
*
|
||||
* @param id string resource name/id
|
||||
* @see #sf(String)
|
||||
*/
|
||||
@NonNull
|
||||
public static StringRef sfc(@NonNull String id) {
|
||||
StringRef ref = strings.get(id);
|
||||
if (ref == null) {
|
||||
ref = new StringRef(id);
|
||||
strings.put(id, ref);
|
||||
}
|
||||
return ref;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance, but does not cache the value.
|
||||
* Should be used for Strings that are loaded exactly once.
|
||||
*
|
||||
* @param id string resource name/id
|
||||
* @see #sfc(String)
|
||||
*/
|
||||
@NonNull
|
||||
public static StringRef sf(@NonNull String id) {
|
||||
return new StringRef(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets string value by string id, shorthand for <code>sfc(id).toString()</code>
|
||||
*
|
||||
* @param id string resource name/id
|
||||
* @return String value from string.xml
|
||||
*/
|
||||
@NonNull
|
||||
public static String str(@NonNull String id) {
|
||||
return sfc(id).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets string value by string id, shorthand for <code>sfc(id).toString()</code> and formats the string
|
||||
* with given args.
|
||||
*
|
||||
* @param id string resource name/id
|
||||
* @param args the args to format the string with
|
||||
* @return String value from string.xml formatted with given args
|
||||
*/
|
||||
@NonNull
|
||||
public static String str(@NonNull String id, Object... args) {
|
||||
return String.format(str(id), args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a StringRef object that'll not change it's value
|
||||
*
|
||||
* @param value value which toString() method returns when invoked on returned object
|
||||
* @return Unique StringRef instance, its value will never change
|
||||
*/
|
||||
@NonNull
|
||||
public static StringRef constant(@NonNull String value) {
|
||||
final StringRef ref = new StringRef(value);
|
||||
ref.resolved = true;
|
||||
return ref;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shorthand for <code>constant("")</code>
|
||||
* Its value always resolves to empty string
|
||||
*/
|
||||
@NonNull
|
||||
public static final StringRef empty = constant("");
|
||||
|
||||
@NonNull
|
||||
private String value;
|
||||
private boolean resolved;
|
||||
|
||||
public StringRef(@NonNull String resName) {
|
||||
this.value = resName;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public String toString() {
|
||||
if (!resolved) {
|
||||
if (resources == null || packageName == null) {
|
||||
Context context = Utils.getContext();
|
||||
resources = context.getResources();
|
||||
packageName = context.getPackageName();
|
||||
}
|
||||
resolved = true;
|
||||
if (resources != null) {
|
||||
final int identifier = resources.getIdentifier(value, "string", packageName);
|
||||
if (identifier == 0)
|
||||
Logger.printException(() -> "Resource not found: " + value);
|
||||
else
|
||||
value = resources.getString(identifier);
|
||||
} else {
|
||||
Logger.printException(() -> "Could not resolve resources!");
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
@ -0,0 +1,813 @@
|
||||
package app.revanced.extension.shared;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.*;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Color;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceGroup;
|
||||
import android.preference.PreferenceScreen;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewParent;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.Toast;
|
||||
import android.widget.Toolbar;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.text.Bidi;
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.SynchronousQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import app.revanced.extension.shared.settings.AppLanguage;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||
import app.revanced.extension.shared.settings.preference.ReVancedAboutPreference;
|
||||
|
||||
public class Utils {
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private static volatile Context context;
|
||||
|
||||
private static String versionName;
|
||||
private static String applicationLabel;
|
||||
|
||||
private Utils() {
|
||||
} // utility class
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* @return The manifest 'Version' entry of the patches.jar used during patching.
|
||||
*/
|
||||
@SuppressWarnings("SameReturnValue")
|
||||
public static String getPatchesReleaseVersion() {
|
||||
return ""; // Value is replaced during patching.
|
||||
}
|
||||
|
||||
private static PackageInfo getPackageInfo() throws PackageManager.NameNotFoundException {
|
||||
final var packageName = Objects.requireNonNull(getContext()).getPackageName();
|
||||
|
||||
PackageManager packageManager = context.getPackageManager();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
return packageManager.getPackageInfo(
|
||||
packageName,
|
||||
PackageManager.PackageInfoFlags.of(0)
|
||||
);
|
||||
}
|
||||
|
||||
return packageManager.getPackageInfo(
|
||||
packageName,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The version name of the app, such as 19.11.43
|
||||
*/
|
||||
public static String getAppVersionName() {
|
||||
if (versionName == null) {
|
||||
try {
|
||||
versionName = getPackageInfo().versionName;
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Failed to get package info", ex);
|
||||
versionName = "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
return versionName;
|
||||
}
|
||||
|
||||
public static String getApplicationName() {
|
||||
if (applicationLabel == null) {
|
||||
try {
|
||||
ApplicationInfo applicationInfo = getPackageInfo().applicationInfo;
|
||||
applicationLabel = (String) applicationInfo.loadLabel(context.getPackageManager());
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Failed to get application name", ex);
|
||||
applicationLabel = "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
return applicationLabel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide a view by setting its layout height and width to 1dp.
|
||||
*
|
||||
* @param condition The setting to check for hiding the view.
|
||||
* @param view The view to hide.
|
||||
*/
|
||||
public static void hideViewBy0dpUnderCondition(BooleanSetting condition, View view) {
|
||||
if (hideViewBy0dpUnderCondition(condition.get(), view)) {
|
||||
Logger.printDebug(() -> "View hidden by setting: " + condition);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide a view by setting its layout height and width to 0dp.
|
||||
*
|
||||
* @param condition The setting to check for hiding the view.
|
||||
* @param view The view to hide.
|
||||
*/
|
||||
public static boolean hideViewBy0dpUnderCondition(boolean condition, View view) {
|
||||
if (condition) {
|
||||
hideViewByLayoutParams(view);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide a view by setting its visibility to GONE.
|
||||
*
|
||||
* @param condition The setting to check for hiding the view.
|
||||
* @param view The view to hide.
|
||||
*/
|
||||
public static void hideViewUnderCondition(BooleanSetting condition, View view) {
|
||||
if (hideViewUnderCondition(condition.get(), view)) {
|
||||
Logger.printDebug(() -> "View hidden by setting: " + condition);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide a view by setting its visibility to GONE.
|
||||
*
|
||||
* @param condition The setting to check for hiding the view.
|
||||
* @param view The view to hide.
|
||||
*/
|
||||
public static boolean hideViewUnderCondition(boolean condition, View view) {
|
||||
if (condition) {
|
||||
view.setVisibility(View.GONE);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void hideViewByRemovingFromParentUnderCondition(BooleanSetting condition, View view) {
|
||||
if (hideViewByRemovingFromParentUnderCondition(condition.get(), view)) {
|
||||
Logger.printDebug(() -> "View hidden by setting: " + condition);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean hideViewByRemovingFromParentUnderCondition(boolean setting, View view) {
|
||||
if (setting) {
|
||||
ViewParent parent = view.getParent();
|
||||
if (parent instanceof ViewGroup) {
|
||||
((ViewGroup) parent).removeView(view);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* General purpose pool for network calls and other background tasks.
|
||||
* All tasks run at max thread priority.
|
||||
*/
|
||||
private static final ThreadPoolExecutor backgroundThreadPool = new ThreadPoolExecutor(
|
||||
3, // 3 threads always ready to go
|
||||
Integer.MAX_VALUE,
|
||||
10, // For any threads over the minimum, keep them alive 10 seconds after they go idle
|
||||
TimeUnit.SECONDS,
|
||||
new SynchronousQueue<>(),
|
||||
r -> { // ThreadFactory
|
||||
Thread t = new Thread(r);
|
||||
t.setPriority(Thread.MAX_PRIORITY); // run at max priority
|
||||
return t;
|
||||
});
|
||||
|
||||
public static void runOnBackgroundThread(@NonNull Runnable task) {
|
||||
backgroundThreadPool.execute(task);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static <T> Future<T> submitOnBackgroundThread(@NonNull Callable<T> call) {
|
||||
return backgroundThreadPool.submit(call);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates a delay by doing meaningless calculations.
|
||||
* Used for debugging to verify UI timeout logic.
|
||||
*/
|
||||
@SuppressWarnings("UnusedReturnValue")
|
||||
public static long doNothingForDuration(long amountOfTimeToWaste) {
|
||||
final long timeCalculationStarted = System.currentTimeMillis();
|
||||
Logger.printDebug(() -> "Artificially creating delay of: " + amountOfTimeToWaste + "ms");
|
||||
|
||||
long meaninglessValue = 0;
|
||||
while (System.currentTimeMillis() - timeCalculationStarted < amountOfTimeToWaste) {
|
||||
// could do a thread sleep, but that will trigger an exception if the thread is interrupted
|
||||
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,
|
||||
// leaving an empty loop that hammers on the System.currentTimeMillis native call
|
||||
return meaninglessValue;
|
||||
}
|
||||
|
||||
|
||||
public static boolean containsAny(@NonNull String value, @NonNull String... targets) {
|
||||
return indexOfFirstFound(value, targets) >= 0;
|
||||
}
|
||||
|
||||
public static int indexOfFirstFound(@NonNull String value, @NonNull String... targets) {
|
||||
for (String string : targets) {
|
||||
if (!string.isEmpty()) {
|
||||
final int indexOf = value.indexOf(string);
|
||||
if (indexOf >= 0) return indexOf;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return zero, if the resource is not found
|
||||
*/
|
||||
@SuppressLint("DiscouragedApi")
|
||||
public static int getResourceIdentifier(@NonNull Context context, @NonNull String resourceIdentifierName, @NonNull String type) {
|
||||
return context.getResources().getIdentifier(resourceIdentifierName, type, context.getPackageName());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return zero, if the resource is not found
|
||||
*/
|
||||
public static int getResourceIdentifier(@NonNull String resourceIdentifierName, @NonNull String type) {
|
||||
return getResourceIdentifier(getContext(), resourceIdentifierName, type);
|
||||
}
|
||||
|
||||
public static int getResourceInteger(@NonNull String resourceIdentifierName) throws Resources.NotFoundException {
|
||||
return getContext().getResources().getInteger(getResourceIdentifier(resourceIdentifierName, "integer"));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static Animation getResourceAnimation(@NonNull String resourceIdentifierName) throws Resources.NotFoundException {
|
||||
return AnimationUtils.loadAnimation(getContext(), getResourceIdentifier(resourceIdentifierName, "anim"));
|
||||
}
|
||||
|
||||
public static int getResourceColor(@NonNull String resourceIdentifierName) throws Resources.NotFoundException {
|
||||
//noinspection deprecation
|
||||
return getContext().getResources().getColor(getResourceIdentifier(resourceIdentifierName, "color"));
|
||||
}
|
||||
|
||||
public static int getResourceDimensionPixelSize(@NonNull String resourceIdentifierName) throws Resources.NotFoundException {
|
||||
return getContext().getResources().getDimensionPixelSize(getResourceIdentifier(resourceIdentifierName, "dimen"));
|
||||
}
|
||||
|
||||
public static float getResourceDimension(@NonNull String resourceIdentifierName) throws Resources.NotFoundException {
|
||||
return getContext().getResources().getDimension(getResourceIdentifier(resourceIdentifierName, "dimen"));
|
||||
}
|
||||
|
||||
public interface MatchFilter<T> {
|
||||
boolean matches(T object);
|
||||
}
|
||||
|
||||
/**
|
||||
* Includes sub children.
|
||||
*
|
||||
* @noinspection unchecked
|
||||
*/
|
||||
public static <R extends View> R getChildViewByResourceName(@NonNull View view, @NonNull String str) {
|
||||
var child = view.findViewById(Utils.getResourceIdentifier(str, "id"));
|
||||
if (child != null) {
|
||||
return (R) child;
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("View with resource name '" + str + "' not found");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param searchRecursively If children ViewGroups should also be
|
||||
* recursively searched using depth first search.
|
||||
* @return The first child view that matches the filter.
|
||||
*/
|
||||
@Nullable
|
||||
public static <T extends View> T getChildView(@NonNull ViewGroup viewGroup, boolean searchRecursively,
|
||||
@NonNull MatchFilter<View> filter) {
|
||||
for (int i = 0, childCount = viewGroup.getChildCount(); i < childCount; i++) {
|
||||
View childAt = viewGroup.getChildAt(i);
|
||||
|
||||
if (filter.matches(childAt)) {
|
||||
//noinspection unchecked
|
||||
return (T) childAt;
|
||||
}
|
||||
// Must do recursive after filter check, in case the filter is looking for a ViewGroup.
|
||||
if (searchRecursively && childAt instanceof ViewGroup) {
|
||||
T match = getChildView((ViewGroup) childAt, true, filter);
|
||||
if (match != null) return match;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static ViewParent getParentView(@NonNull View view, int nthParent) {
|
||||
ViewParent parent = view.getParent();
|
||||
|
||||
int currentDepth = 0;
|
||||
while (++currentDepth < nthParent && parent != null) {
|
||||
parent = parent.getParent();
|
||||
}
|
||||
|
||||
if (currentDepth == nthParent) {
|
||||
return parent;
|
||||
}
|
||||
|
||||
final int currentDepthLog = currentDepth;
|
||||
Logger.printDebug(() -> "Could not find parent view of depth: " + nthParent
|
||||
+ " and instead found at: " + currentDepthLog + " view: " + view);
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void restartApp(@NonNull Context context) {
|
||||
String packageName = context.getPackageName();
|
||||
Intent intent = Objects.requireNonNull(context.getPackageManager().getLaunchIntentForPackage(packageName));
|
||||
Intent mainIntent = Intent.makeRestartActivityTask(intent.getComponent());
|
||||
// Required for API 34 and later
|
||||
// Ref: https://developer.android.com/about/versions/14/behavior-changes-14#safer-intents
|
||||
mainIntent.setPackage(packageName);
|
||||
context.startActivity(mainIntent);
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
public static Context getContext() {
|
||||
if (context == null) {
|
||||
Logger.initializationException(Utils.class, "Context is not set by extension hook, returning null", null);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
public static void setContext(Context appContext) {
|
||||
// Must initially set context to check the app language.
|
||||
context = appContext;
|
||||
Logger.initializationInfo(Utils.class, "Set context: " + appContext);
|
||||
|
||||
AppLanguage language = BaseSettings.REVANCED_LANGUAGE.get();
|
||||
if (language != AppLanguage.DEFAULT) {
|
||||
// Create a new context with the desired language.
|
||||
Logger.printDebug(() -> "Using app language: " + language);
|
||||
Configuration config = appContext.getResources().getConfiguration();
|
||||
config.setLocale(language.getLocale());
|
||||
context = appContext.createConfigurationContext(config);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setClipboard(@NonNull String text) {
|
||||
android.content.ClipboardManager clipboard = (android.content.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
android.content.ClipData clip = android.content.ClipData.newPlainText("ReVanced", text);
|
||||
clipboard.setPrimaryClip(clip);
|
||||
}
|
||||
|
||||
public static boolean isTablet() {
|
||||
return context.getResources().getConfiguration().smallestScreenWidthDp >= 600;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Boolean isRightToLeftTextLayout;
|
||||
|
||||
/**
|
||||
* If the device language uses right to left text layout (hebrew, arabic, etc)
|
||||
*/
|
||||
public static boolean isRightToLeftTextLayout() {
|
||||
if (isRightToLeftTextLayout == null) {
|
||||
String displayLanguage = Locale.getDefault().getDisplayLanguage();
|
||||
isRightToLeftTextLayout = new Bidi(displayLanguage, Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT).isRightToLeft();
|
||||
}
|
||||
return isRightToLeftTextLayout;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return if the text contains at least 1 number character,
|
||||
* including any unicode numbers such as Arabic.
|
||||
*/
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public static boolean containsNumber(@NonNull CharSequence text) {
|
||||
for (int index = 0, length = text.length(); index < length;) {
|
||||
final int codePoint = Character.codePointAt(text, index);
|
||||
if (Character.isDigit(codePoint)) {
|
||||
return true;
|
||||
}
|
||||
index += Character.charCount(codePoint);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ignore this class. It must be public to satisfy Android requirements.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public static final class DialogFragmentWrapper extends DialogFragment {
|
||||
|
||||
private Dialog dialog;
|
||||
@Nullable
|
||||
private DialogFragmentOnStartAction onStartAction;
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
// Do not call super method to prevent state saving.
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
return dialog;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
try {
|
||||
super.onStart();
|
||||
|
||||
if (onStartAction != null) {
|
||||
onStartAction.onStart((AlertDialog) getDialog());
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "onStart failure: " + dialog.getClass().getSimpleName(), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for {@link #showDialog(Activity, AlertDialog, boolean, DialogFragmentOnStartAction)}.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface DialogFragmentOnStartAction {
|
||||
void onStart(AlertDialog dialog);
|
||||
}
|
||||
|
||||
public static void showDialog(Activity activity, AlertDialog dialog) {
|
||||
showDialog(activity, dialog, true, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to allow showing an AlertDialog on top of other alert dialogs.
|
||||
* Calling this will always display the dialog on top of all other dialogs
|
||||
* previously called using this method.
|
||||
* <br>
|
||||
* 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.
|
||||
*<br>
|
||||
* 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.
|
||||
*<br>
|
||||
* For all other situations it's better to not use this method and
|
||||
* call {@link AlertDialog#show()} on the dialog.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public static void showDialog(Activity activity,
|
||||
AlertDialog dialog,
|
||||
boolean isCancelable,
|
||||
@Nullable DialogFragmentOnStartAction onStartAction) {
|
||||
verifyOnMainThread();
|
||||
|
||||
DialogFragmentWrapper fragment = new DialogFragmentWrapper();
|
||||
fragment.dialog = dialog;
|
||||
fragment.onStartAction = onStartAction;
|
||||
fragment.setCancelable(isCancelable);
|
||||
|
||||
fragment.show(activity.getFragmentManager(), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe to call from any thread
|
||||
*/
|
||||
public static void showToastShort(@NonNull String messageToToast) {
|
||||
showToast(messageToToast, Toast.LENGTH_SHORT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe to call from any thread
|
||||
*/
|
||||
public static void showToastLong(@NonNull String messageToToast) {
|
||||
showToast(messageToToast, Toast.LENGTH_LONG);
|
||||
}
|
||||
|
||||
private static void showToast(@NonNull String messageToToast, int toastDuration) {
|
||||
Objects.requireNonNull(messageToToast);
|
||||
runOnMainThreadNowOrLater(() -> {
|
||||
if (context == null) {
|
||||
Logger.initializationException(Utils.class, "Cannot show toast (context is null): " + messageToToast, null);
|
||||
} else {
|
||||
Logger.printDebug(() -> "Showing toast: " + messageToToast);
|
||||
Toast.makeText(context, messageToToast, toastDuration).show();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public static boolean isDarkModeEnabled(Context context) {
|
||||
Configuration config = context.getResources().getConfiguration();
|
||||
final int currentNightMode = config.uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||
return currentNightMode == Configuration.UI_MODE_NIGHT_YES;
|
||||
}
|
||||
|
||||
public static boolean isLandscapeOrientation() {
|
||||
final int orientation = context.getResources().getConfiguration().orientation;
|
||||
return orientation == Configuration.ORIENTATION_LANDSCAPE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically logs any exceptions the runnable throws.
|
||||
*
|
||||
* @see #runOnMainThreadNowOrLater(Runnable)
|
||||
*/
|
||||
public static void runOnMainThread(@NonNull Runnable runnable) {
|
||||
runOnMainThreadDelayed(runnable, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically logs any exceptions the runnable throws
|
||||
*/
|
||||
public static void runOnMainThreadDelayed(@NonNull Runnable runnable, long delayMillis) {
|
||||
Runnable loggingRunnable = () -> {
|
||||
try {
|
||||
runnable.run();
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> runnable.getClass().getSimpleName() + ": " + ex.getMessage(), ex);
|
||||
}
|
||||
};
|
||||
new Handler(Looper.getMainLooper()).postDelayed(loggingRunnable, delayMillis);
|
||||
}
|
||||
|
||||
/**
|
||||
* If called from the main thread, the code is run immediately.<p>
|
||||
* If called off the main thread, this is the same as {@link #runOnMainThread(Runnable)}.
|
||||
*/
|
||||
public static void runOnMainThreadNowOrLater(@NonNull Runnable runnable) {
|
||||
if (isCurrentlyOnMainThread()) {
|
||||
runnable.run();
|
||||
} else {
|
||||
runOnMainThread(runnable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return if the calling thread is on the main thread
|
||||
*/
|
||||
public static boolean isCurrentlyOnMainThread() {
|
||||
return Looper.getMainLooper().isCurrentThread();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws IllegalStateException if the calling thread is _off_ the main thread
|
||||
*/
|
||||
public static void verifyOnMainThread() throws IllegalStateException {
|
||||
if (!isCurrentlyOnMainThread()) {
|
||||
throw new IllegalStateException("Must call _on_ the main thread");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws IllegalStateException if the calling thread is _on_ the main thread
|
||||
*/
|
||||
public static void verifyOffMainThread() throws IllegalStateException {
|
||||
if (isCurrentlyOnMainThread()) {
|
||||
throw new IllegalStateException("Must call _off_ the main thread");
|
||||
}
|
||||
}
|
||||
|
||||
public enum NetworkType {
|
||||
NONE,
|
||||
MOBILE,
|
||||
OTHER,
|
||||
}
|
||||
|
||||
public static boolean isNetworkConnected() {
|
||||
NetworkType networkType = getNetworkType();
|
||||
return networkType == NetworkType.MOBILE
|
||||
|| networkType == NetworkType.OTHER;
|
||||
}
|
||||
|
||||
@SuppressLint({"MissingPermission", "deprecation"}) // Permission already included in YouTube.
|
||||
public static NetworkType getNetworkType() {
|
||||
Context networkContext = getContext();
|
||||
if (networkContext == null) {
|
||||
return NetworkType.NONE;
|
||||
}
|
||||
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
var networkInfo = cm.getActiveNetworkInfo();
|
||||
|
||||
if (networkInfo == null || !networkInfo.isConnected()) {
|
||||
return NetworkType.NONE;
|
||||
}
|
||||
var type = networkInfo.getType();
|
||||
return (type == ConnectivityManager.TYPE_MOBILE)
|
||||
|| (type == ConnectivityManager.TYPE_BLUETOOTH) ? NetworkType.MOBILE : NetworkType.OTHER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide a view by setting its layout params to 0x0
|
||||
* @param view The view to hide.
|
||||
*/
|
||||
public static void hideViewByLayoutParams(View view) {
|
||||
if (view instanceof LinearLayout) {
|
||||
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(0, 0);
|
||||
view.setLayoutParams(layoutParams);
|
||||
} else if (view instanceof FrameLayout) {
|
||||
FrameLayout.LayoutParams layoutParams2 = new FrameLayout.LayoutParams(0, 0);
|
||||
view.setLayoutParams(layoutParams2);
|
||||
} else if (view instanceof RelativeLayout) {
|
||||
RelativeLayout.LayoutParams layoutParams3 = new RelativeLayout.LayoutParams(0, 0);
|
||||
view.setLayoutParams(layoutParams3);
|
||||
} else if (view instanceof Toolbar) {
|
||||
Toolbar.LayoutParams layoutParams4 = new Toolbar.LayoutParams(0, 0);
|
||||
view.setLayoutParams(layoutParams4);
|
||||
} else if (view instanceof ViewGroup) {
|
||||
ViewGroup.LayoutParams layoutParams5 = new ViewGroup.LayoutParams(0, 0);
|
||||
view.setLayoutParams(layoutParams5);
|
||||
} else {
|
||||
ViewGroup.LayoutParams params = view.getLayoutParams();
|
||||
params.width = 0;
|
||||
params.height = 0;
|
||||
view.setLayoutParams(params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link PreferenceScreen} and {@link PreferenceGroup} sorting styles.
|
||||
*/
|
||||
private enum Sort {
|
||||
/**
|
||||
* Sort by the localized preference title.
|
||||
*/
|
||||
BY_TITLE("_sort_by_title"),
|
||||
|
||||
/**
|
||||
* Sort by the preference keys.
|
||||
*/
|
||||
BY_KEY("_sort_by_key"),
|
||||
|
||||
/**
|
||||
* Unspecified sorting.
|
||||
*/
|
||||
UNSORTED("_sort_by_unsorted");
|
||||
|
||||
final String keySuffix;
|
||||
|
||||
Sort(String keySuffix) {
|
||||
this.keySuffix = keySuffix;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
static Sort fromKey(@Nullable String key, @NonNull Sort defaultSort) {
|
||||
if (key != null) {
|
||||
for (Sort sort : values()) {
|
||||
if (key.endsWith(sort.keySuffix)) {
|
||||
return sort;
|
||||
}
|
||||
}
|
||||
}
|
||||
return defaultSort;
|
||||
}
|
||||
}
|
||||
|
||||
private static final Pattern punctuationPattern = Pattern.compile("\\p{P}+");
|
||||
|
||||
/**
|
||||
* Strips all punctuation and converts to lower case. A null parameter returns an empty string.
|
||||
*/
|
||||
public static String removePunctuationConvertToLowercase(@Nullable CharSequence original) {
|
||||
if (original == null) return "";
|
||||
return punctuationPattern.matcher(original).replaceAll("").toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort a PreferenceGroup and all it's sub groups by title or key.
|
||||
*
|
||||
* Sort order is determined by the preferences key {@link Sort} suffix.
|
||||
*
|
||||
* If a preference has no key or no {@link Sort} suffix,
|
||||
* then the preferences are left unsorted.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public static void sortPreferenceGroups(@NonNull PreferenceGroup group) {
|
||||
Sort groupSort = Sort.fromKey(group.getKey(), Sort.UNSORTED);
|
||||
SortedMap<String, Preference> preferences = new TreeMap<>();
|
||||
|
||||
for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) {
|
||||
Preference preference = group.getPreference(i);
|
||||
|
||||
final Sort preferenceSort;
|
||||
if (preference instanceof PreferenceGroup subGroup) {
|
||||
sortPreferenceGroups(subGroup);
|
||||
preferenceSort = groupSort; // Sort value for groups is for it's content, not itself.
|
||||
} else {
|
||||
// Allow individual preferences to set a key sorting.
|
||||
// Used to force a preference to the top or bottom of a group.
|
||||
preferenceSort = Sort.fromKey(preference.getKey(), groupSort);
|
||||
}
|
||||
|
||||
final String sortValue;
|
||||
switch (preferenceSort) {
|
||||
case BY_TITLE:
|
||||
sortValue = removePunctuationConvertToLowercase(preference.getTitle());
|
||||
break;
|
||||
case BY_KEY:
|
||||
sortValue = preference.getKey();
|
||||
break;
|
||||
case UNSORTED:
|
||||
continue; // Keep original sorting.
|
||||
default:
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
preferences.put(sortValue, preference);
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
for (Preference pref : preferences.values()) {
|
||||
int order = index++;
|
||||
|
||||
// Move any screens, intents, and the one off About preference to the top.
|
||||
if (pref instanceof PreferenceScreen || pref instanceof ReVancedAboutPreference
|
||||
|| pref.getIntent() != null) {
|
||||
// Arbitrary high number.
|
||||
order -= 1000;
|
||||
}
|
||||
|
||||
pref.setOrder(order);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* except 2 or 3 preference strings and those do not affect readability.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public static void setPreferenceTitlesToMultiLineIfNeeded(PreferenceGroup group) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
return;
|
||||
}
|
||||
|
||||
String revancedLocale = Utils.getContext().getResources().getConfiguration().locale.getLanguage();
|
||||
if (revancedLocale.equals(Locale.ENGLISH.getLanguage())) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) {
|
||||
Preference pref = group.getPreference(i);
|
||||
pref.setSingleLineTitle(false);
|
||||
|
||||
if (pref instanceof PreferenceGroup subGroup) {
|
||||
setPreferenceTitlesToMultiLineIfNeeded(subGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public static int getColorFromString(String colorString) throws IllegalArgumentException, Resources.NotFoundException {
|
||||
if (colorString.startsWith("#")) {
|
||||
return Color.parseColor(colorString);
|
||||
}
|
||||
return getResourceColor(colorString);
|
||||
}
|
||||
}
|
@ -0,0 +1,164 @@
|
||||
package app.revanced.extension.shared.checks;
|
||||
|
||||
import static android.text.Html.FROM_HTML_MODE_COMPACT;
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
import static app.revanced.extension.shared.Utils.DialogFragmentOnStartAction;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.text.Html;
|
||||
import android.widget.Button;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
|
||||
abstract class Check {
|
||||
private static final int NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING = 2;
|
||||
|
||||
private static final int SECONDS_BEFORE_SHOWING_IGNORE_BUTTON = 15;
|
||||
private static final int SECONDS_BEFORE_SHOWING_WEBSITE_BUTTON = 10;
|
||||
|
||||
private static final Uri GOOD_SOURCE = Uri.parse("https://revanced.app");
|
||||
|
||||
/**
|
||||
* @return If the check conclusively passed or failed. A null value indicates it neither passed nor failed.
|
||||
*/
|
||||
@Nullable
|
||||
protected abstract Boolean check();
|
||||
|
||||
protected abstract String failureReason();
|
||||
|
||||
/**
|
||||
* Specifies a sorting order for displaying the checks that failed.
|
||||
* A lower value indicates to show first before other checks.
|
||||
*/
|
||||
public abstract int uiSortingValue();
|
||||
|
||||
/**
|
||||
* For debugging and development only.
|
||||
* Forces all checks to be performed and the check failed dialog to be shown.
|
||||
* Can be enabled by importing settings text with {@link BaseSettings#CHECK_ENVIRONMENT_WARNINGS_ISSUED}
|
||||
* set to -1.
|
||||
*/
|
||||
static boolean debugAlwaysShowWarning() {
|
||||
final boolean alwaysShowWarning = BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get() < 0;
|
||||
if (alwaysShowWarning) {
|
||||
Logger.printInfo(() -> "Debug forcing environment check warning to show");
|
||||
}
|
||||
|
||||
return alwaysShowWarning;
|
||||
}
|
||||
|
||||
static boolean shouldRun() {
|
||||
return BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get()
|
||||
< NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING;
|
||||
}
|
||||
|
||||
static void disableForever() {
|
||||
Logger.printInfo(() -> "Environment checks disabled forever");
|
||||
|
||||
BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
static void issueWarning(Activity activity, Collection<Check> failedChecks) {
|
||||
final var reasons = new StringBuilder();
|
||||
|
||||
reasons.append("<ul>");
|
||||
for (var check : failedChecks) {
|
||||
// Add a non breaking space to fix bullet points spacing issue.
|
||||
reasons.append("<li> ").append(check.failureReason());
|
||||
}
|
||||
reasons.append("</ul>");
|
||||
|
||||
var message = Html.fromHtml(
|
||||
str("revanced_check_environment_failed_message", reasons.toString()),
|
||||
FROM_HTML_MODE_COMPACT
|
||||
);
|
||||
|
||||
Utils.runOnMainThreadDelayed(() -> {
|
||||
AlertDialog alert = new AlertDialog.Builder(activity)
|
||||
.setCancelable(false)
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setTitle(str("revanced_check_environment_failed_title"))
|
||||
.setMessage(message)
|
||||
.setPositiveButton(
|
||||
" ",
|
||||
(dialog, which) -> {
|
||||
final var intent = new Intent(Intent.ACTION_VIEW, GOOD_SOURCE);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
activity.startActivity(intent);
|
||||
|
||||
// Shutdown to prevent the user from navigating back to this app,
|
||||
// which is no longer showing a warning dialog.
|
||||
activity.finishAffinity();
|
||||
System.exit(0);
|
||||
}
|
||||
).setNegativeButton(
|
||||
" ",
|
||||
(dialog, which) -> {
|
||||
// Cleanup data if the user incorrectly imported a huge negative number.
|
||||
final int current = Math.max(0, BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get());
|
||||
BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(current + 1);
|
||||
|
||||
dialog.dismiss();
|
||||
}
|
||||
).create();
|
||||
|
||||
Utils.showDialog(activity, alert, false, new DialogFragmentOnStartAction() {
|
||||
boolean hasRun;
|
||||
@Override
|
||||
public void onStart(AlertDialog dialog) {
|
||||
// Only run this once, otherwise if the user changes to a different app
|
||||
// then changes back, this handler will run again and disable the buttons.
|
||||
if (hasRun) {
|
||||
return;
|
||||
}
|
||||
hasRun = true;
|
||||
|
||||
var openWebsiteButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
|
||||
openWebsiteButton.setEnabled(false);
|
||||
|
||||
var dismissButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE);
|
||||
dismissButton.setEnabled(false);
|
||||
|
||||
getCountdownRunnable(dismissButton, openWebsiteButton).run();
|
||||
}
|
||||
});
|
||||
}, 1000); // Use a delay, so this dialog is shown on top of any other startup dialogs.
|
||||
}
|
||||
|
||||
private static Runnable getCountdownRunnable(Button dismissButton, Button openWebsiteButton) {
|
||||
return new Runnable() {
|
||||
private int secondsRemaining = SECONDS_BEFORE_SHOWING_IGNORE_BUTTON;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Utils.verifyOnMainThread();
|
||||
|
||||
if (secondsRemaining > 0) {
|
||||
if (secondsRemaining - SECONDS_BEFORE_SHOWING_WEBSITE_BUTTON == 0) {
|
||||
openWebsiteButton.setText(str("revanced_check_environment_dialog_open_official_source_button"));
|
||||
openWebsiteButton.setEnabled(true);
|
||||
}
|
||||
|
||||
secondsRemaining--;
|
||||
|
||||
Utils.runOnMainThreadDelayed(this, 1000);
|
||||
} else {
|
||||
dismissButton.setText(str("revanced_check_environment_dialog_ignore_button"));
|
||||
dismissButton.setEnabled(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,341 @@
|
||||
package app.revanced.extension.shared.checks;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.util.Base64;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.*;
|
||||
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
import static app.revanced.extension.shared.checks.Check.debugAlwaysShowWarning;
|
||||
import static app.revanced.extension.shared.checks.PatchInfo.Build.*;
|
||||
|
||||
/**
|
||||
* This class is used to check if the app was patched by the user
|
||||
* and not downloaded pre-patched, because pre-patched apps are difficult to trust.
|
||||
* <br>
|
||||
* Various indicators help to detect if the app was patched by the user.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public final class CheckEnvironmentPatch {
|
||||
private static final boolean DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG = debugAlwaysShowWarning();
|
||||
|
||||
private enum InstallationType {
|
||||
/**
|
||||
* CLI patching, manual installation of a previously patched using adb,
|
||||
* or root installation if stock app is first installed using adb.
|
||||
*/
|
||||
ADB((String) null),
|
||||
ROOT_MOUNT_ON_APP_STORE("com.android.vending"),
|
||||
MANAGER("app.revanced.manager.flutter",
|
||||
"app.revanced.manager",
|
||||
"app.revanced.manager.debug");
|
||||
|
||||
@Nullable
|
||||
static InstallationType installTypeFromPackageName(@Nullable String packageName) {
|
||||
for (InstallationType type : values()) {
|
||||
for (String installPackageName : type.packageNames) {
|
||||
if (Objects.equals(installPackageName, packageName)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Array elements can be null.
|
||||
*/
|
||||
final String[] packageNames;
|
||||
|
||||
InstallationType(String... packageNames) {
|
||||
this.packageNames = packageNames;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the app is installed by the manager, the app store, or through adb/CLI.
|
||||
* <br>
|
||||
* Does not conclusively
|
||||
* If the app is installed by the manager or the app store, it is likely, the app was patched using the manager,
|
||||
* or installed manually via ADB (in the case of ReVanced CLI for example).
|
||||
* <br>
|
||||
* If the app is not installed by the manager or the app store, then the app was likely downloaded pre-patched
|
||||
* and installed by the browser or another unknown app.
|
||||
*/
|
||||
private static class CheckExpectedInstaller extends Check {
|
||||
@Nullable
|
||||
InstallationType installerFound;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Boolean check() {
|
||||
final var context = Utils.getContext();
|
||||
|
||||
final var installerPackageName =
|
||||
context.getPackageManager().getInstallerPackageName(context.getPackageName());
|
||||
|
||||
Logger.printInfo(() -> "Installed by: " + installerPackageName);
|
||||
|
||||
installerFound = InstallationType.installTypeFromPackageName(installerPackageName);
|
||||
final boolean passed = (installerFound != null);
|
||||
|
||||
Logger.printInfo(() -> passed
|
||||
? "Apk was not installed from an unknown source"
|
||||
: "Apk was installed from an unknown source");
|
||||
|
||||
return passed;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String failureReason() {
|
||||
return str("revanced_check_environment_manager_not_expected_installer");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int uiSortingValue() {
|
||||
return -100; // Show first.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the build properties are the same as during the patch.
|
||||
* <br>
|
||||
* If the build properties are the same as during the patch, it is likely, the app was patched on the same device.
|
||||
* <br>
|
||||
* If the build properties are different, the app was likely downloaded pre-patched or patched on another device.
|
||||
*/
|
||||
private static class CheckWasPatchedOnSameDevice extends Check {
|
||||
@SuppressLint({"NewApi", "HardwareIds"})
|
||||
@Override
|
||||
protected Boolean check() {
|
||||
if (PATCH_BOARD.isEmpty()) {
|
||||
// Did not patch with Manager, and cannot conclusively say where this was from.
|
||||
Logger.printInfo(() -> "APK does not contain a hardware signature and cannot compare to current device");
|
||||
return null;
|
||||
}
|
||||
|
||||
//noinspection deprecation
|
||||
final var passed = buildFieldEqualsHash("BOARD", Build.BOARD, PATCH_BOARD) &
|
||||
buildFieldEqualsHash("BOOTLOADER", Build.BOOTLOADER, PATCH_BOOTLOADER) &
|
||||
buildFieldEqualsHash("BRAND", Build.BRAND, PATCH_BRAND) &
|
||||
buildFieldEqualsHash("CPU_ABI", Build.CPU_ABI, PATCH_CPU_ABI) &
|
||||
buildFieldEqualsHash("CPU_ABI2", Build.CPU_ABI2, PATCH_CPU_ABI2) &
|
||||
buildFieldEqualsHash("DEVICE", Build.DEVICE, PATCH_DEVICE) &
|
||||
buildFieldEqualsHash("DISPLAY", Build.DISPLAY, PATCH_DISPLAY) &
|
||||
buildFieldEqualsHash("FINGERPRINT", Build.FINGERPRINT, PATCH_FINGERPRINT) &
|
||||
buildFieldEqualsHash("HARDWARE", Build.HARDWARE, PATCH_HARDWARE) &
|
||||
buildFieldEqualsHash("HOST", Build.HOST, PATCH_HOST) &
|
||||
buildFieldEqualsHash("ID", Build.ID, PATCH_ID) &
|
||||
buildFieldEqualsHash("MANUFACTURER", Build.MANUFACTURER, PATCH_MANUFACTURER) &
|
||||
buildFieldEqualsHash("MODEL", Build.MODEL, PATCH_MODEL) &
|
||||
buildFieldEqualsHash("PRODUCT", Build.PRODUCT, PATCH_PRODUCT) &
|
||||
buildFieldEqualsHash("RADIO", Build.RADIO, PATCH_RADIO) &
|
||||
buildFieldEqualsHash("TAGS", Build.TAGS, PATCH_TAGS) &
|
||||
buildFieldEqualsHash("TYPE", Build.TYPE, PATCH_TYPE) &
|
||||
buildFieldEqualsHash("USER", Build.USER, PATCH_USER);
|
||||
|
||||
Logger.printInfo(() -> passed
|
||||
? "Device hardware signature matches current device"
|
||||
: "Device hardware signature does not match current device");
|
||||
|
||||
return passed;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String failureReason() {
|
||||
return str("revanced_check_environment_not_same_patching_device");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int uiSortingValue() {
|
||||
return 0; // Show in the middle.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the app was installed within the last 30 minutes after being patched.
|
||||
* <br>
|
||||
* If the app was installed within the last 30 minutes, it is likely, the app was patched by the user.
|
||||
* <br>
|
||||
* If the app was installed much later than the patch time, it is likely the app was
|
||||
* downloaded pre-patched or the user waited too long to install the app.
|
||||
*/
|
||||
private static class CheckIsNearPatchTime extends Check {
|
||||
/**
|
||||
* How soon after patching the app must be installed to pass.
|
||||
*/
|
||||
static final int INSTALL_AFTER_PATCHING_DURATION_THRESHOLD = 30 * 60 * 1000; // 30 minutes.
|
||||
|
||||
/**
|
||||
* Milliseconds between the time the app was patched, and when it was installed/updated.
|
||||
*/
|
||||
long durationBetweenPatchingAndInstallation;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Boolean check() {
|
||||
try {
|
||||
Context context = Utils.getContext();
|
||||
PackageManager packageManager = context.getPackageManager();
|
||||
PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0);
|
||||
|
||||
// Duration since initial install or last update, which ever is sooner.
|
||||
durationBetweenPatchingAndInstallation = packageInfo.lastUpdateTime - PatchInfo.PATCH_TIME;
|
||||
Logger.printInfo(() -> "App was installed/updated: "
|
||||
+ (durationBetweenPatchingAndInstallation / (60 * 1000) + " minutes after patching"));
|
||||
|
||||
if (durationBetweenPatchingAndInstallation < 0) {
|
||||
// Patch time is in the future and clearly wrong.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (durationBetweenPatchingAndInstallation < INSTALL_AFTER_PATCHING_DURATION_THRESHOLD) {
|
||||
return true;
|
||||
}
|
||||
} catch (PackageManager.NameNotFoundException ex) {
|
||||
Logger.printException(() -> "Package name not found exception", ex); // Will never happen.
|
||||
}
|
||||
|
||||
// User installed more than 30 minutes after patching.
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String failureReason() {
|
||||
if (durationBetweenPatchingAndInstallation < 0) {
|
||||
// Could happen if the user has their device clock incorrectly set in the past,
|
||||
// but assume that isn't the case and the apk was patched on a device with the wrong system time.
|
||||
return str("revanced_check_environment_not_near_patch_time_invalid");
|
||||
}
|
||||
|
||||
// If patched over 1 day ago, show how old this pre-patched apk is.
|
||||
// Showing the age can help convey it's better to patch yourself and know it's the latest.
|
||||
final long oneDay = 24 * 60 * 60 * 1000;
|
||||
final long daysSincePatching = durationBetweenPatchingAndInstallation / oneDay;
|
||||
if (daysSincePatching > 1) { // Use over 1 day to avoid singular vs plural strings.
|
||||
return str("revanced_check_environment_not_near_patch_time_days", daysSincePatching);
|
||||
}
|
||||
|
||||
return str("revanced_check_environment_not_near_patch_time");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int uiSortingValue() {
|
||||
return 100; // Show last.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void check(Activity context) {
|
||||
// If the warning was already issued twice, or if the check was successful in the past,
|
||||
// do not run the checks again.
|
||||
if (!Check.shouldRun() && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) {
|
||||
Logger.printDebug(() -> "Environment checks are disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
Utils.runOnBackgroundThread(() -> {
|
||||
try {
|
||||
Logger.printInfo(() -> "Running environment checks");
|
||||
List<Check> failedChecks = new ArrayList<>();
|
||||
|
||||
CheckWasPatchedOnSameDevice sameHardware = new CheckWasPatchedOnSameDevice();
|
||||
Boolean hardwareCheckPassed = sameHardware.check();
|
||||
if (hardwareCheckPassed != null) {
|
||||
if (hardwareCheckPassed && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) {
|
||||
// Patched on the same device using Manager,
|
||||
// and no further checks are needed.
|
||||
Check.disableForever();
|
||||
return;
|
||||
}
|
||||
|
||||
failedChecks.add(sameHardware);
|
||||
}
|
||||
|
||||
CheckExpectedInstaller installerCheck = new CheckExpectedInstaller();
|
||||
if (installerCheck.check() && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) {
|
||||
// If the installer package is Manager but this code is reached,
|
||||
// that means it must not be the right Manager otherwise the hardware hash
|
||||
// signatures would be present and this check would not have run.
|
||||
if (installerCheck.installerFound == InstallationType.MANAGER) {
|
||||
failedChecks.add(installerCheck);
|
||||
// Also could not have been patched on this device.
|
||||
failedChecks.add(sameHardware);
|
||||
} else if (failedChecks.isEmpty()) {
|
||||
// ADB install of CLI build. Allow even if patched a long time ago.
|
||||
Check.disableForever();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
failedChecks.add(installerCheck);
|
||||
}
|
||||
|
||||
CheckIsNearPatchTime nearPatchTime = new CheckIsNearPatchTime();
|
||||
Boolean timeCheckPassed = nearPatchTime.check();
|
||||
if (timeCheckPassed && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) {
|
||||
// Allow installing recently patched apks,
|
||||
// even if the install source is not Manager or ADB.
|
||||
Check.disableForever();
|
||||
return;
|
||||
} else {
|
||||
failedChecks.add(nearPatchTime);
|
||||
}
|
||||
|
||||
if (DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) {
|
||||
// Show all failures for debugging layout.
|
||||
failedChecks = Arrays.asList(
|
||||
sameHardware,
|
||||
nearPatchTime,
|
||||
installerCheck
|
||||
);
|
||||
}
|
||||
|
||||
//noinspection ComparatorCombinators
|
||||
Collections.sort(failedChecks, (o1, o2) -> o1.uiSortingValue() - o2.uiSortingValue());
|
||||
|
||||
Check.issueWarning(
|
||||
context,
|
||||
failedChecks
|
||||
);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "check failure", ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static boolean buildFieldEqualsHash(String buildFieldName, String buildFieldValue, @Nullable String hash) {
|
||||
try {
|
||||
final var sha1 = MessageDigest.getInstance("SHA-1")
|
||||
.digest(buildFieldValue.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// Must be careful to use same base64 encoding Kotlin uses.
|
||||
String runtimeHash = new String(Base64.encode(sha1, Base64.NO_WRAP), StandardCharsets.ISO_8859_1);
|
||||
final boolean equals = runtimeHash.equals(hash);
|
||||
if (!equals) {
|
||||
Logger.printInfo(() -> "Hashes do not match. " + buildFieldName + ": '" + buildFieldValue
|
||||
+ "' runtimeHash: '" + runtimeHash + "' patchTimeHash: '" + hash + "'");
|
||||
}
|
||||
|
||||
return equals;
|
||||
} catch (NoSuchAlgorithmException ex) {
|
||||
Logger.printException(() -> "buildFieldEqualsHash failure", ex); // Will never happen.
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package app.revanced.extension.shared.checks;
|
||||
|
||||
/**
|
||||
* Fields are set by the patch. Do not modify.
|
||||
* Fields are not final, because the compiler is inlining them.
|
||||
*
|
||||
* @noinspection CanBeFinal
|
||||
*/
|
||||
final class PatchInfo {
|
||||
static long PATCH_TIME = 0L;
|
||||
|
||||
final static class Build {
|
||||
static String PATCH_BOARD = "";
|
||||
static String PATCH_BOOTLOADER = "";
|
||||
static String PATCH_BRAND = "";
|
||||
static String PATCH_CPU_ABI = "";
|
||||
static String PATCH_CPU_ABI2 = "";
|
||||
static String PATCH_DEVICE = "";
|
||||
static String PATCH_DISPLAY = "";
|
||||
static String PATCH_FINGERPRINT = "";
|
||||
static String PATCH_HARDWARE = "";
|
||||
static String PATCH_HOST = "";
|
||||
static String PATCH_ID = "";
|
||||
static String PATCH_MANUFACTURER = "";
|
||||
static String PATCH_MODEL = "";
|
||||
static String PATCH_PRODUCT = "";
|
||||
static String PATCH_RADIO = "";
|
||||
static String PATCH_TAGS = "";
|
||||
static String PATCH_TYPE = "";
|
||||
static String PATCH_USER = "";
|
||||
}
|
||||
}
|
@ -0,0 +1,208 @@
|
||||
package app.revanced.extension.shared.fixes.slink;
|
||||
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.NonNull;
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URL;
|
||||
import java.util.Objects;
|
||||
|
||||
import static app.revanced.extension.shared.Utils.getContext;
|
||||
|
||||
|
||||
/**
|
||||
* Base class to implement /s/ link resolution in 3rd party Reddit apps.
|
||||
* <br>
|
||||
* <br>
|
||||
* Usage:
|
||||
* <br>
|
||||
* <br>
|
||||
* An implementation of this class must have two static methods that are called by the app:
|
||||
* <ul>
|
||||
* <li>public static boolean patchResolveSLink(String link)</li>
|
||||
* <li>public static void patchSetAccessToken(String accessToken)</li>
|
||||
* </ul>
|
||||
* The static methods must call the instance methods of the base class.
|
||||
* <br>
|
||||
* The singleton pattern can be used to access the instance of the class:
|
||||
* <pre>
|
||||
* {@code
|
||||
* {
|
||||
* INSTANCE = new FixSLinksPatch();
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
* Set the app's web view activity class as a fallback to open /s/ links if the resolution fails:
|
||||
* <pre>
|
||||
* {@code
|
||||
* private FixSLinksPatch() {
|
||||
* webViewActivityClass = WebViewActivity.class;
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
* Hook the app's navigation handler to call this method before doing any of its own resolution:
|
||||
* <pre>
|
||||
* {@code
|
||||
* public static boolean patchResolveSLink(Context context, String link) {
|
||||
* return INSTANCE.resolveSLink(context, link);
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
* If this method returns true, the app should early return and not do any of its own resolution.
|
||||
* <br>
|
||||
* <br>
|
||||
* Hook the app's access token so that this class can use it to resolve /s/ links:
|
||||
* <pre>
|
||||
* {@code
|
||||
* public static void patchSetAccessToken(String accessToken) {
|
||||
* INSTANCE.setAccessToken(access_token);
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
public abstract class BaseFixSLinksPatch {
|
||||
/**
|
||||
* The class of the activity used to open links in a web view if resolving them fails.
|
||||
*/
|
||||
protected Class<? extends Activity> webViewActivityClass;
|
||||
|
||||
/**
|
||||
* The access token used to resolve the /s/ link.
|
||||
*/
|
||||
protected String accessToken;
|
||||
|
||||
/**
|
||||
* The URL that was trying to be resolved before the access token was set.
|
||||
* If this is not null, the URL will be resolved right after the access token is set.
|
||||
*/
|
||||
protected String pendingUrl;
|
||||
|
||||
/**
|
||||
* The singleton instance of the class.
|
||||
*/
|
||||
protected static BaseFixSLinksPatch INSTANCE;
|
||||
|
||||
public boolean resolveSLink(String link) {
|
||||
switch (resolveLink(link)) {
|
||||
case ACCESS_TOKEN_START: {
|
||||
pendingUrl = link;
|
||||
return true;
|
||||
}
|
||||
case DO_NOTHING:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private ResolveResult resolveLink(String link) {
|
||||
Context context = getContext();
|
||||
if (link.matches(".*reddit\\.com/r/[^/]+/s/[^/]+")) {
|
||||
// A link ends with #bypass if it failed to resolve below.
|
||||
// resolveLink is called with the same link again but this time with #bypass
|
||||
// so that the link is opened in the app browser instead of trying to resolve it again.
|
||||
if (link.endsWith("#bypass")) {
|
||||
openInAppBrowser(context, link);
|
||||
|
||||
return ResolveResult.DO_NOTHING;
|
||||
}
|
||||
|
||||
Logger.printDebug(() -> "Resolving " + link);
|
||||
|
||||
if (accessToken == null) {
|
||||
// This is not optimal.
|
||||
// However, an accessToken is necessary to make an authenticated request to Reddit.
|
||||
// in case Reddit has banned the IP - e.g. VPN.
|
||||
Intent startIntent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName());
|
||||
context.startActivity(startIntent);
|
||||
|
||||
return ResolveResult.ACCESS_TOKEN_START;
|
||||
}
|
||||
|
||||
|
||||
Utils.runOnBackgroundThread(() -> {
|
||||
String bypassLink = link + "#bypass";
|
||||
|
||||
String finalLocation = bypassLink;
|
||||
try {
|
||||
HttpURLConnection connection = getHttpURLConnection(link, accessToken);
|
||||
connection.connect();
|
||||
String location = connection.getHeaderField("location");
|
||||
connection.disconnect();
|
||||
|
||||
Objects.requireNonNull(location, "Location is null");
|
||||
|
||||
finalLocation = location;
|
||||
Logger.printDebug(() -> "Resolved " + link + " to " + location);
|
||||
} catch (SocketTimeoutException e) {
|
||||
Logger.printException(() -> "Timeout when trying to resolve " + link, e);
|
||||
finalLocation = bypassLink;
|
||||
} catch (Exception e) {
|
||||
Logger.printException(() -> "Failed to resolve " + link, e);
|
||||
finalLocation = bypassLink;
|
||||
} finally {
|
||||
Intent startIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(finalLocation));
|
||||
startIntent.setPackage(context.getPackageName());
|
||||
startIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(startIntent);
|
||||
}
|
||||
});
|
||||
|
||||
return ResolveResult.DO_NOTHING;
|
||||
}
|
||||
|
||||
return ResolveResult.CONTINUE;
|
||||
}
|
||||
|
||||
public void setAccessToken(String accessToken) {
|
||||
Logger.printDebug(() -> "Setting access token");
|
||||
|
||||
this.accessToken = accessToken;
|
||||
|
||||
// In case a link was trying to be resolved before access token was set.
|
||||
// The link is resolved now, after the access token is set.
|
||||
if (pendingUrl != null) {
|
||||
String link = pendingUrl;
|
||||
pendingUrl = null;
|
||||
|
||||
Logger.printDebug(() -> "Opening pending URL");
|
||||
|
||||
resolveLink(link);
|
||||
}
|
||||
}
|
||||
|
||||
private void openInAppBrowser(Context context, String link) {
|
||||
Intent intent = new Intent(context, webViewActivityClass);
|
||||
intent.putExtra("url", link);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private HttpURLConnection getHttpURLConnection(String link, String accessToken) throws IOException {
|
||||
URL url = new URL(link);
|
||||
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setInstanceFollowRedirects(false);
|
||||
connection.setRequestMethod("HEAD");
|
||||
connection.setConnectTimeout(2000);
|
||||
connection.setReadTimeout(2000);
|
||||
|
||||
if (accessToken != null) {
|
||||
Logger.printDebug(() -> "Setting access token to make /s/ request");
|
||||
|
||||
connection.setRequestProperty("Authorization", "Bearer " + accessToken);
|
||||
} else {
|
||||
Logger.printDebug(() -> "Not setting access token to make /s/ request, because it is null");
|
||||
}
|
||||
|
||||
return connection;
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package app.revanced.extension.shared.fixes.slink;
|
||||
|
||||
public enum ResolveResult {
|
||||
// Let app handle rest of stuff
|
||||
CONTINUE,
|
||||
// Start app, to make it cache its access_token
|
||||
ACCESS_TOKEN_START,
|
||||
// Don't do anything - we started resolving
|
||||
DO_NOTHING
|
||||
}
|
@ -0,0 +1,145 @@
|
||||
package app.revanced.extension.shared.requests;
|
||||
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
|
||||
public class Requester {
|
||||
private Requester() {
|
||||
}
|
||||
|
||||
public static HttpURLConnection getConnectionFromRoute(String apiUrl, Route route, String... params) throws IOException {
|
||||
return getConnectionFromCompiledRoute(apiUrl, route.compile(params));
|
||||
}
|
||||
|
||||
public static HttpURLConnection getConnectionFromCompiledRoute(String apiUrl, Route.CompiledRoute route) throws IOException {
|
||||
String url = apiUrl + route.getCompiledRoute();
|
||||
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
|
||||
// Request data is in the URL parameters and no body is sent.
|
||||
// The calling code must set a length if using a request body.
|
||||
connection.setFixedLengthStreamingMode(0);
|
||||
connection.setRequestMethod(route.getMethod().name());
|
||||
String agentString = System.getProperty("http.agent")
|
||||
+ "; ReVanced/" + Utils.getAppVersionName()
|
||||
+ " (" + Utils.getPatchesReleaseVersion() + ")";
|
||||
connection.setRequestProperty("User-Agent", agentString);
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the {@link HttpURLConnection}, and closes the underlying InputStream.
|
||||
*/
|
||||
private static String parseInputStreamAndClose(InputStream inputStream) throws IOException {
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
|
||||
StringBuilder jsonBuilder = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
jsonBuilder.append(line);
|
||||
jsonBuilder.append('\n');
|
||||
}
|
||||
return jsonBuilder.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the {@link HttpURLConnection} response as a String.
|
||||
* This does not close the url connection. If further requests to this host are unlikely
|
||||
* in the near future, then instead use {@link #parseStringAndDisconnect(HttpURLConnection)}.
|
||||
*/
|
||||
public static String parseString(HttpURLConnection connection) throws IOException {
|
||||
return parseInputStreamAndClose(connection.getInputStream());
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the {@link HttpURLConnection} response as a String, and disconnect.
|
||||
*
|
||||
* <b>Should only be used if other requests to the server in the near future are unlikely</b>
|
||||
*
|
||||
* @see #parseString(HttpURLConnection)
|
||||
*/
|
||||
public static String parseStringAndDisconnect(HttpURLConnection connection) throws IOException {
|
||||
String result = parseString(connection);
|
||||
connection.disconnect();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the {@link HttpURLConnection} error stream as a String.
|
||||
* If the server sent no error response data, this returns an empty string.
|
||||
*/
|
||||
public static String parseErrorString(HttpURLConnection connection) throws IOException {
|
||||
InputStream errorStream = connection.getErrorStream();
|
||||
if (errorStream == null) {
|
||||
return "";
|
||||
}
|
||||
return parseInputStreamAndClose(errorStream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the {@link HttpURLConnection} error stream as a String, and disconnect.
|
||||
* If the server sent no error response data, this returns an empty string.
|
||||
*
|
||||
* Should only be used if other requests to the server are unlikely in the near future.
|
||||
*
|
||||
* @see #parseErrorString(HttpURLConnection)
|
||||
*/
|
||||
public static String parseErrorStringAndDisconnect(HttpURLConnection connection) throws IOException {
|
||||
String result = parseErrorString(connection);
|
||||
connection.disconnect();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the {@link HttpURLConnection} response into a JSONObject.
|
||||
* This does not close the url connection. If further requests to this host are unlikely
|
||||
* in the near future, then instead use {@link #parseJSONObjectAndDisconnect(HttpURLConnection)}.
|
||||
*/
|
||||
public static JSONObject parseJSONObject(HttpURLConnection connection) throws JSONException, IOException {
|
||||
return new JSONObject(parseString(connection));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the {@link HttpURLConnection}, close the underlying InputStream, and disconnect.
|
||||
*
|
||||
* <b>Should only be used if other requests to the server in the near future are unlikely</b>
|
||||
*
|
||||
* @see #parseJSONObject(HttpURLConnection)
|
||||
*/
|
||||
public static JSONObject parseJSONObjectAndDisconnect(HttpURLConnection connection) throws JSONException, IOException {
|
||||
JSONObject object = parseJSONObject(connection);
|
||||
connection.disconnect();
|
||||
return object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the {@link HttpURLConnection}, and closes the underlying InputStream.
|
||||
* This does not close the url connection. If further requests to this host are unlikely
|
||||
* in the near future, then instead use {@link #parseJSONArrayAndDisconnect(HttpURLConnection)}.
|
||||
*/
|
||||
public static JSONArray parseJSONArray(HttpURLConnection connection) throws JSONException, IOException {
|
||||
return new JSONArray(parseString(connection));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the {@link HttpURLConnection}, close the underlying InputStream, and disconnect.
|
||||
*
|
||||
* <b>Should only be used if other requests to the server in the near future are unlikely</b>
|
||||
*
|
||||
* @see #parseJSONArray(HttpURLConnection)
|
||||
*/
|
||||
public static JSONArray parseJSONArrayAndDisconnect(HttpURLConnection connection) throws JSONException, IOException {
|
||||
JSONArray array = parseJSONArray(connection);
|
||||
connection.disconnect();
|
||||
return array;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
package app.revanced.extension.shared.requests;
|
||||
|
||||
public class Route {
|
||||
private final String route;
|
||||
private final Method method;
|
||||
private final int paramCount;
|
||||
|
||||
public Route(Method method, String route) {
|
||||
this.method = method;
|
||||
this.route = route;
|
||||
this.paramCount = countMatches(route, '{');
|
||||
|
||||
if (paramCount != countMatches(route, '}'))
|
||||
throw new IllegalArgumentException("Not enough parameters");
|
||||
}
|
||||
|
||||
public Method getMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
public CompiledRoute compile(String... params) {
|
||||
if (params.length != paramCount)
|
||||
throw new IllegalArgumentException("Error compiling route [" + route + "], incorrect amount of parameters provided. " +
|
||||
"Expected: " + paramCount + ", provided: " + params.length);
|
||||
|
||||
StringBuilder compiledRoute = new StringBuilder(route);
|
||||
for (int i = 0; i < paramCount; i++) {
|
||||
int paramStart = compiledRoute.indexOf("{");
|
||||
int paramEnd = compiledRoute.indexOf("}");
|
||||
compiledRoute.replace(paramStart, paramEnd + 1, params[i]);
|
||||
}
|
||||
return new CompiledRoute(this, compiledRoute.toString());
|
||||
}
|
||||
|
||||
public static class CompiledRoute {
|
||||
private final Route baseRoute;
|
||||
private final String compiledRoute;
|
||||
|
||||
private CompiledRoute(Route baseRoute, String compiledRoute) {
|
||||
this.baseRoute = baseRoute;
|
||||
this.compiledRoute = compiledRoute;
|
||||
}
|
||||
|
||||
public String getCompiledRoute() {
|
||||
return compiledRoute;
|
||||
}
|
||||
|
||||
public Method getMethod() {
|
||||
return baseRoute.method;
|
||||
}
|
||||
}
|
||||
|
||||
private int countMatches(CharSequence seq, char c) {
|
||||
int count = 0;
|
||||
for (int i = 0; i < seq.length(); i++) {
|
||||
if (seq.charAt(i) == c)
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public enum Method {
|
||||
GET,
|
||||
POST
|
||||
}
|
||||
}
|
@ -0,0 +1,117 @@
|
||||
package app.revanced.extension.shared.settings;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public enum AppLanguage {
|
||||
/**
|
||||
* The current app language.
|
||||
*/
|
||||
DEFAULT,
|
||||
|
||||
// Languages codes not included with YouTube, but are translated on Crowdin
|
||||
GA,
|
||||
|
||||
// Language codes found in locale_config.xml
|
||||
// All region specific variants have been removed.
|
||||
AF,
|
||||
AM,
|
||||
AR,
|
||||
AS,
|
||||
AZ,
|
||||
BE,
|
||||
BG,
|
||||
BN,
|
||||
BS,
|
||||
CA,
|
||||
CS,
|
||||
DA,
|
||||
DE,
|
||||
EL,
|
||||
EN,
|
||||
ES,
|
||||
ET,
|
||||
EU,
|
||||
FA,
|
||||
FI,
|
||||
FR,
|
||||
GL,
|
||||
GU,
|
||||
HI,
|
||||
HE, // App uses obsolete 'IW' and not the modern 'HE' ISO code.
|
||||
HR,
|
||||
HU,
|
||||
HY,
|
||||
ID,
|
||||
IS,
|
||||
IT,
|
||||
JA,
|
||||
KA,
|
||||
KK,
|
||||
KM,
|
||||
KN,
|
||||
KO,
|
||||
KY,
|
||||
LO,
|
||||
LT,
|
||||
LV,
|
||||
MK,
|
||||
ML,
|
||||
MN,
|
||||
MR,
|
||||
MS,
|
||||
MY,
|
||||
NE,
|
||||
NL,
|
||||
NB,
|
||||
OR,
|
||||
PA,
|
||||
PL,
|
||||
PT,
|
||||
RO,
|
||||
RU,
|
||||
SI,
|
||||
SK,
|
||||
SL,
|
||||
SQ,
|
||||
SR,
|
||||
SV,
|
||||
SW,
|
||||
TA,
|
||||
TE,
|
||||
TH,
|
||||
TL,
|
||||
TR,
|
||||
UK,
|
||||
UR,
|
||||
UZ,
|
||||
VI,
|
||||
ZH,
|
||||
ZU;
|
||||
|
||||
private final String language;
|
||||
|
||||
AppLanguage() {
|
||||
language = name().toLowerCase(Locale.US);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The 2 letter ISO 639_1 language code.
|
||||
*/
|
||||
public String getLanguage() {
|
||||
// Changing the app language does not force the app to completely restart,
|
||||
// so the default needs to be the current language and not a static field.
|
||||
if (this == DEFAULT) {
|
||||
return Locale.getDefault().getLanguage();
|
||||
}
|
||||
|
||||
return language;
|
||||
}
|
||||
|
||||
public Locale getLocale() {
|
||||
if (this == DEFAULT) {
|
||||
return Locale.getDefault();
|
||||
}
|
||||
|
||||
return Locale.forLanguageTag(language);
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package app.revanced.extension.shared.settings;
|
||||
|
||||
import static java.lang.Boolean.FALSE;
|
||||
import static java.lang.Boolean.TRUE;
|
||||
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.SpoofiOSAvailability;
|
||||
|
||||
import app.revanced.extension.shared.spoof.ClientType;
|
||||
|
||||
/**
|
||||
* Settings shared across multiple apps.
|
||||
* <p>
|
||||
* To ensure this class is loaded when the UI is created, app specific setting bundles should extend
|
||||
* or reference this class.
|
||||
*/
|
||||
public class BaseSettings {
|
||||
public static final BooleanSetting DEBUG = new BooleanSetting("revanced_debug", FALSE);
|
||||
public static final BooleanSetting DEBUG_STACKTRACE = new BooleanSetting("revanced_debug_stacktrace", FALSE, parent(DEBUG));
|
||||
public static final BooleanSetting DEBUG_TOAST_ON_ERROR = new BooleanSetting("revanced_debug_toast_on_error", TRUE, "revanced_debug_toast_on_error_user_dialog_message");
|
||||
|
||||
public static final IntegerSetting CHECK_ENVIRONMENT_WARNINGS_ISSUED = new IntegerSetting("revanced_check_environment_warnings_issued", 0, true, false);
|
||||
|
||||
public static final EnumSetting<AppLanguage> REVANCED_LANGUAGE = new EnumSetting<>("revanced_language", AppLanguage.DEFAULT, true, "revanced_language_user_dialog_message");
|
||||
|
||||
/**
|
||||
* Use the icons declared in the preferences created during patching. If no icons or styles are declared then this setting does nothing.
|
||||
*/
|
||||
public static final BooleanSetting SHOW_MENU_ICONS = new BooleanSetting("revanced_show_menu_icons", TRUE, true);
|
||||
|
||||
public static final BooleanSetting SPOOF_VIDEO_STREAMS = new BooleanSetting("revanced_spoof_video_streams", TRUE, true, "revanced_spoof_video_streams_user_dialog_message");
|
||||
public static final EnumSetting<AppLanguage> SPOOF_VIDEO_STREAMS_LANGUAGE = new EnumSetting<>("revanced_spoof_video_streams_language", AppLanguage.DEFAULT, new AudioStreamLanguageOverrideAvailability());
|
||||
public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE, parent(SPOOF_VIDEO_STREAMS));
|
||||
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));
|
||||
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
package app.revanced.extension.shared.settings;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class BooleanSetting extends Setting<Boolean> {
|
||||
public BooleanSetting(String key, Boolean defaultValue) {
|
||||
super(key, defaultValue);
|
||||
}
|
||||
public BooleanSetting(String key, Boolean defaultValue, boolean rebootApp) {
|
||||
super(key, defaultValue, rebootApp);
|
||||
}
|
||||
public BooleanSetting(String key, Boolean defaultValue, boolean rebootApp, boolean includeWithImportExport) {
|
||||
super(key, defaultValue, rebootApp, includeWithImportExport);
|
||||
}
|
||||
public BooleanSetting(String key, Boolean defaultValue, String userDialogMessage) {
|
||||
super(key, defaultValue, userDialogMessage);
|
||||
}
|
||||
public BooleanSetting(String key, Boolean defaultValue, Availability availability) {
|
||||
super(key, defaultValue, availability);
|
||||
}
|
||||
public BooleanSetting(String key, Boolean defaultValue, boolean rebootApp, String userDialogMessage) {
|
||||
super(key, defaultValue, rebootApp, userDialogMessage);
|
||||
}
|
||||
public BooleanSetting(String key, Boolean defaultValue, boolean rebootApp, Availability availability) {
|
||||
super(key, defaultValue, rebootApp, availability);
|
||||
}
|
||||
public BooleanSetting(String key, Boolean defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) {
|
||||
super(key, defaultValue, rebootApp, userDialogMessage, availability);
|
||||
}
|
||||
public BooleanSetting(@NonNull String key, @NonNull Boolean defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) {
|
||||
super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets, but does _not_ persistently save the value.
|
||||
* This method is only to be used by the Settings preference code.
|
||||
*
|
||||
* This intentionally is a static method to deter
|
||||
* accidental usage when {@link #save(Boolean)} was intnded.
|
||||
*/
|
||||
public static void privateSetValue(@NonNull BooleanSetting setting, @NonNull Boolean newValue) {
|
||||
setting.value = Objects.requireNonNull(newValue);
|
||||
|
||||
if (setting.isSetToDefault()) {
|
||||
setting.removeFromPreferences();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void load() {
|
||||
value = preferences.getBoolean(key, defaultValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean readFromJSON(JSONObject json, String importExportKey) throws JSONException {
|
||||
return json.getBoolean(importExportKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setValueFromString(@NonNull String newValue) {
|
||||
value = Boolean.valueOf(Objects.requireNonNull(newValue));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveToPreferences() {
|
||||
preferences.saveBoolean(key, value);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Boolean get() {
|
||||
return value;
|
||||
}
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
package app.revanced.extension.shared.settings;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
|
||||
/**
|
||||
* If an Enum value is removed or changed, any saved or imported data using the
|
||||
* non-existent value will be reverted to the default value
|
||||
* (the event is logged, but no user error is displayed).
|
||||
*
|
||||
* All saved JSON text is converted to lowercase to keep the output less obnoxious.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class EnumSetting<T extends Enum<?>> extends Setting<T> {
|
||||
public EnumSetting(String key, T defaultValue) {
|
||||
super(key, defaultValue);
|
||||
}
|
||||
public EnumSetting(String key, T defaultValue, boolean rebootApp) {
|
||||
super(key, defaultValue, rebootApp);
|
||||
}
|
||||
public EnumSetting(String key, T defaultValue, boolean rebootApp, boolean includeWithImportExport) {
|
||||
super(key, defaultValue, rebootApp, includeWithImportExport);
|
||||
}
|
||||
public EnumSetting(String key, T defaultValue, String userDialogMessage) {
|
||||
super(key, defaultValue, userDialogMessage);
|
||||
}
|
||||
public EnumSetting(String key, T defaultValue, Availability availability) {
|
||||
super(key, defaultValue, availability);
|
||||
}
|
||||
public EnumSetting(String key, T defaultValue, boolean rebootApp, String userDialogMessage) {
|
||||
super(key, defaultValue, rebootApp, userDialogMessage);
|
||||
}
|
||||
public EnumSetting(String key, T defaultValue, boolean rebootApp, Availability availability) {
|
||||
super(key, defaultValue, rebootApp, availability);
|
||||
}
|
||||
public EnumSetting(String key, T defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) {
|
||||
super(key, defaultValue, rebootApp, userDialogMessage, availability);
|
||||
}
|
||||
public EnumSetting(@NonNull String key, @NonNull T defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) {
|
||||
super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void load() {
|
||||
value = preferences.getEnum(key, defaultValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected T readFromJSON(JSONObject json, String importExportKey) throws JSONException {
|
||||
String enumName = json.getString(importExportKey);
|
||||
try {
|
||||
return getEnumFromString(enumName);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
// Info level to allow removing enum values in the future without showing any user errors.
|
||||
Logger.printInfo(() -> "Using default, and ignoring unknown enum value: " + enumName, ex);
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeToJSON(JSONObject json, String importExportKey) throws JSONException {
|
||||
// Use lowercase to keep the output less ugly.
|
||||
json.put(importExportKey, value.name().toLowerCase(Locale.ENGLISH));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private T getEnumFromString(String enumName) {
|
||||
//noinspection ConstantConditions
|
||||
for (Enum<?> value : defaultValue.getClass().getEnumConstants()) {
|
||||
if (value.name().equalsIgnoreCase(enumName)) {
|
||||
// noinspection unchecked
|
||||
return (T) value;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Unknown enum value: " + enumName);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setValueFromString(@NonNull String newValue) {
|
||||
value = getEnumFromString(Objects.requireNonNull(newValue));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveToPreferences() {
|
||||
preferences.saveEnumAsString(key, value);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public T get() {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Availability based on if this setting is currently set to any of the provided types.
|
||||
*/
|
||||
@SafeVarargs
|
||||
public final Setting.Availability availability(@NonNull T... types) {
|
||||
return () -> {
|
||||
T currentEnumType = get();
|
||||
for (T enumType : types) {
|
||||
if (currentEnumType == enumType) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
package app.revanced.extension.shared.settings;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class FloatSetting extends Setting<Float> {
|
||||
|
||||
public FloatSetting(String key, Float defaultValue) {
|
||||
super(key, defaultValue);
|
||||
}
|
||||
public FloatSetting(String key, Float defaultValue, boolean rebootApp) {
|
||||
super(key, defaultValue, rebootApp);
|
||||
}
|
||||
public FloatSetting(String key, Float defaultValue, boolean rebootApp, boolean includeWithImportExport) {
|
||||
super(key, defaultValue, rebootApp, includeWithImportExport);
|
||||
}
|
||||
public FloatSetting(String key, Float defaultValue, String userDialogMessage) {
|
||||
super(key, defaultValue, userDialogMessage);
|
||||
}
|
||||
public FloatSetting(String key, Float defaultValue, Availability availability) {
|
||||
super(key, defaultValue, availability);
|
||||
}
|
||||
public FloatSetting(String key, Float defaultValue, boolean rebootApp, String userDialogMessage) {
|
||||
super(key, defaultValue, rebootApp, userDialogMessage);
|
||||
}
|
||||
public FloatSetting(String key, Float defaultValue, boolean rebootApp, Availability availability) {
|
||||
super(key, defaultValue, rebootApp, availability);
|
||||
}
|
||||
public FloatSetting(String key, Float defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) {
|
||||
super(key, defaultValue, rebootApp, userDialogMessage, availability);
|
||||
}
|
||||
public FloatSetting(@NonNull String key, @NonNull Float defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) {
|
||||
super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void load() {
|
||||
value = preferences.getFloatString(key, defaultValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Float readFromJSON(JSONObject json, String importExportKey) throws JSONException {
|
||||
return (float) json.getDouble(importExportKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setValueFromString(@NonNull String newValue) {
|
||||
value = Float.valueOf(Objects.requireNonNull(newValue));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveToPreferences() {
|
||||
preferences.saveFloatString(key, value);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Float get() {
|
||||
return value;
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
package app.revanced.extension.shared.settings;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class IntegerSetting extends Setting<Integer> {
|
||||
|
||||
public IntegerSetting(String key, Integer defaultValue) {
|
||||
super(key, defaultValue);
|
||||
}
|
||||
public IntegerSetting(String key, Integer defaultValue, boolean rebootApp) {
|
||||
super(key, defaultValue, rebootApp);
|
||||
}
|
||||
public IntegerSetting(String key, Integer defaultValue, boolean rebootApp, boolean includeWithImportExport) {
|
||||
super(key, defaultValue, rebootApp, includeWithImportExport);
|
||||
}
|
||||
public IntegerSetting(String key, Integer defaultValue, String userDialogMessage) {
|
||||
super(key, defaultValue, userDialogMessage);
|
||||
}
|
||||
public IntegerSetting(String key, Integer defaultValue, Availability availability) {
|
||||
super(key, defaultValue, availability);
|
||||
}
|
||||
public IntegerSetting(String key, Integer defaultValue, boolean rebootApp, String userDialogMessage) {
|
||||
super(key, defaultValue, rebootApp, userDialogMessage);
|
||||
}
|
||||
public IntegerSetting(String key, Integer defaultValue, boolean rebootApp, Availability availability) {
|
||||
super(key, defaultValue, rebootApp, availability);
|
||||
}
|
||||
public IntegerSetting(String key, Integer defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) {
|
||||
super(key, defaultValue, rebootApp, userDialogMessage, availability);
|
||||
}
|
||||
public IntegerSetting(@NonNull String key, @NonNull Integer defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) {
|
||||
super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void load() {
|
||||
value = preferences.getIntegerString(key, defaultValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Integer readFromJSON(JSONObject json, String importExportKey) throws JSONException {
|
||||
return json.getInt(importExportKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setValueFromString(@NonNull String newValue) {
|
||||
value = Integer.valueOf(Objects.requireNonNull(newValue));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveToPreferences() {
|
||||
preferences.saveIntegerString(key, value);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Integer get() {
|
||||
return value;
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
package app.revanced.extension.shared.settings;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class LongSetting extends Setting<Long> {
|
||||
|
||||
public LongSetting(String key, Long defaultValue) {
|
||||
super(key, defaultValue);
|
||||
}
|
||||
public LongSetting(String key, Long defaultValue, boolean rebootApp) {
|
||||
super(key, defaultValue, rebootApp);
|
||||
}
|
||||
public LongSetting(String key, Long defaultValue, boolean rebootApp, boolean includeWithImportExport) {
|
||||
super(key, defaultValue, rebootApp, includeWithImportExport);
|
||||
}
|
||||
public LongSetting(String key, Long defaultValue, String userDialogMessage) {
|
||||
super(key, defaultValue, userDialogMessage);
|
||||
}
|
||||
public LongSetting(String key, Long defaultValue, Availability availability) {
|
||||
super(key, defaultValue, availability);
|
||||
}
|
||||
public LongSetting(String key, Long defaultValue, boolean rebootApp, String userDialogMessage) {
|
||||
super(key, defaultValue, rebootApp, userDialogMessage);
|
||||
}
|
||||
public LongSetting(String key, Long defaultValue, boolean rebootApp, Availability availability) {
|
||||
super(key, defaultValue, rebootApp, availability);
|
||||
}
|
||||
public LongSetting(String key, Long defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) {
|
||||
super(key, defaultValue, rebootApp, userDialogMessage, availability);
|
||||
}
|
||||
public LongSetting(@NonNull String key, @NonNull Long defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) {
|
||||
super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void load() {
|
||||
value = preferences.getLongString(key, defaultValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Long readFromJSON(JSONObject json, String importExportKey) throws JSONException {
|
||||
return json.getLong(importExportKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setValueFromString(@NonNull String newValue) {
|
||||
value = Long.valueOf(Objects.requireNonNull(newValue));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveToPreferences() {
|
||||
preferences.saveLongString(key, value);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Long get() {
|
||||
return value;
|
||||
}
|
||||
}
|
@ -0,0 +1,495 @@
|
||||
package app.revanced.extension.shared.settings;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.StringRef;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
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> {
|
||||
|
||||
/**
|
||||
* Indicates if a {@link Setting} is available to edit and use.
|
||||
* Typically this is dependent upon other BooleanSetting(s) set to 'true',
|
||||
* but this can be used to call into extension code and check other conditions.
|
||||
*/
|
||||
public interface Availability {
|
||||
boolean isAvailable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Availability based on a single parent setting being enabled.
|
||||
*/
|
||||
@NonNull
|
||||
public static Availability parent(@NonNull BooleanSetting parent) {
|
||||
return parent::get;
|
||||
}
|
||||
|
||||
/**
|
||||
* Availability based on all parents being enabled.
|
||||
*/
|
||||
@NonNull
|
||||
public static Availability parentsAll(@NonNull BooleanSetting... parents) {
|
||||
return () -> {
|
||||
for (BooleanSetting parent : parents) {
|
||||
if (!parent.get()) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Availability based on any parent being enabled.
|
||||
*/
|
||||
@NonNull
|
||||
public static Availability parentsAny(@NonNull BooleanSetting... parents) {
|
||||
return () -> {
|
||||
for (BooleanSetting parent : parents) {
|
||||
if (parent.get()) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for importing/exporting settings.
|
||||
*/
|
||||
public interface ImportExportCallback {
|
||||
/**
|
||||
* Called after all settings have been imported.
|
||||
*/
|
||||
void settingsImported(@Nullable Context context);
|
||||
|
||||
/**
|
||||
* Called after all settings have been exported.
|
||||
*/
|
||||
void settingsExported(@Nullable Context context);
|
||||
}
|
||||
|
||||
private static final List<ImportExportCallback> importExportCallbacks = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Adds a callback for {@link #importFromJSON(Context, String)} and {@link #exportToJson(Context)}.
|
||||
*/
|
||||
public static void addImportExportCallback(@NonNull ImportExportCallback callback) {
|
||||
importExportCallbacks.add(Objects.requireNonNull(callback));
|
||||
}
|
||||
|
||||
/**
|
||||
* All settings that were instantiated.
|
||||
* When a new setting is created, it is automatically added to this list.
|
||||
*/
|
||||
private static final List<Setting<?>> SETTINGS = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Map of setting path to setting object.
|
||||
*/
|
||||
private static final Map<String, Setting<?>> PATH_TO_SETTINGS = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Preference all instances are saved to.
|
||||
*/
|
||||
public static final SharedPrefCategory preferences = new SharedPrefCategory("revanced_prefs");
|
||||
|
||||
@Nullable
|
||||
public static Setting<?> getSettingFromPath(@NonNull String str) {
|
||||
return PATH_TO_SETTINGS.get(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return All settings that have been created.
|
||||
*/
|
||||
@NonNull
|
||||
public static List<Setting<?>> allLoadedSettings() {
|
||||
return Collections.unmodifiableList(SETTINGS);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return All settings that have been created, sorted by keys.
|
||||
*/
|
||||
@NonNull
|
||||
private static List<Setting<?>> allLoadedSettingsSorted() {
|
||||
Collections.sort(SETTINGS, (Setting<?> o1, Setting<?> o2) -> o1.key.compareTo(o2.key));
|
||||
return allLoadedSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* The key used to store the value in the shared preferences.
|
||||
*/
|
||||
@NonNull
|
||||
public final String key;
|
||||
|
||||
/**
|
||||
* The default value of the setting.
|
||||
*/
|
||||
@NonNull
|
||||
public final T defaultValue;
|
||||
|
||||
/**
|
||||
* If the app should be rebooted, if this setting is changed
|
||||
*/
|
||||
public final boolean rebootApp;
|
||||
|
||||
/**
|
||||
* If this setting should be included when importing/exporting settings.
|
||||
*/
|
||||
public final boolean includeWithImportExport;
|
||||
|
||||
/**
|
||||
* If this setting is available to edit and use.
|
||||
* Not to be confused with it's status returned from {@link #get()}.
|
||||
*/
|
||||
@Nullable
|
||||
private final Availability availability;
|
||||
|
||||
/**
|
||||
* Confirmation message to display, if the user tries to change the setting from the default value.
|
||||
*/
|
||||
@Nullable
|
||||
public final StringRef userDialogMessage;
|
||||
|
||||
// Must be volatile, as some settings are read/write from different threads.
|
||||
// Of note, the object value is persistently stored using SharedPreferences (which is thread safe).
|
||||
/**
|
||||
* The value of the setting.
|
||||
*/
|
||||
@NonNull
|
||||
protected volatile T value;
|
||||
|
||||
public Setting(String key, T defaultValue) {
|
||||
this(key, defaultValue, false, true, null, null);
|
||||
}
|
||||
public Setting(String key, T defaultValue, boolean rebootApp) {
|
||||
this(key, defaultValue, rebootApp, true, null, null);
|
||||
}
|
||||
public Setting(String key, T defaultValue, boolean rebootApp, boolean includeWithImportExport) {
|
||||
this(key, defaultValue, rebootApp, includeWithImportExport, null, null);
|
||||
}
|
||||
public Setting(String key, T defaultValue, String userDialogMessage) {
|
||||
this(key, defaultValue, false, true, userDialogMessage, null);
|
||||
}
|
||||
public Setting(String key, T defaultValue, Availability availability) {
|
||||
this(key, defaultValue, false, true, null, availability);
|
||||
}
|
||||
public Setting(String key, T defaultValue, boolean rebootApp, String userDialogMessage) {
|
||||
this(key, defaultValue, rebootApp, true, userDialogMessage, null);
|
||||
}
|
||||
public Setting(String key, T defaultValue, boolean rebootApp, Availability availability) {
|
||||
this(key, defaultValue, rebootApp, true, null, availability);
|
||||
}
|
||||
public Setting(String key, T defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) {
|
||||
this(key, defaultValue, rebootApp, true, userDialogMessage, availability);
|
||||
}
|
||||
|
||||
/**
|
||||
* A setting backed by a shared preference.
|
||||
*
|
||||
* @param key The key used to store the value in the shared preferences.
|
||||
* @param defaultValue The default value of the setting.
|
||||
* @param rebootApp If the app should be rebooted, if this setting is changed.
|
||||
* @param includeWithImportExport If this setting should be shown in the import/export dialog.
|
||||
* @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.
|
||||
*/
|
||||
public Setting(@NonNull String key,
|
||||
@NonNull T defaultValue,
|
||||
boolean rebootApp,
|
||||
boolean includeWithImportExport,
|
||||
@Nullable String userDialogMessage,
|
||||
@Nullable Availability availability
|
||||
) {
|
||||
this.key = Objects.requireNonNull(key);
|
||||
this.value = this.defaultValue = Objects.requireNonNull(defaultValue);
|
||||
this.rebootApp = rebootApp;
|
||||
this.includeWithImportExport = includeWithImportExport;
|
||||
this.userDialogMessage = (userDialogMessage == null) ? null : new StringRef(userDialogMessage);
|
||||
this.availability = availability;
|
||||
|
||||
SETTINGS.add(this);
|
||||
if (PATH_TO_SETTINGS.put(key, this) != null) {
|
||||
// Debug setting may not be created yet so using Logger may cause an initialization crash.
|
||||
// Show a toast instead.
|
||||
Utils.showToastLong(this.getClass().getSimpleName()
|
||||
+ " error: Duplicate Setting key found: " + key);
|
||||
}
|
||||
|
||||
load();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (oldSetting == newSetting) throw new IllegalArgumentException();
|
||||
|
||||
if (!oldSetting.isSetToDefault()) {
|
||||
Logger.printInfo(() -> "Migrating old setting value: " + oldSetting + " into replacement setting: " + newSetting);
|
||||
newSetting.save(oldSetting.value);
|
||||
oldSetting.resetToDefault();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate an old Setting value previously stored in a different SharedPreference.
|
||||
*
|
||||
* This method will be deleted in the future.
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
public static void migrateFromOldPreferences(@NonNull SharedPrefCategory oldPrefs, @NonNull Setting setting, String settingKey) {
|
||||
if (!oldPrefs.preferences.contains(settingKey)) {
|
||||
return; // Nothing to do.
|
||||
}
|
||||
|
||||
Object newValue = setting.get();
|
||||
final Object migratedValue;
|
||||
if (setting instanceof BooleanSetting) {
|
||||
migratedValue = oldPrefs.getBoolean(settingKey, (Boolean) newValue);
|
||||
} else if (setting instanceof IntegerSetting) {
|
||||
migratedValue = oldPrefs.getIntegerString(settingKey, (Integer) newValue);
|
||||
} else if (setting instanceof LongSetting) {
|
||||
migratedValue = oldPrefs.getLongString(settingKey, (Long) newValue);
|
||||
} else if (setting instanceof FloatSetting) {
|
||||
migratedValue = oldPrefs.getFloatString(settingKey, (Float) newValue);
|
||||
} else if (setting instanceof StringSetting) {
|
||||
migratedValue = oldPrefs.getString(settingKey, (String) newValue);
|
||||
} else {
|
||||
Logger.printException(() -> "Unknown setting: " + setting);
|
||||
// Remove otherwise it'll show a toast on every launch
|
||||
oldPrefs.preferences.edit().remove(settingKey).apply();
|
||||
return;
|
||||
}
|
||||
|
||||
oldPrefs.preferences.edit().remove(settingKey).apply(); // Remove the old setting.
|
||||
if (migratedValue.equals(newValue)) {
|
||||
Logger.printDebug(() -> "Value does not need migrating: " + settingKey);
|
||||
return; // Old value is already equal to the new setting value.
|
||||
}
|
||||
|
||||
Logger.printDebug(() -> "Migrating old preference value into current preference: " + settingKey);
|
||||
//noinspection unchecked
|
||||
setting.save(migratedValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets, but does _not_ persistently save the value.
|
||||
* This method is only to be used by the Settings preference code.
|
||||
*
|
||||
* This intentionally is a static method to deter
|
||||
* accidental usage when {@link #save(Object)} was intended.
|
||||
*/
|
||||
public static void privateSetValueFromString(@NonNull Setting<?> setting, @NonNull String newValue) {
|
||||
setting.setValueFromString(newValue);
|
||||
|
||||
// Clear the preference value since default is used, to allow changing
|
||||
// the changing the default for a future release. Without this after upgrading
|
||||
// the saved value will be whatever was the default when the app was first installed.
|
||||
if (setting.isSetToDefault()) {
|
||||
setting.removeFromPreferences();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value of {@link #value}, but do not save to {@link #preferences}.
|
||||
*/
|
||||
protected abstract void setValueFromString(@NonNull String newValue);
|
||||
|
||||
/**
|
||||
* Load and set the value of {@link #value}.
|
||||
*/
|
||||
protected abstract void load();
|
||||
|
||||
/**
|
||||
* Persistently saves the value.
|
||||
*/
|
||||
public final void save(@NonNull T newValue) {
|
||||
if (value.equals(newValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Must set before saving to preferences (otherwise importing fails to update UI correctly).
|
||||
value = Objects.requireNonNull(newValue);
|
||||
|
||||
if (defaultValue.equals(newValue)) {
|
||||
removeFromPreferences();
|
||||
} else {
|
||||
saveToPreferences();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save {@link #value} to {@link #preferences}.
|
||||
*/
|
||||
protected abstract void saveToPreferences();
|
||||
|
||||
/**
|
||||
* Remove {@link #value} from {@link #preferences}.
|
||||
*/
|
||||
protected final void removeFromPreferences() {
|
||||
Logger.printDebug(() -> "Clearing stored preference value (reset to default): " + key);
|
||||
preferences.removeKey(key);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public abstract T get();
|
||||
|
||||
/**
|
||||
* Identical to calling {@link #save(Object)} using {@link #defaultValue}.
|
||||
*
|
||||
* @return The newly saved default value.
|
||||
*/
|
||||
public T resetToDefault() {
|
||||
save(defaultValue);
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return if this setting can be configured and used.
|
||||
*/
|
||||
public boolean isAvailable() {
|
||||
return availability == null || availability.isAvailable();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return if the currently set value is the same as {@link #defaultValue}
|
||||
*/
|
||||
public boolean isSetToDefault() {
|
||||
return value.equals(defaultValue);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return key + "=" + get();
|
||||
}
|
||||
|
||||
// region Import / export
|
||||
|
||||
/**
|
||||
* If a setting path has this prefix, then remove it before importing/exporting.
|
||||
*/
|
||||
private static final String OPTIONAL_REVANCED_SETTINGS_PREFIX = "revanced_";
|
||||
|
||||
/**
|
||||
* The path, minus any 'revanced' prefix to keep json concise.
|
||||
*/
|
||||
private String getImportExportKey() {
|
||||
if (key.startsWith(OPTIONAL_REVANCED_SETTINGS_PREFIX)) {
|
||||
return key.substring(OPTIONAL_REVANCED_SETTINGS_PREFIX.length());
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param importExportKey The JSON key. The JSONObject parameter will contain data for this key.
|
||||
* @return the value stored using the import/export key. Do not set any values in this method.
|
||||
*/
|
||||
protected abstract T readFromJSON(JSONObject json, String importExportKey) throws JSONException;
|
||||
|
||||
/**
|
||||
* Saves this instance to JSON.
|
||||
* <p>
|
||||
* To keep the JSON simple and readable,
|
||||
* subclasses should not write out any embedded types (such as JSON Array or Dictionaries).
|
||||
* <p>
|
||||
* If this instance is not a type supported natively by JSON (ie: it's not a String/Integer/Float/Long),
|
||||
* then subclasses can override this method and write out a String value representing the value.
|
||||
*/
|
||||
protected void writeToJSON(JSONObject json, String importExportKey) throws JSONException {
|
||||
json.put(importExportKey, value);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String exportToJson(@Nullable Context alertDialogContext) {
|
||||
try {
|
||||
JSONObject json = new JSONObject();
|
||||
for (Setting<?> setting : allLoadedSettingsSorted()) {
|
||||
String importExportKey = setting.getImportExportKey();
|
||||
if (json.has(importExportKey)) {
|
||||
throw new IllegalArgumentException("duplicate key found: " + importExportKey);
|
||||
}
|
||||
|
||||
final boolean exportDefaultValues = false; // Enable to see what all settings looks like in the UI.
|
||||
//noinspection ConstantValue
|
||||
if (setting.includeWithImportExport && (!setting.isSetToDefault() || exportDefaultValues)) {
|
||||
setting.writeToJSON(json, importExportKey);
|
||||
}
|
||||
}
|
||||
|
||||
for (ImportExportCallback callback : importExportCallbacks) {
|
||||
callback.settingsExported(alertDialogContext);
|
||||
}
|
||||
|
||||
if (json.length() == 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
String export = json.toString(0);
|
||||
|
||||
// Remove the outer JSON braces to make the output more compact,
|
||||
// and leave less chance of the user forgetting to copy it
|
||||
return export.substring(2, export.length() - 2);
|
||||
} catch (JSONException e) {
|
||||
Logger.printException(() -> "Export failure", e); // should never happen
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return if any settings that require a reboot were changed.
|
||||
*/
|
||||
public static boolean importFromJSON(@NonNull Context alertDialogContext, @NonNull String settingsJsonString) {
|
||||
try {
|
||||
if (!settingsJsonString.matches("[\\s\\S]*\\{")) {
|
||||
settingsJsonString = '{' + settingsJsonString + '}'; // Restore outer JSON braces
|
||||
}
|
||||
JSONObject json = new JSONObject(settingsJsonString);
|
||||
|
||||
boolean rebootSettingChanged = false;
|
||||
int numberOfSettingsImported = 0;
|
||||
//noinspection rawtypes
|
||||
for (Setting setting : SETTINGS) {
|
||||
String key = setting.getImportExportKey();
|
||||
if (json.has(key)) {
|
||||
Object value = setting.readFromJSON(json, key);
|
||||
if (!setting.get().equals(value)) {
|
||||
rebootSettingChanged |= setting.rebootApp;
|
||||
//noinspection unchecked
|
||||
setting.save(value);
|
||||
}
|
||||
numberOfSettingsImported++;
|
||||
} else if (setting.includeWithImportExport && !setting.isSetToDefault()) {
|
||||
Logger.printDebug(() -> "Resetting to default: " + setting);
|
||||
rebootSettingChanged |= setting.rebootApp;
|
||||
setting.resetToDefault();
|
||||
}
|
||||
}
|
||||
|
||||
for (ImportExportCallback callback : importExportCallbacks) {
|
||||
callback.settingsImported(alertDialogContext);
|
||||
}
|
||||
|
||||
Utils.showToastLong(numberOfSettingsImported == 0
|
||||
? str("revanced_settings_import_reset")
|
||||
: str("revanced_settings_import_success", numberOfSettingsImported));
|
||||
|
||||
return rebootSettingChanged;
|
||||
} catch (JSONException | IllegalArgumentException ex) {
|
||||
Utils.showToastLong(str("revanced_settings_import_failure_parse", ex.getMessage()));
|
||||
Logger.printInfo(() -> "", ex);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Import failure: " + ex.getMessage(), ex); // should never happen
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// End import / export
|
||||
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
package app.revanced.extension.shared.settings;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class StringSetting extends Setting<String> {
|
||||
|
||||
public StringSetting(String key, String defaultValue) {
|
||||
super(key, defaultValue);
|
||||
}
|
||||
public StringSetting(String key, String defaultValue, boolean rebootApp) {
|
||||
super(key, defaultValue, rebootApp);
|
||||
}
|
||||
public StringSetting(String key, String defaultValue, boolean rebootApp, boolean includeWithImportExport) {
|
||||
super(key, defaultValue, rebootApp, includeWithImportExport);
|
||||
}
|
||||
public StringSetting(String key, String defaultValue, String userDialogMessage) {
|
||||
super(key, defaultValue, userDialogMessage);
|
||||
}
|
||||
public StringSetting(String key, String defaultValue, Availability availability) {
|
||||
super(key, defaultValue, availability);
|
||||
}
|
||||
public StringSetting(String key, String defaultValue, boolean rebootApp, String userDialogMessage) {
|
||||
super(key, defaultValue, rebootApp, userDialogMessage);
|
||||
}
|
||||
public StringSetting(String key, String defaultValue, boolean rebootApp, Availability availability) {
|
||||
super(key, defaultValue, rebootApp, availability);
|
||||
}
|
||||
public StringSetting(String key, String defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) {
|
||||
super(key, defaultValue, rebootApp, userDialogMessage, availability);
|
||||
}
|
||||
public StringSetting(@NonNull String key, @NonNull String defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) {
|
||||
super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void load() {
|
||||
value = preferences.getString(key, defaultValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String readFromJSON(JSONObject json, String importExportKey) throws JSONException {
|
||||
return json.getString(importExportKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setValueFromString(@NonNull String newValue) {
|
||||
value = Objects.requireNonNull(newValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveToPreferences() {
|
||||
preferences.saveString(key, value);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String get() {
|
||||
return value;
|
||||
}
|
||||
}
|
@ -0,0 +1,321 @@
|
||||
package app.revanced.extension.shared.settings.preference;
|
||||
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.preference.*;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||
import app.revanced.extension.shared.settings.Setting;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
||||
|
||||
/**
|
||||
* Indicates that if a preference changes,
|
||||
* to apply the change from the Setting to the UI component.
|
||||
*/
|
||||
public static boolean settingImportInProgress;
|
||||
|
||||
/**
|
||||
* Prevents recursive calls during preference <-> UI syncing from showing extra dialogs.
|
||||
*/
|
||||
private static boolean updatingPreference;
|
||||
|
||||
/**
|
||||
* Used to prevent showing reboot dialog, if user cancels a setting user dialog.
|
||||
*/
|
||||
private static boolean showingUserDialogMessage;
|
||||
|
||||
/**
|
||||
* Confirm and restart dialog button text and title.
|
||||
* Set by subclasses if Strings cannot be added as a resource.
|
||||
*/
|
||||
@Nullable
|
||||
protected static String restartDialogButtonText, restartDialogTitle, confirmDialogTitle;
|
||||
|
||||
private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
|
||||
try {
|
||||
if (updatingPreference) {
|
||||
Logger.printDebug(() -> "Ignoring preference change as sync is in progress");
|
||||
return;
|
||||
}
|
||||
|
||||
Setting<?> setting = Setting.getSettingFromPath(Objects.requireNonNull(str));
|
||||
if (setting == null) {
|
||||
return;
|
||||
}
|
||||
Preference pref = findPreference(str);
|
||||
if (pref == null) {
|
||||
return;
|
||||
}
|
||||
Logger.printDebug(() -> "Preference changed: " + setting.key);
|
||||
|
||||
if (!settingImportInProgress && !showingUserDialogMessage) {
|
||||
if (setting.userDialogMessage != null && !prefIsSetToDefault(pref, setting)) {
|
||||
// Do not change the setting yet, to allow preserving whatever
|
||||
// list/text value was previously set if it needs to be reverted.
|
||||
showSettingUserDialogConfirmation(pref, setting);
|
||||
return;
|
||||
} else if (setting.rebootApp) {
|
||||
showRestartDialog(getContext());
|
||||
}
|
||||
}
|
||||
|
||||
updatingPreference = true;
|
||||
// Apply 'Setting <- Preference', unless during importing when it needs to be 'Setting -> Preference'.
|
||||
// Updating here can can cause a recursive call back into this same method.
|
||||
updatePreference(pref, setting, true, settingImportInProgress);
|
||||
// Update any other preference availability that may now be different.
|
||||
updateUIAvailability();
|
||||
updatingPreference = false;
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "OnSharedPreferenceChangeListener failure", ex);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize this instance, and do any custom behavior.
|
||||
* <p>
|
||||
* To ensure all {@link Setting} instances are correctly synced to the UI,
|
||||
* it is important that subclasses make a call or otherwise reference their Settings class bundle
|
||||
* so all app specific {@link Setting} instances are loaded before this method returns.
|
||||
*/
|
||||
protected void initialize() {
|
||||
String preferenceResourceName = BaseSettings.SHOW_MENU_ICONS.get()
|
||||
? "revanced_prefs_icons"
|
||||
: "revanced_prefs";
|
||||
final var identifier = Utils.getResourceIdentifier(preferenceResourceName, "xml");
|
||||
if (identifier == 0) return;
|
||||
addPreferencesFromResource(identifier);
|
||||
|
||||
PreferenceScreen screen = getPreferenceScreen();
|
||||
Utils.sortPreferenceGroups(screen);
|
||||
Utils.setPreferenceTitlesToMultiLineIfNeeded(screen);
|
||||
}
|
||||
|
||||
private void showSettingUserDialogConfirmation(Preference pref, Setting<?> setting) {
|
||||
Utils.verifyOnMainThread();
|
||||
|
||||
final var context = getContext();
|
||||
if (confirmDialogTitle == null) {
|
||||
confirmDialogTitle = str("revanced_settings_confirm_user_dialog_title");
|
||||
}
|
||||
|
||||
showingUserDialogMessage = true;
|
||||
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(confirmDialogTitle)
|
||||
.setMessage(Objects.requireNonNull(setting.userDialogMessage).toString())
|
||||
.setPositiveButton(android.R.string.ok, (dialog, id) -> {
|
||||
// User confirmed, save to the Setting.
|
||||
updatePreference(pref, setting, true, false);
|
||||
|
||||
// Update availability of other preferences that may be changed.
|
||||
updateUIAvailability();
|
||||
|
||||
if (setting.rebootApp) {
|
||||
showRestartDialog(context);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, (dialog, id) -> {
|
||||
// Restore whatever the setting was before the change.
|
||||
updatePreference(pref, setting, true, true);
|
||||
})
|
||||
.setOnDismissListener(dialog -> {
|
||||
showingUserDialogMessage = false;
|
||||
})
|
||||
.setCancelable(false)
|
||||
.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates all Preferences values and their availability using the current values in {@link Setting}.
|
||||
*/
|
||||
protected void updateUIToSettingValues() {
|
||||
updatePreferenceScreen(getPreferenceScreen(), true,true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates Preferences availability only using the status of {@link Setting}.
|
||||
*/
|
||||
protected void updateUIAvailability() {
|
||||
updatePreferenceScreen(getPreferenceScreen(), false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return If the preference is currently set to the default value of the Setting.
|
||||
*/
|
||||
protected boolean prefIsSetToDefault(Preference pref, Setting<?> setting) {
|
||||
Object defaultValue = setting.defaultValue;
|
||||
if (pref instanceof SwitchPreference switchPref) {
|
||||
return switchPref.isChecked() == (Boolean) defaultValue;
|
||||
}
|
||||
String defaultValueString = defaultValue.toString();
|
||||
if (pref instanceof EditTextPreference editPreference) {
|
||||
return editPreference.getText().equals(defaultValueString);
|
||||
}
|
||||
if (pref instanceof ListPreference listPref) {
|
||||
return listPref.getValue().equals(defaultValueString);
|
||||
}
|
||||
|
||||
throw new IllegalStateException("Must override method to handle "
|
||||
+ "preference type: " + pref.getClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncs all UI Preferences to any {@link Setting} they represent.
|
||||
*/
|
||||
private void updatePreferenceScreen(@NonNull PreferenceGroup group,
|
||||
boolean syncSettingValue,
|
||||
boolean applySettingToPreference) {
|
||||
// Alternatively this could iterate thru all Settings and check for any matching Preferences,
|
||||
// but there are many more Settings than UI preferences so it's more efficient to only check
|
||||
// the Preferences.
|
||||
for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) {
|
||||
Preference pref = group.getPreference(i);
|
||||
if (pref instanceof PreferenceGroup subGroup) {
|
||||
updatePreferenceScreen(subGroup, syncSettingValue, applySettingToPreference);
|
||||
} else if (pref.hasKey()) {
|
||||
String key = pref.getKey();
|
||||
Setting<?> setting = Setting.getSettingFromPath(key);
|
||||
|
||||
if (setting != null) {
|
||||
updatePreference(pref, setting, syncSettingValue, applySettingToPreference);
|
||||
} else if (BaseSettings.DEBUG.get() && (pref instanceof SwitchPreference
|
||||
|| pref instanceof EditTextPreference || pref instanceof ListPreference)) {
|
||||
// Probably a typo in the patches preference declaration.
|
||||
Logger.printException(() -> "Preference key has no setting: " + key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles syncing a UI Preference with the {@link Setting} that backs it.
|
||||
* If needed, subclasses can override this to handle additional UI Preference types.
|
||||
*
|
||||
* @param applySettingToPreference If true, then apply {@link Setting} -> Preference.
|
||||
* If false, then apply {@link Setting} <- Preference.
|
||||
*/
|
||||
protected void syncSettingWithPreference(@NonNull Preference pref,
|
||||
@NonNull Setting<?> setting,
|
||||
boolean applySettingToPreference) {
|
||||
if (pref instanceof SwitchPreference switchPref) {
|
||||
BooleanSetting boolSetting = (BooleanSetting) setting;
|
||||
if (applySettingToPreference) {
|
||||
switchPref.setChecked(boolSetting.get());
|
||||
} else {
|
||||
BooleanSetting.privateSetValue(boolSetting, switchPref.isChecked());
|
||||
}
|
||||
} else if (pref instanceof EditTextPreference editPreference) {
|
||||
if (applySettingToPreference) {
|
||||
editPreference.setText(setting.get().toString());
|
||||
} else {
|
||||
Setting.privateSetValueFromString(setting, editPreference.getText());
|
||||
}
|
||||
} else if (pref instanceof ListPreference listPref) {
|
||||
if (applySettingToPreference) {
|
||||
listPref.setValue(setting.get().toString());
|
||||
} else {
|
||||
Setting.privateSetValueFromString(setting, listPref.getValue());
|
||||
}
|
||||
updateListPreferenceSummary(listPref, setting);
|
||||
} else {
|
||||
Logger.printException(() -> "Setting cannot be handled: " + pref.getClass() + ": " + pref);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a UI Preference with the {@link Setting} that backs it.
|
||||
*
|
||||
* @param syncSetting If the UI should be synced {@link Setting} <-> Preference
|
||||
* @param applySettingToPreference If true, then apply {@link Setting} -> Preference.
|
||||
* If false, then apply {@link Setting} <- Preference.
|
||||
*/
|
||||
private void updatePreference(@NonNull Preference pref, @NonNull Setting<?> setting,
|
||||
boolean syncSetting, boolean applySettingToPreference) {
|
||||
if (!syncSetting && applySettingToPreference) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
if (syncSetting) {
|
||||
syncSettingWithPreference(pref, setting, applySettingToPreference);
|
||||
}
|
||||
|
||||
updatePreferenceAvailability(pref, setting);
|
||||
}
|
||||
|
||||
protected void updatePreferenceAvailability(@NonNull Preference pref, @NonNull Setting<?> setting) {
|
||||
pref.setEnabled(setting.isAvailable());
|
||||
}
|
||||
|
||||
protected void updateListPreferenceSummary(ListPreference listPreference, Setting<?> setting) {
|
||||
String objectStringValue = setting.get().toString();
|
||||
final int entryIndex = listPreference.findIndexOfValue(objectStringValue);
|
||||
if (entryIndex >= 0) {
|
||||
listPreference.setSummary(listPreference.getEntries()[entryIndex]);
|
||||
} else {
|
||||
// Value is not an available option.
|
||||
// User manually edited import data, or options changed and current selection is no longer available.
|
||||
// Still show the value in the summary, so it's clear that something is selected.
|
||||
listPreference.setSummary(objectStringValue);
|
||||
}
|
||||
}
|
||||
|
||||
public static void showRestartDialog(Context context) {
|
||||
Utils.verifyOnMainThread();
|
||||
if (restartDialogTitle == null) {
|
||||
restartDialogTitle = str("revanced_settings_restart_title");
|
||||
}
|
||||
if (restartDialogButtonText == null) {
|
||||
restartDialogButtonText = str("revanced_settings_restart");
|
||||
}
|
||||
|
||||
new AlertDialog.Builder(context)
|
||||
.setMessage(restartDialogTitle)
|
||||
.setPositiveButton(restartDialogButtonText, (dialog, id)
|
||||
-> Utils.restartApp(context))
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setCancelable(false)
|
||||
.show();
|
||||
}
|
||||
|
||||
@SuppressLint("ResourceType")
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
try {
|
||||
PreferenceManager preferenceManager = getPreferenceManager();
|
||||
preferenceManager.setSharedPreferencesName(Setting.preferences.name);
|
||||
|
||||
// Must initialize before adding change listener,
|
||||
// otherwise the syncing of Setting -> UI
|
||||
// causes a callback to the listener even though nothing changed.
|
||||
initialize();
|
||||
updateUIToSettingValues();
|
||||
|
||||
preferenceManager.getSharedPreferences().registerOnSharedPreferenceChangeListener(listener);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "onCreate() failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(listener);
|
||||
super.onDestroy();
|
||||
}
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
package app.revanced.extension.shared.settings.preference;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.preference.EditTextPreference;
|
||||
import android.preference.Preference;
|
||||
import android.text.InputType;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.TypedValue;
|
||||
import android.widget.EditText;
|
||||
import app.revanced.extension.shared.settings.Setting;
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
|
||||
@SuppressWarnings({"unused", "deprecation"})
|
||||
public class ImportExportPreference extends EditTextPreference implements Preference.OnPreferenceClickListener {
|
||||
|
||||
private String existingSettings;
|
||||
|
||||
private void init() {
|
||||
setSelectable(true);
|
||||
|
||||
EditText editText = getEditText();
|
||||
editText.setTextIsSelectable(true);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
editText.setAutofillHints((String) null);
|
||||
}
|
||||
editText.setInputType(editText.getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
||||
editText.setTextSize(TypedValue.COMPLEX_UNIT_PT, 7); // Use a smaller font to reduce text wrap.
|
||||
|
||||
setOnPreferenceClickListener(this);
|
||||
}
|
||||
|
||||
public ImportExportPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
init();
|
||||
}
|
||||
public ImportExportPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init();
|
||||
}
|
||||
public ImportExportPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
public ImportExportPreference(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
try {
|
||||
// Must set text before preparing dialog, otherwise text is non selectable if this preference is later reopened.
|
||||
existingSettings = Setting.exportToJson(getContext());
|
||||
getEditText().setText(existingSettings);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "showDialog failure", ex);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
||||
try {
|
||||
Utils.setEditTextDialogTheme(builder);
|
||||
|
||||
// Show the user the settings in JSON format.
|
||||
builder.setNeutralButton(str("revanced_settings_import_copy"), (dialog, which) -> {
|
||||
Utils.setClipboard(getEditText().getText().toString());
|
||||
}).setPositiveButton(str("revanced_settings_import"), (dialog, which) -> {
|
||||
importSettings(builder.getContext(), getEditText().getText().toString());
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "onPrepareDialogBuilder failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void importSettings(Context context, String replacementSettings) {
|
||||
try {
|
||||
if (replacementSettings.equals(existingSettings)) {
|
||||
return;
|
||||
}
|
||||
AbstractPreferenceFragment.settingImportInProgress = true;
|
||||
|
||||
final boolean rebootNeeded = Setting.importFromJSON(context, replacementSettings);
|
||||
if (rebootNeeded) {
|
||||
AbstractPreferenceFragment.showRestartDialog(getContext());
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "importSettings failure", ex);
|
||||
} finally {
|
||||
AbstractPreferenceFragment.settingImportInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
package app.revanced.extension.shared.settings.preference;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.preference.PreferenceCategory;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
/**
|
||||
* Empty preference category with no title, used to organize and group related preferences together.
|
||||
*/
|
||||
@SuppressWarnings({"unused", "deprecation"})
|
||||
public class NoTitlePreferenceCategory extends PreferenceCategory {
|
||||
|
||||
public NoTitlePreferenceCategory(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public NoTitlePreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public NoTitlePreferenceCategory(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressLint("MissingSuperCall")
|
||||
protected View onCreateView(ViewGroup parent) {
|
||||
// Return an zero-height view to eliminate empty title space.
|
||||
return new View(getContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getTitle() {
|
||||
// Title can be used for sorting. Return the first sub preference title.
|
||||
if (getPreferenceCount() > 0) {
|
||||
return getPreference(0).getTitle();
|
||||
}
|
||||
|
||||
return super.getTitle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTitleRes() {
|
||||
if (getPreferenceCount() > 0) {
|
||||
return getPreference(0).getTitleRes();
|
||||
}
|
||||
|
||||
return super.getTitleRes();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,359 @@
|
||||
package app.revanced.extension.shared.settings.preference;
|
||||
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
import static app.revanced.extension.shared.requests.Route.Method.GET;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Dialog;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.preference.Preference;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.Window;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.requests.Requester;
|
||||
import app.revanced.extension.shared.requests.Route;
|
||||
|
||||
/**
|
||||
* Opens a dialog showing official links.
|
||||
*/
|
||||
@SuppressWarnings({"unused", "deprecation"})
|
||||
public class ReVancedAboutPreference extends Preference {
|
||||
|
||||
private static String useNonBreakingHyphens(String text) {
|
||||
// Replace any dashes with non breaking dashes, so the English text 'pre-release'
|
||||
// and the dev release number does not break and cover two lines.
|
||||
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.
|
||||
*
|
||||
* @return A localized string to display for the key.
|
||||
*/
|
||||
protected String getString(String key, Object ... args) {
|
||||
return str(key, args);
|
||||
}
|
||||
|
||||
private String createDialogHtml(WebLink[] aboutLinks) {
|
||||
final boolean isNetworkConnected = Utils.isNetworkConnected();
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("<html>");
|
||||
builder.append("<body style=\"text-align: center; padding: 10px;\">");
|
||||
|
||||
final boolean isDarkMode = isDarkModeEnabled();
|
||||
String backgroundColorHex = getColorHexString(isDarkMode ? getDarkColor() : getLightColor());
|
||||
String foregroundColorHex = getColorHexString(isDarkMode ? getLightColor() : getDarkColor());
|
||||
// Apply light/dark mode colors.
|
||||
builder.append(String.format(
|
||||
"<style> body { background-color: %s; color: %s; } a { color: %s; } </style>",
|
||||
backgroundColorHex, foregroundColorHex, foregroundColorHex));
|
||||
|
||||
if (isNetworkConnected) {
|
||||
builder.append("<img style=\"width: 100px; height: 100px;\" "
|
||||
// Hide the image if it does not load.
|
||||
+ "onerror=\"this.style.display='none';\" "
|
||||
+ "src=\"").append(AboutLinksRoutes.aboutLogoUrl).append("\" />");
|
||||
}
|
||||
|
||||
String patchesVersion = Utils.getPatchesReleaseVersion();
|
||||
|
||||
// Add the title.
|
||||
builder.append("<h1>")
|
||||
.append("ReVanced")
|
||||
.append("</h1>");
|
||||
|
||||
builder.append("<p>")
|
||||
// Replace hyphens with non breaking dashes so the version number does not break lines.
|
||||
.append(useNonBreakingHyphens(getString("revanced_settings_about_links_body", patchesVersion)))
|
||||
.append("</p>");
|
||||
|
||||
// Add a disclaimer if using a dev release.
|
||||
if (patchesVersion.contains("dev")) {
|
||||
builder.append("<h3>")
|
||||
// English text 'Pre-release' can break lines.
|
||||
.append(useNonBreakingHyphens(getString("revanced_settings_about_links_dev_header")))
|
||||
.append("</h3>");
|
||||
|
||||
builder.append("<p>")
|
||||
.append(getString("revanced_settings_about_links_dev_body"))
|
||||
.append("</p>");
|
||||
}
|
||||
|
||||
builder.append("<h2 style=\"margin-top: 30px;\">")
|
||||
.append(getString("revanced_settings_about_links_header"))
|
||||
.append("</h2>");
|
||||
|
||||
builder.append("<div>");
|
||||
for (WebLink link : aboutLinks) {
|
||||
builder.append("<div style=\"margin-bottom: 20px;\">");
|
||||
builder.append(String.format("<a href=\"%s\">%s</a>", link.url, link.name));
|
||||
builder.append("</div>");
|
||||
}
|
||||
builder.append("</div>");
|
||||
|
||||
builder.append("</body></html>");
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
{
|
||||
setOnPreferenceClickListener(pref -> {
|
||||
// Show a progress spinner if the social links are not fetched yet.
|
||||
if (!AboutLinksRoutes.hasFetchedLinks() && Utils.isNetworkConnected()) {
|
||||
// Show a progress spinner, but only if the api fetch takes more than a half a second.
|
||||
final long delayToShowProgressSpinner = 500;
|
||||
ProgressDialog progress = new ProgressDialog(getContext());
|
||||
progress.setProgressStyle(ProgressDialog.STYLE_SPINNER);
|
||||
|
||||
Handler handler = new Handler(Looper.getMainLooper());
|
||||
Runnable showDialogRunnable = progress::show;
|
||||
handler.postDelayed(showDialogRunnable, delayToShowProgressSpinner);
|
||||
|
||||
Utils.runOnBackgroundThread(() ->
|
||||
fetchLinksAndShowDialog(handler, showDialogRunnable, progress));
|
||||
} else {
|
||||
// No network call required and can run now.
|
||||
fetchLinksAndShowDialog(null, null, null);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
private void fetchLinksAndShowDialog(@Nullable Handler handler,
|
||||
Runnable showDialogRunnable,
|
||||
@Nullable ProgressDialog progress) {
|
||||
WebLink[] links = AboutLinksRoutes.fetchAboutLinks();
|
||||
String htmlDialog = createDialogHtml(links);
|
||||
|
||||
// Enable to randomly force a delay to debug the spinner logic.
|
||||
final boolean debugSpinnerDelayLogic = false;
|
||||
//noinspection ConstantConditions
|
||||
if (debugSpinnerDelayLogic && handler != null && Math.random() < 0.5f) {
|
||||
Utils.doNothingForDuration((long) (Math.random() * 4000));
|
||||
}
|
||||
|
||||
Utils.runOnMainThreadNowOrLater(() -> {
|
||||
if (handler != null) {
|
||||
handler.removeCallbacks(showDialogRunnable);
|
||||
}
|
||||
if (progress != null) {
|
||||
progress.dismiss();
|
||||
}
|
||||
new WebViewDialog(getContext(), htmlDialog).show();
|
||||
});
|
||||
}
|
||||
|
||||
public ReVancedAboutPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
public ReVancedAboutPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
public ReVancedAboutPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
public ReVancedAboutPreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays html content as a dialog. Any links a user taps on are opened in an external browser.
|
||||
*/
|
||||
class WebViewDialog extends Dialog {
|
||||
|
||||
private final String htmlContent;
|
||||
|
||||
public WebViewDialog(@NonNull Context context, @NonNull String htmlContent) {
|
||||
super(context);
|
||||
this.htmlContent = htmlContent;
|
||||
}
|
||||
|
||||
// JS required to hide any broken images. No remote javascript is ever loaded.
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
|
||||
WebView webView = new WebView(getContext());
|
||||
webView.getSettings().setJavaScriptEnabled(true);
|
||||
webView.setWebViewClient(new OpenLinksExternallyWebClient());
|
||||
webView.loadDataWithBaseURL(null, htmlContent, "text/html", "utf-8", null);
|
||||
|
||||
setContentView(webView);
|
||||
}
|
||||
|
||||
private class OpenLinksExternallyWebClient extends WebViewClient {
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
||||
try {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||
getContext().startActivity(intent);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Open link failure", ex);
|
||||
}
|
||||
// Dismiss the about dialog using a delay,
|
||||
// otherwise without a delay the UI looks hectic with the dialog dismissing
|
||||
// to show the settings while simultaneously a web browser is opening.
|
||||
Utils.runOnMainThreadDelayed(WebViewDialog.this::dismiss, 500);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class WebLink {
|
||||
final boolean preferred;
|
||||
String name;
|
||||
final String url;
|
||||
|
||||
WebLink(JSONObject json) throws JSONException {
|
||||
this(json.getBoolean("preferred"),
|
||||
json.getString("name"),
|
||||
json.getString("url")
|
||||
);
|
||||
}
|
||||
|
||||
WebLink(boolean preferred, String name, String url) {
|
||||
this.preferred = preferred;
|
||||
this.name = name;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "WebLink{" +
|
||||
"preferred=" + preferred +
|
||||
", name='" + name + '\'' +
|
||||
", url='" + url + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
class AboutLinksRoutes {
|
||||
/**
|
||||
* Backup icon url if the API call fails.
|
||||
*/
|
||||
public static volatile String aboutLogoUrl = "https://revanced.app/favicon.ico";
|
||||
|
||||
/**
|
||||
* Links to use if fetch links api call fails.
|
||||
*/
|
||||
private static final WebLink[] NO_CONNECTION_STATIC_LINKS = {
|
||||
new WebLink(true, "ReVanced.app", "https://revanced.app")
|
||||
};
|
||||
|
||||
private static final String SOCIAL_LINKS_PROVIDER = "https://api.revanced.app/v4";
|
||||
private static final Route.CompiledRoute GET_SOCIAL = new Route(GET, "/about").compile();
|
||||
|
||||
@Nullable
|
||||
private static volatile WebLink[] fetchedLinks;
|
||||
|
||||
static boolean hasFetchedLinks() {
|
||||
return fetchedLinks != null;
|
||||
}
|
||||
|
||||
static WebLink[] fetchAboutLinks() {
|
||||
try {
|
||||
if (hasFetchedLinks()) return fetchedLinks;
|
||||
|
||||
// Check if there is no internet connection.
|
||||
if (!Utils.isNetworkConnected()) return NO_CONNECTION_STATIC_LINKS;
|
||||
|
||||
HttpURLConnection connection = Requester.getConnectionFromCompiledRoute(SOCIAL_LINKS_PROVIDER, GET_SOCIAL);
|
||||
connection.setConnectTimeout(5000);
|
||||
connection.setReadTimeout(5000);
|
||||
Logger.printDebug(() -> "Fetching social links from: " + connection.getURL());
|
||||
|
||||
// Do not show an exception toast if the server is down
|
||||
final int responseCode = connection.getResponseCode();
|
||||
if (responseCode != 200) {
|
||||
Logger.printDebug(() -> "Failed to get social links. Response code: " + responseCode);
|
||||
return NO_CONNECTION_STATIC_LINKS;
|
||||
}
|
||||
|
||||
JSONObject json = Requester.parseJSONObjectAndDisconnect(connection);
|
||||
aboutLogoUrl = json.getJSONObject("branding").getString("logo");
|
||||
|
||||
List<WebLink> links = new ArrayList<>();
|
||||
|
||||
JSONArray donations = json.getJSONObject("donations").getJSONArray("links");
|
||||
for (int i = 0, length = donations.length(); i < length; i++) {
|
||||
WebLink link = new WebLink(donations.getJSONObject(i));
|
||||
if (link.preferred) {
|
||||
// This could be localized, but TikTok does not support localized resources.
|
||||
// All link names returned by the api are also non localized.
|
||||
link.name = "Donate";
|
||||
links.add(link);
|
||||
}
|
||||
}
|
||||
|
||||
JSONArray socials = json.getJSONArray("socials");
|
||||
for (int i = 0, length = socials.length(); i < length; i++) {
|
||||
WebLink link = new WebLink(socials.getJSONObject(i));
|
||||
links.add(link);
|
||||
}
|
||||
|
||||
Logger.printDebug(() -> "links: " + links);
|
||||
|
||||
return fetchedLinks = links.toArray(new WebLink[0]);
|
||||
|
||||
} catch (SocketTimeoutException ex) {
|
||||
Logger.printInfo(() -> "Could not fetch social links", ex); // No toast.
|
||||
} catch (JSONException ex) {
|
||||
Logger.printException(() -> "Could not parse about information", ex);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Failed to get about information", ex);
|
||||
}
|
||||
|
||||
return NO_CONNECTION_STATIC_LINKS;
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
package app.revanced.extension.shared.settings.preference;
|
||||
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.preference.EditTextPreference;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.settings.Setting;
|
||||
|
||||
@SuppressWarnings({"unused", "deprecation"})
|
||||
public class ResettableEditTextPreference extends EditTextPreference {
|
||||
|
||||
/**
|
||||
* Setting to reset.
|
||||
*/
|
||||
@Nullable
|
||||
private Setting<?> setting;
|
||||
|
||||
public ResettableEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
public ResettableEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
public ResettableEditTextPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
public ResettableEditTextPreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public void setSetting(@Nullable 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
|
||||
protected void showDialog(Bundle state) {
|
||||
super.showDialog(state);
|
||||
|
||||
// Override the button click listener to prevent dismissing the dialog.
|
||||
Button button = ((AlertDialog) getDialog()).getButton(AlertDialog.BUTTON_NEUTRAL);
|
||||
if (button == null) {
|
||||
return;
|
||||
}
|
||||
button.setOnClickListener(v -> {
|
||||
try {
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,190 @@
|
||||
package app.revanced.extension.shared.settings.preference;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceFragment;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Shared categories, and helper methods.
|
||||
*
|
||||
* The various save methods store numbers as Strings,
|
||||
* which is required if using {@link PreferenceFragment}.
|
||||
*
|
||||
* If saved numbers will not be used with a preference fragment,
|
||||
* then store the primitive numbers using the {@link #preferences} itself.
|
||||
*/
|
||||
public class SharedPrefCategory {
|
||||
@NonNull
|
||||
public final String name;
|
||||
@NonNull
|
||||
public final SharedPreferences preferences;
|
||||
|
||||
public SharedPrefCategory(@NonNull String name) {
|
||||
this.name = Objects.requireNonNull(name);
|
||||
preferences = Objects.requireNonNull(Utils.getContext()).getSharedPreferences(name, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
private void removeConflictingPreferenceKeyValue(@NonNull String key) {
|
||||
Logger.printException(() -> "Found conflicting preference: " + key);
|
||||
removeKey(key);
|
||||
}
|
||||
|
||||
private void saveObjectAsString(@NonNull String key, @Nullable Object value) {
|
||||
preferences.edit().putString(key, (value == null ? null : value.toString())).apply();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes any preference data type that has the specified key.
|
||||
*/
|
||||
public void removeKey(@NonNull String key) {
|
||||
preferences.edit().remove(Objects.requireNonNull(key)).apply();
|
||||
}
|
||||
|
||||
public void saveBoolean(@NonNull String key, boolean value) {
|
||||
preferences.edit().putBoolean(key, value).apply();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param value a NULL parameter removes the value from the preferences
|
||||
*/
|
||||
public void saveEnumAsString(@NonNull String key, @Nullable Enum<?> value) {
|
||||
saveObjectAsString(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param value a NULL parameter removes the value from the preferences
|
||||
*/
|
||||
public void saveIntegerString(@NonNull String key, @Nullable Integer value) {
|
||||
saveObjectAsString(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param value a NULL parameter removes the value from the preferences
|
||||
*/
|
||||
public void saveLongString(@NonNull String key, @Nullable Long value) {
|
||||
saveObjectAsString(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param value a NULL parameter removes the value from the preferences
|
||||
*/
|
||||
public void saveFloatString(@NonNull String key, @Nullable Float value) {
|
||||
saveObjectAsString(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param value a NULL parameter removes the value from the preferences
|
||||
*/
|
||||
public void saveString(@NonNull String key, @Nullable String value) {
|
||||
saveObjectAsString(key, value);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getString(@NonNull String key, @NonNull String _default) {
|
||||
Objects.requireNonNull(_default);
|
||||
try {
|
||||
return preferences.getString(key, _default);
|
||||
} catch (ClassCastException ex) {
|
||||
// Value stored is a completely different type (should never happen).
|
||||
removeConflictingPreferenceKeyValue(key);
|
||||
return _default;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public <T extends Enum<?>> T getEnum(@NonNull String key, @NonNull T _default) {
|
||||
Objects.requireNonNull(_default);
|
||||
try {
|
||||
String enumName = preferences.getString(key, null);
|
||||
if (enumName != null) {
|
||||
try {
|
||||
// noinspection unchecked
|
||||
return (T) Enum.valueOf(_default.getClass(), enumName);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
// Info level to allow removing enum values in the future without showing any user errors.
|
||||
Logger.printInfo(() -> "Using default, and ignoring unknown enum value: " + enumName);
|
||||
removeKey(key);
|
||||
}
|
||||
}
|
||||
} catch (ClassCastException ex) {
|
||||
// Value stored is a completely different type (should never happen).
|
||||
removeConflictingPreferenceKeyValue(key);
|
||||
}
|
||||
return _default;
|
||||
}
|
||||
|
||||
public boolean getBoolean(@NonNull String key, boolean _default) {
|
||||
try {
|
||||
return preferences.getBoolean(key, _default);
|
||||
} catch (ClassCastException ex) {
|
||||
// Value stored is a completely different type (should never happen).
|
||||
removeConflictingPreferenceKeyValue(key);
|
||||
return _default;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Integer getIntegerString(@NonNull String key, @NonNull Integer _default) {
|
||||
try {
|
||||
String value = preferences.getString(key, null);
|
||||
if (value != null) {
|
||||
return Integer.valueOf(value);
|
||||
}
|
||||
} catch (ClassCastException | NumberFormatException ex) {
|
||||
try {
|
||||
// Old data previously stored as primitive.
|
||||
return preferences.getInt(key, _default);
|
||||
} catch (ClassCastException ex2) {
|
||||
// Value stored is a completely different type (should never happen).
|
||||
removeConflictingPreferenceKeyValue(key);
|
||||
}
|
||||
}
|
||||
return _default;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Long getLongString(@NonNull String key, @NonNull Long _default) {
|
||||
try {
|
||||
String value = preferences.getString(key, null);
|
||||
if (value != null) {
|
||||
return Long.valueOf(value);
|
||||
}
|
||||
} catch (ClassCastException | NumberFormatException ex) {
|
||||
try {
|
||||
return preferences.getLong(key, _default);
|
||||
} catch (ClassCastException ex2) {
|
||||
removeConflictingPreferenceKeyValue(key);
|
||||
}
|
||||
}
|
||||
return _default;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Float getFloatString(@NonNull String key, @NonNull Float _default) {
|
||||
try {
|
||||
String value = preferences.getString(key, null);
|
||||
if (value != null) {
|
||||
return Float.valueOf(value);
|
||||
}
|
||||
} catch (ClassCastException | NumberFormatException ex) {
|
||||
try {
|
||||
return preferences.getFloat(key, _default);
|
||||
} catch (ClassCastException ex2) {
|
||||
removeConflictingPreferenceKeyValue(key);
|
||||
}
|
||||
}
|
||||
return _default;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
}
|
@ -0,0 +1,259 @@
|
||||
package app.revanced.extension.shared.spoof;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
|
||||
public enum ClientType {
|
||||
// https://dumps.tadiphone.dev/dumps/oculus/eureka
|
||||
ANDROID_VR_NO_AUTH(
|
||||
28,
|
||||
"ANDROID_VR",
|
||||
"com.google.android.apps.youtube.vr.oculus",
|
||||
"Oculus",
|
||||
"Quest 3",
|
||||
"Android",
|
||||
"12",
|
||||
// Android 12.1
|
||||
"32",
|
||||
"SQ3A.220605.009.A1",
|
||||
"132.0.6808.3",
|
||||
"1.61.48",
|
||||
false,
|
||||
false,
|
||||
"Android VR No auth"
|
||||
),
|
||||
// Chromecast with Google TV 4K.
|
||||
// https://dumps.tadiphone.dev/dumps/google/kirkwood
|
||||
ANDROID_UNPLUGGED(
|
||||
29,
|
||||
"ANDROID_UNPLUGGED",
|
||||
"com.google.android.apps.youtube.unplugged",
|
||||
"Google",
|
||||
"Google TV Streamer",
|
||||
"Android",
|
||||
"14",
|
||||
"34",
|
||||
"UTT3.240625.001.K5",
|
||||
"132.0.6808.3",
|
||||
"8.49.0",
|
||||
true,
|
||||
true,
|
||||
"Android TV"
|
||||
),
|
||||
// Cannot play livestreams and lacks HDR, but can play videos with music and labeled "for children".
|
||||
// Google Pixel 9 Pro Fold
|
||||
// https://dumps.tadiphone.dev/dumps/google/barbet
|
||||
ANDROID_CREATOR(
|
||||
14,
|
||||
"ANDROID_CREATOR",
|
||||
"com.google.android.apps.youtube.creator",
|
||||
"Google",
|
||||
"Pixel 9 Pro Fold",
|
||||
"Android",
|
||||
"15",
|
||||
"35",
|
||||
"AP3A.241005.015.A2",
|
||||
"132.0.6779.0",
|
||||
"23.47.101",
|
||||
true,
|
||||
true,
|
||||
"Android Creator"
|
||||
),
|
||||
IOS_UNPLUGGED(
|
||||
33,
|
||||
"IOS_UNPLUGGED",
|
||||
"com.google.ios.youtubeunplugged",
|
||||
"Apple",
|
||||
forceAVC()
|
||||
// 11 Pro Max (last device with iOS 13)
|
||||
? "iPhone12,5"
|
||||
// 15 Pro Max
|
||||
: "iPhone16,2",
|
||||
"iOS",
|
||||
forceAVC()
|
||||
// iOS 13 and earlier uses only AVC. 14+ adds VP9 and AV1.
|
||||
? "13.7.17H35"
|
||||
: "18.2.22C152",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
// Version number should be a valid iOS release.
|
||||
// https://www.ipa4fun.com/history/152043/
|
||||
forceAVC()
|
||||
// Some newer versions can also force AVC,
|
||||
// but 6.45 is the last version that supports iOS 13.
|
||||
? "6.45"
|
||||
: "8.49",
|
||||
true,
|
||||
true,
|
||||
forceAVC()
|
||||
? "iOS TV Force AVC"
|
||||
: "iOS TV"
|
||||
),
|
||||
ANDROID_VR_AUTH(
|
||||
ANDROID_VR_NO_AUTH.id,
|
||||
ANDROID_VR_NO_AUTH.clientName,
|
||||
ANDROID_VR_NO_AUTH.packageName,
|
||||
ANDROID_VR_NO_AUTH.deviceMake,
|
||||
ANDROID_VR_NO_AUTH.deviceModel,
|
||||
ANDROID_VR_NO_AUTH.osName,
|
||||
ANDROID_VR_NO_AUTH.osVersion,
|
||||
ANDROID_VR_NO_AUTH.androidSdkVersion,
|
||||
ANDROID_VR_NO_AUTH.buildId,
|
||||
ANDROID_VR_NO_AUTH.cronetVersion,
|
||||
ANDROID_VR_NO_AUTH.clientVersion,
|
||||
ANDROID_VR_NO_AUTH.requiresAuth,
|
||||
true,
|
||||
"Android VR Auth"
|
||||
);
|
||||
|
||||
private static boolean forceAVC() {
|
||||
return BaseSettings.SPOOF_VIDEO_STREAMS_IOS_FORCE_AVC.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* YouTube
|
||||
* <a href="https://github.com/zerodytrash/YouTube-Internal-Clients?tab=readme-ov-file#clients">client type</a>
|
||||
*/
|
||||
public final int id;
|
||||
|
||||
public final String clientName;
|
||||
|
||||
/**
|
||||
* App package name.
|
||||
*/
|
||||
private final String packageName;
|
||||
|
||||
/**
|
||||
* Player user-agent.
|
||||
*/
|
||||
public final String userAgent;
|
||||
|
||||
/**
|
||||
* Device model, equivalent to {@link Build#MANUFACTURER} (System property: ro.product.vendor.manufacturer)
|
||||
*/
|
||||
public final String deviceMake;
|
||||
|
||||
/**
|
||||
* Device model, equivalent to {@link Build#MODEL} (System property: ro.product.vendor.model)
|
||||
*/
|
||||
public final String deviceModel;
|
||||
|
||||
/**
|
||||
* Device OS name.
|
||||
*/
|
||||
public final String osName;
|
||||
|
||||
/**
|
||||
* Device OS version.
|
||||
*/
|
||||
public final String osVersion;
|
||||
|
||||
/**
|
||||
* Android SDK version, equivalent to {@link Build.VERSION#SDK} (System property: ro.build.version.sdk)
|
||||
* Field is null if not applicable.
|
||||
*/
|
||||
@Nullable
|
||||
public final String androidSdkVersion;
|
||||
|
||||
/**
|
||||
* Android build id, equivalent to {@link Build#ID}.
|
||||
* Field is null if not applicable.
|
||||
*/
|
||||
@Nullable
|
||||
private final String buildId;
|
||||
|
||||
/**
|
||||
* Cronet release version, as found in decompiled client apk.
|
||||
* Field is null if not applicable.
|
||||
*/
|
||||
@Nullable
|
||||
private final String cronetVersion;
|
||||
|
||||
/**
|
||||
* App version.
|
||||
*/
|
||||
public final String clientVersion;
|
||||
|
||||
/**
|
||||
* If this client requires authentication and does not work
|
||||
* if logged out or in incognito mode.
|
||||
*/
|
||||
public final boolean requiresAuth;
|
||||
|
||||
/**
|
||||
* If the client should use authentication if available.
|
||||
*/
|
||||
public final boolean useAuth;
|
||||
|
||||
/**
|
||||
* Friendly name displayed in stats for nerds.
|
||||
*/
|
||||
public final String friendlyName;
|
||||
|
||||
@SuppressWarnings("ConstantLocale")
|
||||
ClientType(int id,
|
||||
String clientName,
|
||||
String packageName,
|
||||
String deviceMake,
|
||||
String deviceModel,
|
||||
String osName,
|
||||
String osVersion,
|
||||
@Nullable String androidSdkVersion,
|
||||
@Nullable String buildId,
|
||||
@Nullable String cronetVersion,
|
||||
String clientVersion,
|
||||
boolean requiresAuth,
|
||||
boolean useAuth,
|
||||
String friendlyName) {
|
||||
this.id = id;
|
||||
this.clientName = clientName;
|
||||
this.packageName = packageName;
|
||||
this.deviceMake = deviceMake;
|
||||
this.deviceModel = deviceModel;
|
||||
this.osName = osName;
|
||||
this.osVersion = osVersion;
|
||||
this.androidSdkVersion = androidSdkVersion;
|
||||
this.buildId = buildId;
|
||||
this.cronetVersion = cronetVersion;
|
||||
this.clientVersion = clientVersion;
|
||||
this.requiresAuth = requiresAuth;
|
||||
this.useAuth = useAuth;
|
||||
this.friendlyName = friendlyName;
|
||||
|
||||
Locale defaultLocale = Locale.getDefault();
|
||||
if (androidSdkVersion == null) {
|
||||
// Convert version from '18.2.22C152' into '18_2_22'
|
||||
String userAgentOsVersion = osVersion
|
||||
.replaceAll("(\\d+\\.\\d+\\.\\d+).*", "$1")
|
||||
.replace(".", "_");
|
||||
// https://github.com/mitmproxy/mitmproxy/issues/4836
|
||||
this.userAgent = String.format("%s/%s (%s; U; CPU iOS %s like Mac OS X; %s)",
|
||||
packageName,
|
||||
clientVersion,
|
||||
deviceModel,
|
||||
userAgentOsVersion,
|
||||
defaultLocale
|
||||
);
|
||||
} else {
|
||||
this.userAgent = String.format("%s/%s (Linux; U; Android %s; %s; %s; Build/%s; Cronet/%s)",
|
||||
packageName,
|
||||
clientVersion,
|
||||
osVersion,
|
||||
defaultLocale,
|
||||
deviceModel,
|
||||
Objects.requireNonNull(buildId),
|
||||
Objects.requireNonNull(cronetVersion)
|
||||
);
|
||||
}
|
||||
Logger.printDebug(() -> "userAgent: " + this.userAgent);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,267 @@
|
||||
package app.revanced.extension.shared.spoof;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Map;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
import app.revanced.extension.shared.settings.Setting;
|
||||
import app.revanced.extension.shared.spoof.requests.StreamingDataRequest;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class SpoofVideoStreamsPatch {
|
||||
private static final boolean SPOOF_STREAMING_DATA = BaseSettings.SPOOF_VIDEO_STREAMS.get();
|
||||
|
||||
private static final boolean FIX_HLS_CURRENT_TIME = SPOOF_STREAMING_DATA
|
||||
&& BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ClientType.IOS_UNPLUGGED;
|
||||
|
||||
/**
|
||||
* Any unreachable ip address. Used to intentionally fail requests.
|
||||
*/
|
||||
private static final String UNREACHABLE_HOST_URI_STRING = "https://127.0.0.0";
|
||||
private static final Uri UNREACHABLE_HOST_URI = Uri.parse(UNREACHABLE_HOST_URI_STRING);
|
||||
|
||||
/**
|
||||
* @return If this patch was included during patching.
|
||||
*/
|
||||
private static boolean isPatchIncluded() {
|
||||
return false; // Modified during patching.
|
||||
}
|
||||
|
||||
public static boolean notSpoofingToAndroid() {
|
||||
return !isPatchIncluded()
|
||||
|| !BaseSettings.SPOOF_VIDEO_STREAMS.get()
|
||||
|| BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ClientType.IOS_UNPLUGGED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* Blocks /get_watch requests by returning an unreachable URI.
|
||||
*
|
||||
* @param playerRequestUri The URI of the player request.
|
||||
* @return An unreachable URI if the request is a /get_watch request, otherwise the original URI.
|
||||
*/
|
||||
public static Uri blockGetWatchRequest(Uri playerRequestUri) {
|
||||
if (SPOOF_STREAMING_DATA) {
|
||||
try {
|
||||
String path = playerRequestUri.getPath();
|
||||
|
||||
if (path != null && path.contains("get_watch")) {
|
||||
Logger.printDebug(() -> "Blocking 'get_watch' by returning unreachable uri");
|
||||
|
||||
return UNREACHABLE_HOST_URI;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "blockGetWatchRequest failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
return playerRequestUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* <p>
|
||||
* Blocks /initplayback requests.
|
||||
*/
|
||||
public static String blockInitPlaybackRequest(String originalUrlString) {
|
||||
if (SPOOF_STREAMING_DATA) {
|
||||
try {
|
||||
var originalUri = Uri.parse(originalUrlString);
|
||||
String path = originalUri.getPath();
|
||||
|
||||
if (path != null && path.contains("initplayback")) {
|
||||
Logger.printDebug(() -> "Blocking 'initplayback' by clearing query");
|
||||
|
||||
return originalUri.buildUpon().clearQuery().build().toString();
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "blockInitPlaybackRequest failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
return originalUrlString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean isSpoofingEnabled() {
|
||||
return SPOOF_STREAMING_DATA;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* Only invoked when playing a livestream on an iOS client.
|
||||
*/
|
||||
public static boolean fixHLSCurrentTime(boolean original) {
|
||||
if (!SPOOF_STREAMING_DATA) {
|
||||
return original;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* Turns off a feature flag that interferes with spoofing.
|
||||
*/
|
||||
public static boolean useMediaFetchHotConfigReplacement(boolean original) {
|
||||
if (original) {
|
||||
Logger.printDebug(() -> "useMediaFetchHotConfigReplacement is set on");
|
||||
}
|
||||
|
||||
if (!SPOOF_STREAMING_DATA) {
|
||||
return original;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* Turns off a feature flag that interferes with video playback.
|
||||
*/
|
||||
public static boolean usePlaybackStartFeatureFlag(boolean original) {
|
||||
if (original) {
|
||||
Logger.printDebug(() -> "usePlaybackStartFeatureFlag is set on");
|
||||
}
|
||||
|
||||
if (!SPOOF_STREAMING_DATA) {
|
||||
return original;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void fetchStreams(String url, Map<String, String> requestHeaders) {
|
||||
if (SPOOF_STREAMING_DATA) {
|
||||
try {
|
||||
Uri uri = Uri.parse(url);
|
||||
String path = uri.getPath();
|
||||
if (path == null || !path.contains("player")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 'get_drm_license' has no video id and appears to happen when waiting for a paid video to start.
|
||||
// 'heartbeat' has no video id and appears to be only after playback has started.
|
||||
// 'refresh' has no video id and appears to happen when waiting for a livestream to start.
|
||||
// 'ad_break' has no video id.
|
||||
if (path.contains("get_drm_license") || path.contains("heartbeat")
|
||||
|| path.contains("refresh") || path.contains("ad_break")) {
|
||||
Logger.printDebug(() -> "Ignoring path: " + path);
|
||||
return;
|
||||
}
|
||||
|
||||
String id = uri.getQueryParameter("id");
|
||||
if (id == null) {
|
||||
Logger.printException(() -> "Ignoring request with no id: " + url);
|
||||
return;
|
||||
}
|
||||
|
||||
StreamingDataRequest.fetchRequest(id, requestHeaders);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "buildRequest failure", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* Fix playback by replace the streaming data.
|
||||
* Called after {@link #fetchStreams(String, Map)}.
|
||||
*/
|
||||
@Nullable
|
||||
public static ByteBuffer getStreamingData(String videoId) {
|
||||
if (SPOOF_STREAMING_DATA) {
|
||||
try {
|
||||
StreamingDataRequest request = StreamingDataRequest.getRequestForVideoId(videoId);
|
||||
if (request != null) {
|
||||
// This hook is always called off the main thread,
|
||||
// but this can later be called for the same video id from the main thread.
|
||||
// This is not a concern, since the fetch will always be finished
|
||||
// and never block the main thread.
|
||||
// But if debugging, then still verify this is the situation.
|
||||
if (BaseSettings.DEBUG.get() && !request.fetchCompleted() && Utils.isCurrentlyOnMainThread()) {
|
||||
Logger.printException(() -> "Error: Blocking main thread");
|
||||
}
|
||||
|
||||
var stream = request.getStream();
|
||||
if (stream != null) {
|
||||
Logger.printDebug(() -> "Overriding video stream: " + videoId);
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
|
||||
Logger.printDebug(() -> "Not overriding streaming data (video stream is null): " + videoId);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "getStreamingData failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* Called after {@link #getStreamingData(String)}.
|
||||
*/
|
||||
@Nullable
|
||||
public static byte[] removeVideoPlaybackPostBody(Uri uri, int method, byte[] postData) {
|
||||
if (SPOOF_STREAMING_DATA) {
|
||||
try {
|
||||
final int methodPost = 2;
|
||||
if (method == methodPost) {
|
||||
String path = uri.getPath();
|
||||
if (path != null && path.contains("videoplayback")) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "removeVideoPlaybackPostBody failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
return postData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static String appendSpoofedClient(String videoFormat) {
|
||||
try {
|
||||
if (SPOOF_STREAMING_DATA && BaseSettings.SPOOF_STREAMING_DATA_STATS_FOR_NERDS.get()
|
||||
&& !TextUtils.isEmpty(videoFormat)) {
|
||||
// Force LTR layout, to match the same LTR video time/length layout YouTube uses for all languages.
|
||||
return "\u202D" + videoFormat + "\u2009(" // u202D = left to right override
|
||||
+ StreamingDataRequest.getLastSpoofedClientName() + ")";
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "appendSpoofedClient failure", ex);
|
||||
}
|
||||
|
||||
return videoFormat;
|
||||
}
|
||||
|
||||
public static final class AudioStreamLanguageOverrideAvailability implements Setting.Availability {
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
return BaseSettings.SPOOF_VIDEO_STREAMS.get()
|
||||
&& BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ClientType.ANDROID_VR_NO_AUTH;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class SpoofiOSAvailability implements Setting.Availability {
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
return BaseSettings.SPOOF_VIDEO_STREAMS.get()
|
||||
&& BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ClientType.IOS_UNPLUGGED;
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user