mirror of
https://github.com/ReVanced/revanced-patches.git
synced 2026-01-18 00:33:57 +00:00
Compare commits
236 Commits
v5.42.0-de
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3401467a6d | ||
|
|
87247590de | ||
|
|
41e2590584 | ||
|
|
778d13ce8b | ||
|
|
19f146c01d | ||
|
|
12b819d20e | ||
|
|
004b5908db | ||
|
|
f4af27dfec | ||
|
|
4cc315952d | ||
|
|
6312fe8d60 | ||
|
|
3d754575a4 | ||
|
|
a3f7609fe3 | ||
|
|
d25dcfe49a | ||
|
|
1cc2cb9cb2 | ||
|
|
f5cbb31724 | ||
|
|
b42ae27ce6 | ||
|
|
43ab29d03d | ||
|
|
789f0a5628 | ||
|
|
da836b667c | ||
|
|
44e7dbcf4d | ||
|
|
195c239000 | ||
|
|
c47beae213 | ||
|
|
cebcfab86a | ||
|
|
71c6cb569e | ||
|
|
6bb6281149 | ||
|
|
16bd96e2bb | ||
|
|
8f3f4c95bb | ||
|
|
da02d68587 | ||
|
|
a429824bb7 | ||
|
|
e0f33468e6 | ||
|
|
315931cbf8 | ||
|
|
dc69f2433e | ||
|
|
73e43b2a49 | ||
|
|
918f04793f | ||
|
|
f1a9537f01 | ||
|
|
1f4f252c81 | ||
|
|
2b560f5fe9 | ||
|
|
f8bd1239cc | ||
|
|
c825ebda37 | ||
|
|
255c00b183 | ||
|
|
0ea3491227 | ||
|
|
5d437b08dd | ||
|
|
018d176914 | ||
|
|
9a77beea8a | ||
|
|
ded8370207 | ||
|
|
4d1104fc32 | ||
|
|
34830ba63b | ||
|
|
7a6894d809 | ||
|
|
144e6e2694 | ||
|
|
847ee189a9 | ||
|
|
dc813fe617 | ||
|
|
02831a6069 | ||
|
|
5228fd4b58 | ||
|
|
6bd7dca75b | ||
|
|
22ed7bfbb3 | ||
|
|
a7c220a4ae | ||
|
|
d8ca4ee931 | ||
|
|
a5d197b977 | ||
|
|
a0ec4c07f7 | ||
|
|
0928dcd00d | ||
|
|
bbd8932b2e | ||
|
|
300b12f948 | ||
|
|
ac583d40d0 | ||
|
|
c400188c38 | ||
|
|
0af0ee92c4 | ||
|
|
fff29544b9 | ||
|
|
9495cf49ef | ||
|
|
15675b5164 | ||
|
|
654d091e65 | ||
|
|
98371be33c | ||
|
|
2f0de15e67 | ||
|
|
df160370e2 | ||
|
|
bb745b555b | ||
|
|
8df9a46721 | ||
|
|
94ae84ad0f | ||
|
|
4febb2e2e9 | ||
|
|
b9bc7e3e58 | ||
|
|
9f3bb26cb9 | ||
|
|
d64dfc2884 | ||
|
|
a39ef1e0a4 | ||
|
|
1d8e977a43 | ||
|
|
da4cf94091 | ||
|
|
d23fa5e3b7 | ||
|
|
34d29abdfa | ||
|
|
ab808aeb77 | ||
|
|
a6b07cceb1 | ||
|
|
d291881215 | ||
|
|
7a18ebc7ab | ||
|
|
475197af45 | ||
|
|
d390b54dab | ||
|
|
4d1eaa6b14 | ||
|
|
c6364f5b49 | ||
|
|
f177eae385 | ||
|
|
2e9d6959c9 | ||
|
|
81f83690d6 | ||
|
|
f1bd6848c9 | ||
|
|
59d85b28a7 | ||
|
|
f238ae9895 | ||
|
|
e030e9c07a | ||
|
|
5029e979be | ||
|
|
55e1a6784b | ||
|
|
0cad5e73f0 | ||
|
|
ce503d5b58 | ||
|
|
57263538c7 | ||
|
|
70f4955e89 | ||
|
|
582144026d | ||
|
|
d80892cc0e | ||
|
|
6c4b931b8a | ||
|
|
76dcfaefd8 | ||
|
|
e4f52343c0 | ||
|
|
1196b1a147 | ||
|
|
858edbf3e7 | ||
|
|
a52c0153b1 | ||
|
|
cd9ef81354 | ||
|
|
1b2cd64a86 | ||
|
|
0c03599f07 | ||
|
|
5f23bfe833 | ||
|
|
2cf8f0e636 | ||
|
|
c17cf98c7e | ||
|
|
3e4990afff | ||
|
|
292fae440c | ||
|
|
12e7c0943a | ||
|
|
a0c5604951 | ||
|
|
38d9299dfe | ||
|
|
dfdbbfa047 | ||
|
|
e9f45ce926 | ||
|
|
1b38b1a3c8 | ||
|
|
13cf1724bf | ||
|
|
6d01863ec7 | ||
|
|
a32ed30b4c | ||
|
|
ef44eaa119 | ||
|
|
c73a03c9e1 | ||
|
|
c1681f982a | ||
|
|
1502bf7524 | ||
|
|
dffb1e6525 | ||
|
|
b8c2ede2bf | ||
|
|
591e106098 | ||
|
|
e7336d2ef3 | ||
|
|
7a53f8f62d | ||
|
|
3466d9d210 | ||
|
|
876d7a6b03 | ||
|
|
a2aa9cac27 | ||
|
|
08baa19b4a | ||
|
|
7283b93cea | ||
|
|
754b71959a | ||
|
|
c64e29ec57 | ||
|
|
b2dd008aee | ||
|
|
de97562c5d | ||
|
|
6373829fd6 | ||
|
|
cfd244b408 | ||
|
|
e8e28e2b6a | ||
|
|
2e4c6fdcad | ||
|
|
644d6dcb51 | ||
|
|
14dd7346a8 | ||
|
|
0af8c8a766 | ||
|
|
96454c843b | ||
|
|
476ef0fae1 | ||
|
|
bbec724afb | ||
|
|
7a1dcbd4ee | ||
|
|
2a1e31860f | ||
|
|
949d6bdd19 | ||
|
|
7563990750 | ||
|
|
4b605eb270 | ||
|
|
c2d7a7fb8b | ||
|
|
a55560dc25 | ||
|
|
e8522d703e | ||
|
|
068d029a03 | ||
|
|
0c19dbaf30 | ||
|
|
bf73ac8316 | ||
|
|
95eee59a87 | ||
|
|
566875ea53 | ||
|
|
10ea250d4a | ||
|
|
5bd0f11630 | ||
|
|
4547ecb73c | ||
|
|
50f0b9c5ee | ||
|
|
a8c4bdb8a6 | ||
|
|
6555f6e6f8 | ||
|
|
a0e2c5c7b9 | ||
|
|
54846253d7 | ||
|
|
a98e8f7370 | ||
|
|
2d928e0cd6 | ||
|
|
be2b144cc9 | ||
|
|
52c0bb6aa2 | ||
|
|
38a49cc2a1 | ||
|
|
91044b3a50 | ||
|
|
fd4b2e1bb9 | ||
|
|
d0f20c8c7f | ||
|
|
d65dbc749c | ||
|
|
143dcef2b8 | ||
|
|
dbfc5be464 | ||
|
|
0fe545cad6 | ||
|
|
feca17be68 | ||
|
|
7afeaebb5c | ||
|
|
60a581a632 | ||
|
|
104d096ada | ||
|
|
19dcbd8efb | ||
|
|
a50f3b5177 | ||
|
|
64d22a9c31 | ||
|
|
bd4ba2dae8 | ||
|
|
f51b260d1d | ||
|
|
63be54dd09 | ||
|
|
bb222d7a26 | ||
|
|
f03256c471 | ||
|
|
fe16433f20 | ||
|
|
2154d89242 | ||
|
|
277a8b6b47 | ||
|
|
20c413120b | ||
|
|
5ed092bb7d | ||
|
|
19949e1695 | ||
|
|
ec0acc0f13 | ||
|
|
a30a849e6e | ||
|
|
603025a122 | ||
|
|
9441e7acb4 | ||
|
|
963a4ef43f | ||
|
|
0acba30245 | ||
|
|
6b26346914 | ||
|
|
b1511c732d | ||
|
|
26117e744c | ||
|
|
a62ee43441 | ||
|
|
6a799110d7 | ||
|
|
aec17b93f7 | ||
|
|
e7a1706be4 | ||
|
|
9469604fe0 | ||
|
|
1a3a12df1a | ||
|
|
ae4b9474d3 | ||
|
|
83ccd9d3f1 | ||
|
|
526c7c05e2 | ||
|
|
d0d53d109e | ||
|
|
9d6731660b | ||
|
|
5a7e199162 | ||
|
|
0c662c8e3b | ||
|
|
08e8ead04f | ||
|
|
d238a42708 | ||
|
|
673609c2aa | ||
|
|
5f1a485e8f | ||
|
|
6961babee9 |
9
.github/workflows/build_pull_request.yml
vendored
9
.github/workflows/build_pull_request.yml
vendored
@@ -12,10 +12,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
@@ -25,11 +25,12 @@ jobs:
|
|||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ env.GITHUB_ACTOR }}
|
||||||
|
ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: ./gradlew :patches:buildAndroid --no-daemon
|
run: ./gradlew :patches:buildAndroid --no-daemon
|
||||||
|
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: revanced-patches
|
name: revanced-patches
|
||||||
path: patches/build/libs
|
path: patches/build/libs
|
||||||
|
|||||||
2
.github/workflows/open_pull_request.yml
vendored
2
.github/workflows/open_pull_request.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Open pull request
|
- name: Open pull request
|
||||||
uses: repo-sync/pull-request@v2
|
uses: repo-sync/pull-request@v2
|
||||||
|
|||||||
4
.github/workflows/pull_strings.yml
vendored
4
.github/workflows/pull_strings.yml
vendored
@@ -2,7 +2,7 @@ name: Pull strings
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 */12 * * *"
|
- cron: "0 0 * * 0"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
ref: dev
|
ref: dev
|
||||||
clean: true
|
clean: true
|
||||||
|
|||||||
2
.github/workflows/push_strings.yml
vendored
2
.github/workflows/push_strings.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Preprocess strings
|
- name: Preprocess strings
|
||||||
env:
|
env:
|
||||||
|
|||||||
13
.github/workflows/release.yml
vendored
13
.github/workflows/release.yml
vendored
@@ -18,10 +18,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
@@ -31,11 +31,12 @@ jobs:
|
|||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ env.GITHUB_ACTOR }}
|
||||||
|
ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: ./gradlew :patches:buildAndroid clean
|
run: ./gradlew :patches:buildAndroid clean
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 'lts/*'
|
node-version: 'lts/*'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
@@ -51,14 +52,14 @@ jobs:
|
|||||||
fingerprint: ${{ vars.GPG_FINGERPRINT }}
|
fingerprint: ${{ vars.GPG_FINGERPRINT }}
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: cycjimmy/semantic-release-action@v4
|
uses: cycjimmy/semantic-release-action@v5
|
||||||
id: release
|
id: release
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Attest
|
- name: Attest
|
||||||
if: steps.release.outputs.new_release_published == 'true'
|
if: steps.release.outputs.new_release_published == 'true'
|
||||||
uses: actions/attest-build-provenance@v2
|
uses: actions/attest-build-provenance@v3
|
||||||
with:
|
with:
|
||||||
subject-name: 'ReVanced Patches ${{ steps.release.outputs.new_release_git_tag }}'
|
subject-name: 'ReVanced Patches ${{ steps.release.outputs.new_release_git_tag }}'
|
||||||
subject-path: patches/build/libs/patches-*.rvp
|
subject-path: patches/build/libs/patches-*.rvp
|
||||||
|
|||||||
2
.github/workflows/update-gradle-wrapper.yml
vendored
2
.github/workflows/update-gradle-wrapper.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Update Gradle Wrapper
|
- name: Update Gradle Wrapper
|
||||||
uses: gradle-update/update-gradle-wrapper-action@v1
|
uses: gradle-update/update-gradle-wrapper-action@v1
|
||||||
|
|||||||
747
CHANGELOG.md
747
CHANGELOG.md
@@ -1,3 +1,750 @@
|
|||||||
|
# [5.48.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.8...v5.48.0-dev.9) (2026-01-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Add `Disable Sentry telemetry` patch ([#6416](https://github.com/ReVanced/revanced-patches/issues/6416)) ([4cc3159](https://github.com/ReVanced/revanced-patches/commit/4cc315952db557c565872de9e8484805f2e42305))
|
||||||
|
* Disable Play Integrity patch ([#6412](https://github.com/ReVanced/revanced-patches/issues/6412)) ([6312fe8](https://github.com/ReVanced/revanced-patches/commit/6312fe8d60da24465c0c1b0fa4e94ceb79873d9c))
|
||||||
|
|
||||||
|
# [5.48.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.7...v5.48.0-dev.8) (2026-01-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Letterboxd:** Add `Unlock app icons` patch ([#6415](https://github.com/ReVanced/revanced-patches/issues/6415)) ([d25dcfe](https://github.com/ReVanced/revanced-patches/commit/d25dcfe49ac331c9b3dca739ba0be95dbab669cc))
|
||||||
|
|
||||||
|
# [5.48.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.6...v5.48.0-dev.7) (2026-01-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Strava:** Add `Disable Quick Edit` patch ([#6452](https://github.com/ReVanced/revanced-patches/issues/6452)) ([f5cbb31](https://github.com/ReVanced/revanced-patches/commit/f5cbb31724d15f7e939b96ee0186fd0a108f9fdc))
|
||||||
|
* **Strava:** Add `Overwrite media upload parameters` patch ([#6410](https://github.com/ReVanced/revanced-patches/issues/6410)) ([b42ae27](https://github.com/ReVanced/revanced-patches/commit/b42ae27ce66ebad9e9cfc5b70fc121df5bad7567))
|
||||||
|
|
||||||
|
# [5.48.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.5...v5.48.0-dev.6) (2026-01-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Fix build error introduced in `4046bee` ([#6417](https://github.com/ReVanced/revanced-patches/issues/6417)) ([789f0a5](https://github.com/ReVanced/revanced-patches/commit/789f0a562861825065633d172445ebf35a1ba8d8))
|
||||||
|
|
||||||
|
# [5.48.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.4...v5.48.0-dev.5) (2025-12-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Disney+ - Skip ads:** Remove unsupported package names ([#6422](https://github.com/ReVanced/revanced-patches/issues/6422)) ([44e7dbc](https://github.com/ReVanced/revanced-patches/commit/44e7dbcf4d7eaf94dd0164baba847d3e19250154))
|
||||||
|
|
||||||
|
# [5.48.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.3...v5.48.0-dev.4) (2025-12-29)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Strava:** Add `Block Snowplow tracking` patch ([#6413](https://github.com/ReVanced/revanced-patches/issues/6413)) ([c47beae](https://github.com/ReVanced/revanced-patches/commit/c47beae21376dd17ab8bc09afe73e9094481bde9))
|
||||||
|
|
||||||
|
# [5.48.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.2...v5.48.0-dev.3) (2025-12-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Fix compilation error introduced in `6bb6281` ([#6409](https://github.com/ReVanced/revanced-patches/issues/6409)) ([71c6cb5](https://github.com/ReVanced/revanced-patches/commit/71c6cb569ebf7b93cf73ee391839e5220557ce7c))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Instagram - Hides navigation buttons:** Add more buttons to hide ([#6390](https://github.com/ReVanced/revanced-patches/issues/6390)) ([6bb6281](https://github.com/ReVanced/revanced-patches/commit/6bb62811493da04812cc3e392e68d874f95cbef9))
|
||||||
|
|
||||||
|
# [5.48.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.1...v5.48.0-dev.2) (2025-12-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Strava:** Add `Enable password login` patch ([#6396](https://github.com/ReVanced/revanced-patches/issues/6396)) ([8f3f4c9](https://github.com/ReVanced/revanced-patches/commit/8f3f4c95bb8f151fc9a2c272bf7d0e905c2f01fc))
|
||||||
|
|
||||||
|
# [5.48.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.47.0...v5.48.0-dev.1) (2025-12-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Fix compilation error introduced in dc69f243 ([#6392](https://github.com/ReVanced/revanced-patches/issues/6392)) ([a429824](https://github.com/ReVanced/revanced-patches/commit/a429824bb77b49aea14b0b54f2204ae24d5209a1))
|
||||||
|
* **YouTube - Hide layout components:** Hide new type of crowdfunding box ([#6380](https://github.com/ReVanced/revanced-patches/issues/6380)) ([dc69f24](https://github.com/ReVanced/revanced-patches/commit/dc69f2433e2650654e2dffdd76b0b0c8a52bf515))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **ProtonVPN:** Add `Unlock split tunneling` patch ([#6353](https://github.com/ReVanced/revanced-patches/issues/6353)) ([e0f3346](https://github.com/ReVanced/revanced-patches/commit/e0f33468e6e96b9f10cf35ec67622d6488528c90))
|
||||||
|
* **SBS On Demand:** Add `Remove ads` patch ([#6378](https://github.com/ReVanced/revanced-patches/issues/6378)) ([315931c](https://github.com/ReVanced/revanced-patches/commit/315931cbf8f61cd4b3a54ace1ff03685d748614c))
|
||||||
|
|
||||||
|
# [5.47.0](https://github.com/ReVanced/revanced-patches/compare/v5.46.0...v5.47.0) (2025-12-18)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Instagram - Disable signature check:** Change patch to default excluded ([#6283](https://github.com/ReVanced/revanced-patches/issues/6283)) ([bb745b5](https://github.com/ReVanced/revanced-patches/commit/bb745b555b3808b7679c5995319aa365630fbd76))
|
||||||
|
* **Lightroom:** Add `Disable version check` patch to fix opening the app ([#6315](https://github.com/ReVanced/revanced-patches/issues/6315)) ([018d176](https://github.com/ReVanced/revanced-patches/commit/018d176914a06a30e9007a3eb2e6b0f459078413))
|
||||||
|
* **Reddit - Hide ads:** Update patch for new versions of Reddit ([#6342](https://github.com/ReVanced/revanced-patches/issues/6342)) ([f8bd123](https://github.com/ReVanced/revanced-patches/commit/f8bd1239cc0f0bd1c2dca39f846951bf512891e3))
|
||||||
|
* **Spotify:** Make patches work with latest versions again ([#6359](https://github.com/ReVanced/revanced-patches/issues/6359)) ([34830ba](https://github.com/ReVanced/revanced-patches/commit/34830ba63b436146064f0f89f948d51cd0cb9146))
|
||||||
|
* **YouTube - Hide layout components:** Fix "Hide Subscribe button" in channel page not working ([#6363](https://github.com/ReVanced/revanced-patches/issues/6363)) ([ded8370](https://github.com/ReVanced/revanced-patches/commit/ded83702077701aac8a8749d71bf7376427f37d6))
|
||||||
|
* **YouTube - Hide player flyout menu items:** Allow hiding audio menu with 'Android No SDK' client type ([9495cf4](https://github.com/ReVanced/revanced-patches/commit/9495cf49ef8a872be64de6c971c1919b4b9a8720))
|
||||||
|
* **YouTube - Sanitize sharing links:** Handle non hierarchical urls ([654d091](https://github.com/ReVanced/revanced-patches/commit/654d091e650cda37650b57cbf3ba6f1cdd6d47d3))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Disney+ - SkipAds:** Add other package names the patch is compatible with ([#6372](https://github.com/ReVanced/revanced-patches/issues/6372)) ([1f4f252](https://github.com/ReVanced/revanced-patches/commit/1f4f252c81e9a89267f6e37548e66027b1bc1a1a))
|
||||||
|
* **Disney+:** Add `Skip ads` patch ([#6343](https://github.com/ReVanced/revanced-patches/issues/6343)) ([6bd7dca](https://github.com/ReVanced/revanced-patches/commit/6bd7dca75bd2ea335a596aa93a8b767d39be5f83))
|
||||||
|
* **IdAustria - Remove device integrity check:** Update patch to work with latest version ([#6360](https://github.com/ReVanced/revanced-patches/issues/6360)) ([0ea3491](https://github.com/ReVanced/revanced-patches/commit/0ea3491227fc50c03555d43d3fec78eb82906b26))
|
||||||
|
* **Instagram:** Add `Anonymous story viewing` patch ([#6263](https://github.com/ReVanced/revanced-patches/issues/6263)) ([94ae84a](https://github.com/ReVanced/revanced-patches/commit/94ae84ad0fc3a9197c82d5356301d464730c3b17))
|
||||||
|
* **Instagram:** Add `Disable auto story flipping` patch ([#6262](https://github.com/ReVanced/revanced-patches/issues/6262)) ([2f0de15](https://github.com/ReVanced/revanced-patches/commit/2f0de15e67e4f99ed6ecdc136d04cceb23b0d069))
|
||||||
|
* **Instagram:** Add `Disable Reels scrolling` patch ([#6317](https://github.com/ReVanced/revanced-patches/issues/6317)) ([0928dcd](https://github.com/ReVanced/revanced-patches/commit/0928dcd00dc2a9c1eef9a23c1e26ff5dc9ee670a))
|
||||||
|
* **Letterboxd:** Add `Hide ads` patch ([#6309](https://github.com/ReVanced/revanced-patches/issues/6309)) ([0af0ee9](https://github.com/ReVanced/revanced-patches/commit/0af0ee92c48bb2ffc332197e05439e20c5c05d83))
|
||||||
|
* **Peacock TV:** Add `Hide ads` patch ([#6348](https://github.com/ReVanced/revanced-patches/issues/6348)) ([847ee18](https://github.com/ReVanced/revanced-patches/commit/847ee189a971e6d4a99823998569f8e561b8319c))
|
||||||
|
* **ProtonVPN:** Add `Remove delay` patch ([#6326](https://github.com/ReVanced/revanced-patches/issues/6326)) ([bbd8932](https://github.com/ReVanced/revanced-patches/commit/bbd8932b2e740aff96ba047332e541bff3e09436))
|
||||||
|
* **Spoof SIM provider:** Spoof additional TelephonyManager methods ([#6293](https://github.com/ReVanced/revanced-patches/issues/6293)) ([ac583d4](https://github.com/ReVanced/revanced-patches/commit/ac583d40d0f4c0e6544e3661ff3e82a25912f2b0))
|
||||||
|
* **YouTube - Hide layout components:** Add "Hide cell divider", "Hide featured links", and "Hide featured videos" options ([#6335](https://github.com/ReVanced/revanced-patches/issues/6335)) ([a5d197b](https://github.com/ReVanced/revanced-patches/commit/a5d197b9775b98d7a37bfdee9e5f726d5e04d8cf))
|
||||||
|
* **YouTube - Hide layout components:** Add "Hide Join button" and "Hide Subscribe button" options for channel page ([#6345](https://github.com/ReVanced/revanced-patches/issues/6345)) ([02831a6](https://github.com/ReVanced/revanced-patches/commit/02831a6069fc30ffa3a87f8e4de653d003a2187e))
|
||||||
|
* **YouTube - Hide Shorts components:** Add "Hide auto-dubbed label" and "Hide live preview" options ([#6334](https://github.com/ReVanced/revanced-patches/issues/6334)) ([a7c220a](https://github.com/ReVanced/revanced-patches/commit/a7c220a4aea93ea7ae7005b5760443d7571c4228))
|
||||||
|
|
||||||
|
# [5.47.0-dev.18](https://github.com/ReVanced/revanced-patches/compare/v5.47.0-dev.17...v5.47.0-dev.18) (2025-12-18)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Disney+ - SkipAds:** Add other package names the patch is compatible with ([#6372](https://github.com/ReVanced/revanced-patches/issues/6372)) ([1f4f252](https://github.com/ReVanced/revanced-patches/commit/1f4f252c81e9a89267f6e37548e66027b1bc1a1a))
|
||||||
|
|
||||||
|
# [5.47.0-dev.17](https://github.com/ReVanced/revanced-patches/compare/v5.47.0-dev.16...v5.47.0-dev.17) (2025-12-18)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Reddit - Hide ads:** Update patch for new versions of Reddit ([#6342](https://github.com/ReVanced/revanced-patches/issues/6342)) ([f8bd123](https://github.com/ReVanced/revanced-patches/commit/f8bd1239cc0f0bd1c2dca39f846951bf512891e3))
|
||||||
|
|
||||||
|
# [5.47.0-dev.16](https://github.com/ReVanced/revanced-patches/compare/v5.47.0-dev.15...v5.47.0-dev.16) (2025-12-15)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Lightroom:** Add `Disable version check` patch to fix opening the app ([#6315](https://github.com/ReVanced/revanced-patches/issues/6315)) ([018d176](https://github.com/ReVanced/revanced-patches/commit/018d176914a06a30e9007a3eb2e6b0f459078413))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **IdAustria - Remove device integrity check:** Update patch to work with latest version ([#6360](https://github.com/ReVanced/revanced-patches/issues/6360)) ([0ea3491](https://github.com/ReVanced/revanced-patches/commit/0ea3491227fc50c03555d43d3fec78eb82906b26))
|
||||||
|
|
||||||
|
# [5.47.0-dev.15](https://github.com/ReVanced/revanced-patches/compare/v5.47.0-dev.14...v5.47.0-dev.15) (2025-12-13)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube - Hide layout components:** Fix "Hide Subscribe button" in channel page not working ([#6363](https://github.com/ReVanced/revanced-patches/issues/6363)) ([ded8370](https://github.com/ReVanced/revanced-patches/commit/ded83702077701aac8a8749d71bf7376427f37d6))
|
||||||
|
|
||||||
|
# [5.47.0-dev.14](https://github.com/ReVanced/revanced-patches/compare/v5.47.0-dev.13...v5.47.0-dev.14) (2025-12-13)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Spotify:** Make patches work with latest versions again ([#6359](https://github.com/ReVanced/revanced-patches/issues/6359)) ([34830ba](https://github.com/ReVanced/revanced-patches/commit/34830ba63b436146064f0f89f948d51cd0cb9146))
|
||||||
|
|
||||||
|
# [5.47.0-dev.13](https://github.com/ReVanced/revanced-patches/compare/v5.47.0-dev.12...v5.47.0-dev.13) (2025-12-10)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Peacock TV:** Add `Hide ads` patch ([#6348](https://github.com/ReVanced/revanced-patches/issues/6348)) ([847ee18](https://github.com/ReVanced/revanced-patches/commit/847ee189a971e6d4a99823998569f8e561b8319c))
|
||||||
|
|
||||||
|
# [5.47.0-dev.12](https://github.com/ReVanced/revanced-patches/compare/v5.47.0-dev.11...v5.47.0-dev.12) (2025-12-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **YouTube - Hide layout components:** Add "Hide Join button" and "Hide Subscribe button" options for channel page ([#6345](https://github.com/ReVanced/revanced-patches/issues/6345)) ([02831a6](https://github.com/ReVanced/revanced-patches/commit/02831a6069fc30ffa3a87f8e4de653d003a2187e))
|
||||||
|
|
||||||
|
# [5.47.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v5.47.0-dev.10...v5.47.0-dev.11) (2025-12-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Disney+:** Add `Skip ads` patch ([#6343](https://github.com/ReVanced/revanced-patches/issues/6343)) ([6bd7dca](https://github.com/ReVanced/revanced-patches/commit/6bd7dca75bd2ea335a596aa93a8b767d39be5f83))
|
||||||
|
|
||||||
|
# [5.47.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.47.0-dev.9...v5.47.0-dev.10) (2025-12-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **YouTube - Hide Shorts components:** Add "Hide auto-dubbed label" and "Hide live preview" options ([#6334](https://github.com/ReVanced/revanced-patches/issues/6334)) ([a7c220a](https://github.com/ReVanced/revanced-patches/commit/a7c220a4aea93ea7ae7005b5760443d7571c4228))
|
||||||
|
|
||||||
|
# [5.47.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.47.0-dev.8...v5.47.0-dev.9) (2025-12-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **YouTube - Hide layout components:** Add "Hide cell divider", "Hide featured links", and "Hide featured videos" options ([#6335](https://github.com/ReVanced/revanced-patches/issues/6335)) ([a5d197b](https://github.com/ReVanced/revanced-patches/commit/a5d197b9775b98d7a37bfdee9e5f726d5e04d8cf))
|
||||||
|
|
||||||
|
# [5.47.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.47.0-dev.7...v5.47.0-dev.8) (2025-12-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Instagram:** Add `Disable Reels scrolling` patch ([#6317](https://github.com/ReVanced/revanced-patches/issues/6317)) ([0928dcd](https://github.com/ReVanced/revanced-patches/commit/0928dcd00dc2a9c1eef9a23c1e26ff5dc9ee670a))
|
||||||
|
* **ProtonVPN:** Add `Remove delay` patch ([#6326](https://github.com/ReVanced/revanced-patches/issues/6326)) ([bbd8932](https://github.com/ReVanced/revanced-patches/commit/bbd8932b2e740aff96ba047332e541bff3e09436))
|
||||||
|
|
||||||
|
# [5.47.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.47.0-dev.6...v5.47.0-dev.7) (2025-12-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Spoof SIM provider:** Spoof additional TelephonyManager methods ([#6293](https://github.com/ReVanced/revanced-patches/issues/6293)) ([ac583d4](https://github.com/ReVanced/revanced-patches/commit/ac583d40d0f4c0e6544e3661ff3e82a25912f2b0))
|
||||||
|
|
||||||
|
# [5.47.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.47.0-dev.5...v5.47.0-dev.6) (2025-11-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Letterboxd:** Add `Hide ads` patch ([#6309](https://github.com/ReVanced/revanced-patches/issues/6309)) ([0af0ee9](https://github.com/ReVanced/revanced-patches/commit/0af0ee92c48bb2ffc332197e05439e20c5c05d83))
|
||||||
|
|
||||||
|
# [5.47.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.47.0-dev.4...v5.47.0-dev.5) (2025-11-13)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube - Hide player flyout menu items:** Allow hiding audio menu with 'Android No SDK' client type ([9495cf4](https://github.com/ReVanced/revanced-patches/commit/9495cf49ef8a872be64de6c971c1919b4b9a8720))
|
||||||
|
|
||||||
|
# [5.47.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.47.0-dev.3...v5.47.0-dev.4) (2025-11-12)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube - Sanitize sharing links:** Handle non hierarchical urls ([654d091](https://github.com/ReVanced/revanced-patches/commit/654d091e650cda37650b57cbf3ba6f1cdd6d47d3))
|
||||||
|
|
||||||
|
# [5.47.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.47.0-dev.2...v5.47.0-dev.3) (2025-11-12)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Instagram:** Add `Disable auto story flipping` patch ([#6262](https://github.com/ReVanced/revanced-patches/issues/6262)) ([2f0de15](https://github.com/ReVanced/revanced-patches/commit/2f0de15e67e4f99ed6ecdc136d04cceb23b0d069))
|
||||||
|
|
||||||
|
# [5.47.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.47.0-dev.1...v5.47.0-dev.2) (2025-11-12)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Instagram - Disable signature check:** Change patch to default excluded ([#6283](https://github.com/ReVanced/revanced-patches/issues/6283)) ([bb745b5](https://github.com/ReVanced/revanced-patches/commit/bb745b555b3808b7679c5995319aa365630fbd76))
|
||||||
|
|
||||||
|
# [5.47.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.46.0...v5.47.0-dev.1) (2025-11-12)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Instagram:** Add `Anonymous story viewing` patch ([#6263](https://github.com/ReVanced/revanced-patches/issues/6263)) ([94ae84a](https://github.com/ReVanced/revanced-patches/commit/94ae84ad0fc3a9197c82d5356301d464730c3b17))
|
||||||
|
|
||||||
|
# [5.46.0](https://github.com/ReVanced/revanced-patches/compare/v5.45.0...v5.46.0) (2025-11-10)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Duolingo - Disable ads:** Constrain patch to last working app target ([f238ae9](https://github.com/ReVanced/revanced-patches/commit/f238ae9895000f01d1dccb800cc8efde0d5362bd))
|
||||||
|
* **Instagram - Hide navigation buttons:** Constrain patch to last working app target ([e030e9c](https://github.com/ReVanced/revanced-patches/commit/e030e9c07a7748e117ac44f6776a9f6317b20623))
|
||||||
|
* **Spotify - Hide Create button:** Remove obsolete patch that is no longer needed ([#6252](https://github.com/ReVanced/revanced-patches/issues/6252)) ([59d85b2](https://github.com/ReVanced/revanced-patches/commit/59d85b28a7fcb285ff5f2bb6ae654020d76b2019))
|
||||||
|
* **YouTube - Check watch history domain name resolution:** Fix false positive warning message if the internet connection fails halfway into the DNS check ([5726353](https://github.com/ReVanced/revanced-patches/commit/57263538c79f5a561c449229ac8e068c641285d3))
|
||||||
|
* **YouTube - Hide layout components:** Fix "Hide Hype points" ([#6247](https://github.com/ReVanced/revanced-patches/issues/6247)) ([5821440](https://github.com/ReVanced/revanced-patches/commit/582144026d28e57bb7adcbba39244f3c7cdbc0f3))
|
||||||
|
* **YouTube - Settings:** Add additional languages to ReVanced language preference ([d390b54](https://github.com/ReVanced/revanced-patches/commit/d390b54dab92d75b4e0d3e38344eae489dd69d98))
|
||||||
|
* **YouTube - Settings:** Resolve settings search crash when searching for specific words ([#6231](https://github.com/ReVanced/revanced-patches/issues/6231)) ([76dcfae](https://github.com/ReVanced/revanced-patches/commit/76dcfaefd8679e45a70f265b0239436e60c055cf))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **YouTube - Debugging:** Add setting to block experimental client flags ([#6196](https://github.com/ReVanced/revanced-patches/issues/6196)) ([2e9d695](https://github.com/ReVanced/revanced-patches/commit/2e9d6959c94df7588b9e34b18770e9f437e91926))
|
||||||
|
* **YouTube - Hide layout components:** Add "Hide Hype points" ([#6230](https://github.com/ReVanced/revanced-patches/issues/6230)) ([a52c015](https://github.com/ReVanced/revanced-patches/commit/a52c0153b12c3f6f0ad260e03d2e9850c0466392))
|
||||||
|
* **YouTube - Hide layout components:** Add video description "Hide Featured content" and "Hide Subscribe button" ([#6253](https://github.com/ReVanced/revanced-patches/issues/6253)) ([da4cf94](https://github.com/ReVanced/revanced-patches/commit/da4cf940911a4406e2c9dd558b60305385a80c61))
|
||||||
|
* **YouTube - Hide player flyout menu items:** Add "Hide Listen with YouTube Music" ([#6232](https://github.com/ReVanced/revanced-patches/issues/6232)) ([858edbf](https://github.com/ReVanced/revanced-patches/commit/858edbf3e7f394fcc766d767c8dc54cf5ba24370))
|
||||||
|
* **YouTube Music:** Add `Change miniplayer color` patch ([#6259](https://github.com/ReVanced/revanced-patches/issues/6259)) ([ab808ae](https://github.com/ReVanced/revanced-patches/commit/ab808aeb773592cb26c848d8456478a346ec3bad))
|
||||||
|
* **YouTube Music:** Add `Hide buttons` patch ([#6255](https://github.com/ReVanced/revanced-patches/issues/6255)) ([7a18ebc](https://github.com/ReVanced/revanced-patches/commit/7a18ebc7ab74ba30c5d5284a4856c55cdfc31097))
|
||||||
|
|
||||||
|
# [5.46.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.46.0-dev.9...v5.46.0-dev.10) (2025-11-09)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **YouTube - Hide layout components:** Add video description "Hide Featured content" and "Hide Subscribe button" ([#6253](https://github.com/ReVanced/revanced-patches/issues/6253)) ([da4cf94](https://github.com/ReVanced/revanced-patches/commit/da4cf940911a4406e2c9dd558b60305385a80c61))
|
||||||
|
|
||||||
|
# [5.46.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.46.0-dev.8...v5.46.0-dev.9) (2025-11-09)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **YouTube Music:** Add `Change miniplayer color` patch ([#6259](https://github.com/ReVanced/revanced-patches/issues/6259)) ([ab808ae](https://github.com/ReVanced/revanced-patches/commit/ab808aeb773592cb26c848d8456478a346ec3bad))
|
||||||
|
|
||||||
|
# [5.46.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.46.0-dev.7...v5.46.0-dev.8) (2025-11-09)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **YouTube Music:** Add `Hide buttons` patch ([#6255](https://github.com/ReVanced/revanced-patches/issues/6255)) ([7a18ebc](https://github.com/ReVanced/revanced-patches/commit/7a18ebc7ab74ba30c5d5284a4856c55cdfc31097))
|
||||||
|
|
||||||
|
# [5.46.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.46.0-dev.6...v5.46.0-dev.7) (2025-11-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube - Settings:** Add additional languages to ReVanced language preference ([d390b54](https://github.com/ReVanced/revanced-patches/commit/d390b54dab92d75b4e0d3e38344eae489dd69d98))
|
||||||
|
|
||||||
|
# [5.46.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.46.0-dev.5...v5.46.0-dev.6) (2025-11-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **YouTube - Debugging:** Add setting to block experimental client flags ([#6196](https://github.com/ReVanced/revanced-patches/issues/6196)) ([2e9d695](https://github.com/ReVanced/revanced-patches/commit/2e9d6959c94df7588b9e34b18770e9f437e91926))
|
||||||
|
|
||||||
|
# [5.46.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.46.0-dev.4...v5.46.0-dev.5) (2025-11-07)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Duolingo - Disable ads:** Constrain patch to last working app target ([f238ae9](https://github.com/ReVanced/revanced-patches/commit/f238ae9895000f01d1dccb800cc8efde0d5362bd))
|
||||||
|
* **Instagram - Hide navigation buttons:** Constrain patch to last working app target ([e030e9c](https://github.com/ReVanced/revanced-patches/commit/e030e9c07a7748e117ac44f6776a9f6317b20623))
|
||||||
|
* **Spotify - Hide Create button:** Remove obsolete patch that is no longer needed ([#6252](https://github.com/ReVanced/revanced-patches/issues/6252)) ([59d85b2](https://github.com/ReVanced/revanced-patches/commit/59d85b28a7fcb285ff5f2bb6ae654020d76b2019))
|
||||||
|
|
||||||
|
# [5.46.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.46.0-dev.3...v5.46.0-dev.4) (2025-11-07)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube - Check watch history domain name resolution:** Fix false positive warning message if the internet connection fails halfway into the DNS check ([5726353](https://github.com/ReVanced/revanced-patches/commit/57263538c79f5a561c449229ac8e068c641285d3))
|
||||||
|
|
||||||
|
# [5.46.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.46.0-dev.2...v5.46.0-dev.3) (2025-11-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube - Hide layout components:** Fix "Hide Hype points" ([#6247](https://github.com/ReVanced/revanced-patches/issues/6247)) ([5821440](https://github.com/ReVanced/revanced-patches/commit/582144026d28e57bb7adcbba39244f3c7cdbc0f3))
|
||||||
|
|
||||||
|
# [5.46.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.46.0-dev.1...v5.46.0-dev.2) (2025-11-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube - Settings:** Resolve settings search crash when searching for specific words ([#6231](https://github.com/ReVanced/revanced-patches/issues/6231)) ([76dcfae](https://github.com/ReVanced/revanced-patches/commit/76dcfaefd8679e45a70f265b0239436e60c055cf))
|
||||||
|
|
||||||
|
# [5.46.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.45.0...v5.46.0-dev.1) (2025-11-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **YouTube - Hide layout components:** Add "Hide Hype points" ([#6230](https://github.com/ReVanced/revanced-patches/issues/6230)) ([a52c015](https://github.com/ReVanced/revanced-patches/commit/a52c0153b12c3f6f0ad260e03d2e9850c0466392))
|
||||||
|
* **YouTube - Hide player flyout menu items:** Add "Hide Listen with YouTube Music" ([#6232](https://github.com/ReVanced/revanced-patches/issues/6232)) ([858edbf](https://github.com/ReVanced/revanced-patches/commit/858edbf3e7f394fcc766d767c8dc54cf5ba24370))
|
||||||
|
|
||||||
|
# [5.45.0](https://github.com/ReVanced/revanced-patches/compare/v5.44.0...v5.45.0) (2025-11-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Instagram:** Update failing fingerprints on newer versions ([#6181](https://github.com/ReVanced/revanced-patches/issues/6181)) ([c73a03c](https://github.com/ReVanced/revanced-patches/commit/c73a03c9e18a12262939c974cdf16221221d1487))
|
||||||
|
* **TikTok - Downloads:** Fix download path setting ([#6191](https://github.com/ReVanced/revanced-patches/issues/6191)) ([3e4990a](https://github.com/ReVanced/revanced-patches/commit/3e4990afff4c86b93970b153db713ad0f813124d))
|
||||||
|
* **YouTube - Change header:** Do not mirror header graphic with RTL languages ([a0c5604](https://github.com/ReVanced/revanced-patches/commit/a0c56049510ce040e1ccd49257864672c343344d))
|
||||||
|
* **YouTube - Force original audio:** Fall back to visionOS and not Android Studio if Android VR is not available ([6d01863](https://github.com/ReVanced/revanced-patches/commit/6d01863ec70617d9abc864ce6686ed9764dd151d))
|
||||||
|
* **YouTube - Spoof video streams:** Remove spoof stream audio selector that no longer works ([292fae4](https://github.com/ReVanced/revanced-patches/commit/292fae440c6d5694c5e84407becec2d91f1fd156))
|
||||||
|
* **YouTube Music - Hide category bar:** Correctly hide the category bar in newer app targets ([#6175](https://github.com/ReVanced/revanced-patches/issues/6175)) ([13cf172](https://github.com/ReVanced/revanced-patches/commit/13cf1724bf2f946c7129cab0db96721c90f9fe89))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Spoof video streams:** Add experimental "Android No SDK" client type ([5f23bfe](https://github.com/ReVanced/revanced-patches/commit/5f23bfe833c6e01617a7dbc5325b4a3fb931e53e))
|
||||||
|
* **TikTok:** Add `Sanitize sharing links` patch ([#6176](https://github.com/ReVanced/revanced-patches/issues/6176)) ([ef44eaa](https://github.com/ReVanced/revanced-patches/commit/ef44eaa119b9d6c5faec051e22d20f883d0da4f1))
|
||||||
|
* **YouTube - Change Header:** Use SVG for header logo ([#6178](https://github.com/ReVanced/revanced-patches/issues/6178)) ([e9f45ce](https://github.com/ReVanced/revanced-patches/commit/e9f45ce92695d5857473ff71c14b190bded28a73))
|
||||||
|
|
||||||
|
# [5.45.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.45.0-dev.5...v5.45.0-dev.6) (2025-11-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Spoof video streams:** Add experimental "Android No SDK" client type ([5f23bfe](https://github.com/ReVanced/revanced-patches/commit/5f23bfe833c6e01617a7dbc5325b4a3fb931e53e))
|
||||||
|
|
||||||
|
# [5.45.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.45.0-dev.4...v5.45.0-dev.5) (2025-11-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **TikTok - Downloads:** Fix download path setting ([#6191](https://github.com/ReVanced/revanced-patches/issues/6191)) ([3e4990a](https://github.com/ReVanced/revanced-patches/commit/3e4990afff4c86b93970b153db713ad0f813124d))
|
||||||
|
* **YouTube - Spoof video streams:** Remove spoof stream audio selector that no longer works ([292fae4](https://github.com/ReVanced/revanced-patches/commit/292fae440c6d5694c5e84407becec2d91f1fd156))
|
||||||
|
|
||||||
|
# [5.45.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.45.0-dev.3...v5.45.0-dev.4) (2025-10-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube - Change header:** Do not mirror header graphic with RTL languages ([a0c5604](https://github.com/ReVanced/revanced-patches/commit/a0c56049510ce040e1ccd49257864672c343344d))
|
||||||
|
|
||||||
|
# [5.45.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.45.0-dev.2...v5.45.0-dev.3) (2025-10-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **YouTube - Change Header:** Use SVG for header logo ([#6178](https://github.com/ReVanced/revanced-patches/issues/6178)) ([e9f45ce](https://github.com/ReVanced/revanced-patches/commit/e9f45ce92695d5857473ff71c14b190bded28a73))
|
||||||
|
|
||||||
|
# [5.45.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.45.0-dev.1...v5.45.0-dev.2) (2025-10-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube - Force original audio:** Fall back to visionOS and not Android Studio if Android VR is not available ([6d01863](https://github.com/ReVanced/revanced-patches/commit/6d01863ec70617d9abc864ce6686ed9764dd151d))
|
||||||
|
* **YouTube Music - Hide category bar:** Correctly hide the category bar in newer app targets ([#6175](https://github.com/ReVanced/revanced-patches/issues/6175)) ([13cf172](https://github.com/ReVanced/revanced-patches/commit/13cf1724bf2f946c7129cab0db96721c90f9fe89))
|
||||||
|
|
||||||
|
# [5.45.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.44.0...v5.45.0-dev.1) (2025-10-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Instagram:** Update failing fingerprints on newer versions ([#6181](https://github.com/ReVanced/revanced-patches/issues/6181)) ([c73a03c](https://github.com/ReVanced/revanced-patches/commit/c73a03c9e18a12262939c974cdf16221221d1487))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **TikTok:** Add `Sanitize sharing links` patch ([#6176](https://github.com/ReVanced/revanced-patches/issues/6176)) ([ef44eaa](https://github.com/ReVanced/revanced-patches/commit/ef44eaa119b9d6c5faec051e22d20f883d0da4f1))
|
||||||
|
|
||||||
|
# [5.44.0](https://github.com/ReVanced/revanced-patches/compare/v5.43.1...v5.44.0) (2025-10-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Google Photos - Spoof features:** Add support for Pixel 10 devices ([#6161](https://github.com/ReVanced/revanced-patches/issues/6161)) ([754b719](https://github.com/ReVanced/revanced-patches/commit/754b71959a0155413eb33cf1bdc2c8976eaca634))
|
||||||
|
* **X / Twitter - Change link sharing domain:** Use bytecode patching to resolve patching with Manager ([#6125](https://github.com/ReVanced/revanced-patches/issues/6125)) ([0af8c8a](https://github.com/ReVanced/revanced-patches/commit/0af8c8a766ae4ba6926404d59da2f14d649f91f7))
|
||||||
|
* **YouTube - Hide layout components:** Hide new kind of community post ([#6146](https://github.com/ReVanced/revanced-patches/issues/6146)) ([cfd244b](https://github.com/ReVanced/revanced-patches/commit/cfd244b4088daacd2788ec38357ac521e4b296d5))
|
||||||
|
* **YouTube Music:** Resolve patching 7.29 target ([2e4c6fd](https://github.com/ReVanced/revanced-patches/commit/2e4c6fdcadeef45a80733e374421d52e5e8af910))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Add `Custom network security` patch ([#6151](https://github.com/ReVanced/revanced-patches/issues/6151)) ([e7336d2](https://github.com/ReVanced/revanced-patches/commit/e7336d2ef361cc5d6fe6e8442b36d9cf1f542931))
|
||||||
|
* **Duolingo - Enable debug menu:** Support latest app target ([#6163](https://github.com/ReVanced/revanced-patches/issues/6163)) ([08baa19](https://github.com/ReVanced/revanced-patches/commit/08baa19b4a62e62bd103d177c3f4454de199cf16))
|
||||||
|
* **Duolingo:** Add `Skip energy recharge ads` patch ([#6167](https://github.com/ReVanced/revanced-patches/issues/6167)) ([591e106](https://github.com/ReVanced/revanced-patches/commit/591e106098c6eff431b8b7ac7d985ce7373d701e))
|
||||||
|
* **Samsung Radio:** Add `Disable device checks` patch ([#6145](https://github.com/ReVanced/revanced-patches/issues/6145)) ([de97562](https://github.com/ReVanced/revanced-patches/commit/de97562c5ddc8ec707761c1e04e74c4e18f9c158))
|
||||||
|
|
||||||
|
# [5.44.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.44.0-dev.3...v5.44.0-dev.4) (2025-10-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Add `Custom network security` patch ([#6151](https://github.com/ReVanced/revanced-patches/issues/6151)) ([e7336d2](https://github.com/ReVanced/revanced-patches/commit/e7336d2ef361cc5d6fe6e8442b36d9cf1f542931))
|
||||||
|
* **Duolingo:** Add `Skip energy recharge ads` patch ([#6167](https://github.com/ReVanced/revanced-patches/issues/6167)) ([591e106](https://github.com/ReVanced/revanced-patches/commit/591e106098c6eff431b8b7ac7d985ce7373d701e))
|
||||||
|
|
||||||
|
# [5.44.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.44.0-dev.2...v5.44.0-dev.3) (2025-10-22)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Duolingo - Enable debug menu:** Support latest app target ([#6163](https://github.com/ReVanced/revanced-patches/issues/6163)) ([08baa19](https://github.com/ReVanced/revanced-patches/commit/08baa19b4a62e62bd103d177c3f4454de199cf16))
|
||||||
|
|
||||||
|
# [5.44.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.44.0-dev.1...v5.44.0-dev.2) (2025-10-22)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Google Photos - Spoof features:** Add support for Pixel 10 devices ([#6161](https://github.com/ReVanced/revanced-patches/issues/6161)) ([754b719](https://github.com/ReVanced/revanced-patches/commit/754b71959a0155413eb33cf1bdc2c8976eaca634))
|
||||||
|
|
||||||
|
# [5.44.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.43.2-dev.3...v5.44.0-dev.1) (2025-10-22)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Samsung Radio:** Add `Disable device checks` patch ([#6145](https://github.com/ReVanced/revanced-patches/issues/6145)) ([de97562](https://github.com/ReVanced/revanced-patches/commit/de97562c5ddc8ec707761c1e04e74c4e18f9c158))
|
||||||
|
|
||||||
|
## [5.43.2-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.43.2-dev.2...v5.43.2-dev.3) (2025-10-19)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube - Hide layout components:** Hide new kind of community post ([#6146](https://github.com/ReVanced/revanced-patches/issues/6146)) ([cfd244b](https://github.com/ReVanced/revanced-patches/commit/cfd244b4088daacd2788ec38357ac521e4b296d5))
|
||||||
|
|
||||||
|
## [5.43.2-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.43.2-dev.1...v5.43.2-dev.2) (2025-10-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube Music:** Resolve patching 7.29 target ([2e4c6fd](https://github.com/ReVanced/revanced-patches/commit/2e4c6fdcadeef45a80733e374421d52e5e8af910))
|
||||||
|
|
||||||
|
## [5.43.2-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.43.1...v5.43.2-dev.1) (2025-10-16)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **X / Twitter - Change link sharing domain:** Use bytecode patching to resolve patching with Manager ([#6125](https://github.com/ReVanced/revanced-patches/issues/6125)) ([0af8c8a](https://github.com/ReVanced/revanced-patches/commit/0af8c8a766ae4ba6926404d59da2f14d649f91f7))
|
||||||
|
|
||||||
|
## [5.43.1](https://github.com/ReVanced/revanced-patches/compare/v5.43.0...v5.43.1) (2025-10-15)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **X / Twitter - Change link sharing domain:** Resolve duplicate patch option ([#6119](https://github.com/ReVanced/revanced-patches/issues/6119)) ([7563990](https://github.com/ReVanced/revanced-patches/commit/75639907502382f63fa127a886362d4a4573e6e3))
|
||||||
|
* **X / Twitter:** Do not crash Manager when clicking on domain patch option ([2a1e318](https://github.com/ReVanced/revanced-patches/commit/2a1e31860f22f537d51b40a5b71d9ad9d538789e))
|
||||||
|
|
||||||
|
## [5.43.1-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.43.1-dev.1...v5.43.1-dev.2) (2025-10-14)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **X / Twitter:** Do not crash Manager when clicking on domain patch option ([2a1e318](https://github.com/ReVanced/revanced-patches/commit/2a1e31860f22f537d51b40a5b71d9ad9d538789e))
|
||||||
|
|
||||||
|
## [5.43.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.43.0...v5.43.1-dev.1) (2025-10-14)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **X / Twitter - Change link sharing domain:** Resolve duplicate patch option ([#6119](https://github.com/ReVanced/revanced-patches/issues/6119)) ([7563990](https://github.com/ReVanced/revanced-patches/commit/75639907502382f63fa127a886362d4a4573e6e3))
|
||||||
|
|
||||||
|
# [5.43.0](https://github.com/ReVanced/revanced-patches/compare/v5.42.1...v5.43.0) (2025-10-14)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Custom branding:** Use white notification icon for expanded status bar panel ([95eee59](https://github.com/ReVanced/revanced-patches/commit/95eee59a87a680e212a3ba06e1afefee8d91ee9d))
|
||||||
|
* **Instagram - Change sharing domain:** Display patch option ([#6089](https://github.com/ReVanced/revanced-patches/issues/6089)) ([be2b144](https://github.com/ReVanced/revanced-patches/commit/be2b144cc9c4108ec37e16f3dd20573d88ffaa2b))
|
||||||
|
* **X / Twitter - Change Link Sharing Domain:** Change link domain of share copy action ([#6091](https://github.com/ReVanced/revanced-patches/issues/6091)) ([5484625](https://github.com/ReVanced/revanced-patches/commit/54846253d748f4e7e30b2bba427c7d2fb9c341e2))
|
||||||
|
* **YouTube - Custom branding:** Do not add a broken custom icon if the user provides an invalid custom icon path ([6555f6e](https://github.com/ReVanced/revanced-patches/commit/6555f6e6f8b52c2f1ddab1f52c6704cd2d8cfc12))
|
||||||
|
* **YouTube - Custom branding:** Use ReVanced icon for status bar notification icon ([#6108](https://github.com/ReVanced/revanced-patches/issues/6108)) ([10ea250](https://github.com/ReVanced/revanced-patches/commit/10ea250d4a91f8ab3b7f865612a403fc93a857b5))
|
||||||
|
* **YouTube - Force original audio:** Do not use translated audio if stream spoofing is off and force audio is on ([0c19dba](https://github.com/ReVanced/revanced-patches/commit/0c19dbaf30bcb95a29448d98b028ebeea54cc7d3))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Instagram:** Add `Hide suggested content` patch ([#6075](https://github.com/ReVanced/revanced-patches/issues/6075)) ([50f0b9c](https://github.com/ReVanced/revanced-patches/commit/50f0b9c5eee95ff5f9974e344802e1d2a4aab47b))
|
||||||
|
|
||||||
|
# [5.43.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.43.0-dev.3...v5.43.0-dev.4) (2025-10-14)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube - Force original audio:** Do not use translated audio if stream spoofing is off and force audio is on ([0c19dba](https://github.com/ReVanced/revanced-patches/commit/0c19dbaf30bcb95a29448d98b028ebeea54cc7d3))
|
||||||
|
|
||||||
|
# [5.43.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.43.0-dev.2...v5.43.0-dev.3) (2025-10-14)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Custom branding:** Use white notification icon for expanded status bar panel ([95eee59](https://github.com/ReVanced/revanced-patches/commit/95eee59a87a680e212a3ba06e1afefee8d91ee9d))
|
||||||
|
|
||||||
|
# [5.43.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.43.0-dev.1...v5.43.0-dev.2) (2025-10-14)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube - Custom branding:** Use ReVanced icon for status bar notification icon ([#6108](https://github.com/ReVanced/revanced-patches/issues/6108)) ([10ea250](https://github.com/ReVanced/revanced-patches/commit/10ea250d4a91f8ab3b7f865612a403fc93a857b5))
|
||||||
|
|
||||||
|
# [5.43.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.42.2-dev.3...v5.43.0-dev.1) (2025-10-11)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Instagram:** Add `Hide suggested content` patch ([#6075](https://github.com/ReVanced/revanced-patches/issues/6075)) ([50f0b9c](https://github.com/ReVanced/revanced-patches/commit/50f0b9c5eee95ff5f9974e344802e1d2a4aab47b))
|
||||||
|
|
||||||
|
## [5.42.2-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.42.2-dev.2...v5.42.2-dev.3) (2025-10-11)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube - Custom branding:** Do not add a broken custom icon if the user provides an invalid custom icon path ([6555f6e](https://github.com/ReVanced/revanced-patches/commit/6555f6e6f8b52c2f1ddab1f52c6704cd2d8cfc12))
|
||||||
|
|
||||||
|
## [5.42.2-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.42.2-dev.1...v5.42.2-dev.2) (2025-10-10)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **X / Twitter - Change Link Sharing Domain:** Change link domain of share copy action ([#6091](https://github.com/ReVanced/revanced-patches/issues/6091)) ([5484625](https://github.com/ReVanced/revanced-patches/commit/54846253d748f4e7e30b2bba427c7d2fb9c341e2))
|
||||||
|
|
||||||
|
## [5.42.2-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.42.1...v5.42.2-dev.1) (2025-10-09)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Instagram - Change sharing domain:** Display patch option ([#6089](https://github.com/ReVanced/revanced-patches/issues/6089)) ([be2b144](https://github.com/ReVanced/revanced-patches/commit/be2b144cc9c4108ec37e16f3dd20573d88ffaa2b))
|
||||||
|
|
||||||
|
## [5.42.1](https://github.com/ReVanced/revanced-patches/compare/v5.42.0...v5.42.1) (2025-10-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube - Custom Branding:** Resolve startup crash with root installation ([fd4b2e1](https://github.com/ReVanced/revanced-patches/commit/fd4b2e1bb98c6e507178e5b46b896ef7d320bc3d))
|
||||||
|
|
||||||
|
## [5.42.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.42.0...v5.42.1-dev.1) (2025-10-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube - Custom Branding:** Resolve startup crash with root installation ([fd4b2e1](https://github.com/ReVanced/revanced-patches/commit/fd4b2e1bb98c6e507178e5b46b896ef7d320bc3d))
|
||||||
|
|
||||||
|
# [5.42.0](https://github.com/ReVanced/revanced-patches/compare/v5.41.0...v5.42.0) (2025-10-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Custom branding:** Update ReVanced logo ([#6049](https://github.com/ReVanced/revanced-patches/issues/6049)) ([9441e7a](https://github.com/ReVanced/revanced-patches/commit/9441e7acb4817e12d1443d438ef6c448518bd614))
|
||||||
|
* **Custom branding:** Update ReVanced logo sizing ([#6029](https://github.com/ReVanced/revanced-patches/issues/6029)) ([ae4b947](https://github.com/ReVanced/revanced-patches/commit/ae4b9474d3fb62528fc21397c19954d31605e9da))
|
||||||
|
* **Instagram - Hide navigation buttons:** Resolve app startup crash ([080a226](https://github.com/ReVanced/revanced-patches/commit/080a2266146798be71789c939deef2f289697523))
|
||||||
|
* **Spotify:** Change `Hide Create button` patch to default off ([#6067](https://github.com/ReVanced/revanced-patches/issues/6067)) ([19949e1](https://github.com/ReVanced/revanced-patches/commit/19949e1695cc252ff0f94a33b6e3fb62e967d7fd))
|
||||||
|
* **X / Twitter:** Remove non functional and obsolete patch `Open links with app chooser` ([#6033](https://github.com/ReVanced/revanced-patches/issues/6033)) ([673609c](https://github.com/ReVanced/revanced-patches/commit/673609c2aa87988cdc138eab101b9750fe6a7b62))
|
||||||
|
* **YouTube - Force original audio:** Change patch to default on ([#6070](https://github.com/ReVanced/revanced-patches/issues/6070)) ([bd4ba2d](https://github.com/ReVanced/revanced-patches/commit/bd4ba2dae85ee6fd8d7e6078c3de775ca336e0b6))
|
||||||
|
* **YouTube - Force original language:** Resolve some videos using Swedish audio track ([9d67316](https://github.com/ReVanced/revanced-patches/commit/9d6731660ba0e19b863d05d54aa04f74a879f69b))
|
||||||
|
* **YouTube - Hide end screen cards:** Hide new type of end screen card ([#6027](https://github.com/ReVanced/revanced-patches/issues/6027)) ([76b0364](https://github.com/ReVanced/revanced-patches/commit/76b0364c5b5562c6a0d178d2bbe5b220f48aaca9))
|
||||||
|
* **YouTube - Spoof video streams:** Add "Allow Android VR AV1" setting ([#6071](https://github.com/ReVanced/revanced-patches/issues/6071)) ([f03256c](https://github.com/ReVanced/revanced-patches/commit/f03256c471e1ee6a12267c1b56b531ca8f89278c))
|
||||||
|
* **YouTube - Spoof video streams:** Do not allow VR AV1 if "Force AVC" is enabled ([7afeaeb](https://github.com/ReVanced/revanced-patches/commit/7afeaebb5cc22eb4f4512d8aa0cf4e835e7a2daf))
|
||||||
|
* **YouTube - Spoof video streams:** Resolve playback dropping frames ([#6051](https://github.com/ReVanced/revanced-patches/issues/6051)) ([a62ee43](https://github.com/ReVanced/revanced-patches/commit/a62ee43441b197f5c8352ae373bb8919ad66f0bd))
|
||||||
|
* **YouTube Music - GmsCore support:** Handle sharing links to certain apps such as Instagram ([#6026](https://github.com/ReVanced/revanced-patches/issues/6026)) ([328234f](https://github.com/ReVanced/revanced-patches/commit/328234f39ada81542e596f04e8ce410c787c15c8))
|
||||||
|
* **YouTube Music - Hide cast button:** Fix patching error ([28799a5](https://github.com/ReVanced/revanced-patches/commit/28799a548a73651134ef304cb6cb542cf8e55abe))
|
||||||
|
* **YouTube Music - Hide cast button:** Resolve button not hiding ([7817885](https://github.com/ReVanced/revanced-patches/commit/7817885cffed66608039ab45881537cbd3069c9d))
|
||||||
|
* **YouTube:** Resolve UI components not hiding for some users ([#6054](https://github.com/ReVanced/revanced-patches/issues/6054)) ([6b26346](https://github.com/ReVanced/revanced-patches/commit/6b2634691423f5ce25a28b3f2fbc420977b81748))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Custom branding:** Add in-app settings to change icon and name ([#6059](https://github.com/ReVanced/revanced-patches/issues/6059)) ([a50f3b5](https://github.com/ReVanced/revanced-patches/commit/a50f3b5177808f07d84041c946caccb5a08ad387))
|
||||||
|
* **Instagram:** Add `Custom share domain` patch ([#5998](https://github.com/ReVanced/revanced-patches/issues/5998)) ([20c4131](https://github.com/ReVanced/revanced-patches/commit/20c413120bad97af6121718e76b22a1b5540aa44))
|
||||||
|
* **Instagram:** Add `Enable developer menu` patch ([#6043](https://github.com/ReVanced/revanced-patches/issues/6043)) ([2154d89](https://github.com/ReVanced/revanced-patches/commit/2154d89242fd8d7f7460145d5d35a4f1986944a3))
|
||||||
|
* **Instagram:** Add `Open links externally` patch ([#6012](https://github.com/ReVanced/revanced-patches/issues/6012)) ([08e8ead](https://github.com/ReVanced/revanced-patches/commit/08e8ead04ffff47a4608a3db7aadc8d5feccd4ad))
|
||||||
|
* **Instagram:** Add `Sanitize sharing links` patch ([#5986](https://github.com/ReVanced/revanced-patches/issues/5986)) ([963a4ef](https://github.com/ReVanced/revanced-patches/commit/963a4ef43fd513de7a2d7d019992f06b62fdcc10))
|
||||||
|
* **Viber:** Add `Hide navigation buttons` patch ([#5991](https://github.com/ReVanced/revanced-patches/issues/5991)) ([5cb46c4](https://github.com/ReVanced/revanced-patches/commit/5cb46c4e9180ebc16eddb983dad73d137d8ec047))
|
||||||
|
* **YouTube Music:** Add `Custom branding` patch ([#6007](https://github.com/ReVanced/revanced-patches/issues/6007)) ([4c8b56f](https://github.com/ReVanced/revanced-patches/commit/4c8b56f5466b244737f501654eb7c5d34b6b2f88))
|
||||||
|
* **YouTube Music:** Add `Force original audio` patch ([#6036](https://github.com/ReVanced/revanced-patches/issues/6036)) ([d0d53d1](https://github.com/ReVanced/revanced-patches/commit/d0d53d109e451759a029326873adfa36fba12b23))
|
||||||
|
|
||||||
|
# [5.42.0](https://github.com/ReVanced/revanced-patches/compare/v5.41.0...v5.42.0) (2025-10-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Custom branding:** Update ReVanced logo ([#6049](https://github.com/ReVanced/revanced-patches/issues/6049)) ([9441e7a](https://github.com/ReVanced/revanced-patches/commit/9441e7acb4817e12d1443d438ef6c448518bd614))
|
||||||
|
* **Custom branding:** Update ReVanced logo sizing ([#6029](https://github.com/ReVanced/revanced-patches/issues/6029)) ([ae4b947](https://github.com/ReVanced/revanced-patches/commit/ae4b9474d3fb62528fc21397c19954d31605e9da))
|
||||||
|
* **Instagram - Hide navigation buttons:** Resolve app startup crash ([080a226](https://github.com/ReVanced/revanced-patches/commit/080a2266146798be71789c939deef2f289697523))
|
||||||
|
* **Spotify:** Change `Hide Create button` patch to default off ([#6067](https://github.com/ReVanced/revanced-patches/issues/6067)) ([19949e1](https://github.com/ReVanced/revanced-patches/commit/19949e1695cc252ff0f94a33b6e3fb62e967d7fd))
|
||||||
|
* **X / Twitter:** Remove non functional and obsolete patch `Open links with app chooser` ([#6033](https://github.com/ReVanced/revanced-patches/issues/6033)) ([673609c](https://github.com/ReVanced/revanced-patches/commit/673609c2aa87988cdc138eab101b9750fe6a7b62))
|
||||||
|
* **YouTube - Force original audio:** Change patch to default on ([#6070](https://github.com/ReVanced/revanced-patches/issues/6070)) ([bd4ba2d](https://github.com/ReVanced/revanced-patches/commit/bd4ba2dae85ee6fd8d7e6078c3de775ca336e0b6))
|
||||||
|
* **YouTube - Force original language:** Resolve some videos using Swedish audio track ([9d67316](https://github.com/ReVanced/revanced-patches/commit/9d6731660ba0e19b863d05d54aa04f74a879f69b))
|
||||||
|
* **YouTube - Hide end screen cards:** Hide new type of end screen card ([#6027](https://github.com/ReVanced/revanced-patches/issues/6027)) ([76b0364](https://github.com/ReVanced/revanced-patches/commit/76b0364c5b5562c6a0d178d2bbe5b220f48aaca9))
|
||||||
|
* **YouTube - Spoof video streams:** Add "Allow Android VR AV1" setting ([#6071](https://github.com/ReVanced/revanced-patches/issues/6071)) ([f03256c](https://github.com/ReVanced/revanced-patches/commit/f03256c471e1ee6a12267c1b56b531ca8f89278c))
|
||||||
|
* **YouTube - Spoof video streams:** Do not allow VR AV1 if "Force AVC" is enabled ([7afeaeb](https://github.com/ReVanced/revanced-patches/commit/7afeaebb5cc22eb4f4512d8aa0cf4e835e7a2daf))
|
||||||
|
* **YouTube - Spoof video streams:** Resolve playback dropping frames ([#6051](https://github.com/ReVanced/revanced-patches/issues/6051)) ([a62ee43](https://github.com/ReVanced/revanced-patches/commit/a62ee43441b197f5c8352ae373bb8919ad66f0bd))
|
||||||
|
* **YouTube Music - GmsCore support:** Handle sharing links to certain apps such as Instagram ([#6026](https://github.com/ReVanced/revanced-patches/issues/6026)) ([328234f](https://github.com/ReVanced/revanced-patches/commit/328234f39ada81542e596f04e8ce410c787c15c8))
|
||||||
|
* **YouTube Music - Hide cast button:** Fix patching error ([28799a5](https://github.com/ReVanced/revanced-patches/commit/28799a548a73651134ef304cb6cb542cf8e55abe))
|
||||||
|
* **YouTube Music - Hide cast button:** Resolve button not hiding ([7817885](https://github.com/ReVanced/revanced-patches/commit/7817885cffed66608039ab45881537cbd3069c9d))
|
||||||
|
* **YouTube:** Resolve UI components not hiding for some users ([#6054](https://github.com/ReVanced/revanced-patches/issues/6054)) ([6b26346](https://github.com/ReVanced/revanced-patches/commit/6b2634691423f5ce25a28b3f2fbc420977b81748))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Custom branding:** Add in-app settings to change icon and name ([#6059](https://github.com/ReVanced/revanced-patches/issues/6059)) ([a50f3b5](https://github.com/ReVanced/revanced-patches/commit/a50f3b5177808f07d84041c946caccb5a08ad387))
|
||||||
|
* **Instagram:** Add `Custom share domain` patch ([#5998](https://github.com/ReVanced/revanced-patches/issues/5998)) ([20c4131](https://github.com/ReVanced/revanced-patches/commit/20c413120bad97af6121718e76b22a1b5540aa44))
|
||||||
|
* **Instagram:** Add `Enable developer menu` patch ([#6043](https://github.com/ReVanced/revanced-patches/issues/6043)) ([2154d89](https://github.com/ReVanced/revanced-patches/commit/2154d89242fd8d7f7460145d5d35a4f1986944a3))
|
||||||
|
* **Instagram:** Add `Open links externally` patch ([#6012](https://github.com/ReVanced/revanced-patches/issues/6012)) ([08e8ead](https://github.com/ReVanced/revanced-patches/commit/08e8ead04ffff47a4608a3db7aadc8d5feccd4ad))
|
||||||
|
* **Instagram:** Add `Sanitize sharing links` patch ([#5986](https://github.com/ReVanced/revanced-patches/issues/5986)) ([963a4ef](https://github.com/ReVanced/revanced-patches/commit/963a4ef43fd513de7a2d7d019992f06b62fdcc10))
|
||||||
|
* **Viber:** Add `Hide navigation buttons` patch ([#5991](https://github.com/ReVanced/revanced-patches/issues/5991)) ([5cb46c4](https://github.com/ReVanced/revanced-patches/commit/5cb46c4e9180ebc16eddb983dad73d137d8ec047))
|
||||||
|
* **YouTube Music:** Add `Custom branding` patch ([#6007](https://github.com/ReVanced/revanced-patches/issues/6007)) ([4c8b56f](https://github.com/ReVanced/revanced-patches/commit/4c8b56f5466b244737f501654eb7c5d34b6b2f88))
|
||||||
|
* **YouTube Music:** Add `Force original audio` patch ([#6036](https://github.com/ReVanced/revanced-patches/issues/6036)) ([d0d53d1](https://github.com/ReVanced/revanced-patches/commit/d0d53d109e451759a029326873adfa36fba12b23))
|
||||||
|
|
||||||
|
# [5.42.0-dev.19](https://github.com/ReVanced/revanced-patches/compare/v5.42.0-dev.18...v5.42.0-dev.19) (2025-10-07)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube - Spoof video streams:** Do not allow VR AV1 if "Force AVC" is enabled ([7afeaeb](https://github.com/ReVanced/revanced-patches/commit/7afeaebb5cc22eb4f4512d8aa0cf4e835e7a2daf))
|
||||||
|
|
||||||
|
# [5.42.0-dev.18](https://github.com/ReVanced/revanced-patches/compare/v5.42.0-dev.17...v5.42.0-dev.18) (2025-10-07)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Custom branding:** Add in-app settings to change icon and name ([#6059](https://github.com/ReVanced/revanced-patches/issues/6059)) ([a50f3b5](https://github.com/ReVanced/revanced-patches/commit/a50f3b5177808f07d84041c946caccb5a08ad387))
|
||||||
|
|
||||||
|
# [5.42.0-dev.17](https://github.com/ReVanced/revanced-patches/compare/v5.42.0-dev.16...v5.42.0-dev.17) (2025-10-07)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube - Force original audio:** Change patch to default on ([#6070](https://github.com/ReVanced/revanced-patches/issues/6070)) ([bd4ba2d](https://github.com/ReVanced/revanced-patches/commit/bd4ba2dae85ee6fd8d7e6078c3de775ca336e0b6))
|
||||||
|
|
||||||
|
# [5.42.0-dev.16](https://github.com/ReVanced/revanced-patches/compare/v5.42.0-dev.15...v5.42.0-dev.16) (2025-10-07)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube - Spoof video streams:** Add "Allow Android VR AV1" setting ([#6071](https://github.com/ReVanced/revanced-patches/issues/6071)) ([f03256c](https://github.com/ReVanced/revanced-patches/commit/f03256c471e1ee6a12267c1b56b531ca8f89278c))
|
||||||
|
|
||||||
|
# [5.42.0-dev.15](https://github.com/ReVanced/revanced-patches/compare/v5.42.0-dev.14...v5.42.0-dev.15) (2025-10-07)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Instagram:** Add `Enable developer menu` patch ([#6043](https://github.com/ReVanced/revanced-patches/issues/6043)) ([2154d89](https://github.com/ReVanced/revanced-patches/commit/2154d89242fd8d7f7460145d5d35a4f1986944a3))
|
||||||
|
|
||||||
|
# [5.42.0-dev.14](https://github.com/ReVanced/revanced-patches/compare/v5.42.0-dev.13...v5.42.0-dev.14) (2025-10-07)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Instagram:** Add `Custom share domain` patch ([#5998](https://github.com/ReVanced/revanced-patches/issues/5998)) ([20c4131](https://github.com/ReVanced/revanced-patches/commit/20c413120bad97af6121718e76b22a1b5540aa44))
|
||||||
|
|
||||||
|
# [5.42.0-dev.13](https://github.com/ReVanced/revanced-patches/compare/v5.42.0-dev.12...v5.42.0-dev.13) (2025-10-07)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Spotify:** Change `Hide Create button` patch to default off ([#6067](https://github.com/ReVanced/revanced-patches/issues/6067)) ([19949e1](https://github.com/ReVanced/revanced-patches/commit/19949e1695cc252ff0f94a33b6e3fb62e967d7fd))
|
||||||
|
|
||||||
|
# [5.42.0-dev.12](https://github.com/ReVanced/revanced-patches/compare/v5.42.0-dev.11...v5.42.0-dev.12) (2025-10-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Custom branding:** Update ReVanced logo ([#6049](https://github.com/ReVanced/revanced-patches/issues/6049)) ([9441e7a](https://github.com/ReVanced/revanced-patches/commit/9441e7acb4817e12d1443d438ef6c448518bd614))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Instagram:** Add `Sanitize sharing links` patch ([#5986](https://github.com/ReVanced/revanced-patches/issues/5986)) ([963a4ef](https://github.com/ReVanced/revanced-patches/commit/963a4ef43fd513de7a2d7d019992f06b62fdcc10))
|
||||||
|
|
||||||
|
# [5.42.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v5.42.0-dev.10...v5.42.0-dev.11) (2025-10-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube:** Resolve UI components not hiding for some users ([#6054](https://github.com/ReVanced/revanced-patches/issues/6054)) ([6b26346](https://github.com/ReVanced/revanced-patches/commit/6b2634691423f5ce25a28b3f2fbc420977b81748))
|
||||||
|
|
||||||
|
# [5.42.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.42.0-dev.9...v5.42.0-dev.10) (2025-10-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube - Spoof video streams:** Resolve playback dropping frames ([#6051](https://github.com/ReVanced/revanced-patches/issues/6051)) ([a62ee43](https://github.com/ReVanced/revanced-patches/commit/a62ee43441b197f5c8352ae373bb8919ad66f0bd))
|
||||||
|
|
||||||
|
# [5.42.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.42.0-dev.8...v5.42.0-dev.9) (2025-10-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Custom branding:** Update ReVanced logo sizing ([#6029](https://github.com/ReVanced/revanced-patches/issues/6029)) ([ae4b947](https://github.com/ReVanced/revanced-patches/commit/ae4b9474d3fb62528fc21397c19954d31605e9da))
|
||||||
|
|
||||||
|
# [5.42.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.42.0-dev.7...v5.42.0-dev.8) (2025-10-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube - Force original language:** Resolve some videos using Swedish audio track ([9d67316](https://github.com/ReVanced/revanced-patches/commit/9d6731660ba0e19b863d05d54aa04f74a879f69b))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **YouTube Music:** Add `Force original audio` patch ([#6036](https://github.com/ReVanced/revanced-patches/issues/6036)) ([d0d53d1](https://github.com/ReVanced/revanced-patches/commit/d0d53d109e451759a029326873adfa36fba12b23))
|
||||||
|
|
||||||
|
# [5.42.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.42.0-dev.6...v5.42.0-dev.7) (2025-10-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Instagram:** Add `Open links externally` patch ([#6012](https://github.com/ReVanced/revanced-patches/issues/6012)) ([08e8ead](https://github.com/ReVanced/revanced-patches/commit/08e8ead04ffff47a4608a3db7aadc8d5feccd4ad))
|
||||||
|
|
||||||
|
# [5.42.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.42.0-dev.5...v5.42.0-dev.6) (2025-09-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **X / Twitter:** Remove non functional and obsolete patch `Open links with app chooser` ([#6033](https://github.com/ReVanced/revanced-patches/issues/6033)) ([673609c](https://github.com/ReVanced/revanced-patches/commit/673609c2aa87988cdc138eab101b9750fe6a7b62))
|
||||||
|
|
||||||
# [5.42.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.42.0-dev.4...v5.42.0-dev.5) (2025-09-28)
|
# [5.42.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.42.0-dev.4...v5.42.0-dev.5) (2025-09-28)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -97,9 +97,9 @@ Thank you for considering contributing to ReVanced Patches. You can find the con
|
|||||||
|
|
||||||
To build ReVanced Patches, you can follow the [ReVanced documentation](https://github.com/ReVanced/revanced-documentation).
|
To build ReVanced Patches, you can follow the [ReVanced documentation](https://github.com/ReVanced/revanced-documentation).
|
||||||
|
|
||||||
## 📜 Licence
|
## 📜 License
|
||||||
|
|
||||||
ReVanced Patches is licensed under the GPLv3 license. Please see the [license file](LICENSE) for more information.
|
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.
|
[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,
|
Any modifications to ReVanced Patches must also be made available under the GPL,
|
||||||
along with build & install instructions.
|
along with build & install instructions.
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
project_id_env: "CROWDIN_PROJECT_ID"
|
project_id_env: "CROWDIN_PROJECT_ID"
|
||||||
api_token_env: "CROWDIN_PERSONAL_TOKEN"
|
api_token_env: "CROWDIN_PERSONAL_TOKEN"
|
||||||
|
|
||||||
preserve_hierarchy: false
|
preserve_hierarchy: true
|
||||||
files:
|
files:
|
||||||
- source: patches/src/main/resources/addresources/values/strings.xml
|
- source: patches/src/main/resources/addresources/values/strings.xml
|
||||||
|
dest: patches.xml
|
||||||
translation: patches/src/main/resources/addresources/values-%android_code%/strings.xml
|
translation: patches/src/main/resources/addresources/values-%android_code%/strings.xml
|
||||||
skip_untranslated_strings: true
|
skip_untranslated_strings: true
|
||||||
|
|||||||
20
extensions/all/misc/disable-play-integrity/build.gradle.kts
Normal file
20
extensions/all/misc/disable-play-integrity/build.gradle.kts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
android {
|
||||||
|
namespace = "app.revanced.extension"
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 21
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
aidl = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly(libs.annotation)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<manifest/>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.google.android.play.core.integrity.protocol;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import com.google.android.play.core.integrity.protocol.IExpressIntegrityServiceCallback;
|
||||||
|
|
||||||
|
interface IExpressIntegrityService {
|
||||||
|
oneway void requestIntegrityToken(in Bundle request, IExpressIntegrityServiceCallback callback) = 2;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.google.android.play.core.integrity.protocol;
|
||||||
|
|
||||||
|
interface IExpressIntegrityServiceCallback {
|
||||||
|
oneway void onRequestExpressIntegrityTokenResult(in Bundle result) = 2;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.google.android.play.core.integrity.protocol;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import com.google.android.play.core.integrity.protocol.IIntegrityServiceCallback;
|
||||||
|
|
||||||
|
interface IIntegrityService {
|
||||||
|
oneway void requestIntegrityToken(in Bundle request, IIntegrityServiceCallback callback) = 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.google.android.play.core.integrity.protocol;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
interface IIntegrityServiceCallback {
|
||||||
|
oneway void onResult(in Bundle result) = 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package android.ext;
|
||||||
|
/** @hide */
|
||||||
|
// Int values that are assigned to packages in this interface can be retrieved at runtime from
|
||||||
|
// ApplicationInfo.ext().getPackageId() or from AndroidPackage.ext().getPackageId() (in system_server).
|
||||||
|
//
|
||||||
|
// PackageIds are assigned to parsed APKs only after they are verified, either by a certificate check
|
||||||
|
// or by a check that the APK is stored on an immutable OS partition.
|
||||||
|
public interface PackageId {
|
||||||
|
String PLAY_STORE_NAME = "com.android.vending";
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package android.os;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.io.FileDescriptor;
|
||||||
|
|
||||||
|
/** @hide */
|
||||||
|
public class BinderWrapper implements IBinder {
|
||||||
|
protected final IBinder base;
|
||||||
|
|
||||||
|
public BinderWrapper(IBinder base) {
|
||||||
|
this.base = base;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean transact(int code, @NonNull Parcel data, @Nullable Parcel reply, int flags) throws RemoteException {
|
||||||
|
return base.transact(code, data, reply, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public IInterface queryLocalInterface(@NonNull String descriptor) {
|
||||||
|
return base.queryLocalInterface(descriptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public String getInterfaceDescriptor() throws RemoteException {
|
||||||
|
return base.getInterfaceDescriptor();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean pingBinder() {
|
||||||
|
return base.pingBinder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isBinderAlive() {
|
||||||
|
return base.isBinderAlive();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dump(@NonNull FileDescriptor fd, @Nullable String[] args) throws RemoteException {
|
||||||
|
base.dump(fd, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dumpAsync(@NonNull FileDescriptor fd, @Nullable String[] args) throws RemoteException {
|
||||||
|
base.dumpAsync(fd, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void linkToDeath(@NonNull DeathRecipient recipient, int flags) throws RemoteException {
|
||||||
|
base.linkToDeath(recipient, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean unlinkToDeath(@NonNull DeathRecipient recipient, int flags) {
|
||||||
|
return base.unlinkToDeath(recipient, flags);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package app.grapheneos.gmscompat.lib.playintegrity;
|
||||||
|
|
||||||
|
import android.os.Binder;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.os.RemoteException;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.android.internal.os.FakeBackgroundHandler;
|
||||||
|
import com.google.android.play.core.integrity.protocol.IIntegrityService;
|
||||||
|
import com.google.android.play.core.integrity.protocol.IIntegrityServiceCallback;
|
||||||
|
|
||||||
|
class ClassicPlayIntegrityServiceWrapper extends PlayIntegrityServiceWrapper {
|
||||||
|
|
||||||
|
ClassicPlayIntegrityServiceWrapper(IBinder base) {
|
||||||
|
super(base);
|
||||||
|
requestIntegrityTokenTxnCode = 2; // IIntegrityService.Stub.TRANSACTION_requestIntegrityToken
|
||||||
|
}
|
||||||
|
|
||||||
|
static class TokenRequestStub extends IIntegrityService.Stub {
|
||||||
|
public void requestIntegrityToken(Bundle request, IIntegrityServiceCallback callback) {
|
||||||
|
Runnable r = () -> {
|
||||||
|
var result = new Bundle();
|
||||||
|
// https://developer.android.com/google/play/integrity/reference/com/google/android/play/core/integrity/model/IntegrityErrorCode.html#API_NOT_AVAILABLE
|
||||||
|
final int API_NOT_AVAILABLE = -1;
|
||||||
|
result.putInt("error", API_NOT_AVAILABLE);
|
||||||
|
try {
|
||||||
|
callback.onResult(result);
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
Log.e("IIntegrityService.Stub", "", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
FakeBackgroundHandler.getHandler().postDelayed(r, getTokenRequestResultDelay());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Binder createTokenRequestStub() {
|
||||||
|
return new TokenRequestStub();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package app.grapheneos.gmscompat.lib.playintegrity;
|
||||||
|
|
||||||
|
import android.os.Binder;
|
||||||
|
import android.os.BinderWrapper;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.RemoteException;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
abstract class PlayIntegrityServiceWrapper extends BinderWrapper {
|
||||||
|
final String TAG;
|
||||||
|
protected int requestIntegrityTokenTxnCode;
|
||||||
|
|
||||||
|
public PlayIntegrityServiceWrapper(IBinder base) {
|
||||||
|
super(base);
|
||||||
|
TAG = getClass().getSimpleName();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract Binder createTokenRequestStub();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean transact(int code, Parcel data, @Nullable Parcel reply, int flags) throws RemoteException {
|
||||||
|
if (code == requestIntegrityTokenTxnCode) {
|
||||||
|
if (maybeStubOutIntegrityTokenRequest(code, data, reply, flags)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.transact(code, data, reply, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean maybeStubOutIntegrityTokenRequest(int code, Parcel data, @Nullable Parcel reply, int flags) {
|
||||||
|
Log.d(TAG, "integrity token request detected");
|
||||||
|
|
||||||
|
try {
|
||||||
|
createTokenRequestStub().transact(code, data, reply, flags);
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
// this is a local call
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static long getTokenRequestResultDelay() {
|
||||||
|
return 500L;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package app.grapheneos.gmscompat.lib.playintegrity;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.ServiceConnection;
|
||||||
|
import android.ext.PackageId;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import app.grapheneos.gmscompat.lib.util.ServiceConnectionWrapper;
|
||||||
|
import java.util.function.UnaryOperator;
|
||||||
|
|
||||||
|
public class PlayIntegrityUtils {
|
||||||
|
|
||||||
|
public static @Nullable ServiceConnection maybeReplaceServiceConnection(Intent service, ServiceConnection orig) {
|
||||||
|
if (PackageId.PLAY_STORE_NAME.equals(service.getPackage())) {
|
||||||
|
UnaryOperator<IBinder> binderOverride = null;
|
||||||
|
|
||||||
|
final String CLASSIC_SERVICE =
|
||||||
|
"com.google.android.play.core.integrityservice.BIND_INTEGRITY_SERVICE";
|
||||||
|
final String STANDARD_SERVICE =
|
||||||
|
"com.google.android.play.core.expressintegrityservice.BIND_EXPRESS_INTEGRITY_SERVICE";
|
||||||
|
|
||||||
|
String action = service.getAction();
|
||||||
|
if (STANDARD_SERVICE.equals(action)) {
|
||||||
|
binderOverride = StandardPlayIntegrityServiceWrapper::new;
|
||||||
|
} else if (CLASSIC_SERVICE.equals(action)) {
|
||||||
|
binderOverride = ClassicPlayIntegrityServiceWrapper::new;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (binderOverride != null) {
|
||||||
|
return new ServiceConnectionWrapper(orig, binderOverride);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package app.grapheneos.gmscompat.lib.playintegrity;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.os.Binder;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.os.RemoteException;
|
||||||
|
import android.util.Log;
|
||||||
|
import com.android.internal.os.FakeBackgroundHandler;
|
||||||
|
import com.google.android.play.core.integrity.protocol.IExpressIntegrityService;
|
||||||
|
import com.google.android.play.core.integrity.protocol.IExpressIntegrityServiceCallback;
|
||||||
|
|
||||||
|
@SuppressLint("LongLogTag")
|
||||||
|
class StandardPlayIntegrityServiceWrapper extends PlayIntegrityServiceWrapper {
|
||||||
|
|
||||||
|
StandardPlayIntegrityServiceWrapper(IBinder base) {
|
||||||
|
super(base);
|
||||||
|
requestIntegrityTokenTxnCode = 3; // IExpressIntegrityService.Stub.TRANSACTION_requestIntegrityToken
|
||||||
|
}
|
||||||
|
|
||||||
|
static class TokenRequestStub extends IExpressIntegrityService.Stub {
|
||||||
|
public void requestIntegrityToken(Bundle request, IExpressIntegrityServiceCallback callback) {
|
||||||
|
Runnable r = () -> {
|
||||||
|
var result = new Bundle();
|
||||||
|
// https://developer.android.com/google/play/integrity/reference/com/google/android/play/core/integrity/model/StandardIntegrityErrorCode.html#API_NOT_AVAILABLE
|
||||||
|
final int API_NOT_AVAILABLE = -1;
|
||||||
|
result.putInt("error", API_NOT_AVAILABLE);
|
||||||
|
try {
|
||||||
|
callback.onRequestExpressIntegrityTokenResult(result);
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
Log.e("IExpressIntegrityService.Stub", "", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
FakeBackgroundHandler.getHandler().postDelayed(r, getTokenRequestResultDelay());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Binder createTokenRequestStub() {
|
||||||
|
return new TokenRequestStub();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package app.grapheneos.gmscompat.lib.util;
|
||||||
|
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.ServiceConnection;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.IBinder;
|
||||||
|
|
||||||
|
import java.util.function.UnaryOperator;
|
||||||
|
|
||||||
|
public class ServiceConnectionWrapper implements ServiceConnection {
|
||||||
|
private final ServiceConnection base;
|
||||||
|
private final UnaryOperator<IBinder> binderOverride;
|
||||||
|
|
||||||
|
public ServiceConnectionWrapper(ServiceConnection base, UnaryOperator<IBinder> binderOverride) {
|
||||||
|
this.base = base;
|
||||||
|
this.binderOverride = binderOverride;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
IBinder override = binderOverride.apply(service);
|
||||||
|
if (override != null) {
|
||||||
|
service = override;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
base.onServiceConnected(name, service);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServiceDisconnected(ComponentName name) {
|
||||||
|
base.onServiceDisconnected(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindingDied(ComponentName name) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
base.onBindingDied(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNullBinding(ComponentName name) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
base.onNullBinding(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package app.revanced.extension.playintegrity;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.ServiceConnection;
|
||||||
|
import app.grapheneos.gmscompat.lib.playintegrity.PlayIntegrityUtils;
|
||||||
|
|
||||||
|
public class DisablePlayIntegrityPatch {
|
||||||
|
public static boolean bindService(Context context, Intent service, ServiceConnection conn, int flags) {
|
||||||
|
ServiceConnection override = PlayIntegrityUtils.maybeReplaceServiceConnection(service, conn);
|
||||||
|
if (override != null) {
|
||||||
|
conn = override;
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.bindService(service, conn, flags);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.android.internal.os;
|
||||||
|
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
|
||||||
|
public class FakeBackgroundHandler {
|
||||||
|
|
||||||
|
public static Handler getHandler() {
|
||||||
|
return new Handler(Looper.getMainLooper());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package app.revanced.extension.instagram.misc.links;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public final class OpenLinksExternallyPatch {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static boolean openExternally(String url) {
|
||||||
|
try {
|
||||||
|
// The "url" parameter to this function will be of the form.
|
||||||
|
// https://l.instagram.com/?u=<actual url>&e=<tracking id>
|
||||||
|
String actualUrl = Uri.parse(url).getQueryParameter("u");
|
||||||
|
if (actualUrl != null) {
|
||||||
|
Utils.openLink(actualUrl);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "openExternally failure", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package app.revanced.extension.instagram.misc.privacy;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.privacy.LinkSanitizer;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public final class SanitizeSharingLinksPatch {
|
||||||
|
private static final LinkSanitizer sanitizer = new LinkSanitizer("igsh");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static String sanitizeSharingLink(String url) {
|
||||||
|
return sanitizer.sanitizeUrlString(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package app.revanced.extension.instagram.misc.share.domain;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public final class ChangeLinkSharingDomainPatch {
|
||||||
|
|
||||||
|
private static String getCustomShareDomain() {
|
||||||
|
// Method is modified during patching.
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static String setCustomShareDomain(String url) {
|
||||||
|
try {
|
||||||
|
Uri uri = Uri.parse(url);
|
||||||
|
Uri.Builder builder = uri
|
||||||
|
.buildUpon()
|
||||||
|
.authority(getCustomShareDomain())
|
||||||
|
.clearQuery();
|
||||||
|
|
||||||
|
String patchedUrl = builder.build().toString();
|
||||||
|
Logger.printInfo(() -> "Domain change from : " + url + " to: " + patchedUrl);
|
||||||
|
return patchedUrl;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "setCustomShareDomain failure with " + url, ex);
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package app.revanced.extension.instagram.misc.share.privacy;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.privacy.LinkSanitizer;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public final class SanitizeSharingLinksPatch {
|
||||||
|
private static final LinkSanitizer sanitizer = new LinkSanitizer("igsh");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static String sanitizeSharingLink(String url) {
|
||||||
|
return sanitizer.sanitizeUrlString(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package app.revanced.extension.music.patches;
|
||||||
|
|
||||||
|
import app.revanced.extension.music.settings.Settings;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class ChangeMiniplayerColorPatch {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point
|
||||||
|
*/
|
||||||
|
public static boolean changeMiniplayerColor() {
|
||||||
|
return Settings.CHANGE_MINIPLAYER_COLOR.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package app.revanced.extension.music.patches;
|
||||||
|
|
||||||
|
import app.revanced.extension.music.settings.Settings;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class ForceOriginalAudioPatch {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static void setEnabled() {
|
||||||
|
app.revanced.extension.shared.patches.ForceOriginalAudioPatch.setEnabled(
|
||||||
|
Settings.FORCE_ORIGINAL_AUDIO.get(),
|
||||||
|
Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package app.revanced.extension.music.patches;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.Utils.hideViewBy0dpUnderCondition;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import app.revanced.extension.music.settings.Settings;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class HideButtonsPatch {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point
|
||||||
|
*/
|
||||||
|
public static int hideCastButton(int original) {
|
||||||
|
return Settings.HIDE_CAST_BUTTON.get() ? View.GONE : original;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point
|
||||||
|
*/
|
||||||
|
public static void hideCastButton(View view) {
|
||||||
|
hideViewBy0dpUnderCondition(Settings.HIDE_CAST_BUTTON, view);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point
|
||||||
|
*/
|
||||||
|
public static boolean hideHistoryButton(boolean original) {
|
||||||
|
return original && !Settings.HIDE_HISTORY_BUTTON.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point
|
||||||
|
*/
|
||||||
|
public static void hideNotificationButton(View view) {
|
||||||
|
if (view.getParent() instanceof ViewGroup viewGroup) {
|
||||||
|
hideViewBy0dpUnderCondition(Settings.HIDE_NOTIFICATION_BUTTON, viewGroup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point
|
||||||
|
*/
|
||||||
|
public static void hideSearchButton(View view) {
|
||||||
|
hideViewBy0dpUnderCondition(Settings.HIDE_SEARCH_BUTTON, view);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package app.revanced.extension.music.patches;
|
|
||||||
|
|
||||||
import static app.revanced.extension.shared.Utils.hideViewBy0dpUnderCondition;
|
|
||||||
|
|
||||||
import android.view.View;
|
|
||||||
import app.revanced.extension.music.settings.Settings;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public class HideCastButtonPatch {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point
|
|
||||||
*/
|
|
||||||
public static int hideCastButton(int original) {
|
|
||||||
return Settings.HIDE_CAST_BUTTON.get() ? View.GONE : original;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point
|
|
||||||
*/
|
|
||||||
public static void hideCastButton(View view) {
|
|
||||||
hideViewBy0dpUnderCondition(Settings.HIDE_CAST_BUTTON.get(), view);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
package app.revanced.extension.music.patches;
|
package app.revanced.extension.music.patches;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.Utils.hideViewBy0dpUnderCondition;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
import app.revanced.extension.music.settings.Settings;
|
import app.revanced.extension.music.settings.Settings;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
@@ -8,7 +12,7 @@ public class HideCategoryBarPatch {
|
|||||||
/**
|
/**
|
||||||
* Injection point
|
* Injection point
|
||||||
*/
|
*/
|
||||||
public static boolean hideCategoryBar() {
|
public static void hideCategoryBar(View view) {
|
||||||
return Settings.HIDE_CATEGORY_BAR.get();
|
hideViewBy0dpUnderCondition(Settings.HIDE_CATEGORY_BAR, view);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package app.revanced.extension.music.patches.spoof;
|
package app.revanced.extension.music.patches.spoof;
|
||||||
|
|
||||||
import static app.revanced.extension.music.settings.Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE;
|
import static app.revanced.extension.music.settings.Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE;
|
||||||
|
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_NO_SDK;
|
||||||
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_43_32;
|
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_43_32;
|
||||||
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_61_48;
|
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_61_48;
|
||||||
import static app.revanced.extension.shared.spoof.ClientType.VISIONOS;
|
import static app.revanced.extension.shared.spoof.ClientType.VISIONOS;
|
||||||
@@ -18,8 +19,9 @@ public class SpoofVideoStreamsPatch {
|
|||||||
public static void setClientOrderToUse() {
|
public static void setClientOrderToUse() {
|
||||||
List<ClientType> availableClients = List.of(
|
List<ClientType> availableClients = List.of(
|
||||||
ANDROID_VR_1_43_32,
|
ANDROID_VR_1_43_32,
|
||||||
ANDROID_VR_1_61_48,
|
ANDROID_NO_SDK,
|
||||||
VISIONOS
|
VISIONOS,
|
||||||
|
ANDROID_VR_1_61_48
|
||||||
);
|
);
|
||||||
|
|
||||||
app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch.setClientsToUse(
|
app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch.setClientsToUse(
|
||||||
|
|||||||
@@ -16,8 +16,11 @@ public class Settings extends BaseSettings {
|
|||||||
public static final BooleanSetting HIDE_GET_PREMIUM_LABEL = new BooleanSetting("revanced_music_hide_get_premium_label", TRUE, true);
|
public static final BooleanSetting HIDE_GET_PREMIUM_LABEL = new BooleanSetting("revanced_music_hide_get_premium_label", TRUE, true);
|
||||||
|
|
||||||
// General
|
// General
|
||||||
public static final BooleanSetting HIDE_CAST_BUTTON = new BooleanSetting("revanced_music_hide_cast_button", TRUE, false);
|
public static final BooleanSetting HIDE_CAST_BUTTON = new BooleanSetting("revanced_music_hide_cast_button", TRUE, true);
|
||||||
public static final BooleanSetting HIDE_CATEGORY_BAR = new BooleanSetting("revanced_music_hide_category_bar", FALSE, true);
|
public static final BooleanSetting HIDE_CATEGORY_BAR = new BooleanSetting("revanced_music_hide_category_bar", FALSE, true);
|
||||||
|
public static final BooleanSetting HIDE_HISTORY_BUTTON = new BooleanSetting("revanced_music_hide_history_button", FALSE, true);
|
||||||
|
public static final BooleanSetting HIDE_SEARCH_BUTTON = new BooleanSetting("revanced_music_hide_search_button", FALSE, true);
|
||||||
|
public static final BooleanSetting HIDE_NOTIFICATION_BUTTON = new BooleanSetting("revanced_music_hide_notification_button", FALSE, true);
|
||||||
public static final BooleanSetting HIDE_NAVIGATION_BAR_HOME_BUTTON = new BooleanSetting("revanced_music_hide_navigation_bar_home_button", FALSE, true);
|
public static final BooleanSetting HIDE_NAVIGATION_BAR_HOME_BUTTON = new BooleanSetting("revanced_music_hide_navigation_bar_home_button", FALSE, true);
|
||||||
public static final BooleanSetting HIDE_NAVIGATION_BAR_SAMPLES_BUTTON = new BooleanSetting("revanced_music_hide_navigation_bar_samples_button", FALSE, true);
|
public static final BooleanSetting HIDE_NAVIGATION_BAR_SAMPLES_BUTTON = new BooleanSetting("revanced_music_hide_navigation_bar_samples_button", FALSE, true);
|
||||||
public static final BooleanSetting HIDE_NAVIGATION_BAR_EXPLORE_BUTTON = new BooleanSetting("revanced_music_hide_navigation_bar_explore_button", FALSE, true);
|
public static final BooleanSetting HIDE_NAVIGATION_BAR_EXPLORE_BUTTON = new BooleanSetting("revanced_music_hide_navigation_bar_explore_button", FALSE, true);
|
||||||
@@ -27,9 +30,12 @@ public class Settings extends BaseSettings {
|
|||||||
public static final BooleanSetting HIDE_NAVIGATION_BAR_LABEL = new BooleanSetting("revanced_music_hide_navigation_bar_labels", FALSE, true);
|
public static final BooleanSetting HIDE_NAVIGATION_BAR_LABEL = new BooleanSetting("revanced_music_hide_navigation_bar_labels", FALSE, true);
|
||||||
|
|
||||||
// Player
|
// Player
|
||||||
|
public static final BooleanSetting CHANGE_MINIPLAYER_COLOR = new BooleanSetting("revanced_music_change_miniplayer_color", FALSE, true);
|
||||||
public static final BooleanSetting PERMANENT_REPEAT = new BooleanSetting("revanced_music_play_permanent_repeat", FALSE, true);
|
public static final BooleanSetting PERMANENT_REPEAT = new BooleanSetting("revanced_music_play_permanent_repeat", FALSE, true);
|
||||||
|
|
||||||
// Miscellaneous
|
// Miscellaneous
|
||||||
public static final EnumSetting<ClientType> SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_video_streams_client_type",
|
public static final EnumSetting<ClientType> SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_video_streams_client_type",
|
||||||
ClientType.ANDROID_VR_1_43_32, true, parent(SPOOF_VIDEO_STREAMS));
|
ClientType.ANDROID_VR_1_43_32, true, parent(SPOOF_VIDEO_STREAMS));
|
||||||
|
|
||||||
|
public static final BooleanSetting FORCE_ORIGINAL_AUDIO = new BooleanSetting("revanced_force_original_audio", TRUE, true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import android.preference.PreferenceScreen;
|
|||||||
import android.widget.Toolbar;
|
import android.widget.Toolbar;
|
||||||
|
|
||||||
import app.revanced.extension.music.settings.MusicActivityHook;
|
import app.revanced.extension.music.settings.MusicActivityHook;
|
||||||
|
import app.revanced.extension.shared.GmsCoreSupport;
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
import app.revanced.extension.shared.settings.preference.ToolbarPreferenceFragment;
|
import app.revanced.extension.shared.settings.preference.ToolbarPreferenceFragment;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,6 +32,17 @@ public class MusicPreferenceFragment extends ToolbarPreferenceFragment {
|
|||||||
preferenceScreen = getPreferenceScreen();
|
preferenceScreen = getPreferenceScreen();
|
||||||
Utils.sortPreferenceGroups(preferenceScreen);
|
Utils.sortPreferenceGroups(preferenceScreen);
|
||||||
setPreferenceScreenToolbar(preferenceScreen);
|
setPreferenceScreenToolbar(preferenceScreen);
|
||||||
|
|
||||||
|
// Clunky work around until preferences are custom classes that manage themselves.
|
||||||
|
// Custom branding only works with non-root install. But the preferences must be
|
||||||
|
// added during patched because of difficulties detecting during patching if it's
|
||||||
|
// a root install. So instead the non-functional preferences are removed during
|
||||||
|
// runtime if the app is mount (root) installation.
|
||||||
|
if (GmsCoreSupport.isPackageNameOriginal()) {
|
||||||
|
removePreferences(
|
||||||
|
BaseSettings.CUSTOM_BRANDING_ICON.key,
|
||||||
|
BaseSettings.CUSTOM_BRANDING_NAME.key);
|
||||||
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "initialize failure", ex);
|
Logger.printException(() -> "initialize failure", ex);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import java.util.Arrays;
|
|||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.shared.ui.Dim;
|
||||||
|
|
||||||
import com.amazon.video.sdk.player.Player;
|
import com.amazon.video.sdk.player.Player;
|
||||||
|
|
||||||
@@ -64,9 +65,8 @@ public class PlaybackSpeedPatch {
|
|||||||
SpeedIconDrawable speedIcon = new SpeedIconDrawable();
|
SpeedIconDrawable speedIcon = new SpeedIconDrawable();
|
||||||
speedButton.setImageDrawable(speedIcon);
|
speedButton.setImageDrawable(speedIcon);
|
||||||
|
|
||||||
int buttonSize = Utils.dipToPixels(48);
|
speedButton.setMinimumWidth(Dim.dp48);
|
||||||
speedButton.setMinimumWidth(buttonSize);
|
speedButton.setMinimumHeight(Dim.dp48);
|
||||||
speedButton.setMinimumHeight(buttonSize);
|
|
||||||
|
|
||||||
return speedButton;
|
return speedButton;
|
||||||
}
|
}
|
||||||
@@ -197,11 +197,11 @@ class SpeedIconDrawable extends Drawable {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getIntrinsicWidth() {
|
public int getIntrinsicWidth() {
|
||||||
return Utils.dipToPixels(32);
|
return Dim.dp32;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getIntrinsicHeight() {
|
public int getIntrinsicHeight() {
|
||||||
return Utils.dipToPixels(32);
|
return Dim.dp32;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4
extensions/samsung/radio/build.gradle.kts
Normal file
4
extensions/samsung/radio/build.gradle.kts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
dependencies {
|
||||||
|
compileOnly(project(":extensions:shared:library"))
|
||||||
|
compileOnly(project(":extensions:samsung:radio:stub"))
|
||||||
|
}
|
||||||
1
extensions/samsung/radio/src/main/AndroidManifest.xml
Normal file
1
extensions/samsung/radio/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest/>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package app.revanced.extension.samsung.radio.misc.fix.crash;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public final class FixCrashPatch {
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
* <p>
|
||||||
|
* Add the required permissions to the request list to avoid crashes on API 34+.
|
||||||
|
**/
|
||||||
|
public static final String[] fixPermissionRequestList(String[] perms) {
|
||||||
|
List<String> permsList = new ArrayList<>(Arrays.asList(perms));
|
||||||
|
if (permsList.contains("android.permission.POST_NOTIFICATIONS")) {
|
||||||
|
permsList.addAll(Arrays.asList("android.permission.RECORD_AUDIO", "android.permission.READ_PHONE_STATE", "android.permission.FOREGROUND_SERVICE_MICROPHONE"));
|
||||||
|
}
|
||||||
|
if (permsList.contains("android.permission.RECORD_AUDIO")) {
|
||||||
|
permsList.add("android.permission.FOREGROUND_SERVICE_MICROPHONE");
|
||||||
|
}
|
||||||
|
return permsList.toArray(new String[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package app.revanced.extension.samsung.radio.restrictions.device;
|
||||||
|
|
||||||
|
import android.os.SemSystemProperties;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public final class BypassDeviceChecksPatch {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
* <p>
|
||||||
|
* Check if the device has the required hardware
|
||||||
|
**/
|
||||||
|
public static final boolean checkIfDeviceIsIncompatible(String[] deviceList) {
|
||||||
|
String currentDevice = SemSystemProperties.getSalesCode();
|
||||||
|
return Arrays.asList(deviceList).contains(currentDevice);
|
||||||
|
}
|
||||||
|
}
|
||||||
17
extensions/samsung/radio/stub/build.gradle.kts
Normal file
17
extensions/samsung/radio/stub/build.gradle.kts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.library)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "app.revanced.extension"
|
||||||
|
compileSdk = 34
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 24
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<manifest/>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package android.os;
|
||||||
|
|
||||||
|
public class SemSystemProperties {
|
||||||
|
public static String getSalesCode() {
|
||||||
|
throw new UnsupportedOperationException("Stub");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,9 +31,6 @@ import app.revanced.extension.shared.ui.CustomDialog;
|
|||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public class GmsCoreSupport {
|
public class GmsCoreSupport {
|
||||||
private static final String PACKAGE_NAME_YOUTUBE = "com.google.android.youtube";
|
|
||||||
private static final String PACKAGE_NAME_YOUTUBE_MUSIC = "com.google.android.apps.youtube.music";
|
|
||||||
|
|
||||||
private static final String GMS_CORE_PACKAGE_NAME
|
private static final String GMS_CORE_PACKAGE_NAME
|
||||||
= getGmsCoreVendorGroupId() + ".android.gms";
|
= getGmsCoreVendorGroupId() + ".android.gms";
|
||||||
private static final Uri GMS_CORE_PROVIDER
|
private static final Uri GMS_CORE_PROVIDER
|
||||||
@@ -53,6 +50,20 @@ public class GmsCoreSupport {
|
|||||||
@Nullable
|
@Nullable
|
||||||
private static volatile Boolean DONT_KILL_MY_APP_MANUFACTURER_SUPPORTED;
|
private static volatile Boolean DONT_KILL_MY_APP_MANUFACTURER_SUPPORTED;
|
||||||
|
|
||||||
|
private static String getOriginalPackageName() {
|
||||||
|
return null; // Modified during patching.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return If the current package name is the same as the original unpatched app.
|
||||||
|
* If `GmsCore support` was not included during patching, this returns true;
|
||||||
|
*/
|
||||||
|
public static boolean isPackageNameOriginal() {
|
||||||
|
String originalPackageName = getOriginalPackageName();
|
||||||
|
return originalPackageName == null
|
||||||
|
|| originalPackageName.equals(Utils.getContext().getPackageName());
|
||||||
|
}
|
||||||
|
|
||||||
private static void open(String queryOrLink) {
|
private static void open(String queryOrLink) {
|
||||||
Logger.printInfo(() -> "Opening link: " + queryOrLink);
|
Logger.printInfo(() -> "Opening link: " + queryOrLink);
|
||||||
|
|
||||||
@@ -113,11 +124,10 @@ public class GmsCoreSupport {
|
|||||||
// Verify the user has not included GmsCore for a root installation.
|
// Verify the user has not included GmsCore for a root installation.
|
||||||
// GmsCore Support changes the package name, but with a mounted installation
|
// GmsCore Support changes the package name, but with a mounted installation
|
||||||
// all manifest changes are ignored and the original package name is used.
|
// all manifest changes are ignored and the original package name is used.
|
||||||
String packageName = context.getPackageName();
|
if (isPackageNameOriginal()) {
|
||||||
if (packageName.equals(PACKAGE_NAME_YOUTUBE) || packageName.equals(PACKAGE_NAME_YOUTUBE_MUSIC)) {
|
|
||||||
Logger.printInfo(() -> "App is mounted with root, but GmsCore patch was included");
|
Logger.printInfo(() -> "App is mounted with root, but GmsCore patch was included");
|
||||||
// Cannot use localize text here, since the app will load
|
// Cannot use localize text here, since the app will load resources
|
||||||
// resources from the unpatched app and all patch strings are missing.
|
// from the unpatched app and all patch strings are missing.
|
||||||
Utils.showToastLong("The 'GmsCore support' patch breaks mount installations");
|
Utils.showToastLong("The 'GmsCore support' patch breaks mount installations");
|
||||||
|
|
||||||
// Do not exit. If the app exits before launch completes (and without
|
// Do not exit. If the app exits before launch completes (and without
|
||||||
@@ -250,8 +260,7 @@ public class GmsCoreSupport {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modified by a patch. Do not touch.
|
|
||||||
private static String getGmsCoreVendorGroupId() {
|
private static String getGmsCoreVendorGroupId() {
|
||||||
return "app.revanced";
|
return "app.revanced"; // Modified during patching.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import android.content.res.Configuration;
|
|||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.net.ConnectivityManager;
|
import android.net.ConnectivityManager;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
@@ -22,9 +23,7 @@ import android.os.Looper;
|
|||||||
import android.preference.Preference;
|
import android.preference.Preference;
|
||||||
import android.preference.PreferenceGroup;
|
import android.preference.PreferenceGroup;
|
||||||
import android.preference.PreferenceScreen;
|
import android.preference.PreferenceScreen;
|
||||||
import android.util.DisplayMetrics;
|
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import android.util.TypedValue;
|
|
||||||
import android.view.Gravity;
|
import android.view.Gravity;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
@@ -33,17 +32,15 @@ import android.view.Window;
|
|||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
import android.view.animation.Animation;
|
import android.view.animation.Animation;
|
||||||
import android.view.animation.AnimationUtils;
|
import android.view.animation.AnimationUtils;
|
||||||
import android.widget.FrameLayout;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.RelativeLayout;
|
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
import android.widget.Toolbar;
|
|
||||||
|
|
||||||
import androidx.annotation.ColorInt;
|
import androidx.annotation.ColorInt;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import java.text.Bidi;
|
import java.text.Bidi;
|
||||||
|
import java.text.Collator;
|
||||||
|
import java.text.Normalizer;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -60,6 +57,7 @@ import app.revanced.extension.shared.settings.AppLanguage;
|
|||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||||
import app.revanced.extension.shared.settings.preference.ReVancedAboutPreference;
|
import app.revanced.extension.shared.settings.preference.ReVancedAboutPreference;
|
||||||
|
import app.revanced.extension.shared.ui.Dim;
|
||||||
|
|
||||||
@SuppressWarnings("NewApi")
|
@SuppressWarnings("NewApi")
|
||||||
public class Utils {
|
public class Utils {
|
||||||
@@ -78,6 +76,15 @@ public class Utils {
|
|||||||
@Nullable
|
@Nullable
|
||||||
private static Boolean isDarkModeEnabled;
|
private static Boolean isDarkModeEnabled;
|
||||||
|
|
||||||
|
// Cached Collator instance with its locale.
|
||||||
|
@Nullable
|
||||||
|
private static Locale cachedCollatorLocale;
|
||||||
|
@Nullable
|
||||||
|
private static Collator cachedCollator;
|
||||||
|
|
||||||
|
private static final Pattern PUNCTUATION_PATTERN = Pattern.compile("\\p{P}+");
|
||||||
|
private static final Pattern DIACRITICS_PATTERN = Pattern.compile("\\p{M}");
|
||||||
|
|
||||||
private Utils() {
|
private Utils() {
|
||||||
} // utility class
|
} // utility class
|
||||||
|
|
||||||
@@ -304,6 +311,10 @@ public class Utils {
|
|||||||
return resourceId;
|
return resourceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String getResourceString(int id) throws Resources.NotFoundException {
|
||||||
|
return getContext().getResources().getString(id);
|
||||||
|
}
|
||||||
|
|
||||||
public static int getResourceInteger(String resourceIdentifierName) throws Resources.NotFoundException {
|
public static int getResourceInteger(String resourceIdentifierName) throws Resources.NotFoundException {
|
||||||
return getContext().getResources().getInteger(getResourceIdentifierOrThrow(resourceIdentifierName, "integer"));
|
return getContext().getResources().getInteger(getResourceIdentifierOrThrow(resourceIdentifierName, "integer"));
|
||||||
}
|
}
|
||||||
@@ -696,6 +707,18 @@ public class Utils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void openLink(String url) {
|
||||||
|
try {
|
||||||
|
Intent intent = new Intent("android.intent.action.VIEW", Uri.parse(url));
|
||||||
|
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
|
||||||
|
Logger.printInfo(() -> "Opening link with external browser: " + intent);
|
||||||
|
getContext().startActivity(intent);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "openLink failure", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public enum NetworkType {
|
public enum NetworkType {
|
||||||
NONE,
|
NONE,
|
||||||
MOBILE,
|
MOBILE,
|
||||||
@@ -736,31 +759,25 @@ public class Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hide a view by setting its layout params to 0x0
|
* Hides a view by setting its layout width and height to 0dp.
|
||||||
* @param view The view to hide.
|
* Handles null layout params safely.
|
||||||
|
*
|
||||||
|
* @param view The view to hide. If null, does nothing.
|
||||||
*/
|
*/
|
||||||
public static void hideViewByLayoutParams(View view) {
|
public static void hideViewByLayoutParams(@Nullable View view) {
|
||||||
if (view instanceof LinearLayout) {
|
if (view == null) return;
|
||||||
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(0, 0);
|
|
||||||
view.setLayoutParams(layoutParams);
|
ViewGroup.LayoutParams params = view.getLayoutParams();
|
||||||
} else if (view instanceof FrameLayout) {
|
|
||||||
FrameLayout.LayoutParams layoutParams2 = new FrameLayout.LayoutParams(0, 0);
|
if (params == null) {
|
||||||
view.setLayoutParams(layoutParams2);
|
// Create generic 0x0 layout params accepted by all ViewGroups.
|
||||||
} else if (view instanceof RelativeLayout) {
|
params = new ViewGroup.LayoutParams(0, 0);
|
||||||
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 {
|
} else {
|
||||||
ViewGroup.LayoutParams params = view.getLayoutParams();
|
|
||||||
params.width = 0;
|
params.width = 0;
|
||||||
params.height = 0;
|
params.height = 0;
|
||||||
view.setLayoutParams(params);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
view.setLayoutParams(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -777,13 +794,10 @@ public class Utils {
|
|||||||
public static void setDialogWindowParameters(Window window, int gravity, int yOffsetDip, int widthPercentage, boolean dimAmount) {
|
public static void setDialogWindowParameters(Window window, int gravity, int yOffsetDip, int widthPercentage, boolean dimAmount) {
|
||||||
WindowManager.LayoutParams params = window.getAttributes();
|
WindowManager.LayoutParams params = window.getAttributes();
|
||||||
|
|
||||||
DisplayMetrics displayMetrics = Resources.getSystem().getDisplayMetrics();
|
params.width = Dim.pctPortraitWidth(widthPercentage);
|
||||||
int portraitWidth = Math.min(displayMetrics.widthPixels, displayMetrics.heightPixels);
|
|
||||||
|
|
||||||
params.width = (int) (portraitWidth * (widthPercentage / 100.0f)); // Set width based on parameters.
|
|
||||||
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
|
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
|
||||||
params.gravity = gravity;
|
params.gravity = gravity;
|
||||||
params.y = yOffsetDip > 0 ? dipToPixels(yOffsetDip) : 0;
|
params.y = yOffsetDip > 0 ? Dim.dp(yOffsetDip) : 0;
|
||||||
if (dimAmount) {
|
if (dimAmount) {
|
||||||
params.dimAmount = 0f;
|
params.dimAmount = 0f;
|
||||||
}
|
}
|
||||||
@@ -792,18 +806,6 @@ public class Utils {
|
|||||||
window.setBackgroundDrawable(null); // Remove default dialog background
|
window.setBackgroundDrawable(null); // Remove default dialog background
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an array of corner radii for a rounded rectangle shape.
|
|
||||||
*
|
|
||||||
* @param dp Radius in density-independent pixels (dip) to apply to all corners.
|
|
||||||
* @return An array of eight float values representing the corner radii
|
|
||||||
* (top-left, top-right, bottom-right, bottom-left).
|
|
||||||
*/
|
|
||||||
public static float[] createCornerRadii(float dp) {
|
|
||||||
final float radius = dipToPixels(dp);
|
|
||||||
return new float[]{radius, radius, radius, radius, radius, radius, radius, radius};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the theme light color used by the app.
|
* Sets the theme light color used by the app.
|
||||||
*/
|
*/
|
||||||
@@ -963,30 +965,60 @@ public class Utils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final Pattern punctuationPattern = Pattern.compile("\\p{P}+");
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Strips all punctuation and converts to lower case. A null parameter returns an empty string.
|
* Removes punctuation and converts text to lowercase. Returns an empty string if input is null.
|
||||||
*/
|
*/
|
||||||
public static String removePunctuationToLowercase(@Nullable CharSequence original) {
|
public static String removePunctuationToLowercase(@Nullable CharSequence original) {
|
||||||
if (original == null) return "";
|
if (original == null) return "";
|
||||||
return punctuationPattern.matcher(original).replaceAll("")
|
return PUNCTUATION_PATTERN.matcher(original).replaceAll("")
|
||||||
.toLowerCase(BaseSettings.REVANCED_LANGUAGE.get().getLocale());
|
.toLowerCase(BaseSettings.REVANCED_LANGUAGE.get().getLocale());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sort a PreferenceGroup and all it's sub groups by title or key.
|
* Normalizes text for search: applies NFD, removes diacritics, and lowercases (locale-neutral).
|
||||||
|
* Returns an empty string if input is null.
|
||||||
|
*/
|
||||||
|
public static String normalizeTextToLowercase(@Nullable CharSequence original) {
|
||||||
|
if (original == null) return "";
|
||||||
|
return DIACRITICS_PATTERN.matcher(Normalizer.normalize(original, Normalizer.Form.NFD))
|
||||||
|
.replaceAll("").toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a cached Collator for the current locale, or creates a new one if locale changed.
|
||||||
|
*/
|
||||||
|
private static Collator getCollator() {
|
||||||
|
Locale currentLocale = BaseSettings.REVANCED_LANGUAGE.get().getLocale();
|
||||||
|
|
||||||
|
if (cachedCollator == null || !currentLocale.equals(cachedCollatorLocale)) {
|
||||||
|
cachedCollatorLocale = currentLocale;
|
||||||
|
cachedCollator = Collator.getInstance(currentLocale);
|
||||||
|
cachedCollator.setStrength(Collator.SECONDARY); // Case-insensitive, diacritic-insensitive.
|
||||||
|
}
|
||||||
|
|
||||||
|
return cachedCollator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sorts a {@link PreferenceGroup} and all nested subgroups by title or key.
|
||||||
* <p>
|
* <p>
|
||||||
* Sort order is determined by the preferences key {@link Sort} suffix.
|
* The sort order is controlled by the {@link Sort} suffix present in the preference key.
|
||||||
|
* Preferences without a key or without a {@link Sort} suffix remain in their original order.
|
||||||
* <p>
|
* <p>
|
||||||
* If a preference has no key or no {@link Sort} suffix,
|
* Sorting is performed using {@link Collator} with the current user locale,
|
||||||
* then the preferences are left unsorted.
|
* ensuring correct alphabetical ordering for all supported languages
|
||||||
|
* (e.g., Ukrainian "і", German "ß", French accented characters, etc.).
|
||||||
|
*
|
||||||
|
* @param group the {@link PreferenceGroup} to sort
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
public static void sortPreferenceGroups(PreferenceGroup group) {
|
public static void sortPreferenceGroups(PreferenceGroup group) {
|
||||||
Sort groupSort = Sort.fromKey(group.getKey(), Sort.UNSORTED);
|
Sort groupSort = Sort.fromKey(group.getKey(), Sort.UNSORTED);
|
||||||
List<Pair<String, Preference>> preferences = new ArrayList<>();
|
List<Pair<String, Preference>> preferences = new ArrayList<>();
|
||||||
|
|
||||||
|
// Get cached Collator for locale-aware string comparison.
|
||||||
|
Collator collator = getCollator();
|
||||||
|
|
||||||
for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) {
|
for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) {
|
||||||
Preference preference = group.getPreference(i);
|
Preference preference = group.getPreference(i);
|
||||||
|
|
||||||
@@ -1017,10 +1049,11 @@ public class Utils {
|
|||||||
preferences.add(new Pair<>(sortValue, preference));
|
preferences.add(new Pair<>(sortValue, preference));
|
||||||
}
|
}
|
||||||
|
|
||||||
//noinspection ComparatorCombinators
|
// Sort the list using locale-specific collation rules.
|
||||||
Collections.sort(preferences, (pair1, pair2)
|
Collections.sort(preferences, (pair1, pair2)
|
||||||
-> pair1.first.compareTo(pair2.first));
|
-> collator.compare(pair1.first, pair2.first));
|
||||||
|
|
||||||
|
// Reassign order values to reflect the new sorted sequence
|
||||||
int index = 0;
|
int index = 0;
|
||||||
for (Pair<String, Preference> pair : preferences) {
|
for (Pair<String, Preference> pair : preferences) {
|
||||||
int order = index++;
|
int order = index++;
|
||||||
@@ -1077,42 +1110,6 @@ public class Utils {
|
|||||||
return getResourceColor(colorString);
|
return getResourceColor(colorString);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts dip value to actual device pixels.
|
|
||||||
*
|
|
||||||
* @param dip The density-independent pixels value.
|
|
||||||
* @return The device pixel value.
|
|
||||||
*/
|
|
||||||
public static int dipToPixels(float dip) {
|
|
||||||
return (int) TypedValue.applyDimension(
|
|
||||||
TypedValue.COMPLEX_UNIT_DIP,
|
|
||||||
dip,
|
|
||||||
Resources.getSystem().getDisplayMetrics()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a percentage of the screen height to actual device pixels.
|
|
||||||
*
|
|
||||||
* @param percentage The percentage of the screen height (e.g., 30 for 30%).
|
|
||||||
* @return The device pixel value corresponding to the percentage of screen height.
|
|
||||||
*/
|
|
||||||
public static int percentageHeightToPixels(int percentage) {
|
|
||||||
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
|
|
||||||
return (int) (metrics.heightPixels * (percentage / 100.0f));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a percentage of the screen width to actual device pixels.
|
|
||||||
*
|
|
||||||
* @param percentage The percentage of the screen width (e.g., 30 for 30%).
|
|
||||||
* @return The device pixel value corresponding to the percentage of screen width.
|
|
||||||
*/
|
|
||||||
public static int percentageWidthToPixels(int percentage) {
|
|
||||||
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
|
|
||||||
return (int) (metrics.widthPixels * (percentage / 100.0f));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uses {@link #adjustColorBrightness(int, float)} depending if light or dark mode is active.
|
* Uses {@link #adjustColorBrightness(int, float)} depending if light or dark mode is active.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ public class CheckWatchHistoryDomainNameResolutionPatch {
|
|||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*
|
*
|
||||||
* Checks if s.youtube.com is blacklisted and playback history will fail to work.
|
* Checks if YouTube watch history endpoint cannot be reached.
|
||||||
*/
|
*/
|
||||||
public static void checkDnsResolver(Activity context) {
|
public static void checkDnsResolver(Activity context) {
|
||||||
if (!Utils.isNetworkConnected() || !BaseSettings.CHECK_WATCH_HISTORY_DOMAIN_NAME.get()) return;
|
if (!Utils.isNetworkConnected() || !BaseSettings.CHECK_WATCH_HISTORY_DOMAIN_NAME.get()) return;
|
||||||
@@ -61,34 +61,30 @@ public class CheckWatchHistoryDomainNameResolutionPatch {
|
|||||||
// Prevent this false positive by verify youtube.com resolves.
|
// Prevent this false positive by verify youtube.com resolves.
|
||||||
// If youtube.com does not resolve, then it's not a watch history domain resolving error
|
// If youtube.com does not resolve, then it's not a watch history domain resolving error
|
||||||
// because the entire app will not work since no domains are resolving.
|
// because the entire app will not work since no domains are resolving.
|
||||||
if (!domainResolvesToValidIP("youtube.com")
|
String domainYouTube = "youtube.com";
|
||||||
|
if (!domainResolvesToValidIP(domainYouTube)
|
||||||
|
|| domainResolvesToValidIP(HISTORY_TRACKING_ENDPOINT)
|
||||||
|
// Check multiple times, so a false positive from a flaky connection is almost impossible.
|
||||||
|
|| !domainResolvesToValidIP(domainYouTube)
|
||||||
|| domainResolvesToValidIP(HISTORY_TRACKING_ENDPOINT)) {
|
|| domainResolvesToValidIP(HISTORY_TRACKING_ENDPOINT)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Utils.runOnMainThread(() -> {
|
Utils.runOnMainThread(() -> {
|
||||||
try {
|
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
|
||||||
// Create the custom dialog.
|
context,
|
||||||
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
|
str("revanced_check_watch_history_domain_name_dialog_title"), // Title.
|
||||||
context,
|
Html.fromHtml(str("revanced_check_watch_history_domain_name_dialog_message")), // Message (HTML).
|
||||||
str("revanced_check_watch_history_domain_name_dialog_title"), // Title.
|
null, // No EditText.
|
||||||
Html.fromHtml(str("revanced_check_watch_history_domain_name_dialog_message")), // Message (HTML).
|
null, // OK button text.
|
||||||
null, // No EditText.
|
() -> {}, // OK button action (just dismiss).
|
||||||
null, // OK button text.
|
null, // No cancel button.
|
||||||
() -> {}, // OK button action (just dismiss).
|
str("revanced_check_watch_history_domain_name_dialog_ignore"), // Neutral button text.
|
||||||
() -> {}, // Cancel button action (just dismiss).
|
() -> BaseSettings.CHECK_WATCH_HISTORY_DOMAIN_NAME.save(false), // Neutral button action (Ignore).
|
||||||
str("revanced_check_watch_history_domain_name_dialog_ignore"), // Neutral button text.
|
true // Dismiss dialog on Neutral button click.
|
||||||
() -> BaseSettings.CHECK_WATCH_HISTORY_DOMAIN_NAME.save(false), // Neutral button action (Ignore).
|
);
|
||||||
true // Dismiss dialog on Neutral button click.
|
|
||||||
);
|
|
||||||
|
|
||||||
// Show the dialog.
|
Utils.showDialog(context, dialogPair.first, false, null);
|
||||||
Dialog dialog = dialogPair.first;
|
|
||||||
|
|
||||||
Utils.showDialog(context, dialog, false, null);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "checkDnsResolver dialog creation failure", ex);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "checkDnsResolver failure", ex);
|
Logger.printException(() -> "checkDnsResolver failure", ex);
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
package app.revanced.extension.shared.patches;
|
||||||
|
|
||||||
|
import android.app.Notification;
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.GmsCoreSupport;
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Patch shared by YouTube and YT Music.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class CustomBrandingPatch {
|
||||||
|
|
||||||
|
// Important: In the future, additional branding themes can be added but all existing and prior
|
||||||
|
// themes cannot be removed or renamed.
|
||||||
|
//
|
||||||
|
// This is because if a user has a branding theme selected, then only that launch alias is enabled.
|
||||||
|
// If a future update removes or renames that alias, then after updating the app is effectively
|
||||||
|
// broken and it cannot be opened and not even clearing the app data will fix it.
|
||||||
|
// In that situation the only fix is to completely uninstall and reinstall again.
|
||||||
|
//
|
||||||
|
// The most that can be done is to hide a theme from the UI and keep the alias with dummy data.
|
||||||
|
public enum BrandingTheme {
|
||||||
|
/**
|
||||||
|
* Original unpatched icon.
|
||||||
|
*/
|
||||||
|
ORIGINAL,
|
||||||
|
ROUNDED,
|
||||||
|
MINIMAL,
|
||||||
|
SCALED,
|
||||||
|
/**
|
||||||
|
* User provided custom icon.
|
||||||
|
*/
|
||||||
|
CUSTOM;
|
||||||
|
|
||||||
|
private String packageAndNameIndexToClassAlias(String packageName, int appIndex) {
|
||||||
|
if (appIndex <= 0) {
|
||||||
|
throw new IllegalArgumentException("App index starts at index 1");
|
||||||
|
}
|
||||||
|
return packageName + ".revanced_" + name().toLowerCase(Locale.US) + '_' + appIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final int notificationSmallIcon;
|
||||||
|
|
||||||
|
static {
|
||||||
|
BrandingTheme branding = BaseSettings.CUSTOM_BRANDING_ICON.get();
|
||||||
|
if (branding == BrandingTheme.ORIGINAL) {
|
||||||
|
notificationSmallIcon = 0;
|
||||||
|
} else {
|
||||||
|
// Original icon is quantum_ic_video_youtube_white_24
|
||||||
|
String iconName = "revanced_notification_icon";
|
||||||
|
if (branding == BrandingTheme.CUSTOM) {
|
||||||
|
iconName += "_custom";
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationSmallIcon = Utils.getResourceIdentifier(iconName, "drawable");
|
||||||
|
if (notificationSmallIcon == 0) {
|
||||||
|
Logger.printException(() -> "Could not load notification small icon");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static View getLottieViewOrNull(View lottieStartupView) {
|
||||||
|
if (BaseSettings.CUSTOM_BRANDING_ICON.get() == BrandingTheme.ORIGINAL) {
|
||||||
|
return lottieStartupView;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static void setNotificationIcon(Notification.Builder builder) {
|
||||||
|
try {
|
||||||
|
if (notificationSmallIcon != 0) {
|
||||||
|
builder.setSmallIcon(notificationSmallIcon)
|
||||||
|
.setColor(Color.TRANSPARENT); // Remove YT red tint.
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "setNotificationIcon failure", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*
|
||||||
|
* The total number of app name aliases, including dummy aliases.
|
||||||
|
*/
|
||||||
|
private static int numberOfPresetAppNames() {
|
||||||
|
// Modified during patching.
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("ConstantConditions")
|
||||||
|
public static void setBranding() {
|
||||||
|
try {
|
||||||
|
if (GmsCoreSupport.isPackageNameOriginal()) {
|
||||||
|
Logger.printInfo(() -> "App is root mounted. Cannot dynamically change app icon");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Context context = Utils.getContext();
|
||||||
|
PackageManager pm = context.getPackageManager();
|
||||||
|
String packageName = context.getPackageName();
|
||||||
|
|
||||||
|
BrandingTheme selectedBranding = BaseSettings.CUSTOM_BRANDING_ICON.get();
|
||||||
|
final int selectedNameIndex = BaseSettings.CUSTOM_BRANDING_NAME.get();
|
||||||
|
ComponentName componentToEnable = null;
|
||||||
|
ComponentName defaultComponent = null;
|
||||||
|
List<ComponentName> componentsToDisable = new ArrayList<>();
|
||||||
|
|
||||||
|
for (BrandingTheme theme : BrandingTheme.values()) {
|
||||||
|
// Must always update all aliases including custom alias (last index).
|
||||||
|
final int numberOfPresetAppNames = numberOfPresetAppNames();
|
||||||
|
|
||||||
|
// App name indices starts at 1.
|
||||||
|
for (int index = 1; index <= numberOfPresetAppNames; index++) {
|
||||||
|
String aliasClass = theme.packageAndNameIndexToClassAlias(packageName, index);
|
||||||
|
ComponentName component = new ComponentName(packageName, aliasClass);
|
||||||
|
if (defaultComponent == null) {
|
||||||
|
// Default is always the first alias.
|
||||||
|
defaultComponent = component;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index == selectedNameIndex && theme == selectedBranding) {
|
||||||
|
componentToEnable = component;
|
||||||
|
} else {
|
||||||
|
componentsToDisable.add(component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (componentToEnable == null) {
|
||||||
|
// User imported a bad app name index value. Either the imported data
|
||||||
|
// was corrupted, or they previously had custom name enabled and the app
|
||||||
|
// no longer has a custom name specified.
|
||||||
|
Utils.showToastLong("Custom branding reset");
|
||||||
|
BaseSettings.CUSTOM_BRANDING_ICON.resetToDefault();
|
||||||
|
BaseSettings.CUSTOM_BRANDING_NAME.resetToDefault();
|
||||||
|
|
||||||
|
componentToEnable = defaultComponent;
|
||||||
|
componentsToDisable.remove(defaultComponent);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ComponentName disable : componentsToDisable) {
|
||||||
|
pm.setComponentEnabledSetting(disable,
|
||||||
|
PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use info logging because if the alias status become corrupt the app cannot launch.
|
||||||
|
ComponentName componentToEnableFinal = componentToEnable;
|
||||||
|
Logger.printInfo(() -> "Enabling: " + componentToEnableFinal.getClassName());
|
||||||
|
|
||||||
|
pm.setComponentEnabledSetting(componentToEnable,
|
||||||
|
PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 0);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "setBranding failure", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
package app.revanced.extension.shared.patches;
|
package app.revanced.extension.shared.patches;
|
||||||
|
|
||||||
|
import static java.lang.Boolean.TRUE;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.ConcurrentMap;
|
import java.util.concurrent.ConcurrentMap;
|
||||||
|
|
||||||
@@ -21,12 +25,28 @@ public final class EnableDebuggingPatch {
|
|||||||
? new ConcurrentHashMap<>(800, 0.5f, 1)
|
? new ConcurrentHashMap<>(800, 0.5f, 1)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
private static final Set<Long> DISABLED_FEATURE_FLAGS = parseFlags(BaseSettings.DISABLED_FEATURE_FLAGS.get());
|
||||||
|
|
||||||
|
// Log all disabled flags on app startup.
|
||||||
|
static {
|
||||||
|
if (LOG_FEATURE_FLAGS && !DISABLED_FEATURE_FLAGS.isEmpty()) {
|
||||||
|
StringBuilder sb = new StringBuilder("Disabled feature flags:\n");
|
||||||
|
for (Long flag : DISABLED_FEATURE_FLAGS) {
|
||||||
|
sb.append(" ").append(flag).append('\n');
|
||||||
|
}
|
||||||
|
Logger.printDebug(sb::toString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
public static boolean isBooleanFeatureFlagEnabled(boolean value, Long flag) {
|
public static boolean isBooleanFeatureFlagEnabled(boolean value, Long flag) {
|
||||||
if (LOG_FEATURE_FLAGS && value) {
|
if (LOG_FEATURE_FLAGS && value) {
|
||||||
if (featureFlags.putIfAbsent(flag, true) == null) {
|
if (DISABLED_FEATURE_FLAGS.contains(flag)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (featureFlags.putIfAbsent(flag, TRUE) == null) {
|
||||||
Logger.printDebug(() -> "boolean feature is enabled: " + flag);
|
Logger.printDebug(() -> "boolean feature is enabled: " + flag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,10 +90,44 @@ public final class EnableDebuggingPatch {
|
|||||||
if (LOG_FEATURE_FLAGS && !defaultValue.equals(value)) {
|
if (LOG_FEATURE_FLAGS && !defaultValue.equals(value)) {
|
||||||
if (featureFlags.putIfAbsent(flag, true) == null) {
|
if (featureFlags.putIfAbsent(flag, true) == null) {
|
||||||
Logger.printDebug(() -> " string feature is enabled: " + flag
|
Logger.printDebug(() -> " string feature is enabled: " + flag
|
||||||
+ " value: " + value + (defaultValue.isEmpty() ? "" : " default: " + defaultValue));
|
+ " value: " + value + (defaultValue.isEmpty() ? "" : " default: " + defaultValue));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all logged feature flags.
|
||||||
|
* @return Set of all known flags
|
||||||
|
*/
|
||||||
|
public static Set<Long> getAllLoggedFlags() {
|
||||||
|
if (featureFlags != null) {
|
||||||
|
return new HashSet<>(featureFlags.keySet());
|
||||||
|
}
|
||||||
|
|
||||||
|
return new HashSet<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public method for parsing flags.
|
||||||
|
* @param flags String containing newline-separated flag IDs
|
||||||
|
* @return Set of parsed flag IDs
|
||||||
|
*/
|
||||||
|
public static Set<Long> parseFlags(String flags) {
|
||||||
|
Set<Long> parsedFlags = new HashSet<>();
|
||||||
|
if (!flags.isBlank()) {
|
||||||
|
for (String flag : flags.split("\n")) {
|
||||||
|
String trimmedFlag = flag.trim();
|
||||||
|
if (trimmedFlag.isEmpty()) continue; // Skip empty lines.
|
||||||
|
try {
|
||||||
|
parsedFlags.add(Long.parseLong(trimmedFlag));
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
Logger.printException(() -> "Invalid flag ID: " + flag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedFlags;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package app.revanced.extension.shared.patches;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.settings.AppLanguage;
|
||||||
|
import app.revanced.extension.shared.spoof.ClientType;
|
||||||
|
import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class ForceOriginalAudioPatch {
|
||||||
|
|
||||||
|
private static final String DEFAULT_AUDIO_TRACKS_SUFFIX = ".4";
|
||||||
|
|
||||||
|
private static volatile boolean enabled;
|
||||||
|
|
||||||
|
public static void setEnabled(boolean isEnabled, ClientType client) {
|
||||||
|
enabled = isEnabled;
|
||||||
|
|
||||||
|
if (isEnabled && !client.useAuth && !client.supportsMultiAudioTracks) {
|
||||||
|
// If client spoofing does not use authentication and lacks multi-audio streams,
|
||||||
|
// then can use any language code for the request and if that requested language is
|
||||||
|
// not available YT uses the original audio language. Authenticated requests ignore
|
||||||
|
// the language code and always use the account language. Use a language that is
|
||||||
|
// not auto-dubbed by YouTube: https://support.google.com/youtube/answer/15569972
|
||||||
|
// but the language is also supported natively by the Meta Quest device that
|
||||||
|
// Android VR is spoofing.
|
||||||
|
AppLanguage override = AppLanguage.NB; // Norwegian Bokmal.
|
||||||
|
Logger.printDebug(() -> "Setting language override: " + override);
|
||||||
|
SpoofVideoStreamsPatch.setLanguageOverride(override);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static boolean ignoreDefaultAudioStream(boolean original) {
|
||||||
|
if (enabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return original;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static boolean isDefaultAudioStream(boolean isDefault, String audioTrackId, String audioTrackDisplayName) {
|
||||||
|
try {
|
||||||
|
if (!enabled) {
|
||||||
|
return isDefault;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audioTrackId.isEmpty()) {
|
||||||
|
// Older app targets can have empty audio tracks and these might be placeholders.
|
||||||
|
// The real audio tracks are called after these.
|
||||||
|
return isDefault;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.printDebug(() -> "default: " + String.format("%-5s", isDefault) + " id: "
|
||||||
|
+ String.format("%-8s", audioTrackId) + " name:" + audioTrackDisplayName);
|
||||||
|
|
||||||
|
final boolean isOriginal = audioTrackId.endsWith(DEFAULT_AUDIO_TRACKS_SUFFIX);
|
||||||
|
if (isOriginal) {
|
||||||
|
Logger.printDebug(() -> "Using audio: " + audioTrackId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isOriginal;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "isDefaultAudioStream failure", ex);
|
||||||
|
return isDefault;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.revanced.extension.shared.patches;
|
package app.revanced.extension.shared.patches;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.privacy.LinkSanitizer;
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -7,17 +8,18 @@ import app.revanced.extension.shared.settings.BaseSettings;
|
|||||||
*/
|
*/
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public final class SanitizeSharingLinksPatch {
|
public final class SanitizeSharingLinksPatch {
|
||||||
private static final String NEW_TRACKING_PARAMETER_REGEX = ".si=.+";
|
|
||||||
private static final String OLD_TRACKING_PARAMETER_REGEX = ".feature=.+";
|
private static final LinkSanitizer sanitizer = new LinkSanitizer(
|
||||||
|
"si",
|
||||||
|
"feature" // Old tracking parameter name, and may be obsolete.
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
public static String sanitize(String url) {
|
public static String sanitize(String url) {
|
||||||
if (BaseSettings.SANITIZE_SHARED_LINKS.get()) {
|
if (BaseSettings.SANITIZE_SHARED_LINKS.get()) {
|
||||||
url = url
|
url = sanitizer.sanitizeUrlString(url);
|
||||||
.replaceAll(NEW_TRACKING_PARAMETER_REGEX, "")
|
|
||||||
.replaceAll(OLD_TRACKING_PARAMETER_REGEX, "");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (BaseSettings.REPLACE_MUSIC_LINKS_WITH_YOUTUBE.get()) {
|
if (BaseSettings.REPLACE_MUSIC_LINKS_WITH_YOUTUBE.get()) {
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package app.revanced.extension.shared.privacy;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strips away specific parameters from URLs.
|
||||||
|
*/
|
||||||
|
public class LinkSanitizer {
|
||||||
|
|
||||||
|
private final Collection<String> parametersToRemove;
|
||||||
|
|
||||||
|
public LinkSanitizer(String ... parametersToRemove) {
|
||||||
|
final int parameterCount = parametersToRemove.length;
|
||||||
|
|
||||||
|
// List is faster if only checking a few parameters.
|
||||||
|
this.parametersToRemove = parameterCount > 4
|
||||||
|
? Set.of(parametersToRemove)
|
||||||
|
: List.of(parametersToRemove);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String sanitizeUrlString(String url) {
|
||||||
|
try {
|
||||||
|
return sanitizeUri(Uri.parse(url)).toString();
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "sanitizeUrlString failure: " + url, ex);
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Uri sanitizeUri(Uri uri) {
|
||||||
|
try {
|
||||||
|
String scheme = uri.getScheme();
|
||||||
|
if (scheme == null || !(scheme.equals("http") || scheme.equals("https"))) {
|
||||||
|
// Opening YouTube share sheet 'other' option passes the video title as a URI.
|
||||||
|
// Checking !uri.isHierarchical() works for all cases, except if the
|
||||||
|
// video title starts with / and then it's hierarchical but still an invalid URI.
|
||||||
|
Logger.printDebug(() -> "Ignoring uri: " + uri);
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uri.Builder builder = uri.buildUpon().clearQuery();
|
||||||
|
|
||||||
|
if (!parametersToRemove.isEmpty()) {
|
||||||
|
for (String paramName : uri.getQueryParameterNames()) {
|
||||||
|
if (!parametersToRemove.contains(paramName)) {
|
||||||
|
for (String value : uri.getQueryParameters(paramName)) {
|
||||||
|
builder.appendQueryParameter(paramName, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Uri sanitizedUrl = builder.build();
|
||||||
|
Logger.printInfo(() -> "Sanitized url: " + uri + " to: " + sanitizedUrl);
|
||||||
|
|
||||||
|
return sanitizedUrl;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "sanitizeUri failure: " + uri, ex);
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,8 +36,8 @@ public enum AppLanguage {
|
|||||||
FR,
|
FR,
|
||||||
GL,
|
GL,
|
||||||
GU,
|
GU,
|
||||||
HI,
|
|
||||||
HE, // App uses obsolete 'IW' and not the modern 'HE' ISO code.
|
HE, // App uses obsolete 'IW' and not the modern 'HE' ISO code.
|
||||||
|
HI,
|
||||||
HR,
|
HR,
|
||||||
HU,
|
HU,
|
||||||
HY,
|
HY,
|
||||||
@@ -60,9 +60,9 @@ public enum AppLanguage {
|
|||||||
MR,
|
MR,
|
||||||
MS,
|
MS,
|
||||||
MY,
|
MY,
|
||||||
|
NB,
|
||||||
NE,
|
NE,
|
||||||
NL,
|
NL,
|
||||||
NB,
|
|
||||||
OR,
|
OR,
|
||||||
PA,
|
PA,
|
||||||
PL,
|
PL,
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import android.app.Activity;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.preference.PreferenceFragment;
|
import android.preference.PreferenceFragment;
|
||||||
import android.util.TypedValue;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
@@ -16,6 +15,7 @@ import android.widget.Toolbar;
|
|||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.preference.ToolbarPreferenceFragment;
|
import app.revanced.extension.shared.settings.preference.ToolbarPreferenceFragment;
|
||||||
|
import app.revanced.extension.shared.ui.Dim;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for hooking activities to inject a custom PreferenceFragment with a toolbar.
|
* Base class for hooking activities to inject a custom PreferenceFragment with a toolbar.
|
||||||
@@ -109,13 +109,12 @@ public abstract class BaseActivityHook extends Activity {
|
|||||||
toolbar.setNavigationOnClickListener(getNavigationClickListener(activity));
|
toolbar.setNavigationOnClickListener(getNavigationClickListener(activity));
|
||||||
toolbar.setTitle(STRING_REVANCED_SETTINGS_TITLE);
|
toolbar.setTitle(STRING_REVANCED_SETTINGS_TITLE);
|
||||||
|
|
||||||
final int margin = Utils.dipToPixels(16);
|
toolbar.setTitleMarginStart(Dim.dp16);
|
||||||
toolbar.setTitleMarginStart(margin);
|
toolbar.setTitleMarginEnd(Dim.dp16);
|
||||||
toolbar.setTitleMarginEnd(margin);
|
|
||||||
TextView toolbarTextView = Utils.getChildView(toolbar, false, view -> view instanceof TextView);
|
TextView toolbarTextView = Utils.getChildView(toolbar, false, view -> view instanceof TextView);
|
||||||
if (toolbarTextView != null) {
|
if (toolbarTextView != null) {
|
||||||
toolbarTextView.setTextColor(Utils.getAppForegroundColor());
|
toolbarTextView.setTextColor(Utils.getAppForegroundColor());
|
||||||
toolbarTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20);
|
toolbarTextView.setTextSize(20);
|
||||||
}
|
}
|
||||||
setToolbarLayoutParams(toolbar);
|
setToolbarLayoutParams(toolbar);
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package app.revanced.extension.shared.settings;
|
|||||||
|
|
||||||
import static java.lang.Boolean.FALSE;
|
import static java.lang.Boolean.FALSE;
|
||||||
import static java.lang.Boolean.TRUE;
|
import static java.lang.Boolean.TRUE;
|
||||||
|
import static app.revanced.extension.shared.patches.CustomBrandingPatch.BrandingTheme;
|
||||||
import static app.revanced.extension.shared.settings.Setting.parent;
|
import static app.revanced.extension.shared.settings.Setting.parent;
|
||||||
import static app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch.AudioStreamLanguageOverrideAvailability;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Settings shared across multiple apps.
|
* Settings shared across multiple apps.
|
||||||
@@ -33,11 +33,15 @@ public class BaseSettings {
|
|||||||
//
|
//
|
||||||
|
|
||||||
public static final BooleanSetting SPOOF_VIDEO_STREAMS = new BooleanSetting("revanced_spoof_video_streams", TRUE, true, "revanced_spoof_video_streams_user_dialog_message");
|
public static final BooleanSetting SPOOF_VIDEO_STREAMS = new BooleanSetting("revanced_spoof_video_streams", TRUE, true, "revanced_spoof_video_streams_user_dialog_message");
|
||||||
public static final EnumSetting<AppLanguage> SPOOF_VIDEO_STREAMS_LANGUAGE = new EnumSetting<>("revanced_spoof_video_streams_language", AppLanguage.DEFAULT, new AudioStreamLanguageOverrideAvailability());
|
|
||||||
public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE, parent(SPOOF_VIDEO_STREAMS));
|
public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE, parent(SPOOF_VIDEO_STREAMS));
|
||||||
|
|
||||||
public static final BooleanSetting SANITIZE_SHARED_LINKS = new BooleanSetting("revanced_sanitize_sharing_links", TRUE);
|
public static final BooleanSetting SANITIZE_SHARED_LINKS = new BooleanSetting("revanced_sanitize_sharing_links", TRUE);
|
||||||
public static final BooleanSetting REPLACE_MUSIC_LINKS_WITH_YOUTUBE = new BooleanSetting("revanced_replace_music_with_youtube", FALSE);
|
public static final BooleanSetting REPLACE_MUSIC_LINKS_WITH_YOUTUBE = new BooleanSetting("revanced_replace_music_with_youtube", FALSE);
|
||||||
|
|
||||||
public static final BooleanSetting CHECK_WATCH_HISTORY_DOMAIN_NAME = new BooleanSetting("revanced_check_watch_history_domain_name", TRUE, false, false);
|
public static final BooleanSetting CHECK_WATCH_HISTORY_DOMAIN_NAME = new BooleanSetting("revanced_check_watch_history_domain_name", TRUE, false, false);
|
||||||
|
|
||||||
|
public static final EnumSetting<BrandingTheme> CUSTOM_BRANDING_ICON = new EnumSetting<>("revanced_custom_branding_icon", BrandingTheme.ORIGINAL, true);
|
||||||
|
public static final IntegerSetting CUSTOM_BRANDING_NAME = new IntegerSetting("revanced_custom_branding_name", 1, true);
|
||||||
|
|
||||||
|
public static final StringSetting DISABLED_FEATURE_FLAGS = new StringSetting("revanced_disabled_feature_flags", "", true, parent(DEBUG));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,23 @@ public abstract class Setting<T> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Availability based on a single parent setting being disabled.
|
||||||
|
*/
|
||||||
|
public static Availability parentNot(BooleanSetting parent) {
|
||||||
|
return new Availability() {
|
||||||
|
@Override
|
||||||
|
public boolean isAvailable() {
|
||||||
|
return !parent.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Setting<?>> getParentSettings() {
|
||||||
|
return Collections.singletonList(parent);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Availability based on all parents being enabled.
|
* Availability based on all parents being enabled.
|
||||||
*/
|
*/
|
||||||
@@ -392,10 +409,13 @@ public abstract class Setting<T> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the parent Settings that this setting depends on.
|
* Get the parent Settings that this setting depends on.
|
||||||
* @return List of parent Settings (e.g., BooleanSetting or EnumSetting), or empty list if no dependencies exist.
|
* @return List of parent Settings, or empty list if no dependencies exist.
|
||||||
|
* Defensive: handles null availability or missing getParentSettings() override.
|
||||||
*/
|
*/
|
||||||
public List<Setting<?>> getParentSettings() {
|
public List<Setting<?>> getParentSettings() {
|
||||||
return availability == null ? Collections.emptyList() : availability.getParentSettings();
|
return availability == null
|
||||||
|
? Collections.emptyList()
|
||||||
|
: Objects.requireNonNullElse(availability.getParentSettings(), Collections.emptyList());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package app.revanced.extension.shared.settings.preference;
|
package app.revanced.extension.shared.settings.preference;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
import static app.revanced.extension.shared.Utils.dipToPixels;
|
|
||||||
import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
|
import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
|
||||||
|
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
@@ -37,6 +36,7 @@ import app.revanced.extension.shared.settings.Setting;
|
|||||||
import app.revanced.extension.shared.settings.StringSetting;
|
import app.revanced.extension.shared.settings.StringSetting;
|
||||||
import app.revanced.extension.shared.ui.ColorDot;
|
import app.revanced.extension.shared.ui.ColorDot;
|
||||||
import app.revanced.extension.shared.ui.CustomDialog;
|
import app.revanced.extension.shared.ui.CustomDialog;
|
||||||
|
import app.revanced.extension.shared.ui.Dim;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A custom preference for selecting a color via a hexadecimal code or a color picker dialog.
|
* A custom preference for selecting a color via a hexadecimal code or a color picker dialog.
|
||||||
@@ -310,11 +310,8 @@ public class ColorPickerPreference extends EditTextPreference {
|
|||||||
inputLayout.setGravity(Gravity.CENTER_VERTICAL);
|
inputLayout.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
|
||||||
dialogColorDot = new View(context);
|
dialogColorDot = new View(context);
|
||||||
LinearLayout.LayoutParams previewParams = new LinearLayout.LayoutParams(
|
LinearLayout.LayoutParams previewParams = new LinearLayout.LayoutParams(Dim.dp20,Dim.dp20);
|
||||||
dipToPixels(20),
|
previewParams.setMargins(Dim.dp16, 0, Dim.dp10, 0);
|
||||||
dipToPixels(20)
|
|
||||||
);
|
|
||||||
previewParams.setMargins(dipToPixels(16), 0, dipToPixels(10), 0);
|
|
||||||
dialogColorDot.setLayoutParams(previewParams);
|
dialogColorDot.setLayoutParams(previewParams);
|
||||||
inputLayout.addView(dialogColorDot);
|
inputLayout.addView(dialogColorDot);
|
||||||
updateDialogColorDot();
|
updateDialogColorDot();
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package app.revanced.extension.shared.settings.preference;
|
package app.revanced.extension.shared.settings.preference;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.Utils.dipToPixels;
|
|
||||||
import static app.revanced.extension.shared.settings.preference.ColorPickerPreference.getColorString;
|
import static app.revanced.extension.shared.settings.preference.ColorPickerPreference.getColorString;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
@@ -21,6 +20,7 @@ import androidx.annotation.ColorInt;
|
|||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.shared.ui.Dim;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A custom color picker view that allows the user to select a color using a hue slider, a saturation-value selector
|
* A custom color picker view that allows the user to select a color using a hue slider, a saturation-value selector
|
||||||
@@ -54,28 +54,28 @@ public class ColorPickerView extends View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Expanded touch area for the hue and opacity bars to increase the touch-sensitive area. */
|
/** Expanded touch area for the hue and opacity bars to increase the touch-sensitive area. */
|
||||||
public static final float TOUCH_EXPANSION = dipToPixels(20f);
|
public static final float TOUCH_EXPANSION = Dim.dp20;
|
||||||
|
|
||||||
/** Margin between different areas of the view (saturation-value selector, hue bar, and opacity slider). */
|
/** Margin between different areas of the view (saturation-value selector, hue bar, and opacity slider). */
|
||||||
private static final float MARGIN_BETWEEN_AREAS = dipToPixels(24);
|
private static final float MARGIN_BETWEEN_AREAS = Dim.dp24;
|
||||||
|
|
||||||
/** Padding around the view. */
|
/** Padding around the view. */
|
||||||
private static final float VIEW_PADDING = dipToPixels(16);
|
private static final float VIEW_PADDING = Dim.dp16;
|
||||||
|
|
||||||
/** Height of the hue bar. */
|
/** Height of the hue bar. */
|
||||||
private static final float HUE_BAR_HEIGHT = dipToPixels(12);
|
private static final float HUE_BAR_HEIGHT = Dim.dp12;
|
||||||
|
|
||||||
/** Height of the opacity slider. */
|
/** Height of the opacity slider. */
|
||||||
private static final float OPACITY_BAR_HEIGHT = dipToPixels(12);
|
private static final float OPACITY_BAR_HEIGHT = Dim.dp12;
|
||||||
|
|
||||||
/** Corner radius for the hue bar. */
|
/** Corner radius for the hue bar. */
|
||||||
private static final float HUE_CORNER_RADIUS = dipToPixels(6);
|
private static final float HUE_CORNER_RADIUS = Dim.dp6;
|
||||||
|
|
||||||
/** Corner radius for the opacity slider. */
|
/** Corner radius for the opacity slider. */
|
||||||
private static final float OPACITY_CORNER_RADIUS = dipToPixels(6);
|
private static final float OPACITY_CORNER_RADIUS = Dim.dp6;
|
||||||
|
|
||||||
/** Radius of the selector handles. */
|
/** Radius of the selector handles. */
|
||||||
private static final float SELECTOR_RADIUS = dipToPixels(12);
|
private static final float SELECTOR_RADIUS = Dim.dp12;
|
||||||
|
|
||||||
/** Stroke width for the selector handle outlines. */
|
/** Stroke width for the selector handle outlines. */
|
||||||
private static final float SELECTOR_STROKE_WIDTH = 8;
|
private static final float SELECTOR_STROKE_WIDTH = 8;
|
||||||
@@ -202,7 +202,7 @@ public class ColorPickerView extends View {
|
|||||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||||
final float DESIRED_ASPECT_RATIO = 0.8f; // height = width * 0.8
|
final float DESIRED_ASPECT_RATIO = 0.8f; // height = width * 0.8
|
||||||
|
|
||||||
final int minWidth = dipToPixels(250);
|
final int minWidth = Dim.dp(250);
|
||||||
final int minHeight = (int) (minWidth * DESIRED_ASPECT_RATIO) + (int) (HUE_BAR_HEIGHT + MARGIN_BETWEEN_AREAS)
|
final int minHeight = (int) (minWidth * DESIRED_ASPECT_RATIO) + (int) (HUE_BAR_HEIGHT + MARGIN_BETWEEN_AREAS)
|
||||||
+ (opacitySliderEnabled ? (int) (OPACITY_BAR_HEIGHT + MARGIN_BETWEEN_AREAS) : 0);
|
+ (opacitySliderEnabled ? (int) (OPACITY_BAR_HEIGHT + MARGIN_BETWEEN_AREAS) : 0);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,623 @@
|
|||||||
|
package app.revanced.extension.shared.settings.preference;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.TypedArray;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.graphics.drawable.ShapeDrawable;
|
||||||
|
import android.graphics.drawable.shapes.RoundRectShape;
|
||||||
|
import android.preference.Preference;
|
||||||
|
import android.text.Editable;
|
||||||
|
import android.text.InputType;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.text.TextWatcher;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.util.Pair;
|
||||||
|
import android.util.SparseBooleanArray;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.Window;
|
||||||
|
import android.widget.ArrayAdapter;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.ImageButton;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.ListView;
|
||||||
|
import android.widget.Space;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.shared.patches.EnableDebuggingPatch;
|
||||||
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
|
import app.revanced.extension.shared.ui.CustomDialog;
|
||||||
|
import app.revanced.extension.shared.ui.Dim;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom preference that opens a dialog for managing feature flags.
|
||||||
|
* Allows moving boolean flags between active and blocked states with advanced selection.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({"deprecation", "unused"})
|
||||||
|
public class FeatureFlagsManagerPreference extends Preference {
|
||||||
|
|
||||||
|
private static final int DRAWABLE_REVANCED_SETTINGS_SELECT_ALL =
|
||||||
|
getResourceIdentifierOrThrow("revanced_settings_select_all", "drawable");
|
||||||
|
private static final int DRAWABLE_REVANCED_SETTINGS_DESELECT_ALL =
|
||||||
|
getResourceIdentifierOrThrow("revanced_settings_deselect_all", "drawable");
|
||||||
|
private static final int DRAWABLE_REVANCED_SETTINGS_COPY_ALL =
|
||||||
|
getResourceIdentifierOrThrow("revanced_settings_copy_all", "drawable");
|
||||||
|
private static final int DRAWABLE_REVANCED_SETTINGS_ARROW_RIGHT_ONE =
|
||||||
|
getResourceIdentifierOrThrow("revanced_settings_arrow_right_one", "drawable");
|
||||||
|
private static final int DRAWABLE_REVANCED_SETTINGS_ARROW_RIGHT_DOUBLE =
|
||||||
|
getResourceIdentifierOrThrow("revanced_settings_arrow_right_double", "drawable");
|
||||||
|
private static final int DRAWABLE_REVANCED_SETTINGS_ARROW_LEFT_ONE =
|
||||||
|
getResourceIdentifierOrThrow("revanced_settings_arrow_left_one", "drawable");
|
||||||
|
private static final int DRAWABLE_REVANCED_SETTINGS_ARROW_LEFT_DOUBLE =
|
||||||
|
getResourceIdentifierOrThrow("revanced_settings_arrow_left_double", "drawable");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flags to hide from the UI.
|
||||||
|
*/
|
||||||
|
private static final Set<Long> FLAGS_TO_IGNORE = Set.of(
|
||||||
|
45386834L // 'You' tab settings icon.
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks state for range selection in ListView.
|
||||||
|
*/
|
||||||
|
private static class ListViewSelectionState {
|
||||||
|
int lastClickedPosition = -1; // Position of the last clicked item.
|
||||||
|
boolean isRangeSelecting = false; // True while a range is being selected.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class to pass ListView and Adapter together.
|
||||||
|
*/
|
||||||
|
private record ColumnViews(ListView listView, FlagAdapter adapter) {}
|
||||||
|
|
||||||
|
{
|
||||||
|
setOnPreferenceClickListener(pref -> {
|
||||||
|
showFlagsManagerDialog();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public FeatureFlagsManagerPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FeatureFlagsManagerPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FeatureFlagsManagerPreference(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FeatureFlagsManagerPreference(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the main dialog for managing feature flags.
|
||||||
|
*/
|
||||||
|
private void showFlagsManagerDialog() {
|
||||||
|
if (!BaseSettings.DEBUG.get()) {
|
||||||
|
Utils.showToastShort(str("revanced_debug_logs_disabled"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Context context = getContext();
|
||||||
|
|
||||||
|
// Load all known and disabled flags.
|
||||||
|
TreeSet<Long> allKnownFlags = new TreeSet<>(EnableDebuggingPatch.getAllLoggedFlags());
|
||||||
|
allKnownFlags.removeAll(FLAGS_TO_IGNORE);
|
||||||
|
|
||||||
|
TreeSet<Long> disabledFlags = new TreeSet<>(EnableDebuggingPatch.parseFlags(
|
||||||
|
BaseSettings.DISABLED_FEATURE_FLAGS.get()));
|
||||||
|
disabledFlags.removeAll(FLAGS_TO_IGNORE);
|
||||||
|
|
||||||
|
if (allKnownFlags.isEmpty() && disabledFlags.isEmpty()) {
|
||||||
|
// String does not need to be localized because it's basically impossible
|
||||||
|
// to reach the settings menu without encountering at least 1 flag.
|
||||||
|
Utils.showToastShort("No feature flags logged yet");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TreeSet<Long> availableFlags = new TreeSet<>(allKnownFlags);
|
||||||
|
availableFlags.removeAll(disabledFlags);
|
||||||
|
TreeSet<Long> blockedFlags = new TreeSet<>(disabledFlags);
|
||||||
|
|
||||||
|
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
|
||||||
|
context,
|
||||||
|
getTitle() != null ? getTitle().toString() : "",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
str("revanced_settings_save"),
|
||||||
|
() -> saveFlags(blockedFlags),
|
||||||
|
() -> {},
|
||||||
|
str("revanced_settings_reset"),
|
||||||
|
this::resetFlags,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
LinearLayout mainLayout = dialogPair.second;
|
||||||
|
LinearLayout.LayoutParams contentParams = new LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT, 0, 1.0f);
|
||||||
|
|
||||||
|
// Insert content before the dialog button row.
|
||||||
|
View contentView = createContentView(context, availableFlags, blockedFlags);
|
||||||
|
mainLayout.addView(contentView, mainLayout.getChildCount() - 1, contentParams);
|
||||||
|
|
||||||
|
Dialog dialog = dialogPair.first;
|
||||||
|
dialog.show();
|
||||||
|
|
||||||
|
Window window = dialog.getWindow();
|
||||||
|
if (window != null) {
|
||||||
|
Utils.setDialogWindowParameters(window, Gravity.CENTER, 0, 100, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the main content view with two columns.
|
||||||
|
*/
|
||||||
|
private View createContentView(Context context, TreeSet<Long> availableFlags, TreeSet<Long> blockedFlags) {
|
||||||
|
LinearLayout contentLayout = new LinearLayout(context);
|
||||||
|
contentLayout.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
|
||||||
|
// Headers.
|
||||||
|
TextView availableHeader = createHeader(context, "revanced_debug_feature_flags_manager_active_header");
|
||||||
|
TextView blockedHeader = createHeader(context, "revanced_debug_feature_flags_manager_blocked_header");
|
||||||
|
|
||||||
|
LinearLayout headersLayout = new LinearLayout(context);
|
||||||
|
headersLayout.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
headersLayout.addView(availableHeader, new LinearLayout.LayoutParams(
|
||||||
|
0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f));
|
||||||
|
headersLayout.addView(blockedHeader, new LinearLayout.LayoutParams(
|
||||||
|
0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f));
|
||||||
|
|
||||||
|
// Columns.
|
||||||
|
View leftColumn = createColumn(context, availableFlags, availableHeader);
|
||||||
|
View rightColumn = createColumn(context, blockedFlags, blockedHeader);
|
||||||
|
|
||||||
|
ColumnViews leftViews = (ColumnViews) leftColumn.getTag();
|
||||||
|
ColumnViews rightViews = (ColumnViews) rightColumn.getTag();
|
||||||
|
|
||||||
|
updateHeaderCount(availableHeader, leftViews.adapter);
|
||||||
|
updateHeaderCount(blockedHeader, rightViews.adapter);
|
||||||
|
|
||||||
|
// Main columns layout.
|
||||||
|
LinearLayout columnsLayout = new LinearLayout(context);
|
||||||
|
columnsLayout.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
columnsLayout.setLayoutParams(new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f));
|
||||||
|
columnsLayout.addView(leftColumn, new LinearLayout.LayoutParams(
|
||||||
|
0, ViewGroup.LayoutParams.MATCH_PARENT, 1f));
|
||||||
|
|
||||||
|
Space spaceBetweenColumns = new Space(context);
|
||||||
|
spaceBetweenColumns.setLayoutParams(new LinearLayout.LayoutParams(Dim.dp8, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||||
|
columnsLayout.addView(spaceBetweenColumns);
|
||||||
|
|
||||||
|
columnsLayout.addView(rightColumn, new LinearLayout.LayoutParams(
|
||||||
|
0, ViewGroup.LayoutParams.MATCH_PARENT, 1f));
|
||||||
|
|
||||||
|
// Move buttons below columns.
|
||||||
|
Pair<LinearLayout, LinearLayout> moveButtons = createMoveButtons(context,
|
||||||
|
leftViews.listView, rightViews.listView,
|
||||||
|
availableFlags, blockedFlags, availableHeader, blockedHeader);
|
||||||
|
|
||||||
|
// Layout for buttons row.
|
||||||
|
LinearLayout buttonsRow = new LinearLayout(context);
|
||||||
|
buttonsRow.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
buttonsRow.setLayoutParams(new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||||
|
|
||||||
|
buttonsRow.addView(moveButtons.first, new LinearLayout.LayoutParams(
|
||||||
|
0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f));
|
||||||
|
|
||||||
|
Space spaceBetweenButtons = new Space(context);
|
||||||
|
spaceBetweenButtons.setLayoutParams(new LinearLayout.LayoutParams(Dim.dp8, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||||
|
buttonsRow.addView(spaceBetweenButtons);
|
||||||
|
|
||||||
|
buttonsRow.addView(moveButtons.second, new LinearLayout.LayoutParams(
|
||||||
|
0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f));
|
||||||
|
|
||||||
|
contentLayout.addView(headersLayout);
|
||||||
|
contentLayout.addView(columnsLayout);
|
||||||
|
contentLayout.addView(buttonsRow);
|
||||||
|
|
||||||
|
return contentLayout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a header TextView.
|
||||||
|
*/
|
||||||
|
private TextView createHeader(Context context, String tag) {
|
||||||
|
TextView textview = new TextView(context);
|
||||||
|
textview.setTag(tag);
|
||||||
|
textview.setTextSize(16);
|
||||||
|
textview.setTextColor(Utils.getAppForegroundColor());
|
||||||
|
textview.setGravity(Gravity.CENTER);
|
||||||
|
|
||||||
|
return textview;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a single column (search + buttons + list).
|
||||||
|
*/
|
||||||
|
private View createColumn(Context context, TreeSet<Long> flags, TextView countText) {
|
||||||
|
LinearLayout wrapper = new LinearLayout(context);
|
||||||
|
wrapper.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
|
||||||
|
Pair<ListView, FlagAdapter> pair = createListView(context, flags, countText);
|
||||||
|
ListView listView = pair.first;
|
||||||
|
FlagAdapter adapter = pair.second;
|
||||||
|
|
||||||
|
EditText search = createSearchBox(context, adapter, listView, countText);
|
||||||
|
LinearLayout buttons = createActionButtons(context, listView, adapter);
|
||||||
|
|
||||||
|
listView.setLayoutParams(new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f));
|
||||||
|
ShapeDrawable background = new ShapeDrawable(new RoundRectShape(
|
||||||
|
Dim.roundedCorners(10), null, null));
|
||||||
|
background.getPaint().setColor(Utils.getEditTextBackground());
|
||||||
|
listView.setPadding(0, Dim.dp4, 0, Dim.dp4);
|
||||||
|
listView.setBackground(background);
|
||||||
|
listView.setOverScrollMode(View.OVER_SCROLL_NEVER);
|
||||||
|
|
||||||
|
wrapper.addView(search);
|
||||||
|
wrapper.addView(buttons);
|
||||||
|
wrapper.addView(listView);
|
||||||
|
|
||||||
|
// Save references for move buttons.
|
||||||
|
wrapper.setTag(new ColumnViews(listView, adapter));
|
||||||
|
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the header text with the current count.
|
||||||
|
*/
|
||||||
|
private void updateHeaderCount(TextView header, FlagAdapter adapter) {
|
||||||
|
header.setText(str((String) header.getTag(), adapter.getCount()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a search box that filters the list.
|
||||||
|
*/
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
private EditText createSearchBox(Context context, FlagAdapter adapter, ListView listView, TextView countText) {
|
||||||
|
EditText search = new EditText(context);
|
||||||
|
search.setInputType(InputType.TYPE_CLASS_NUMBER);
|
||||||
|
search.setTextSize(16);
|
||||||
|
search.setHint(str("revanced_debug_feature_flags_manager_search_hint"));
|
||||||
|
search.setHapticFeedbackEnabled(false);
|
||||||
|
search.setLayoutParams(new LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
|
||||||
|
|
||||||
|
search.addTextChangedListener(new TextWatcher() {
|
||||||
|
@Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||||
|
@Override public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||||
|
adapter.setSearchQuery(s.toString());
|
||||||
|
listView.clearChoices();
|
||||||
|
updateHeaderCount(countText, adapter);
|
||||||
|
Drawable clearIcon = context.getResources().getDrawable(android.R.drawable.ic_menu_close_clear_cancel);
|
||||||
|
clearIcon.setBounds(0, 0, Dim.dp20, Dim.dp20);
|
||||||
|
search.setCompoundDrawables(null, null, TextUtils.isEmpty(s) ? null : clearIcon, null);
|
||||||
|
}
|
||||||
|
@Override public void afterTextChanged(Editable s) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
search.setOnTouchListener((v, event) -> {
|
||||||
|
if (event.getAction() == MotionEvent.ACTION_UP) {
|
||||||
|
Drawable[] compoundDrawables = search.getCompoundDrawables();
|
||||||
|
if (compoundDrawables[2] != null &&
|
||||||
|
event.getRawX() >= (search.getRight() - compoundDrawables[2].getBounds().width())) {
|
||||||
|
search.setText("");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
return search;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates action buttons.
|
||||||
|
*/
|
||||||
|
private LinearLayout createActionButtons(Context context, ListView listView, FlagAdapter adapter) {
|
||||||
|
LinearLayout row = new LinearLayout(context);
|
||||||
|
row.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
row.setGravity(Gravity.CENTER);
|
||||||
|
row.setLayoutParams(new LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
|
||||||
|
|
||||||
|
ImageButton selectAll = createButton(context, DRAWABLE_REVANCED_SETTINGS_SELECT_ALL,
|
||||||
|
() -> {
|
||||||
|
for (int i = 0, count = adapter.getCount(); i < count; i++) {
|
||||||
|
listView.setItemChecked(i, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ImageButton clearAll = createButton(context, DRAWABLE_REVANCED_SETTINGS_DESELECT_ALL,
|
||||||
|
() -> {
|
||||||
|
listView.clearChoices();
|
||||||
|
adapter.notifyDataSetChanged();
|
||||||
|
});
|
||||||
|
|
||||||
|
ImageButton copy = createButton(context, DRAWABLE_REVANCED_SETTINGS_COPY_ALL,
|
||||||
|
() -> {
|
||||||
|
List<String> items = new ArrayList<>();
|
||||||
|
SparseBooleanArray checked = listView.getCheckedItemPositions();
|
||||||
|
|
||||||
|
if (checked.size() > 0) {
|
||||||
|
for (int i = 0, count = adapter.getCount(); i < count; i++) {
|
||||||
|
if (checked.get(i)) {
|
||||||
|
items.add(adapter.getItem(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (Long flag : adapter.getFullFlags()) {
|
||||||
|
items.add(String.valueOf(flag));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Utils.setClipboard(TextUtils.join("\n", items));
|
||||||
|
|
||||||
|
Utils.showToastShort(str("revanced_debug_feature_flags_manager_toast_copied"));
|
||||||
|
});
|
||||||
|
|
||||||
|
row.addView(selectAll);
|
||||||
|
row.addView(clearAll);
|
||||||
|
row.addView(copy);
|
||||||
|
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the move buttons (left and right groups).
|
||||||
|
*/
|
||||||
|
private Pair<LinearLayout, LinearLayout> createMoveButtons(Context context,
|
||||||
|
ListView availableListView, ListView blockedListView,
|
||||||
|
TreeSet<Long> availableFlags, TreeSet<Long> blockedFlags,
|
||||||
|
TextView availableCountText, TextView blockedCountText) {
|
||||||
|
// Left group: >> >
|
||||||
|
LinearLayout leftButtons = new LinearLayout(context);
|
||||||
|
leftButtons.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
leftButtons.setGravity(Gravity.CENTER);
|
||||||
|
|
||||||
|
ImageButton moveAllRight = createButton(context, DRAWABLE_REVANCED_SETTINGS_ARROW_RIGHT_DOUBLE,
|
||||||
|
() -> moveFlags(availableListView, blockedListView, availableFlags, blockedFlags,
|
||||||
|
availableCountText, blockedCountText, true));
|
||||||
|
|
||||||
|
ImageButton moveOneRight = createButton(context, DRAWABLE_REVANCED_SETTINGS_ARROW_RIGHT_ONE,
|
||||||
|
() -> moveFlags(availableListView, blockedListView, availableFlags, blockedFlags,
|
||||||
|
availableCountText, blockedCountText, false));
|
||||||
|
|
||||||
|
leftButtons.addView(moveAllRight);
|
||||||
|
leftButtons.addView(moveOneRight);
|
||||||
|
|
||||||
|
// Right group: < <<
|
||||||
|
LinearLayout rightButtons = new LinearLayout(context);
|
||||||
|
rightButtons.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
rightButtons.setGravity(Gravity.CENTER);
|
||||||
|
|
||||||
|
ImageButton moveOneLeft = createButton(context, DRAWABLE_REVANCED_SETTINGS_ARROW_LEFT_ONE,
|
||||||
|
() -> moveFlags(blockedListView, availableListView, blockedFlags, availableFlags,
|
||||||
|
blockedCountText, availableCountText, false));
|
||||||
|
|
||||||
|
ImageButton moveAllLeft = createButton(context, DRAWABLE_REVANCED_SETTINGS_ARROW_LEFT_DOUBLE,
|
||||||
|
() -> moveFlags(blockedListView, availableListView, blockedFlags, availableFlags,
|
||||||
|
blockedCountText, availableCountText, true));
|
||||||
|
|
||||||
|
rightButtons.addView(moveOneLeft);
|
||||||
|
rightButtons.addView(moveAllLeft);
|
||||||
|
|
||||||
|
return new Pair<>(leftButtons, rightButtons);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a styled ImageButton.
|
||||||
|
*/
|
||||||
|
@SuppressLint("ResourceType")
|
||||||
|
private ImageButton createButton(Context context, int drawableResId, Runnable action) {
|
||||||
|
ImageButton button = new ImageButton(context);
|
||||||
|
|
||||||
|
button.setImageResource(drawableResId);
|
||||||
|
button.setScaleType(ImageView.ScaleType.CENTER);
|
||||||
|
int[] attrs = {android.R.attr.selectableItemBackgroundBorderless};
|
||||||
|
//noinspection Recycle
|
||||||
|
TypedArray ripple = context.obtainStyledAttributes(attrs);
|
||||||
|
button.setBackgroundDrawable(ripple.getDrawable(0));
|
||||||
|
ripple.close();
|
||||||
|
|
||||||
|
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(Dim.dp32, Dim.dp32);
|
||||||
|
params.setMargins(Dim.dp8, Dim.dp8, Dim.dp8, Dim.dp8);
|
||||||
|
button.setLayoutParams(params);
|
||||||
|
|
||||||
|
button.setOnClickListener(v -> action.run());
|
||||||
|
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom adapter with search filtering.
|
||||||
|
*/
|
||||||
|
private static class FlagAdapter extends ArrayAdapter<String> {
|
||||||
|
private final TreeSet<Long> fullFlags;
|
||||||
|
private String searchQuery = "";
|
||||||
|
|
||||||
|
public FlagAdapter(Context context, TreeSet<Long> fullFlags) {
|
||||||
|
super(context, android.R.layout.simple_list_item_multiple_choice, new ArrayList<>());
|
||||||
|
this.fullFlags = fullFlags;
|
||||||
|
updateFiltered();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSearchQuery(String query) {
|
||||||
|
searchQuery = query == null ? "" : query.trim();
|
||||||
|
updateFiltered();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateFiltered() {
|
||||||
|
clear();
|
||||||
|
for (Long flag : fullFlags) {
|
||||||
|
String flagString = String.valueOf(flag);
|
||||||
|
if (searchQuery.isEmpty() || flagString.contains(searchQuery)) {
|
||||||
|
add(flagString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refresh() {
|
||||||
|
updateFiltered();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Long> getFullFlags() {
|
||||||
|
return new ArrayList<>(fullFlags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a ListView with filtering, multi-select, and range selection.
|
||||||
|
*/
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
private Pair<ListView, FlagAdapter> createListView(Context context,
|
||||||
|
TreeSet<Long> flags, TextView countText) {
|
||||||
|
ListView listView = new ListView(context);
|
||||||
|
listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
|
||||||
|
listView.setDividerHeight(0);
|
||||||
|
|
||||||
|
FlagAdapter adapter = new FlagAdapter(context, flags);
|
||||||
|
listView.setAdapter(adapter);
|
||||||
|
|
||||||
|
final ListViewSelectionState state = new ListViewSelectionState();
|
||||||
|
|
||||||
|
listView.setOnItemClickListener((parent, view, position, id) -> {
|
||||||
|
if (!state.isRangeSelecting) {
|
||||||
|
state.lastClickedPosition = position;
|
||||||
|
} else {
|
||||||
|
state.isRangeSelecting = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
listView.setOnItemLongClickListener((parent, view, position, id) -> {
|
||||||
|
if (state.lastClickedPosition == -1) {
|
||||||
|
listView.setItemChecked(position, true);
|
||||||
|
state.lastClickedPosition = position;
|
||||||
|
} else {
|
||||||
|
int start = Math.min(state.lastClickedPosition, position);
|
||||||
|
int end = Math.max(state.lastClickedPosition, position);
|
||||||
|
for (int i = start; i <= end; i++) {
|
||||||
|
listView.setItemChecked(i, true);
|
||||||
|
}
|
||||||
|
state.isRangeSelecting = true;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
listView.setOnTouchListener((view, event) -> {
|
||||||
|
if (event.getAction() == MotionEvent.ACTION_UP && state.isRangeSelecting) {
|
||||||
|
state.isRangeSelecting = false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Pair<>(listView, adapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves selected or all flags from one list to another.
|
||||||
|
*
|
||||||
|
* @param fromListView Source ListView.
|
||||||
|
* @param toListView Destination ListView.
|
||||||
|
* @param fromFlags Source flag set.
|
||||||
|
* @param toFlags Destination flag set.
|
||||||
|
* @param fromCountText Header showing count of source items.
|
||||||
|
* @param toCountText Header showing count of destination items.
|
||||||
|
* @param moveAll If true, move all items; if false, move only selected.
|
||||||
|
*/
|
||||||
|
private void moveFlags(ListView fromListView, ListView toListView,
|
||||||
|
TreeSet<Long> fromFlags, TreeSet<Long> toFlags,
|
||||||
|
TextView fromCountText, TextView toCountText,
|
||||||
|
boolean moveAll) {
|
||||||
|
if (fromListView == null || toListView == null) return;
|
||||||
|
|
||||||
|
List<Long> flagsToMove = new ArrayList<>();
|
||||||
|
FlagAdapter fromAdapter = (FlagAdapter) fromListView.getAdapter();
|
||||||
|
|
||||||
|
if (moveAll) {
|
||||||
|
flagsToMove.addAll(fromFlags);
|
||||||
|
} else {
|
||||||
|
SparseBooleanArray checked = fromListView.getCheckedItemPositions();
|
||||||
|
for (int i = 0, count = fromAdapter.getCount(); i < count; i++) {
|
||||||
|
if (checked.get(i)) {
|
||||||
|
String item = fromAdapter.getItem(i);
|
||||||
|
if (item != null) {
|
||||||
|
flagsToMove.add(Long.parseLong(item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flagsToMove.isEmpty()) return;
|
||||||
|
|
||||||
|
for (Long flag : flagsToMove) {
|
||||||
|
fromFlags.remove(flag);
|
||||||
|
toFlags.add(flag);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear selections before refreshing.
|
||||||
|
fromListView.clearChoices();
|
||||||
|
toListView.clearChoices();
|
||||||
|
|
||||||
|
// Refresh both adapters.
|
||||||
|
fromAdapter.refresh();
|
||||||
|
((FlagAdapter) toListView.getAdapter()).refresh();
|
||||||
|
|
||||||
|
// Update headers.
|
||||||
|
updateHeaderCount(fromCountText, fromAdapter);
|
||||||
|
updateHeaderCount(toCountText, (FlagAdapter) toListView.getAdapter());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves blocked flags to settings.
|
||||||
|
*/
|
||||||
|
private void saveFlags(TreeSet<Long> blockedFlags) {
|
||||||
|
StringBuilder flagsString = new StringBuilder();
|
||||||
|
for (Long flag : blockedFlags) {
|
||||||
|
if (flagsString.length() > 0) {
|
||||||
|
flagsString.append("\n");
|
||||||
|
}
|
||||||
|
flagsString.append(flag);
|
||||||
|
}
|
||||||
|
|
||||||
|
BaseSettings.DISABLED_FEATURE_FLAGS.save(flagsString.toString());
|
||||||
|
Utils.showToastShort(str("revanced_debug_feature_flags_manager_toast_saved"));
|
||||||
|
Logger.printDebug(() -> "Feature flags saved. Blocked: " + blockedFlags.size());
|
||||||
|
|
||||||
|
AbstractPreferenceFragment.showRestartDialog(getContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets all blocked flags.
|
||||||
|
*/
|
||||||
|
private void resetFlags() {
|
||||||
|
BaseSettings.DISABLED_FEATURE_FLAGS.save("");
|
||||||
|
Utils.showToastShort(str("revanced_debug_feature_flags_manager_toast_reset"));
|
||||||
|
|
||||||
|
AbstractPreferenceFragment.showRestartDialog(getContext());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package app.revanced.extension.youtube.settings.preference;
|
package app.revanced.extension.shared.settings.preference;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
|
||||||
@@ -6,17 +6,17 @@ import android.content.Context;
|
|||||||
import android.preference.SwitchPreference;
|
import android.preference.SwitchPreference;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
import app.revanced.extension.shared.spoof.ClientType;
|
import app.revanced.extension.shared.spoof.ClientType;
|
||||||
import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch;
|
import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
|
||||||
|
|
||||||
@SuppressWarnings({"deprecation", "unused"})
|
@SuppressWarnings({"deprecation", "unused"})
|
||||||
public class ForceOriginalAudioSwitchPreference extends SwitchPreference {
|
public class ForceOriginalAudioSwitchPreference extends SwitchPreference {
|
||||||
|
|
||||||
// Spoof stream patch is not included, or is not currently spoofing to Android Studio.
|
// Spoof stream patch is not included, or is not currently spoofing to Android Studio.
|
||||||
private static final boolean available = !SpoofVideoStreamsPatch.isPatchIncluded()
|
private static final boolean available = !SpoofVideoStreamsPatch.isPatchIncluded()
|
||||||
|| !(Settings.SPOOF_VIDEO_STREAMS.get()
|
|| !(BaseSettings.SPOOF_VIDEO_STREAMS.get()
|
||||||
&& Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ClientType.ANDROID_CREATOR);
|
&& SpoofVideoStreamsPatch.getPreferredClient() == ClientType.ANDROID_CREATOR);
|
||||||
|
|
||||||
{
|
{
|
||||||
if (!available) {
|
if (!available) {
|
||||||
@@ -11,7 +11,6 @@ import android.preference.Preference;
|
|||||||
import android.text.InputType;
|
import android.text.InputType;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import android.util.TypedValue;
|
|
||||||
import android.view.inputmethod.InputMethodManager;
|
import android.view.inputmethod.InputMethodManager;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
@@ -35,7 +34,7 @@ public class ImportExportPreference extends EditTextPreference implements Prefer
|
|||||||
editText.setAutofillHints((String) null);
|
editText.setAutofillHints((String) null);
|
||||||
}
|
}
|
||||||
editText.setInputType(editText.getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
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.
|
editText.setTextSize(14);
|
||||||
|
|
||||||
setOnPreferenceClickListener(this);
|
setOnPreferenceClickListener(this);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package app.revanced.extension.shared.settings.preference;
|
package app.revanced.extension.shared.settings.preference;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
import static app.revanced.extension.shared.Utils.dipToPixels;
|
|
||||||
import static app.revanced.extension.shared.requests.Route.Method.GET;
|
import static app.revanced.extension.shared.requests.Route.Method.GET;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
|
import android.app.Activity;
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.app.ProgressDialog;
|
import android.app.ProgressDialog;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
@@ -40,6 +40,7 @@ import app.revanced.extension.shared.Logger;
|
|||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.requests.Requester;
|
import app.revanced.extension.shared.requests.Requester;
|
||||||
import app.revanced.extension.shared.requests.Route;
|
import app.revanced.extension.shared.requests.Route;
|
||||||
|
import app.revanced.extension.shared.ui.Dim;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a dialog showing official links.
|
* Opens a dialog showing official links.
|
||||||
@@ -125,6 +126,8 @@ public class ReVancedAboutPreference extends Preference {
|
|||||||
|
|
||||||
{
|
{
|
||||||
setOnPreferenceClickListener(pref -> {
|
setOnPreferenceClickListener(pref -> {
|
||||||
|
Context context = pref.getContext();
|
||||||
|
|
||||||
// Show a progress spinner if the social links are not fetched yet.
|
// Show a progress spinner if the social links are not fetched yet.
|
||||||
if (!AboutLinksRoutes.hasFetchedLinks() && Utils.isNetworkConnected()) {
|
if (!AboutLinksRoutes.hasFetchedLinks() && Utils.isNetworkConnected()) {
|
||||||
// Show a progress spinner, but only if the api fetch takes more than a half a second.
|
// Show a progress spinner, but only if the api fetch takes more than a half a second.
|
||||||
@@ -137,17 +140,18 @@ public class ReVancedAboutPreference extends Preference {
|
|||||||
handler.postDelayed(showDialogRunnable, delayToShowProgressSpinner);
|
handler.postDelayed(showDialogRunnable, delayToShowProgressSpinner);
|
||||||
|
|
||||||
Utils.runOnBackgroundThread(() ->
|
Utils.runOnBackgroundThread(() ->
|
||||||
fetchLinksAndShowDialog(handler, showDialogRunnable, progress));
|
fetchLinksAndShowDialog(context, handler, showDialogRunnable, progress));
|
||||||
} else {
|
} else {
|
||||||
// No network call required and can run now.
|
// No network call required and can run now.
|
||||||
fetchLinksAndShowDialog(null, null, null);
|
fetchLinksAndShowDialog(context, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void fetchLinksAndShowDialog(@Nullable Handler handler,
|
private void fetchLinksAndShowDialog(Context context,
|
||||||
|
@Nullable Handler handler,
|
||||||
Runnable showDialogRunnable,
|
Runnable showDialogRunnable,
|
||||||
@Nullable ProgressDialog progress) {
|
@Nullable ProgressDialog progress) {
|
||||||
WebLink[] links = AboutLinksRoutes.fetchAboutLinks();
|
WebLink[] links = AboutLinksRoutes.fetchAboutLinks();
|
||||||
@@ -164,7 +168,17 @@ public class ReVancedAboutPreference extends Preference {
|
|||||||
if (handler != null) {
|
if (handler != null) {
|
||||||
handler.removeCallbacks(showDialogRunnable);
|
handler.removeCallbacks(showDialogRunnable);
|
||||||
}
|
}
|
||||||
if (progress != null) {
|
|
||||||
|
// Don't continue if the activity is done. To test this tap the
|
||||||
|
// about dialog and immediately press back before the dialog can show.
|
||||||
|
if (context instanceof Activity activity) {
|
||||||
|
if (activity.isFinishing() || activity.isDestroyed()) {
|
||||||
|
Logger.printDebug(() -> "Not showing about dialog, activity is closed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progress != null && progress.isShowing()) {
|
||||||
progress.dismiss();
|
progress.dismiss();
|
||||||
}
|
}
|
||||||
new WebViewDialog(getContext(), htmlDialog).show();
|
new WebViewDialog(getContext(), htmlDialog).show();
|
||||||
@@ -208,11 +222,10 @@ class WebViewDialog extends Dialog {
|
|||||||
LinearLayout mainLayout = new LinearLayout(getContext());
|
LinearLayout mainLayout = new LinearLayout(getContext());
|
||||||
mainLayout.setOrientation(LinearLayout.VERTICAL);
|
mainLayout.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
|
||||||
final int padding = dipToPixels(10);
|
mainLayout.setPadding(Dim.dp10, Dim.dp10, Dim.dp10, Dim.dp10);
|
||||||
mainLayout.setPadding(padding, padding, padding, padding);
|
|
||||||
// Set rounded rectangle background.
|
// Set rounded rectangle background.
|
||||||
ShapeDrawable mainBackground = new ShapeDrawable(new RoundRectShape(
|
ShapeDrawable mainBackground = new ShapeDrawable(new RoundRectShape(
|
||||||
Utils.createCornerRadii(28), null, null));
|
Dim.roundedCorners(28), null, null));
|
||||||
mainBackground.getPaint().setColor(Utils.getDialogBackgroundColor());
|
mainBackground.getPaint().setColor(Utils.getDialogBackgroundColor());
|
||||||
mainLayout.setBackground(mainBackground);
|
mainLayout.setBackground(mainBackground);
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import android.graphics.Insets;
|
|||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.preference.Preference;
|
import android.preference.Preference;
|
||||||
|
import android.preference.PreferenceGroup;
|
||||||
import android.preference.PreferenceScreen;
|
import android.preference.PreferenceScreen;
|
||||||
import android.util.TypedValue;
|
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.Window;
|
import android.view.Window;
|
||||||
import android.view.WindowInsets;
|
import android.view.WindowInsets;
|
||||||
@@ -19,9 +19,28 @@ import androidx.annotation.Nullable;
|
|||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.BaseActivityHook;
|
import app.revanced.extension.shared.settings.BaseActivityHook;
|
||||||
|
import app.revanced.extension.shared.ui.Dim;
|
||||||
|
|
||||||
@SuppressWarnings({"deprecation", "NewApi"})
|
@SuppressWarnings({"deprecation", "NewApi"})
|
||||||
public class ToolbarPreferenceFragment extends AbstractPreferenceFragment {
|
public class ToolbarPreferenceFragment extends AbstractPreferenceFragment {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the list of preferences from this fragment, if they exist.
|
||||||
|
* @param keys Preference keys.
|
||||||
|
*/
|
||||||
|
protected void removePreferences(String ... keys) {
|
||||||
|
for (String key : keys) {
|
||||||
|
Preference pref = findPreference(key);
|
||||||
|
if (pref != null) {
|
||||||
|
PreferenceGroup parent = pref.getParent();
|
||||||
|
if (parent != null) {
|
||||||
|
Logger.printDebug(() -> "Removing preference: " + key);
|
||||||
|
parent.removePreference(pref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets toolbar for all nested preference screens.
|
* Sets toolbar for all nested preference screens.
|
||||||
*/
|
*/
|
||||||
@@ -69,14 +88,13 @@ public class ToolbarPreferenceFragment extends AbstractPreferenceFragment {
|
|||||||
toolbar.setNavigationIcon(getBackButtonDrawable());
|
toolbar.setNavigationIcon(getBackButtonDrawable());
|
||||||
toolbar.setNavigationOnClickListener(view -> preferenceScreenDialog.dismiss());
|
toolbar.setNavigationOnClickListener(view -> preferenceScreenDialog.dismiss());
|
||||||
|
|
||||||
final int margin = Utils.dipToPixels(16);
|
toolbar.setTitleMargin(Dim.dp16, 0, Dim.dp16, 0);
|
||||||
toolbar.setTitleMargin(margin, 0, margin, 0);
|
|
||||||
|
|
||||||
TextView toolbarTextView = Utils.getChildView(toolbar,
|
TextView toolbarTextView = Utils.getChildView(toolbar,
|
||||||
true, TextView.class::isInstance);
|
true, TextView.class::isInstance);
|
||||||
if (toolbarTextView != null) {
|
if (toolbarTextView != null) {
|
||||||
toolbarTextView.setTextColor(Utils.getAppForegroundColor());
|
toolbarTextView.setTextColor(Utils.getAppForegroundColor());
|
||||||
toolbarTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20);
|
toolbarTextView.setTextSize(20);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow package-specific toolbar customization.
|
// Allow package-specific toolbar customization.
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ public abstract class BaseSearchResultItem {
|
|||||||
|
|
||||||
// Shared method for highlighting text with search query.
|
// Shared method for highlighting text with search query.
|
||||||
protected static CharSequence highlightSearchQuery(CharSequence text, Pattern queryPattern) {
|
protected static CharSequence highlightSearchQuery(CharSequence text, Pattern queryPattern) {
|
||||||
if (TextUtils.isEmpty(text)) return text;
|
if (TextUtils.isEmpty(text) || queryPattern == null) return text;
|
||||||
|
|
||||||
final int adjustedColor = Utils.adjustColorBrightness(
|
final int adjustedColor = Utils.adjustColorBrightness(
|
||||||
Utils.getAppBackgroundColor(), 0.95f, 1.20f);
|
Utils.getAppBackgroundColor(), 0.95f, 1.20f);
|
||||||
@@ -84,7 +84,10 @@ public abstract class BaseSearchResultItem {
|
|||||||
|
|
||||||
Matcher matcher = queryPattern.matcher(text);
|
Matcher matcher = queryPattern.matcher(text);
|
||||||
while (matcher.find()) {
|
while (matcher.find()) {
|
||||||
spannable.setSpan(highlightSpan, matcher.start(), matcher.end(),
|
int start = matcher.start();
|
||||||
|
int end = matcher.end();
|
||||||
|
if (start == end) continue; // Skip zero matches.
|
||||||
|
spannable.setSpan(highlightSpan, start, end,
|
||||||
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
|
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,10 +227,14 @@ public abstract class BaseSearchResultItem {
|
|||||||
return searchBuilder.toString();
|
return searchBuilder.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends normalized searchable text to the builder.
|
||||||
|
* Uses full Unicode normalization for accurate search across all languages.
|
||||||
|
*/
|
||||||
private void appendText(StringBuilder builder, CharSequence text) {
|
private void appendText(StringBuilder builder, CharSequence text) {
|
||||||
if (!TextUtils.isEmpty(text)) {
|
if (!TextUtils.isEmpty(text)) {
|
||||||
if (builder.length() > 0) builder.append(" ");
|
if (builder.length() > 0) builder.append(" ");
|
||||||
builder.append(Utils.removePunctuationToLowercase(text));
|
builder.append(Utils.normalizeTextToLowercase(text));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,7 +279,7 @@ public abstract class BaseSearchResultItem {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
boolean matchesQuery(String query) {
|
boolean matchesQuery(String query) {
|
||||||
return searchableText.contains(Utils.removePunctuationToLowercase(query));
|
return searchableText.contains(Utils.normalizeTextToLowercase(query));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -484,7 +484,7 @@ public abstract class BaseSearchResultsAdapter extends ArrayAdapter<BaseSearchRe
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < adapter.getCount(); i++) {
|
for (int i = 0, count = adapter.getCount(); i < count; i++) {
|
||||||
Object item = adapter.getItem(i);
|
Object item = adapter.getItem(i);
|
||||||
if (item == targetPreference) {
|
if (item == targetPreference) {
|
||||||
return i;
|
return i;
|
||||||
@@ -522,8 +522,8 @@ public abstract class BaseSearchResultsAdapter extends ArrayAdapter<BaseSearchRe
|
|||||||
if (currentAnimator != null && currentAnimator.isRunning()) {
|
if (currentAnimator != null && currentAnimator.isRunning()) {
|
||||||
currentAnimator.cancel();
|
currentAnimator.cancel();
|
||||||
}
|
}
|
||||||
int startColor = Utils.getAppBackgroundColor();
|
final int startColor = Utils.getAppBackgroundColor();
|
||||||
int highlightColor = Utils.adjustColorBrightness(
|
final int highlightColor = Utils.adjustColorBrightness(
|
||||||
startColor,
|
startColor,
|
||||||
Utils.isDarkModeEnabled() ? 1.25f : 0.8f
|
Utils.isDarkModeEnabled() ? 1.25f : 0.8f
|
||||||
);
|
);
|
||||||
@@ -566,7 +566,7 @@ public abstract class BaseSearchResultsAdapter extends ArrayAdapter<BaseSearchRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
// First search on current level.
|
// First search on current level.
|
||||||
for (int i = 0; i < group.getPreferenceCount(); i++) {
|
for (int i = 0, count = group.getPreferenceCount(); i < count; i++) {
|
||||||
Preference pref = group.getPreference(i);
|
Preference pref = group.getPreference(i);
|
||||||
if (key.equals(pref.getKey())) {
|
if (key.equals(pref.getKey())) {
|
||||||
return pref;
|
return pref;
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import android.preference.PreferenceCategory;
|
|||||||
import android.preference.PreferenceGroup;
|
import android.preference.PreferenceGroup;
|
||||||
import android.preference.PreferenceScreen;
|
import android.preference.PreferenceScreen;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.TypedValue;
|
|
||||||
import android.view.Gravity;
|
import android.view.Gravity;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
@@ -45,6 +44,7 @@ import app.revanced.extension.shared.settings.Setting;
|
|||||||
import app.revanced.extension.shared.settings.preference.ColorPickerPreference;
|
import app.revanced.extension.shared.settings.preference.ColorPickerPreference;
|
||||||
import app.revanced.extension.shared.settings.preference.CustomDialogListPreference;
|
import app.revanced.extension.shared.settings.preference.CustomDialogListPreference;
|
||||||
import app.revanced.extension.shared.settings.preference.NoTitlePreferenceCategory;
|
import app.revanced.extension.shared.settings.preference.NoTitlePreferenceCategory;
|
||||||
|
import app.revanced.extension.shared.ui.Dim;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract controller for managing the overlay search view in ReVanced settings.
|
* Abstract controller for managing the overlay search view in ReVanced settings.
|
||||||
@@ -123,7 +123,7 @@ public abstract class BaseSearchViewController {
|
|||||||
searchView.setQueryHint(str("revanced_settings_search_hint"));
|
searchView.setQueryHint(str("revanced_settings_search_hint"));
|
||||||
|
|
||||||
// Set text size.
|
// Set text size.
|
||||||
searchEditText.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
|
searchEditText.setTextSize(16);
|
||||||
|
|
||||||
// Set cursor color.
|
// Set cursor color.
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
@@ -149,7 +149,7 @@ public abstract class BaseSearchViewController {
|
|||||||
// Create cursor drawable.
|
// Create cursor drawable.
|
||||||
GradientDrawable cursorDrawable = new GradientDrawable();
|
GradientDrawable cursorDrawable = new GradientDrawable();
|
||||||
cursorDrawable.setShape(GradientDrawable.RECTANGLE);
|
cursorDrawable.setShape(GradientDrawable.RECTANGLE);
|
||||||
cursorDrawable.setSize(Utils.dipToPixels(2), -1); // Width: 2dp, Height: match text height.
|
cursorDrawable.setSize(Dim.dp2, -1); // Width: 2dp, Height: match text height.
|
||||||
cursorDrawable.setColor(cursorColor);
|
cursorDrawable.setColor(cursorColor);
|
||||||
|
|
||||||
// Set cursor drawable.
|
// Set cursor drawable.
|
||||||
@@ -164,7 +164,7 @@ public abstract class BaseSearchViewController {
|
|||||||
overlayContainer = new FrameLayout(activity);
|
overlayContainer = new FrameLayout(activity);
|
||||||
overlayContainer.setVisibility(View.GONE);
|
overlayContainer.setVisibility(View.GONE);
|
||||||
overlayContainer.setBackgroundColor(Utils.getAppBackgroundColor());
|
overlayContainer.setBackgroundColor(Utils.getAppBackgroundColor());
|
||||||
overlayContainer.setElevation(Utils.dipToPixels(8));
|
overlayContainer.setElevation(Dim.dp8);
|
||||||
|
|
||||||
// Container for search results.
|
// Container for search results.
|
||||||
FrameLayout searchResultsContainer = new FrameLayout(activity);
|
FrameLayout searchResultsContainer = new FrameLayout(activity);
|
||||||
@@ -450,7 +450,7 @@ public abstract class BaseSearchViewController {
|
|||||||
|
|
||||||
filteredSearchItems.clear();
|
filteredSearchItems.clear();
|
||||||
|
|
||||||
String queryLower = Utils.removePunctuationToLowercase(query);
|
String queryLower = Utils.normalizeTextToLowercase(query);
|
||||||
Pattern queryPattern = Pattern.compile(Pattern.quote(queryLower), Pattern.CASE_INSENSITIVE);
|
Pattern queryPattern = Pattern.compile(Pattern.quote(queryLower), Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
// Clear highlighting only for items that were previously visible.
|
// Clear highlighting only for items that were previously visible.
|
||||||
@@ -669,7 +669,7 @@ public abstract class BaseSearchViewController {
|
|||||||
protected static GradientDrawable createBackgroundDrawable() {
|
protected static GradientDrawable createBackgroundDrawable() {
|
||||||
GradientDrawable background = new GradientDrawable();
|
GradientDrawable background = new GradientDrawable();
|
||||||
background.setShape(GradientDrawable.RECTANGLE);
|
background.setShape(GradientDrawable.RECTANGLE);
|
||||||
background.setCornerRadius(Utils.dipToPixels(28));
|
background.setCornerRadius(Dim.dp28);
|
||||||
background.setColor(getSearchViewBackground());
|
background.setColor(getSearchViewBackground());
|
||||||
return background;
|
return background;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ public enum ClientType {
|
|||||||
"132.0.6808.3",
|
"132.0.6808.3",
|
||||||
"1.61.48",
|
"1.61.48",
|
||||||
false,
|
false,
|
||||||
|
false,
|
||||||
"Android VR 1.61"
|
"Android VR 1.61"
|
||||||
),
|
),
|
||||||
/**
|
/**
|
||||||
@@ -50,8 +51,36 @@ public enum ClientType {
|
|||||||
"107.0.5284.2",
|
"107.0.5284.2",
|
||||||
"1.43.32",
|
"1.43.32",
|
||||||
ANDROID_VR_1_61_48.useAuth,
|
ANDROID_VR_1_61_48.useAuth,
|
||||||
|
ANDROID_VR_1_61_48.supportsMultiAudioTracks,
|
||||||
"Android VR 1.43"
|
"Android VR 1.43"
|
||||||
),
|
),
|
||||||
|
/**
|
||||||
|
* Video not playable: Paid / Movie / Private / Age-restricted.
|
||||||
|
* Note: The 'Authorization' key must be excluded from the header.
|
||||||
|
*
|
||||||
|
* According to TeamNewPipe in 2022, if the 'androidSdkVersion' field is missing,
|
||||||
|
* the GVS did not return a valid response:
|
||||||
|
* [NewPipe#8713 (comment)](https://github.com/TeamNewPipe/NewPipe/issues/8713#issuecomment-1207443550).
|
||||||
|
*
|
||||||
|
* According to the latest commit in yt-dlp, the GVS returns a valid response
|
||||||
|
* even if the 'androidSdkVersion' field is missing:
|
||||||
|
* [yt-dlp#14693](https://github.com/yt-dlp/yt-dlp/pull/14693).
|
||||||
|
*
|
||||||
|
* For some reason, PoToken is not required.
|
||||||
|
*/
|
||||||
|
ANDROID_NO_SDK(
|
||||||
|
3,
|
||||||
|
"ANDROID",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
Build.VERSION.RELEASE,
|
||||||
|
"20.05.46",
|
||||||
|
"com.google.android.youtube/20.05.46 (Linux; U; Android " + Build.VERSION.RELEASE + ") gzip",
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
"Android No SDK"
|
||||||
|
),
|
||||||
/**
|
/**
|
||||||
* Cannot play livestreams and lacks HDR, but can play videos with music and labeled "for children".
|
* Cannot play livestreams and lacks HDR, but can play videos with music and labeled "for children".
|
||||||
* <a href="https://dumps.tadiphone.dev/dumps/google/barbet">Google Pixel 9 Pro Fold</a>
|
* <a href="https://dumps.tadiphone.dev/dumps/google/barbet">Google Pixel 9 Pro Fold</a>
|
||||||
@@ -69,6 +98,7 @@ public enum ClientType {
|
|||||||
"132.0.6779.0",
|
"132.0.6779.0",
|
||||||
"23.47.101",
|
"23.47.101",
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
"Android Studio"
|
"Android Studio"
|
||||||
),
|
),
|
||||||
/**
|
/**
|
||||||
@@ -83,6 +113,7 @@ public enum ClientType {
|
|||||||
"0.1",
|
"0.1",
|
||||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15",
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15",
|
||||||
false,
|
false,
|
||||||
|
false,
|
||||||
"visionOS"
|
"visionOS"
|
||||||
),
|
),
|
||||||
/**
|
/**
|
||||||
@@ -107,6 +138,7 @@ public enum ClientType {
|
|||||||
"19.22.3",
|
"19.22.3",
|
||||||
"com.google.ios.youtube/19.22.3 (iPad7,6; U; CPU iPadOS 17_7_10 like Mac OS X; " + Locale.getDefault() + ")",
|
"com.google.ios.youtube/19.22.3 (iPad7,6; U; CPU iPadOS 17_7_10 like Mac OS X; " + Locale.getDefault() + ")",
|
||||||
false,
|
false,
|
||||||
|
true,
|
||||||
"iPadOS"
|
"iPadOS"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -180,6 +212,11 @@ public enum ClientType {
|
|||||||
*/
|
*/
|
||||||
public final boolean useAuth;
|
public final boolean useAuth;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the client supports multiple audio tracks.
|
||||||
|
*/
|
||||||
|
public final boolean supportsMultiAudioTracks;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Friendly name displayed in stats for nerds.
|
* Friendly name displayed in stats for nerds.
|
||||||
*/
|
*/
|
||||||
@@ -200,6 +237,7 @@ public enum ClientType {
|
|||||||
@NonNull String cronetVersion,
|
@NonNull String cronetVersion,
|
||||||
String clientVersion,
|
String clientVersion,
|
||||||
boolean useAuth,
|
boolean useAuth,
|
||||||
|
boolean supportsMultiAudioTracks,
|
||||||
String friendlyName) {
|
String friendlyName) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.clientName = clientName;
|
this.clientName = clientName;
|
||||||
@@ -213,6 +251,7 @@ public enum ClientType {
|
|||||||
this.cronetVersion = cronetVersion;
|
this.cronetVersion = cronetVersion;
|
||||||
this.clientVersion = clientVersion;
|
this.clientVersion = clientVersion;
|
||||||
this.useAuth = useAuth;
|
this.useAuth = useAuth;
|
||||||
|
this.supportsMultiAudioTracks = supportsMultiAudioTracks;
|
||||||
this.friendlyName = friendlyName;
|
this.friendlyName = friendlyName;
|
||||||
|
|
||||||
Locale defaultLocale = Locale.getDefault();
|
Locale defaultLocale = Locale.getDefault();
|
||||||
@@ -238,6 +277,7 @@ public enum ClientType {
|
|||||||
String clientVersion,
|
String clientVersion,
|
||||||
String userAgent,
|
String userAgent,
|
||||||
boolean useAuth,
|
boolean useAuth,
|
||||||
|
boolean supportsMultiAudioTracks,
|
||||||
String friendlyName) {
|
String friendlyName) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.clientName = clientName;
|
this.clientName = clientName;
|
||||||
@@ -248,6 +288,7 @@ public enum ClientType {
|
|||||||
this.clientVersion = clientVersion;
|
this.clientVersion = clientVersion;
|
||||||
this.userAgent = userAgent;
|
this.userAgent = userAgent;
|
||||||
this.useAuth = useAuth;
|
this.useAuth = useAuth;
|
||||||
|
this.supportsMultiAudioTracks = supportsMultiAudioTracks;
|
||||||
this.friendlyName = friendlyName;
|
this.friendlyName = friendlyName;
|
||||||
this.packageName = null;
|
this.packageName = null;
|
||||||
this.androidSdkVersion = null;
|
this.androidSdkVersion = null;
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ import app.revanced.extension.shared.Logger;
|
|||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.AppLanguage;
|
import app.revanced.extension.shared.settings.AppLanguage;
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
import app.revanced.extension.shared.settings.Setting;
|
|
||||||
import app.revanced.extension.shared.spoof.requests.StreamingDataRequest;
|
import app.revanced.extension.shared.spoof.requests.StreamingDataRequest;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public class SpoofVideoStreamsPatch {
|
public class SpoofVideoStreamsPatch {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Domain used for internet connectivity verification.
|
* Domain used for internet connectivity verification.
|
||||||
* It has an empty response body and is only used to check for a 204 response code.
|
* It has an empty response body and is only used to check for a 204 response code.
|
||||||
@@ -39,7 +39,7 @@ public class SpoofVideoStreamsPatch {
|
|||||||
@Nullable
|
@Nullable
|
||||||
private static volatile AppLanguage languageOverride;
|
private static volatile AppLanguage languageOverride;
|
||||||
|
|
||||||
private static volatile ClientType preferredClient = ClientType.ANDROID_VR_1_61_48;
|
private static volatile ClientType preferredClient = ClientType.ANDROID_VR_1_43_32;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return If this patch was included during patching.
|
* @return If this patch was included during patching.
|
||||||
@@ -54,8 +54,7 @@ public class SpoofVideoStreamsPatch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param language Language override for non-authenticated requests. If this is null then
|
* @param language Language override for non-authenticated requests.
|
||||||
* {@link BaseSettings#SPOOF_VIDEO_STREAMS_LANGUAGE} is used.
|
|
||||||
*/
|
*/
|
||||||
public static void setLanguageOverride(@Nullable AppLanguage language) {
|
public static void setLanguageOverride(@Nullable AppLanguage language) {
|
||||||
languageOverride = language;
|
languageOverride = language;
|
||||||
@@ -66,10 +65,14 @@ public class SpoofVideoStreamsPatch {
|
|||||||
StreamingDataRequest.setClientOrderToUse(availableClients, client);
|
StreamingDataRequest.setClientOrderToUse(availableClients, client);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ClientType getPreferredClient() {
|
||||||
|
return preferredClient;
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean spoofingToClientWithNoMultiAudioStreams() {
|
public static boolean spoofingToClientWithNoMultiAudioStreams() {
|
||||||
return isPatchIncluded()
|
return isPatchIncluded()
|
||||||
&& SPOOF_STREAMING_DATA
|
&& SPOOF_STREAMING_DATA
|
||||||
&& preferredClient != ClientType.IPADOS;
|
&& !preferredClient.supportsMultiAudioTracks;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -317,11 +320,4 @@ public class SpoofVideoStreamsPatch {
|
|||||||
|
|
||||||
return videoFormat;
|
return videoFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final class AudioStreamLanguageOverrideAvailability implements Setting.Availability {
|
|
||||||
@Override
|
|
||||||
public boolean isAvailable() {
|
|
||||||
return BaseSettings.SPOOF_VIDEO_STREAMS.get() && !preferredClient.useAuth;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package app.revanced.extension.shared.spoof.requests;
|
package app.revanced.extension.shared.spoof.requests;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_43_32;
|
|
||||||
|
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
@@ -13,7 +11,6 @@ import app.revanced.extension.shared.Logger;
|
|||||||
import app.revanced.extension.shared.requests.Requester;
|
import app.revanced.extension.shared.requests.Requester;
|
||||||
import app.revanced.extension.shared.requests.Route;
|
import app.revanced.extension.shared.requests.Route;
|
||||||
import app.revanced.extension.shared.settings.AppLanguage;
|
import app.revanced.extension.shared.settings.AppLanguage;
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
|
||||||
import app.revanced.extension.shared.spoof.ClientType;
|
import app.revanced.extension.shared.spoof.ClientType;
|
||||||
import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch;
|
import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch;
|
||||||
|
|
||||||
@@ -44,7 +41,7 @@ final class PlayerRoutes {
|
|||||||
AppLanguage language = SpoofVideoStreamsPatch.getLanguageOverride();
|
AppLanguage language = SpoofVideoStreamsPatch.getLanguageOverride();
|
||||||
if (language == null) {
|
if (language == null) {
|
||||||
// Force original audio has not overrode the language.
|
// Force original audio has not overrode the language.
|
||||||
language = BaseSettings.SPOOF_VIDEO_STREAMS_LANGUAGE.get();
|
language = AppLanguage.DEFAULT;
|
||||||
}
|
}
|
||||||
//noinspection ExtractMethodRecommender
|
//noinspection ExtractMethodRecommender
|
||||||
Locale streamLocale = language.getLocale();
|
Locale streamLocale = language.getLocale();
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package app.revanced.extension.shared.ui;
|
package app.revanced.extension.shared.ui;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.Utils.adjustColorBrightness;
|
import static app.revanced.extension.shared.Utils.adjustColorBrightness;
|
||||||
import static app.revanced.extension.shared.Utils.dipToPixels;
|
|
||||||
import static app.revanced.extension.shared.Utils.getAppBackgroundColor;
|
import static app.revanced.extension.shared.Utils.getAppBackgroundColor;
|
||||||
import static app.revanced.extension.shared.Utils.isDarkModeEnabled;
|
import static app.revanced.extension.shared.Utils.isDarkModeEnabled;
|
||||||
import static app.revanced.extension.shared.settings.preference.ColorPickerPreference.DISABLED_ALPHA;
|
import static app.revanced.extension.shared.settings.preference.ColorPickerPreference.DISABLED_ALPHA;
|
||||||
@@ -13,7 +12,7 @@ import android.view.View;
|
|||||||
import androidx.annotation.ColorInt;
|
import androidx.annotation.ColorInt;
|
||||||
|
|
||||||
public class ColorDot {
|
public class ColorDot {
|
||||||
private static final int STROKE_WIDTH = dipToPixels(1.5f); // Stroke width in dp.
|
private static final int STROKE_WIDTH = Dim.dp(1.5f);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a circular drawable with a main fill and a stroke.
|
* Creates a circular drawable with a main fill and a stroke.
|
||||||
@@ -55,7 +54,7 @@ public class ColorDot {
|
|||||||
targetView.setAlpha(enabled ? 1.0f : DISABLED_ALPHA);
|
targetView.setAlpha(enabled ? 1.0f : DISABLED_ALPHA);
|
||||||
if (!isDarkModeEnabled()) {
|
if (!isDarkModeEnabled()) {
|
||||||
targetView.setClipToOutline(true);
|
targetView.setClipToOutline(true);
|
||||||
targetView.setElevation(dipToPixels(2));
|
targetView.setElevation(Dim.dp2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package app.revanced.extension.shared.ui;
|
package app.revanced.extension.shared.ui;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.Utils.dipToPixels;
|
|
||||||
|
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
@@ -37,7 +35,6 @@ public class CustomDialog {
|
|||||||
private final Context context;
|
private final Context context;
|
||||||
private final Dialog dialog;
|
private final Dialog dialog;
|
||||||
private final LinearLayout mainLayout;
|
private final LinearLayout mainLayout;
|
||||||
private final int dip4, dip8, dip16, dip24, dip36;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a custom dialog with a styled layout, including a title, message, buttons, and an optional EditText.
|
* Creates a custom dialog with a styled layout, including a title, message, buttons, and an optional EditText.
|
||||||
@@ -93,13 +90,6 @@ public class CustomDialog {
|
|||||||
this.dialog = new Dialog(context);
|
this.dialog = new Dialog(context);
|
||||||
this.dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); // Remove default title bar.
|
this.dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); // Remove default title bar.
|
||||||
|
|
||||||
// Preset size constants.
|
|
||||||
dip4 = dipToPixels(4);
|
|
||||||
dip8 = dipToPixels(8);
|
|
||||||
dip16 = dipToPixels(16);
|
|
||||||
dip24 = dipToPixels(24);
|
|
||||||
dip36 = dipToPixels(36);
|
|
||||||
|
|
||||||
// Create main layout.
|
// Create main layout.
|
||||||
mainLayout = createMainLayout();
|
mainLayout = createMainLayout();
|
||||||
addTitle(title);
|
addTitle(title);
|
||||||
@@ -122,11 +112,11 @@ public class CustomDialog {
|
|||||||
private LinearLayout createMainLayout() {
|
private LinearLayout createMainLayout() {
|
||||||
LinearLayout layout = new LinearLayout(context);
|
LinearLayout layout = new LinearLayout(context);
|
||||||
layout.setOrientation(LinearLayout.VERTICAL);
|
layout.setOrientation(LinearLayout.VERTICAL);
|
||||||
layout.setPadding(dip24, dip16, dip24, dip24);
|
layout.setPadding(Dim.dp24, Dim.dp16, Dim.dp24, Dim.dp24);
|
||||||
|
|
||||||
// Set rounded rectangle background.
|
// Set rounded rectangle background.
|
||||||
ShapeDrawable background = new ShapeDrawable(new RoundRectShape(
|
ShapeDrawable background = new ShapeDrawable(new RoundRectShape(
|
||||||
Utils.createCornerRadii(28), null, null));
|
Dim.roundedCorners(28), null, null));
|
||||||
// Dialog background.
|
// Dialog background.
|
||||||
background.getPaint().setColor(Utils.getDialogBackgroundColor());
|
background.getPaint().setColor(Utils.getDialogBackgroundColor());
|
||||||
layout.setBackground(background);
|
layout.setBackground(background);
|
||||||
@@ -152,7 +142,7 @@ public class CustomDialog {
|
|||||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT);
|
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
params.setMargins(0, 0, 0, dip16);
|
params.setMargins(0, 0, 0, Dim.dp16);
|
||||||
titleView.setLayoutParams(params);
|
titleView.setLayoutParams(params);
|
||||||
|
|
||||||
mainLayout.addView(titleView);
|
mainLayout.addView(titleView);
|
||||||
@@ -180,9 +170,9 @@ public class CustomDialog {
|
|||||||
// EditText (if provided).
|
// EditText (if provided).
|
||||||
if (editText != null) {
|
if (editText != null) {
|
||||||
ShapeDrawable background = new ShapeDrawable(new RoundRectShape(
|
ShapeDrawable background = new ShapeDrawable(new RoundRectShape(
|
||||||
Utils.createCornerRadii(10), null, null));
|
Dim.roundedCorners(10), null, null));
|
||||||
background.getPaint().setColor(Utils.getEditTextBackground());
|
background.getPaint().setColor(Utils.getEditTextBackground());
|
||||||
scrollView.setPadding(dip8, dip8, dip8, dip8);
|
scrollView.setPadding(Dim.dp8, Dim.dp8, Dim.dp8, Dim.dp8);
|
||||||
scrollView.setBackground(background);
|
scrollView.setBackground(background);
|
||||||
scrollView.setClipToOutline(true);
|
scrollView.setClipToOutline(true);
|
||||||
|
|
||||||
@@ -241,7 +231,7 @@ public class CustomDialog {
|
|||||||
LinearLayout.LayoutParams buttonContainerParams = new LinearLayout.LayoutParams(
|
LinearLayout.LayoutParams buttonContainerParams = new LinearLayout.LayoutParams(
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
LinearLayout.LayoutParams.WRAP_CONTENT);
|
LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||||
buttonContainerParams.setMargins(0, dip16, 0, 0);
|
buttonContainerParams.setMargins(0, Dim.dp16, 0, 0);
|
||||||
buttonContainer.setLayoutParams(buttonContainerParams);
|
buttonContainer.setLayoutParams(buttonContainerParams);
|
||||||
|
|
||||||
List<Button> buttons = new ArrayList<>();
|
List<Button> buttons = new ArrayList<>();
|
||||||
@@ -289,12 +279,12 @@ public class CustomDialog {
|
|||||||
button.setEllipsize(TextUtils.TruncateAt.END);
|
button.setEllipsize(TextUtils.TruncateAt.END);
|
||||||
button.setGravity(Gravity.CENTER);
|
button.setGravity(Gravity.CENTER);
|
||||||
// Set internal padding.
|
// Set internal padding.
|
||||||
button.setPadding(dip16, 0, dip16, 0);
|
button.setPadding(Dim.dp16, 0, Dim.dp16, 0);
|
||||||
|
|
||||||
// Background color for OK button (inversion).
|
// Background color for OK button (inversion).
|
||||||
// Background color for Cancel or Neutral buttons.
|
// Background color for Cancel or Neutral buttons.
|
||||||
ShapeDrawable background = new ShapeDrawable(new RoundRectShape(
|
ShapeDrawable background = new ShapeDrawable(new RoundRectShape(
|
||||||
Utils.createCornerRadii(20), null, null));
|
Dim.roundedCorners(20), null, null));
|
||||||
background.getPaint().setColor(isOkButton
|
background.getPaint().setColor(isOkButton
|
||||||
? Utils.getOkButtonBackgroundColor()
|
? Utils.getOkButtonBackgroundColor()
|
||||||
: Utils.getCancelOrNeutralButtonBackgroundColor());
|
: Utils.getCancelOrNeutralButtonBackgroundColor());
|
||||||
@@ -331,20 +321,19 @@ public class CustomDialog {
|
|||||||
if (buttons.isEmpty()) return;
|
if (buttons.isEmpty()) return;
|
||||||
|
|
||||||
// Check if buttons fit in one row.
|
// Check if buttons fit in one row.
|
||||||
int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
|
|
||||||
int totalWidth = 0;
|
int totalWidth = 0;
|
||||||
for (Integer width : buttonWidths) {
|
for (Integer width : buttonWidths) {
|
||||||
totalWidth += width;
|
totalWidth += width;
|
||||||
}
|
}
|
||||||
if (buttonWidths.size() > 1) {
|
if (buttonWidths.size() > 1) {
|
||||||
// Add margins for gaps.
|
// Add margins for gaps.
|
||||||
totalWidth += (buttonWidths.size() - 1) * dip8;
|
totalWidth += (buttonWidths.size() - 1) * Dim.dp8;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Single button: stretch to full width.
|
// Single button: stretch to full width.
|
||||||
if (buttons.size() == 1) {
|
if (buttons.size() == 1) {
|
||||||
layoutSingleButton(buttonContainer, buttons.get(0));
|
layoutSingleButton(buttonContainer, buttons.get(0));
|
||||||
} else if (totalWidth <= screenWidth * 0.8) {
|
} else if (totalWidth <= Dim.pctWidth(80)) {
|
||||||
// Single row: Neutral, Cancel, OK.
|
// Single row: Neutral, Cancel, OK.
|
||||||
layoutButtonsInRow(buttonContainer, buttons, buttonWidths);
|
layoutButtonsInRow(buttonContainer, buttons, buttonWidths);
|
||||||
} else {
|
} else {
|
||||||
@@ -369,7 +358,7 @@ public class CustomDialog {
|
|||||||
|
|
||||||
button.setLayoutParams(new LinearLayout.LayoutParams(
|
button.setLayoutParams(new LinearLayout.LayoutParams(
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
dip36));
|
Dim.dp36));
|
||||||
singleContainer.addView(button);
|
singleContainer.addView(button);
|
||||||
buttonContainer.addView(singleContainer);
|
buttonContainer.addView(singleContainer);
|
||||||
}
|
}
|
||||||
@@ -405,17 +394,17 @@ public class CustomDialog {
|
|||||||
if (parent != null) parent.removeView(button);
|
if (parent != null) parent.removeView(button);
|
||||||
|
|
||||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
||||||
0, dip36, buttonWidths.get(i));
|
0, Dim.dp36, buttonWidths.get(i));
|
||||||
|
|
||||||
// Set margins based on button type and combination.
|
// Set margins based on button type and combination.
|
||||||
if (buttons.size() == 2) {
|
if (buttons.size() == 2) {
|
||||||
// Neutral + OK or Cancel + OK.
|
// Neutral + OK or Cancel + OK.
|
||||||
params.setMargins(i == 0 ? 0 : dip4, 0, i == 0 ? dip4 : 0, 0);
|
params.setMargins(i == 0 ? 0 : Dim.dp4, 0, i == 0 ? Dim.dp4 : 0, 0);
|
||||||
} else if (buttons.size() == 3) {
|
} else if (buttons.size() == 3) {
|
||||||
// Neutral.
|
// Neutral.
|
||||||
// Cancel.
|
// Cancel.
|
||||||
// OK.
|
// OK.
|
||||||
params.setMargins(i == 0 ? 0 : dip4, 0, i == 2 ? 0 : dip4, 0);
|
params.setMargins(i == 0 ? 0 : Dim.dp4, 0, i == 2 ? 0 : Dim.dp4, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
button.setLayoutParams(params);
|
button.setLayoutParams(params);
|
||||||
@@ -447,14 +436,14 @@ public class CustomDialog {
|
|||||||
singleContainer.setGravity(Gravity.CENTER);
|
singleContainer.setGravity(Gravity.CENTER);
|
||||||
singleContainer.setLayoutParams(new LinearLayout.LayoutParams(
|
singleContainer.setLayoutParams(new LinearLayout.LayoutParams(
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
dip36));
|
Dim.dp36));
|
||||||
|
|
||||||
ViewGroup parent = (ViewGroup) button.getParent();
|
ViewGroup parent = (ViewGroup) button.getParent();
|
||||||
if (parent != null) parent.removeView(button);
|
if (parent != null) parent.removeView(button);
|
||||||
|
|
||||||
button.setLayoutParams(new LinearLayout.LayoutParams(
|
button.setLayoutParams(new LinearLayout.LayoutParams(
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
dip36));
|
Dim.dp36));
|
||||||
singleContainer.addView(button);
|
singleContainer.addView(button);
|
||||||
buttonContainer.addView(singleContainer);
|
buttonContainer.addView(singleContainer);
|
||||||
|
|
||||||
@@ -463,7 +452,7 @@ public class CustomDialog {
|
|||||||
View spacer = new View(context);
|
View spacer = new View(context);
|
||||||
LinearLayout.LayoutParams spacerParams = new LinearLayout.LayoutParams(
|
LinearLayout.LayoutParams spacerParams = new LinearLayout.LayoutParams(
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
dip8);
|
Dim.dp8);
|
||||||
spacer.setLayoutParams(spacerParams);
|
spacer.setLayoutParams(spacerParams);
|
||||||
buttonContainer.addView(spacer);
|
buttonContainer.addView(spacer);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package app.revanced.extension.shared.ui;
|
||||||
|
|
||||||
|
import android.content.res.Resources;
|
||||||
|
import android.util.DisplayMetrics;
|
||||||
|
import android.util.TypedValue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class for converting design units (dp) and screen percentages to pixels.
|
||||||
|
*/
|
||||||
|
public final class Dim {
|
||||||
|
private Dim() {} // Prevent instantiation.
|
||||||
|
|
||||||
|
private static final DisplayMetrics METRICS = Resources.getSystem().getDisplayMetrics();
|
||||||
|
public static final int SCREEN_WIDTH = METRICS.widthPixels;
|
||||||
|
public static final int SCREEN_HEIGHT = METRICS.heightPixels;
|
||||||
|
|
||||||
|
// DP constants (density-independent pixels).
|
||||||
|
public static final int dp1 = dp(1);
|
||||||
|
public static final int dp2 = dp(2);
|
||||||
|
public static final int dp4 = dp(4);
|
||||||
|
public static final int dp6 = dp(6);
|
||||||
|
public static final int dp7 = dp(7);
|
||||||
|
public static final int dp8 = dp(8);
|
||||||
|
public static final int dp10 = dp(10);
|
||||||
|
public static final int dp12 = dp(12);
|
||||||
|
public static final int dp16 = dp(16);
|
||||||
|
public static final int dp20 = dp(20);
|
||||||
|
public static final int dp24 = dp(24);
|
||||||
|
public static final int dp28 = dp(28);
|
||||||
|
public static final int dp32 = dp(32);
|
||||||
|
public static final int dp36 = dp(36);
|
||||||
|
public static final int dp40 = dp(40);
|
||||||
|
public static final int dp48 = dp(48);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts dp (density-independent pixels) to actual device pixels.
|
||||||
|
* Uses Android's official TypedValue.applyDimension() for accurate rounding.
|
||||||
|
*
|
||||||
|
* @param dp The dp value to convert (supports float, e.g. 1.2f).
|
||||||
|
* @return The equivalent pixel value as int.
|
||||||
|
*/
|
||||||
|
public static int dp(float dp) {
|
||||||
|
return (int) TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP, dp, METRICS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a percentage of the screen height to pixels.
|
||||||
|
*
|
||||||
|
* @param percent The percentage (0–100).
|
||||||
|
* @return The pixel value corresponding to the percentage of screen height.
|
||||||
|
*/
|
||||||
|
public static int pctHeight(int percent) {
|
||||||
|
return (SCREEN_HEIGHT * percent) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a percentage of the screen width to pixels.
|
||||||
|
*
|
||||||
|
* @param percent The percentage (0–100).
|
||||||
|
* @return The pixel value corresponding to the percentage of screen width.
|
||||||
|
*/
|
||||||
|
public static int pctWidth(int percent) {
|
||||||
|
return (SCREEN_WIDTH * percent) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a percentage of the screen's portrait width (min side) to pixels.
|
||||||
|
*
|
||||||
|
* @param percent The percentage (0–100).
|
||||||
|
* @return The pixel value.
|
||||||
|
*/
|
||||||
|
public static int pctPortraitWidth(int percent) {
|
||||||
|
final int portraitWidth = Math.min(SCREEN_WIDTH, SCREEN_HEIGHT);
|
||||||
|
return (int) (portraitWidth * (percent / 100.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an array of corner radii for a rounded rectangle.
|
||||||
|
* All corners use the same radius.
|
||||||
|
*
|
||||||
|
* @param dp radius in density-independent pixels
|
||||||
|
* @return array of 8 floats: [top-left-x, top-left-y, top-right-x, top-right-y, ...]
|
||||||
|
*/
|
||||||
|
public static float[] roundedCorners(float dp) {
|
||||||
|
final float r = dp(dp);
|
||||||
|
return new float[]{r, r, r, r, r, r, r, r};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
package app.revanced.extension.shared.ui;
|
package app.revanced.extension.shared.ui;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.Utils.dipToPixels;
|
|
||||||
|
|
||||||
import android.animation.Animator;
|
import android.animation.Animator;
|
||||||
import android.animation.AnimatorListenerAdapter;
|
import android.animation.AnimatorListenerAdapter;
|
||||||
import android.animation.ValueAnimator;
|
import android.animation.ValueAnimator;
|
||||||
@@ -63,9 +61,8 @@ public class SheetBottomDialog {
|
|||||||
|
|
||||||
// Add top spacer.
|
// Add top spacer.
|
||||||
View spacer = new View(context);
|
View spacer = new View(context);
|
||||||
final int dip40 = dipToPixels(40);
|
|
||||||
LinearLayout.LayoutParams spacerParams = new LinearLayout.LayoutParams(
|
LinearLayout.LayoutParams spacerParams = new LinearLayout.LayoutParams(
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT, dip40);
|
LinearLayout.LayoutParams.MATCH_PARENT, Dim.dp40);
|
||||||
spacer.setLayoutParams(spacerParams);
|
spacer.setLayoutParams(spacerParams);
|
||||||
spacer.setClickable(true);
|
spacer.setClickable(true);
|
||||||
dragContainer.addView(spacer);
|
dragContainer.addView(spacer);
|
||||||
@@ -105,20 +102,15 @@ public class SheetBottomDialog {
|
|||||||
* @return A configured {@link DraggableLinearLayout} with a handle bar and styled background.
|
* @return A configured {@link DraggableLinearLayout} with a handle bar and styled background.
|
||||||
*/
|
*/
|
||||||
public static DraggableLinearLayout createMainLayout(@NonNull Context context, @Nullable Integer backgroundColor) {
|
public static DraggableLinearLayout createMainLayout(@NonNull Context context, @Nullable Integer backgroundColor) {
|
||||||
// Preset size constants.
|
|
||||||
final int dip4 = dipToPixels(4); // Handle bar height.
|
|
||||||
final int dip8 = dipToPixels(8); // Dialog padding.
|
|
||||||
final int dip40 = dipToPixels(40); // Handle bar width.
|
|
||||||
|
|
||||||
DraggableLinearLayout mainLayout = new DraggableLinearLayout(context);
|
DraggableLinearLayout mainLayout = new DraggableLinearLayout(context);
|
||||||
mainLayout.setOrientation(LinearLayout.VERTICAL);
|
mainLayout.setOrientation(LinearLayout.VERTICAL);
|
||||||
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
|
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||||
layoutParams.setMargins(dip8, 0, dip8, dip8);
|
layoutParams.setMargins(Dim.dp8, 0, Dim.dp8, Dim.dp8);
|
||||||
mainLayout.setLayoutParams(layoutParams);
|
mainLayout.setLayoutParams(layoutParams);
|
||||||
|
|
||||||
ShapeDrawable background = new ShapeDrawable(new RoundRectShape(
|
ShapeDrawable background = new ShapeDrawable(new RoundRectShape(
|
||||||
Utils.createCornerRadii(12), null, null));
|
Dim.roundedCorners(12), null, null));
|
||||||
int color = (backgroundColor != null) ? backgroundColor : Utils.getDialogBackgroundColor();
|
int color = (backgroundColor != null) ? backgroundColor : Utils.getDialogBackgroundColor();
|
||||||
background.getPaint().setColor(color);
|
background.getPaint().setColor(color);
|
||||||
mainLayout.setBackground(background);
|
mainLayout.setBackground(background);
|
||||||
@@ -127,14 +119,14 @@ public class SheetBottomDialog {
|
|||||||
LinearLayout handleContainer = new LinearLayout(context);
|
LinearLayout handleContainer = new LinearLayout(context);
|
||||||
LinearLayout.LayoutParams containerParams = new LinearLayout.LayoutParams(
|
LinearLayout.LayoutParams containerParams = new LinearLayout.LayoutParams(
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||||
containerParams.setMargins(0, dip8, 0, 0);
|
containerParams.setMargins(0, Dim.dp8, 0, 0);
|
||||||
handleContainer.setLayoutParams(containerParams);
|
handleContainer.setLayoutParams(containerParams);
|
||||||
handleContainer.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
|
handleContainer.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
|
||||||
View handleBar = new View(context);
|
View handleBar = new View(context);
|
||||||
ShapeDrawable handleBackground = new ShapeDrawable(new RoundRectShape(
|
ShapeDrawable handleBackground = new ShapeDrawable(new RoundRectShape(
|
||||||
Utils.createCornerRadii(4), null, null));
|
Dim.roundedCorners(4), null, null));
|
||||||
handleBackground.getPaint().setColor(Utils.adjustColorBrightness(color, 0.9f, 1.25f));
|
handleBackground.getPaint().setColor(Utils.adjustColorBrightness(color, 0.9f, 1.25f));
|
||||||
LinearLayout.LayoutParams handleParams = new LinearLayout.LayoutParams(dip40, dip4);
|
LinearLayout.LayoutParams handleParams = new LinearLayout.LayoutParams(Dim.dp40, Dim.dp4);
|
||||||
handleBar.setLayoutParams(handleParams);
|
handleBar.setLayoutParams(handleParams);
|
||||||
handleBar.setBackground(handleBackground);
|
handleBar.setBackground(handleBackground);
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import app.revanced.extension.spotify.shared.ComponentFilters.StringComponentFil
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@Deprecated(forRemoval = true)
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public final class HideCreateButtonPatch {
|
public final class HideCreateButtonPatch {
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,11 @@
|
|||||||
package app.revanced.extension.spotify.misc.privacy;
|
package app.revanced.extension.spotify.misc.privacy;
|
||||||
|
|
||||||
import android.net.Uri;
|
import app.revanced.extension.shared.privacy.LinkSanitizer;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public final class SanitizeSharingLinksPatch {
|
public final class SanitizeSharingLinksPatch {
|
||||||
|
|
||||||
/**
|
private static final LinkSanitizer sanitizer = new LinkSanitizer(
|
||||||
* Parameters that are considered undesirable and should be stripped away.
|
|
||||||
*/
|
|
||||||
private static final List<String> SHARE_PARAMETERS_TO_REMOVE = List.of(
|
|
||||||
"si", // Share tracking parameter.
|
"si", // Share tracking parameter.
|
||||||
"utm_source" // Share source, such as "copy-link".
|
"utm_source" // Share source, such as "copy-link".
|
||||||
);
|
);
|
||||||
@@ -20,25 +13,7 @@ public final class SanitizeSharingLinksPatch {
|
|||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
public static String sanitizeUrl(String url) {
|
public static String sanitizeSharingLink(String url) {
|
||||||
try {
|
return sanitizer.sanitizeUrlString(url);
|
||||||
Uri uri = Uri.parse(url);
|
|
||||||
Uri.Builder builder = uri.buildUpon().clearQuery();
|
|
||||||
|
|
||||||
for (String paramName : uri.getQueryParameterNames()) {
|
|
||||||
if (!SHARE_PARAMETERS_TO_REMOVE.contains(paramName)) {
|
|
||||||
for (String value : uri.getQueryParameters(paramName)) {
|
|
||||||
builder.appendQueryParameter(paramName, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String sanitizedUrl = builder.build().toString();
|
|
||||||
Logger.printInfo(() -> "Sanitized url " + url + " to " + sanitizedUrl);
|
|
||||||
return sanitizedUrl;
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "sanitizeUrl failure with " + url, ex);
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
5
extensions/strava/build.gradle.kts
Normal file
5
extensions/strava/build.gradle.kts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
dependencies {
|
||||||
|
compileOnly(project(":extensions:shared:library"))
|
||||||
|
compileOnly(project(":extensions:strava:stub"))
|
||||||
|
compileOnly(libs.okhttp)
|
||||||
|
}
|
||||||
1
extensions/strava/src/main/AndroidManifest.xml
Normal file
1
extensions/strava/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest/>
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
package app.revanced.extension.strava;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Environment;
|
||||||
|
import android.provider.MediaStore;
|
||||||
|
import android.webkit.MimeTypeMap;
|
||||||
|
|
||||||
|
import com.strava.core.data.MediaType;
|
||||||
|
import com.strava.photos.data.Media;
|
||||||
|
|
||||||
|
import okhttp3.*;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
|
||||||
|
@SuppressLint("NewApi")
|
||||||
|
public final class AddMediaDownloadPatch {
|
||||||
|
public static final int ACTION_DOWNLOAD = -1;
|
||||||
|
public static final int ACTION_OPEN_LINK = -2;
|
||||||
|
public static final int ACTION_COPY_LINK = -3;
|
||||||
|
|
||||||
|
private static final OkHttpClient client = new OkHttpClient();
|
||||||
|
|
||||||
|
public static boolean handleAction(int actionId, Media media) {
|
||||||
|
String url = getUrl(media);
|
||||||
|
switch (actionId) {
|
||||||
|
case ACTION_DOWNLOAD:
|
||||||
|
String name = media.getId();
|
||||||
|
if (media.getType() == MediaType.VIDEO) {
|
||||||
|
downloadVideo(url, name);
|
||||||
|
} else {
|
||||||
|
downloadPhoto(url, name);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
case ACTION_OPEN_LINK:
|
||||||
|
Utils.openLink(url);
|
||||||
|
return true;
|
||||||
|
case ACTION_COPY_LINK:
|
||||||
|
copyLink(url);
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void copyLink(CharSequence url) {
|
||||||
|
Utils.setClipboard(url);
|
||||||
|
showInfoToast("link_copied_to_clipboard", "🔗");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void downloadPhoto(String url, String name) {
|
||||||
|
showInfoToast("loading", "⏳");
|
||||||
|
Utils.runOnBackgroundThread(() -> {
|
||||||
|
try (Response response = fetch(url)) {
|
||||||
|
ResponseBody body = response.body();
|
||||||
|
String mimeType = body.contentType().toString();
|
||||||
|
String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
|
||||||
|
ContentResolver resolver = Utils.getContext().getContentResolver();
|
||||||
|
ContentValues values = new ContentValues();
|
||||||
|
values.put(MediaStore.Images.Media.DISPLAY_NAME, name + '.' + extension);
|
||||||
|
values.put(MediaStore.Images.Media.IS_PENDING, 1);
|
||||||
|
values.put(MediaStore.Images.Media.MIME_TYPE, mimeType);
|
||||||
|
values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/Strava");
|
||||||
|
Uri collection = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
|
||||||
|
? MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)
|
||||||
|
: MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
|
||||||
|
Uri row = resolver.insert(collection, values);
|
||||||
|
try (OutputStream outputStream = resolver.openOutputStream(row)) {
|
||||||
|
transferTo(body.byteStream(), outputStream);
|
||||||
|
} finally {
|
||||||
|
values.clear();
|
||||||
|
values.put(MediaStore.Images.Media.IS_PENDING, 0);
|
||||||
|
resolver.update(row, values, null);
|
||||||
|
}
|
||||||
|
showInfoToast("yis_2024_local_save_image_success", "✔️");
|
||||||
|
} catch (IOException e) {
|
||||||
|
showErrorToast("download_failure", "❌", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads a video in the M3U8 / HLS (HTTP Live Streaming) format.
|
||||||
|
*/
|
||||||
|
public static void downloadVideo(String url, String name) {
|
||||||
|
// The first request yields multiple URLs with different stream options.
|
||||||
|
// In case of Strava, the first one is always of highest quality.
|
||||||
|
// Each stream can consist of multiple chunks.
|
||||||
|
// The second request yields the URLs of all of these chunks.
|
||||||
|
// Fetch all of them concurrently and pipe their streams into the file in order.
|
||||||
|
showInfoToast("loading", "⏳");
|
||||||
|
Utils.runOnBackgroundThread(() -> {
|
||||||
|
try {
|
||||||
|
String highestQualityStreamUrl;
|
||||||
|
try (Response response = fetch(url)) {
|
||||||
|
highestQualityStreamUrl = replaceFileName(url, lines(response).findFirst().get());
|
||||||
|
}
|
||||||
|
List<Future<Response>> futures;
|
||||||
|
try (Response response = fetch(highestQualityStreamUrl)) {
|
||||||
|
futures = lines(response)
|
||||||
|
.map(line -> replaceFileName(highestQualityStreamUrl, line))
|
||||||
|
.map(chunkUrl -> Utils.submitOnBackgroundThread(() -> fetch(chunkUrl)))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
ContentResolver resolver = Utils.getContext().getContentResolver();
|
||||||
|
ContentValues values = new ContentValues();
|
||||||
|
values.put(MediaStore.Video.Media.DISPLAY_NAME, name + '.' + "mp4");
|
||||||
|
values.put(MediaStore.Video.Media.IS_PENDING, 1);
|
||||||
|
values.put(MediaStore.Video.Media.MIME_TYPE, MimeTypeMap.getSingleton().getMimeTypeFromExtension("mp4"));
|
||||||
|
values.put(MediaStore.Video.Media.RELATIVE_PATH, Environment.DIRECTORY_MOVIES + "/Strava");
|
||||||
|
Uri collection = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
|
||||||
|
? MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)
|
||||||
|
: MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
|
||||||
|
Uri row = resolver.insert(collection, values);
|
||||||
|
try (OutputStream outputStream = resolver.openOutputStream(row)) {
|
||||||
|
Throwable error = null;
|
||||||
|
for (Future<Response> future : futures) {
|
||||||
|
if (error != null) {
|
||||||
|
if (future.cancel(true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try (Response response = future.get()) {
|
||||||
|
if (error == null) {
|
||||||
|
transferTo(response.body().byteStream(), outputStream);
|
||||||
|
}
|
||||||
|
} catch (InterruptedException | IOException e) {
|
||||||
|
error = e;
|
||||||
|
} catch (ExecutionException e) {
|
||||||
|
error = e.getCause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (error != null) {
|
||||||
|
throw new IOException(error);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
values.clear();
|
||||||
|
values.put(MediaStore.Video.Media.IS_PENDING, 0);
|
||||||
|
resolver.update(row, values, null);
|
||||||
|
}
|
||||||
|
showInfoToast("yis_2024_local_save_video_success", "✔️");
|
||||||
|
} catch (IOException e) {
|
||||||
|
showErrorToast("download_failure", "❌", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getUrl(Media media) {
|
||||||
|
return media.getType() == MediaType.VIDEO
|
||||||
|
? ((Media.Video) media).getVideoUrl()
|
||||||
|
: media.getLargestUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getString(String name, String fallback) {
|
||||||
|
int id = Utils.getResourceIdentifier(name, "string");
|
||||||
|
return id != 0
|
||||||
|
? Utils.getResourceString(id)
|
||||||
|
: fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void showInfoToast(String resourceName, String fallback) {
|
||||||
|
String text = getString(resourceName, fallback);
|
||||||
|
Utils.showToastShort(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void showErrorToast(String resourceName, String fallback, IOException exception) {
|
||||||
|
String text = getString(resourceName, fallback);
|
||||||
|
Utils.showToastLong(text + ' ' + exception.getLocalizedMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Response fetch(String url) throws IOException {
|
||||||
|
Request request = new Request.Builder().url(url).build();
|
||||||
|
Response response = client.newCall(request).execute();
|
||||||
|
if (!response.isSuccessful()) {
|
||||||
|
throw new IOException("Got HTTP status code " + response.code());
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@code inputStream.transferTo(outputStream)} is "too new".
|
||||||
|
*/
|
||||||
|
private static void transferTo(InputStream in, OutputStream out) throws IOException {
|
||||||
|
byte[] buffer = new byte[1024 * 8];
|
||||||
|
int length;
|
||||||
|
while ((length = in.read(buffer)) != -1) {
|
||||||
|
out.write(buffer, 0, length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all file names.
|
||||||
|
*/
|
||||||
|
private static Stream<String> lines(Response response) {
|
||||||
|
BufferedReader reader = new BufferedReader(response.body().charStream());
|
||||||
|
return reader.lines().filter(line -> !line.startsWith("#"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String replaceFileName(String uri, String newName) {
|
||||||
|
return uri.substring(0, uri.lastIndexOf('/') + 1) + newName;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
extensions/strava/stub/build.gradle.kts
Normal file
12
extensions/strava/stub/build.gradle.kts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.library)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "app.revanced.extension"
|
||||||
|
compileSdk = 34
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 21
|
||||||
|
}
|
||||||
|
}
|
||||||
1
extensions/strava/stub/src/main/AndroidManifest.xml
Normal file
1
extensions/strava/stub/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest/>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.strava.core.data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
public interface MediaContent extends Serializable {
|
||||||
|
String getCaption();
|
||||||
|
|
||||||
|
String getId();
|
||||||
|
|
||||||
|
String getReferenceId();
|
||||||
|
|
||||||
|
MediaType getType();
|
||||||
|
|
||||||
|
void setCaption(String caption);
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.strava.core.data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
public final class MediaDimension implements Comparable<MediaDimension>, Serializable {
|
||||||
|
private final int height;
|
||||||
|
private final int width;
|
||||||
|
|
||||||
|
public MediaDimension(int width, int height) {
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getHeight() {
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getHeightScale() {
|
||||||
|
if (width <= 0 || height <= 0) {
|
||||||
|
return 1f;
|
||||||
|
}
|
||||||
|
return height / width;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getWidth() {
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getWidthScale() {
|
||||||
|
if (width <= 0 || height <= 0) {
|
||||||
|
return 1f;
|
||||||
|
}
|
||||||
|
return width / height;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isLandscape() {
|
||||||
|
return width > 0 && width >= height;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(MediaDimension other) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.strava.core.data;
|
||||||
|
|
||||||
|
public enum MediaType {
|
||||||
|
PHOTO(1),
|
||||||
|
VIDEO(2);
|
||||||
|
|
||||||
|
private final int remoteValue;
|
||||||
|
|
||||||
|
private MediaType(int remoteValue) {
|
||||||
|
this.remoteValue = remoteValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRemoteValue() {
|
||||||
|
return remoteValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.strava.core.data;
|
||||||
|
|
||||||
|
import java.util.SortedMap;
|
||||||
|
|
||||||
|
public interface RemoteMediaContent extends MediaContent {
|
||||||
|
MediaDimension getLargestSize();
|
||||||
|
|
||||||
|
String getLargestUrl();
|
||||||
|
|
||||||
|
SortedMap<Integer, MediaDimension> getSizes();
|
||||||
|
|
||||||
|
String getSmallestUrl();
|
||||||
|
|
||||||
|
RemoteMediaStatus getStatus();
|
||||||
|
|
||||||
|
SortedMap<Integer, String> getUrls();
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.strava.core.data;
|
||||||
|
|
||||||
|
public enum RemoteMediaStatus {
|
||||||
|
NEW,
|
||||||
|
PENDING,
|
||||||
|
PROCESSED,
|
||||||
|
REPORTED,
|
||||||
|
REINSTATED,
|
||||||
|
DELETED,
|
||||||
|
FAILED
|
||||||
|
}
|
||||||
@@ -0,0 +1,286 @@
|
|||||||
|
package com.strava.photos.data;
|
||||||
|
|
||||||
|
import com.strava.core.data.MediaDimension;
|
||||||
|
import com.strava.core.data.MediaType;
|
||||||
|
import com.strava.core.data.RemoteMediaContent;
|
||||||
|
import com.strava.core.data.RemoteMediaStatus;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
|
||||||
|
public abstract class Media implements RemoteMediaContent {
|
||||||
|
public static final class Photo extends Media {
|
||||||
|
private final Long activityId;
|
||||||
|
private final String activityName;
|
||||||
|
private final long athleteId;
|
||||||
|
private String caption;
|
||||||
|
private final String createdAt;
|
||||||
|
private final String createdAtLocal;
|
||||||
|
private final String cursor;
|
||||||
|
private final String id;
|
||||||
|
private final SortedMap<Integer, MediaDimension> sizes;
|
||||||
|
private final RemoteMediaStatus status;
|
||||||
|
private final String tag;
|
||||||
|
private final MediaType type;
|
||||||
|
private final SortedMap<Integer, String> urls;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Long getActivityId() {
|
||||||
|
return activityId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getActivityName() {
|
||||||
|
return activityName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getAthleteId() {
|
||||||
|
return athleteId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCaption() {
|
||||||
|
return caption;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCreatedAtLocal() {
|
||||||
|
return createdAtLocal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCursor() {
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SortedMap<Integer, MediaDimension> getSizes() {
|
||||||
|
return sizes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RemoteMediaStatus getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getTag() {
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MediaType getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SortedMap<Integer, String> getUrls() {
|
||||||
|
return urls;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setCaption(String caption) {
|
||||||
|
this.caption = caption;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Photo(String id,
|
||||||
|
String caption,
|
||||||
|
SortedMap<Integer, String> urls,
|
||||||
|
SortedMap<Integer, MediaDimension> sizes,
|
||||||
|
long athleteId,
|
||||||
|
String createdAt,
|
||||||
|
String createdAtLocal,
|
||||||
|
Long activityId,
|
||||||
|
String activityName,
|
||||||
|
RemoteMediaStatus status,
|
||||||
|
String tag,
|
||||||
|
String cursor) {
|
||||||
|
this.id = id;
|
||||||
|
this.caption = caption;
|
||||||
|
this.urls = urls;
|
||||||
|
this.sizes = sizes;
|
||||||
|
this.athleteId = athleteId;
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
this.createdAtLocal = createdAtLocal;
|
||||||
|
this.activityId = activityId;
|
||||||
|
this.activityName = activityName;
|
||||||
|
this.status = status;
|
||||||
|
this.tag = tag;
|
||||||
|
this.cursor = cursor;
|
||||||
|
this.type = MediaType.PHOTO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Video extends Media {
|
||||||
|
private final Long activityId;
|
||||||
|
private final String activityName;
|
||||||
|
private final long athleteId;
|
||||||
|
private String caption;
|
||||||
|
private final String createdAt;
|
||||||
|
private final String createdAtLocal;
|
||||||
|
private final String cursor;
|
||||||
|
private final Float durationSeconds;
|
||||||
|
private final String id;
|
||||||
|
private final SortedMap<Integer, MediaDimension> sizes;
|
||||||
|
private final RemoteMediaStatus status;
|
||||||
|
private final String tag;
|
||||||
|
private final MediaType type;
|
||||||
|
private final SortedMap<Integer, String> urls;
|
||||||
|
private final String videoUrl;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Long getActivityId() {
|
||||||
|
return activityId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getActivityName() {
|
||||||
|
return activityName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getAthleteId() {
|
||||||
|
return athleteId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCaption() {
|
||||||
|
return caption;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCreatedAtLocal() {
|
||||||
|
return createdAtLocal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCursor() {
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final Float getDurationSeconds() {
|
||||||
|
return durationSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SortedMap<Integer, MediaDimension> getSizes() {
|
||||||
|
return sizes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RemoteMediaStatus getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getTag() {
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MediaType getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SortedMap<Integer, String> getUrls() {
|
||||||
|
return urls;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final String getVideoUrl() {
|
||||||
|
return videoUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setCaption(String caption) {
|
||||||
|
this.caption = caption;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Video(String id,
|
||||||
|
String caption,
|
||||||
|
SortedMap<Integer, String> urls,
|
||||||
|
SortedMap<Integer, MediaDimension> sizes,
|
||||||
|
long athleteId,
|
||||||
|
String createdAt,
|
||||||
|
String createdAtLocal,
|
||||||
|
Long activityId,
|
||||||
|
String activityName,
|
||||||
|
RemoteMediaStatus status,
|
||||||
|
String videoUrl,
|
||||||
|
Float durationSeconds,
|
||||||
|
String tag,
|
||||||
|
String cursor) {
|
||||||
|
this.id = id;
|
||||||
|
this.caption = caption;
|
||||||
|
this.urls = urls;
|
||||||
|
this.sizes = sizes;
|
||||||
|
this.athleteId = athleteId;
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
this.createdAtLocal = createdAtLocal;
|
||||||
|
this.activityId = activityId;
|
||||||
|
this.activityName = activityName;
|
||||||
|
this.status = status;
|
||||||
|
this.videoUrl = videoUrl;
|
||||||
|
this.durationSeconds = durationSeconds;
|
||||||
|
this.tag = tag;
|
||||||
|
this.cursor = cursor;
|
||||||
|
this.type = MediaType.VIDEO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract Long getActivityId();
|
||||||
|
|
||||||
|
public abstract String getActivityName();
|
||||||
|
|
||||||
|
public abstract long getAthleteId();
|
||||||
|
|
||||||
|
public abstract String getCreatedAt();
|
||||||
|
|
||||||
|
public abstract String getCreatedAtLocal();
|
||||||
|
|
||||||
|
public abstract String getCursor();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MediaDimension getLargestSize() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getLargestUrl() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getReferenceId() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getSmallestUrl() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract String getTag();
|
||||||
|
|
||||||
|
private Media() {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,12 @@ public class ExtensionPreferenceCategory extends ConditionalPreferenceCategory {
|
|||||||
public void addPreferences(Context context) {
|
public void addPreferences(Context context) {
|
||||||
addPreference(new ReVancedTikTokAboutPreference(context));
|
addPreference(new ReVancedTikTokAboutPreference(context));
|
||||||
|
|
||||||
|
addPreference(new TogglePreference(context,
|
||||||
|
"Sanitize sharing links",
|
||||||
|
"Remove tracking parameters from shared links.",
|
||||||
|
BaseSettings.SANITIZE_SHARED_LINKS
|
||||||
|
));
|
||||||
|
|
||||||
addPreference(new TogglePreference(context,
|
addPreference(new TogglePreference(context,
|
||||||
"Enable debug log",
|
"Enable debug log",
|
||||||
"Show extension debug log.",
|
"Show extension debug log.",
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package app.revanced.extension.tiktok.share;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.privacy.LinkSanitizer;
|
||||||
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public final class ShareUrlSanitizer {
|
||||||
|
|
||||||
|
private static final LinkSanitizer sanitizer = new LinkSanitizer();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point for setting check.
|
||||||
|
*/
|
||||||
|
public static boolean shouldSanitize() {
|
||||||
|
return BaseSettings.SANITIZE_SHARED_LINKS.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point for URL sanitization.
|
||||||
|
*/
|
||||||
|
public static String sanitizeShareUrl(final String url) {
|
||||||
|
if (url == null || url.isEmpty()) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sanitizer.sanitizeUrlString(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,3 @@
|
|||||||
// Do not remove. Necessary for the extension plugin to be applied to the project.
|
dependencies {
|
||||||
|
compileOnly(project(":extensions:shared:library"))
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,16 +1,30 @@
|
|||||||
package app.revanced.twitter.patches.links;
|
package app.revanced.twitter.patches.links;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
public final class ChangeLinkSharingDomainPatch {
|
public final class ChangeLinkSharingDomainPatch {
|
||||||
private static final String DOMAIN_NAME = "https://fxtwitter.com";
|
private static final String LINK_FORMAT = "https://%s/%s/status/%s";
|
||||||
private static final String LINK_FORMAT = "%s/%s/status/%s";
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method is modified during patching. Do not change.
|
||||||
|
*/
|
||||||
|
private static String getShareDomain() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO remove this once changeLinkSharingDomainResourcePatch is restored
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
public static String formatResourceLink(Object... formatArgs) {
|
public static String formatResourceLink(Object... formatArgs) {
|
||||||
String username = (String) formatArgs[0];
|
String username = (String) formatArgs[0];
|
||||||
String tweetId = (String) formatArgs[1];
|
String tweetId = (String) formatArgs[1];
|
||||||
return String.format(LINK_FORMAT, DOMAIN_NAME, username, tweetId);
|
return String.format(LINK_FORMAT, getShareDomain(), username, tweetId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
public static String formatLink(long tweetId, String username) {
|
public static String formatLink(long tweetId, String username) {
|
||||||
return String.format(LINK_FORMAT, DOMAIN_NAME, username, tweetId);
|
return String.format(LINK_FORMAT, getShareDomain(), username, tweetId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,18 @@ package app.revanced.twitter.patches.links;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
@Deprecated(forRemoval = true)
|
||||||
public final class OpenLinksWithAppChooserPatch {
|
public final class OpenLinksWithAppChooserPatch {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
public static void openWithChooser(final Context context, final Intent intent) {
|
public static void openWithChooser(final Context context, final Intent intent) {
|
||||||
Log.d("ReVanced", "Opening intent with chooser: " + intent);
|
Logger.printInfo(() -> "Opening intent with chooser: " + intent);
|
||||||
|
|
||||||
intent.setAction("android.intent.action.VIEW");
|
intent.setAction("android.intent.action.VIEW");
|
||||||
|
|
||||||
|
|||||||
@@ -17,15 +17,25 @@ public class ChangeHeaderPatch {
|
|||||||
DEFAULT(null, null),
|
DEFAULT(null, null),
|
||||||
REGULAR("ytWordmarkHeader", "yt_ringo2_wordmark_header"),
|
REGULAR("ytWordmarkHeader", "yt_ringo2_wordmark_header"),
|
||||||
PREMIUM("ytPremiumWordmarkHeader", "yt_ringo2_premium_wordmark_header"),
|
PREMIUM("ytPremiumWordmarkHeader", "yt_ringo2_premium_wordmark_header"),
|
||||||
REVANCED("revanced_header_logo", "revanced_header_logo"),
|
ROUNDED("revanced_header_rounded"),
|
||||||
REVANCED_MINIMAL("revanced_header_logo_minimal", "revanced_header_logo_minimal"),
|
MINIMAL("revanced_header_minimal"),
|
||||||
CUSTOM("custom_header", "custom_header");
|
CUSTOM("revanced_header_custom"),
|
||||||
|
|
||||||
|
// Old enum names for data migration. TODO: Eventually delete these.
|
||||||
|
@Deprecated
|
||||||
|
REVANCED(ROUNDED.attributeName),
|
||||||
|
@Deprecated
|
||||||
|
REVANCED_MINIMAL(MINIMAL.attributeName);
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private final String attributeName;
|
private final String attributeName;
|
||||||
@Nullable
|
@Nullable
|
||||||
private final String drawableName;
|
private final String drawableName;
|
||||||
|
|
||||||
|
HeaderLogo(String attributeName) {
|
||||||
|
this(Objects.requireNonNull(attributeName), Objects.requireNonNull(attributeName));
|
||||||
|
}
|
||||||
|
|
||||||
HeaderLogo(@Nullable String attributeName, @Nullable String drawableName) {
|
HeaderLogo(@Nullable String attributeName, @Nullable String drawableName) {
|
||||||
this.attributeName = attributeName;
|
this.attributeName = attributeName;
|
||||||
this.drawableName = drawableName;
|
this.drawableName = drawableName;
|
||||||
@@ -42,9 +52,8 @@ public class ChangeHeaderPatch {
|
|||||||
|
|
||||||
final int identifier = Utils.getResourceIdentifier(attributeName, "attr");
|
final int identifier = Utils.getResourceIdentifier(attributeName, "attr");
|
||||||
if (identifier == 0) {
|
if (identifier == 0) {
|
||||||
// Identifier is zero if custom header setting was included in imported settings
|
// Should never happen.
|
||||||
// and a custom image was not included during patching.
|
Logger.printException(() -> "Could not find attribute: " + drawableName);
|
||||||
Logger.printDebug(() -> "Could not find attribute: " + drawableName);
|
|
||||||
Settings.HEADER_LOGO.resetToDefault();
|
Settings.HEADER_LOGO.resetToDefault();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -63,12 +72,14 @@ public class ChangeHeaderPatch {
|
|||||||
: "_light");
|
: "_light");
|
||||||
|
|
||||||
final int identifier = Utils.getResourceIdentifier(drawableFullName, "drawable");
|
final int identifier = Utils.getResourceIdentifier(drawableFullName, "drawable");
|
||||||
if (identifier == 0) {
|
if (identifier != 0) {
|
||||||
Logger.printDebug(() -> "Could not find drawable: " + drawableFullName);
|
return Utils.getContext().getDrawable(identifier);
|
||||||
Settings.HEADER_LOGO.resetToDefault();
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
return Utils.getContext().getDrawable(identifier);
|
|
||||||
|
// Should never happen.
|
||||||
|
Logger.printException(() -> "Could not find drawable: " + drawableFullName);
|
||||||
|
Settings.HEADER_LOGO.resetToDefault();
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,72 +1,17 @@
|
|||||||
package app.revanced.extension.youtube.patches;
|
package app.revanced.extension.youtube.patches;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
|
||||||
import app.revanced.extension.shared.settings.AppLanguage;
|
|
||||||
import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch;
|
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public class ForceOriginalAudioPatch {
|
public class ForceOriginalAudioPatch {
|
||||||
|
|
||||||
private static final String DEFAULT_AUDIO_TRACKS_SUFFIX = ".4";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
public static void setPreferredLanguage() {
|
public static void setEnabled() {
|
||||||
if (Settings.FORCE_ORIGINAL_AUDIO.get()
|
app.revanced.extension.shared.patches.ForceOriginalAudioPatch.setEnabled(
|
||||||
&& SpoofVideoStreamsPatch.spoofingToClientWithNoMultiAudioStreams()
|
Settings.FORCE_ORIGINAL_AUDIO.get(),
|
||||||
&& !Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get().useAuth) {
|
Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get()
|
||||||
// If client spoofing does not use authentication and lacks multi-audio streams,
|
);
|
||||||
// then can use any language code for the request and if that requested language is
|
|
||||||
// not available YT uses the original audio language. Authenticated requests ignore
|
|
||||||
// the language code and always use the account language. Use a language that is
|
|
||||||
// not auto-dubbed by YouTube: https://support.google.com/youtube/answer/15569972
|
|
||||||
// but the language is also supported natively by the Meta Quest device that
|
|
||||||
// Android VR is spoofing.
|
|
||||||
AppLanguage override = AppLanguage.SV;
|
|
||||||
Logger.printDebug(() -> "Setting language override: " + override);
|
|
||||||
SpoofVideoStreamsPatch.setLanguageOverride(override);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point.
|
|
||||||
*/
|
|
||||||
public static boolean ignoreDefaultAudioStream(boolean original) {
|
|
||||||
if (Settings.FORCE_ORIGINAL_AUDIO.get()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return original;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point.
|
|
||||||
*/
|
|
||||||
public static boolean isDefaultAudioStream(boolean isDefault, String audioTrackId, String audioTrackDisplayName) {
|
|
||||||
try {
|
|
||||||
if (!Settings.FORCE_ORIGINAL_AUDIO.get()) {
|
|
||||||
return isDefault;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (audioTrackId.isEmpty()) {
|
|
||||||
// Older app targets can have empty audio tracks and these might be placeholders.
|
|
||||||
// The real audio tracks are called after these.
|
|
||||||
return isDefault;
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.printDebug(() -> "default: " + String.format("%-5s", isDefault) + " id: "
|
|
||||||
+ String.format("%-8s", audioTrackId) + " name:" + audioTrackDisplayName);
|
|
||||||
|
|
||||||
final boolean isOriginal = audioTrackId.endsWith(DEFAULT_AUDIO_TRACKS_SUFFIX);
|
|
||||||
if (isOriginal) {
|
|
||||||
Logger.printDebug(() -> "Using audio: " + audioTrackId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return isOriginal;
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "isDefaultAudioStream failure", ex);
|
|
||||||
return isDefault;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package app.revanced.extension.youtube.patches;
|
||||||
|
|
||||||
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class PauseOnAudioInterruptPatch {
|
||||||
|
|
||||||
|
private static final int AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK = -3;
|
||||||
|
private static final int AUDIOFOCUS_LOSS_TRANSIENT = -2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point for AudioFocusRequest builder.
|
||||||
|
* Returns true if audio ducking should be disabled (willPauseWhenDucked = true).
|
||||||
|
*/
|
||||||
|
public static boolean shouldPauseOnAudioInterrupt() {
|
||||||
|
return Settings.PAUSE_ON_AUDIO_INTERRUPT.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point for onAudioFocusChange callback.
|
||||||
|
* Converts AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK to AUDIOFOCUS_LOSS_TRANSIENT
|
||||||
|
* when the setting is enabled, causing YouTube to pause instead of ducking.
|
||||||
|
*/
|
||||||
|
public static int overrideAudioFocusChange(int focusChange) {
|
||||||
|
if (Settings.PAUSE_ON_AUDIO_INTERRUPT.get() && focusChange == AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
|
||||||
|
return AUDIOFOCUS_LOSS_TRANSIENT;
|
||||||
|
}
|
||||||
|
return focusChange;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -131,11 +131,11 @@ public class ReturnYouTubeDislikePatch {
|
|||||||
|
|
||||||
String conversionContextString = conversionContext.toString();
|
String conversionContextString = conversionContext.toString();
|
||||||
|
|
||||||
if (isRollingNumber && !conversionContextString.contains("video_action_bar.eml")) {
|
if (isRollingNumber && !conversionContextString.contains("video_action_bar.e")) {
|
||||||
return original;
|
return original;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (conversionContextString.contains("segmented_like_dislike_button.eml")) {
|
if (conversionContextString.contains("segmented_like_dislike_button.e")) {
|
||||||
// Regular video.
|
// Regular video.
|
||||||
ReturnYouTubeDislike videoData = currentVideoData;
|
ReturnYouTubeDislike videoData = currentVideoData;
|
||||||
if (videoData == null) {
|
if (videoData == null) {
|
||||||
@@ -153,12 +153,12 @@ public class ReturnYouTubeDislikePatch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Utils.containsAny(conversionContextString,
|
if (Utils.containsAny(conversionContextString,
|
||||||
"|shorts_dislike_button.eml", "|reel_dislike_button.eml")) {
|
"|shorts_dislike_button.e", "|reel_dislike_button.e")) {
|
||||||
return getShortsSpan(original, true);
|
return getShortsSpan(original, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Utils.containsAny(conversionContextString,
|
if (Utils.containsAny(conversionContextString,
|
||||||
"|shorts_like_button.eml", "|reel_like_button.eml")) {
|
"|shorts_like_button.e", "|reel_like_button.e")) {
|
||||||
if (!Utils.containsNumber(original)) {
|
if (!Utils.containsNumber(original)) {
|
||||||
Logger.printDebug(() -> "Replacing hidden likes count");
|
Logger.printDebug(() -> "Replacing hidden likes count");
|
||||||
return getShortsSpan(original, false);
|
return getShortsSpan(original, false);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.view.View;
|
|||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.shared.ui.Dim;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
@@ -31,7 +32,7 @@ public final class WideSearchbarPatch {
|
|||||||
final int paddingRight = searchBarView.getPaddingRight();
|
final int paddingRight = searchBarView.getPaddingRight();
|
||||||
final int paddingTop = searchBarView.getPaddingTop();
|
final int paddingTop = searchBarView.getPaddingTop();
|
||||||
final int paddingBottom = searchBarView.getPaddingBottom();
|
final int paddingBottom = searchBarView.getPaddingBottom();
|
||||||
final int paddingStart = Utils.dipToPixels(8);
|
final int paddingStart = Dim.dp8;
|
||||||
|
|
||||||
if (Utils.isRightToLeftLocale()) {
|
if (Utils.isRightToLeftLocale()) {
|
||||||
searchBarView.setPadding(paddingLeft, paddingTop, paddingStart, paddingBottom);
|
searchBarView.setPadding(paddingLeft, paddingTop, paddingStart, paddingBottom);
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package app.revanced.extension.youtube.patches.announcements;
|
|||||||
|
|
||||||
import static android.text.Html.FROM_HTML_MODE_COMPACT;
|
import static android.text.Html.FROM_HTML_MODE_COMPACT;
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
import static app.revanced.extension.shared.Utils.dipToPixels;
|
|
||||||
import static app.revanced.extension.youtube.patches.announcements.requests.AnnouncementsRoutes.GET_LATEST_ANNOUNCEMENTS;
|
import static app.revanced.extension.youtube.patches.announcements.requests.AnnouncementsRoutes.GET_LATEST_ANNOUNCEMENTS;
|
||||||
import static app.revanced.extension.youtube.patches.announcements.requests.AnnouncementsRoutes.GET_LATEST_ANNOUNCEMENT_IDS;
|
import static app.revanced.extension.youtube.patches.announcements.requests.AnnouncementsRoutes.GET_LATEST_ANNOUNCEMENT_IDS;
|
||||||
|
|
||||||
@@ -24,6 +23,7 @@ import java.time.LocalDateTime;
|
|||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.requests.Requester;
|
import app.revanced.extension.shared.requests.Requester;
|
||||||
|
import app.revanced.extension.shared.ui.Dim;
|
||||||
import app.revanced.extension.youtube.patches.announcements.requests.AnnouncementsRoutes;
|
import app.revanced.extension.youtube.patches.announcements.requests.AnnouncementsRoutes;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
@@ -148,7 +148,7 @@ public final class AnnouncementsPatch {
|
|||||||
if (child instanceof TextView childTextView && finalTitle.equals(childTextView.getText().toString())) {
|
if (child instanceof TextView childTextView && finalTitle.equals(childTextView.getText().toString())) {
|
||||||
childTextView.setCompoundDrawablesWithIntrinsicBounds(
|
childTextView.setCompoundDrawablesWithIntrinsicBounds(
|
||||||
finalLevel.icon, 0, 0, 0);
|
finalLevel.icon, 0, 0, 0);
|
||||||
childTextView.setCompoundDrawablePadding(dipToPixels(8));
|
childTextView.setCompoundDrawablePadding(Dim.dp8);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -105,17 +105,17 @@ public final class AdsFilter extends Filter {
|
|||||||
Settings.HIDE_VIEW_PRODUCTS_BANNER,
|
Settings.HIDE_VIEW_PRODUCTS_BANNER,
|
||||||
"product_item",
|
"product_item",
|
||||||
"products_in_video",
|
"products_in_video",
|
||||||
"shopping_overlay.eml" // Video player overlay shopping links.
|
"shopping_overlay.e" // Video player overlay shopping links.
|
||||||
);
|
);
|
||||||
|
|
||||||
final var shoppingLinks = new StringFilterGroup(
|
final var shoppingLinks = new StringFilterGroup(
|
||||||
Settings.HIDE_SHOPPING_LINKS,
|
Settings.HIDE_SHOPPING_LINKS,
|
||||||
"shopping_description_shelf.eml"
|
"shopping_description_shelf.e"
|
||||||
);
|
);
|
||||||
|
|
||||||
playerShoppingShelf = new StringFilterGroup(
|
playerShoppingShelf = new StringFilterGroup(
|
||||||
Settings.HIDE_CREATOR_STORE_SHELF,
|
Settings.HIDE_CREATOR_STORE_SHELF,
|
||||||
"horizontal_shelf.eml"
|
"horizontal_shelf.e"
|
||||||
);
|
);
|
||||||
|
|
||||||
playerShoppingShelfBuffer = new ByteArrayFilterGroup(
|
playerShoppingShelfBuffer = new ByteArrayFilterGroup(
|
||||||
@@ -131,7 +131,7 @@ public final class AdsFilter extends Filter {
|
|||||||
final var merchandise = new StringFilterGroup(
|
final var merchandise = new StringFilterGroup(
|
||||||
Settings.HIDE_MERCHANDISE_BANNERS,
|
Settings.HIDE_MERCHANDISE_BANNERS,
|
||||||
"product_carousel",
|
"product_carousel",
|
||||||
"shopping_carousel.eml" // Channel profile shopping shelf.
|
"shopping_carousel.e" // Channel profile shopping shelf.
|
||||||
);
|
);
|
||||||
|
|
||||||
final var selfSponsor = new StringFilterGroup(
|
final var selfSponsor = new StringFilterGroup(
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ public final class AdvancedVideoQualityMenuFilter extends Filter {
|
|||||||
public AdvancedVideoQualityMenuFilter() {
|
public AdvancedVideoQualityMenuFilter() {
|
||||||
addPathCallbacks(new StringFilterGroup(
|
addPathCallbacks(new StringFilterGroup(
|
||||||
Settings.ADVANCED_VIDEO_QUALITY_MENU,
|
Settings.ADVANCED_VIDEO_QUALITY_MENU,
|
||||||
"quick_quality_sheet_content.eml-js"
|
"quick_quality_sheet_content.e"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import app.revanced.extension.youtube.settings.Settings;
|
|||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
final class ButtonsFilter extends Filter {
|
final class ButtonsFilter extends Filter {
|
||||||
private static final String COMPACT_CHANNEL_BAR_PATH_PREFIX = "compact_channel_bar.eml";
|
private static final String COMPACT_CHANNEL_BAR_PATH_PREFIX = "compact_channel_bar.e";
|
||||||
private static final String VIDEO_ACTION_BAR_PATH_PREFIX = "video_action_bar.eml";
|
private static final String VIDEO_ACTION_BAR_PATH_PREFIX = "video_action_bar.e";
|
||||||
private static final String VIDEO_ACTION_BAR_PATH = "video_action_bar.eml";
|
private static final String VIDEO_ACTION_BAR_PATH = "video_action_bar.e";
|
||||||
/**
|
/**
|
||||||
* Video bar path when the video information is collapsed. Seems to shown only with 20.14+
|
* Video bar path when the video information is collapsed. Seems to shown only with 20.14+
|
||||||
*/
|
*/
|
||||||
private static final String COMPACTIFY_VIDEO_ACTION_BAR_PATH = "compactify_video_action_bar.eml";
|
private static final String COMPACTIFY_VIDEO_ACTION_BAR_PATH = "compactify_video_action_bar.e";
|
||||||
private static final String ANIMATED_VECTOR_TYPE_PATH = "AnimatedVectorType";
|
private static final String ANIMATED_VECTOR_TYPE_PATH = "AnimatedVectorType";
|
||||||
|
|
||||||
private final StringFilterGroup likeSubscribeGlow;
|
private final StringFilterGroup likeSubscribeGlow;
|
||||||
@@ -28,12 +28,12 @@ final class ButtonsFilter extends Filter {
|
|||||||
|
|
||||||
likeSubscribeGlow = new StringFilterGroup(
|
likeSubscribeGlow = new StringFilterGroup(
|
||||||
Settings.DISABLE_LIKE_SUBSCRIBE_GLOW,
|
Settings.DISABLE_LIKE_SUBSCRIBE_GLOW,
|
||||||
"animated_button_border.eml"
|
"animated_button_border.e"
|
||||||
);
|
);
|
||||||
|
|
||||||
bufferFilterPathGroup = new StringFilterGroup(
|
bufferFilterPathGroup = new StringFilterGroup(
|
||||||
null,
|
null,
|
||||||
"|ContainerType|button.eml"
|
"|ContainerType|button.e"
|
||||||
);
|
);
|
||||||
|
|
||||||
addPathCallbacks(
|
addPathCallbacks(
|
||||||
@@ -45,7 +45,7 @@ final class ButtonsFilter extends Filter {
|
|||||||
),
|
),
|
||||||
new StringFilterGroup(
|
new StringFilterGroup(
|
||||||
Settings.HIDE_DOWNLOAD_BUTTON,
|
Settings.HIDE_DOWNLOAD_BUTTON,
|
||||||
"|download_button.eml"
|
"|download_button.e"
|
||||||
),
|
),
|
||||||
new StringFilterGroup(
|
new StringFilterGroup(
|
||||||
Settings.HIDE_SAVE_BUTTON,
|
Settings.HIDE_SAVE_BUTTON,
|
||||||
@@ -53,7 +53,7 @@ final class ButtonsFilter extends Filter {
|
|||||||
),
|
),
|
||||||
new StringFilterGroup(
|
new StringFilterGroup(
|
||||||
Settings.HIDE_CLIP_BUTTON,
|
Settings.HIDE_CLIP_BUTTON,
|
||||||
"|clip_button.eml"
|
"|clip_button.e"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user