mirror of
https://github.com/revanced/revanced-patches.git
synced 2025-12-07 18:03:55 +01:00
Compare commits
340 Commits
v5.0.1-dev
...
v5.10.1-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f9f668435 | ||
|
|
76fd33ca54 | ||
|
|
9a653e9c5a | ||
|
|
f81b658fb7 | ||
|
|
7ff39d89d6 | ||
|
|
78ab0ec2bd | ||
|
|
3ab67f1539 | ||
|
|
8652cd613f | ||
|
|
bc8388713c | ||
|
|
d4b2e3be3e | ||
|
|
57c48b7829 | ||
|
|
aaa7523ee4 | ||
|
|
785df4fe69 | ||
|
|
83208eb50d | ||
|
|
9437db11eb | ||
|
|
1843c8bf70 | ||
|
|
778b51fbff | ||
|
|
ee0fdcdf86 | ||
|
|
57cc73d9c4 | ||
|
|
043ebbb6d4 | ||
|
|
d5551923fc | ||
|
|
f844a1cd76 | ||
|
|
a7e3277cc1 | ||
|
|
6fa2deea69 | ||
|
|
dcca2a3697 | ||
|
|
018160fd9c | ||
|
|
680252967e | ||
|
|
e79eba81d9 | ||
|
|
a73db03671 | ||
|
|
055ad04281 | ||
|
|
aaeee4a895 | ||
|
|
654b339f66 | ||
|
|
64cdce28a6 | ||
|
|
d01b9a67c5 | ||
|
|
a72404eeab | ||
|
|
3ff104528e | ||
|
|
76bbd7ed2f | ||
|
|
2fdf0f85c1 | ||
|
|
1d12c4156d | ||
|
|
c43050dce8 | ||
|
|
8104bbd7d7 | ||
|
|
8487888e6b | ||
|
|
6721a284cd | ||
|
|
6cde702854 | ||
|
|
7c8efcaf41 | ||
|
|
350ee02e3b | ||
|
|
df2d070a43 | ||
|
|
8167aaccc8 | ||
|
|
f4989ed0a5 | ||
|
|
8f5a0531bc | ||
|
|
622554de14 | ||
|
|
66e330ffe6 | ||
|
|
2afcd3d63d | ||
|
|
80d7c78cf6 | ||
|
|
d85bcc3c16 | ||
|
|
21368ea696 | ||
|
|
e687d3ed37 | ||
|
|
064b859d39 | ||
|
|
89882ddaf8 | ||
|
|
41881ba161 | ||
|
|
0615990138 | ||
|
|
70532313db | ||
|
|
e5e897de77 | ||
|
|
1e57ce9658 | ||
|
|
fcad0ab5bb | ||
|
|
91471eccf9 | ||
|
|
d559f016c6 | ||
|
|
5a82d26f03 | ||
|
|
e2eae499d9 | ||
|
|
64919d6443 | ||
|
|
c6ffaf86ae | ||
|
|
3ee99b7bf1 | ||
|
|
6f9bf4873f | ||
|
|
29a73089a3 | ||
|
|
74ef1841eb | ||
|
|
0c544d28e3 | ||
|
|
b1e5b99b44 | ||
|
|
7b90baadb5 | ||
|
|
4a6f3c8555 | ||
|
|
e7c6943ca7 | ||
|
|
ae1b987c0d | ||
|
|
9496438da1 | ||
|
|
fa51631ea6 | ||
|
|
8bf7108001 | ||
|
|
030eece04a | ||
|
|
30009b723d | ||
|
|
53b25ea7e9 | ||
|
|
189e1c90c4 | ||
|
|
f01603b3f3 | ||
|
|
3db5651e5c | ||
|
|
f3c4d6fd64 | ||
|
|
29dbc9ffbf | ||
|
|
fa4aa54f0c | ||
|
|
1d89ada07f | ||
|
|
8c529abad5 | ||
|
|
4ade7c7329 | ||
|
|
f35247a872 | ||
|
|
4de768febf | ||
|
|
1a5c86db93 | ||
|
|
dbba795468 | ||
|
|
0a9320551d | ||
|
|
9fac1614e7 | ||
|
|
2de3523c59 | ||
|
|
ad1e40b130 | ||
|
|
094a6aa6de | ||
|
|
a14e03e4bb | ||
|
|
6f40b6d30f | ||
|
|
1711e1c39d | ||
|
|
25372828d1 | ||
|
|
f58245c6cd | ||
|
|
87e1c7f4c8 | ||
|
|
55d01c92d1 | ||
|
|
ca21a69550 | ||
|
|
634d0b4058 | ||
|
|
47ea8d5ec8 | ||
|
|
9509ed53f3 | ||
|
|
39542ddf55 | ||
|
|
e1741130af | ||
|
|
e54eb3ce87 | ||
|
|
0ae756b0fc | ||
|
|
77a0ac5c9c | ||
|
|
899121b9de | ||
|
|
838edb48e7 | ||
|
|
b2665c916a | ||
|
|
4b81f7009b | ||
|
|
1a4c39a2ee | ||
|
|
99334d1e53 | ||
|
|
2850a6ed4e | ||
|
|
f28eb5105b | ||
|
|
69bed4d9fa | ||
|
|
a5f1efac27 | ||
|
|
b51be82cff | ||
|
|
b8635d0b88 | ||
|
|
78699c8bbf | ||
|
|
aeedec7fed | ||
|
|
32b614696b | ||
|
|
a0b63dfa23 | ||
|
|
f0f53cf72f | ||
|
|
cdb68209d1 | ||
|
|
7369f7b8d5 | ||
|
|
db521b940b | ||
|
|
25d7cc68ae | ||
|
|
9495064e6e | ||
|
|
64864c2cdb | ||
|
|
ad0ffb3328 | ||
|
|
06800324aa | ||
|
|
ec746cb05a | ||
|
|
67c5530ea6 | ||
|
|
cd08717783 | ||
|
|
7bac023ea5 | ||
|
|
1d0ec98bec | ||
|
|
3c603fac2d | ||
|
|
20a7ad4715 | ||
|
|
25a60e305e | ||
|
|
c7f42d9a3c | ||
|
|
670f100a29 | ||
|
|
19140e5918 | ||
|
|
1dde485013 | ||
|
|
5efcdd31c8 | ||
|
|
e6529837cb | ||
|
|
fe07033444 | ||
|
|
246333f3dc | ||
|
|
d82b02e4f5 | ||
|
|
44995a9f15 | ||
|
|
c87c788a26 | ||
|
|
4ef30618d1 | ||
|
|
b23e6c39fc | ||
|
|
de26766543 | ||
|
|
9168b5eaaf | ||
|
|
c43b9b3b03 | ||
|
|
5e8dfed3e8 | ||
|
|
d67dbba76f | ||
|
|
5dc93156e0 | ||
|
|
5275413ab7 | ||
|
|
248c05b670 | ||
|
|
9e6669d962 | ||
|
|
9c81d01cc8 | ||
|
|
59654788fc | ||
|
|
4c44982cde | ||
|
|
a7aab9aeca | ||
|
|
7a8486f562 | ||
|
|
ccb6a7f161 | ||
|
|
c792edfb77 | ||
|
|
339cd6cc70 | ||
|
|
68304fd96a | ||
|
|
4033048c9b | ||
|
|
9525137800 | ||
|
|
0cf05fa2b0 | ||
|
|
a9bfaf44e2 | ||
|
|
7b08051371 | ||
|
|
b217ca9f9d | ||
|
|
9482092579 | ||
|
|
134c2e52bd | ||
|
|
c348b10a35 | ||
|
|
9a9ec7ef18 | ||
|
|
e746507339 | ||
|
|
862ca077db | ||
|
|
138d43b34b | ||
|
|
8d06a4a8ad | ||
|
|
d7ca7c1733 | ||
|
|
8e0b7db82a | ||
|
|
b9d7867cee | ||
|
|
11216cd942 | ||
|
|
b163e5f64d | ||
|
|
5c2bbd0671 | ||
|
|
2062660d60 | ||
|
|
2d9f08a08e | ||
|
|
78c51182f2 | ||
|
|
feac2ab439 | ||
|
|
32be03c28d | ||
|
|
6a345eee37 | ||
|
|
61be7731e3 | ||
|
|
8295356f88 | ||
|
|
3ec25778eb | ||
|
|
3faf0ac160 | ||
|
|
3ff559878b | ||
|
|
ed9c78da1e | ||
|
|
eefb59020e | ||
|
|
18f18849f3 | ||
|
|
b172c38284 | ||
|
|
5b15602896 | ||
|
|
89c45afcc6 | ||
|
|
3c47bfff1a | ||
|
|
6af8e1b625 | ||
|
|
c44a4af406 | ||
|
|
cb857b0fce | ||
|
|
e0322afbf0 | ||
|
|
5f02f583be | ||
|
|
6462fb8cba | ||
|
|
f9dcce927e | ||
|
|
69f9ab8345 | ||
|
|
dd400ac2a0 | ||
|
|
538ed6d876 | ||
|
|
5ff94dc34a | ||
|
|
b04a11a885 | ||
|
|
4983e021f9 | ||
|
|
bee917f4ed | ||
|
|
c94376bc4c | ||
|
|
87fe83aacf | ||
|
|
92d282e963 | ||
|
|
4a88f650c2 | ||
|
|
8b67716506 | ||
|
|
95d56b1529 | ||
|
|
b1f3b12fa1 | ||
|
|
cf4456c2ba | ||
|
|
d509a3f397 | ||
|
|
d1ae1f1da7 | ||
|
|
9c1c90864c | ||
|
|
5ae76f4df8 | ||
|
|
87eaf61ef1 | ||
|
|
35594d0a20 | ||
|
|
e3c54d8a64 | ||
|
|
06202c8807 | ||
|
|
53efe10222 | ||
|
|
decd3fcb47 | ||
|
|
c7692d7561 | ||
|
|
73c7c8c93a | ||
|
|
3a4a124f0b | ||
|
|
3015993f55 | ||
|
|
e04c681424 | ||
|
|
de492de77d | ||
|
|
fc5dcbd13c | ||
|
|
91a5c95f9a | ||
|
|
a7aa8de6a8 | ||
|
|
4ee70e3869 | ||
|
|
c912a662ab | ||
|
|
d3b3262a31 | ||
|
|
78390a8bca | ||
|
|
85bfa4ca91 | ||
|
|
9bcde94724 | ||
|
|
0cfd8e6760 | ||
|
|
3265372035 | ||
|
|
57a8e47041 | ||
|
|
cd476c1227 | ||
|
|
064be93ee2 | ||
|
|
f74fd7113f | ||
|
|
628afc22bc | ||
|
|
8686bd9f20 | ||
|
|
534996f251 | ||
|
|
ca4a16dbd8 | ||
|
|
e33082f765 | ||
|
|
18360464a9 | ||
|
|
968e6e9b69 | ||
|
|
02732ab432 | ||
|
|
77aea074a9 | ||
|
|
fe15213cf9 | ||
|
|
046bd3ec88 | ||
|
|
d6bc998365 | ||
|
|
545e16913a | ||
|
|
fafed099c5 | ||
|
|
a65bbebfdb | ||
|
|
1a910a2cf6 | ||
|
|
6d23a4e000 | ||
|
|
5c3c68406e | ||
|
|
b0c3709be7 | ||
|
|
cd19f976e7 | ||
|
|
c181135cc1 | ||
|
|
7f6775950e | ||
|
|
4b2abaf17e | ||
|
|
677b18c41a | ||
|
|
736b6a96b8 | ||
|
|
8c371d8579 | ||
|
|
abcaa6336a | ||
|
|
11537526a4 | ||
|
|
403116f591 | ||
|
|
a1d14cffe9 | ||
|
|
10f221f374 | ||
|
|
ba1aab6d4d | ||
|
|
01cc8e0abf | ||
|
|
518958350d | ||
|
|
a625309d1f | ||
|
|
a7fc08a491 | ||
|
|
97b129e088 | ||
|
|
8c6c8e0442 | ||
|
|
16c090d2c0 | ||
|
|
ed35a2a4a9 | ||
|
|
c3701c4b6e | ||
|
|
e0dc821c50 | ||
|
|
b9efb05271 | ||
|
|
2e3b3dca4b | ||
|
|
19eaee09d0 | ||
|
|
78f3fd6aa4 | ||
|
|
71ed37beb1 | ||
|
|
5aae234c43 | ||
|
|
17b5b2e384 | ||
|
|
462b61c2e9 | ||
|
|
f23b7fffc8 | ||
|
|
69c504ca2f | ||
|
|
fc4b0d7c39 | ||
|
|
02e66b3d43 | ||
|
|
a75c15b950 | ||
|
|
e4417455c9 | ||
|
|
5253f4bfa4 | ||
|
|
273bedc74c | ||
|
|
68ec011003 | ||
|
|
f3d1103287 | ||
|
|
50a3541e98 | ||
|
|
c6069a7ff6 | ||
|
|
b10b624b4b | ||
|
|
3e1b5cbaf5 |
8
.github/workflows/build_pull_request.yml
vendored
8
.github/workflows/build_pull_request.yml
vendored
@@ -28,4 +28,10 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: ./gradlew build --no-daemon
|
run: ./gradlew :patches:buildAndroid --no-daemon
|
||||||
|
|
||||||
|
- name: Upload artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: revanced-patches
|
||||||
|
path: patches/build/libs
|
||||||
|
|||||||
24
.github/workflows/pull_strings.yml
vendored
24
.github/workflows/pull_strings.yml
vendored
@@ -1,6 +1,8 @@
|
|||||||
name: Pull strings
|
name: Pull strings
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 */6 * * *"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -14,23 +16,29 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
|
||||||
ref: dev
|
ref: dev
|
||||||
|
fetch-depth: 0
|
||||||
|
clean: true
|
||||||
|
|
||||||
- name: Pull strings
|
- name: Pull strings
|
||||||
uses: crowdin/github-action@v2
|
uses: crowdin/github-action@v2
|
||||||
with:
|
with:
|
||||||
config: crowdin.yml
|
config: crowdin.yml
|
||||||
|
upload_sources: false
|
||||||
download_translations: true
|
download_translations: true
|
||||||
|
skip_ref_checkout: true
|
||||||
localization_branch_name: feat/translations
|
localization_branch_name: feat/translations
|
||||||
create_pull_request: true
|
create_pull_request: false
|
||||||
pull_request_title: "chore: Sync translations"
|
|
||||||
pull_request_body: "Sync translations from [crowdin.com/project/revanced](https://crowdin.com/project/revanced)"
|
|
||||||
pull_request_base_branch_name: "dev"
|
|
||||||
commit_message: "chore: Sync translations"
|
|
||||||
github_user_name: revanced-bot
|
|
||||||
github_user_email: github@revanced.app
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||||
|
|
||||||
|
- name: Open pull request
|
||||||
|
if: github.event_name == 'workflow_dispatch'
|
||||||
|
uses: repo-sync/pull-request@v2
|
||||||
|
with:
|
||||||
|
source_branch: feat/translations
|
||||||
|
destination_branch: dev
|
||||||
|
pr_title: "chore: Sync translations"
|
||||||
|
pr_body: "Sync translations from [crowdin.com/project/revanced](https://crowdin.com/project/revanced)"
|
||||||
|
|||||||
5
.github/workflows/push_strings.yml
vendored
5
.github/workflows/push_strings.yml
vendored
@@ -18,6 +18,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Preprocess strings
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: ./gradlew clean preprocessCrowdinStrings
|
||||||
|
|
||||||
- name: Push strings
|
- name: Push strings
|
||||||
uses: crowdin/github-action@v2
|
uses: crowdin/github-action@v2
|
||||||
with:
|
with:
|
||||||
|
|||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: ./gradlew build clean
|
run: ./gradlew :patches:buildAndroid clean
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
|
|||||||
1045
CHANGELOG.md
1045
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
|||||||
|
android.namespace = "app.revanced.extension"
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly(libs.annotation)
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
dependencies {
|
||||||
|
compileOnly(libs.annotation)
|
||||||
|
}
|
||||||
@@ -0,0 +1,334 @@
|
|||||||
|
package app.revanced.extension.all.misc.directory.documentsprovider;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.pm.ApplicationInfo;
|
||||||
|
import android.content.pm.ProviderInfo;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.MatrixCursor;
|
||||||
|
import android.os.CancellationSignal;
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
|
import android.provider.DocumentsContract;
|
||||||
|
import android.provider.DocumentsProvider;
|
||||||
|
import android.system.ErrnoException;
|
||||||
|
import android.system.Os;
|
||||||
|
import android.system.StructStat;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.webkit.MimeTypeMap;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A DocumentsProvider that allows access to the app's internal data directory.
|
||||||
|
*/
|
||||||
|
public class InternalDataDocumentsProvider extends DocumentsProvider {
|
||||||
|
private static final String[] rootColumns =
|
||||||
|
{"root_id", "mime_types", "flags", "icon", "title", "summary", "document_id"};
|
||||||
|
private static final String[] directoryColumns =
|
||||||
|
{"document_id", "mime_type", "_display_name", "last_modified", "flags",
|
||||||
|
"_size", "full_path", "lstat_info"};
|
||||||
|
private static final int S_IFLNK = 0x8000;
|
||||||
|
|
||||||
|
private String packageName;
|
||||||
|
private File dataDirectory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively delete a file or directory and all its children.
|
||||||
|
*
|
||||||
|
* @param root The file or directory to delete.
|
||||||
|
* @return True if the file or directory and all its children were successfully deleted.
|
||||||
|
*/
|
||||||
|
private static boolean deleteRecursively(File root) {
|
||||||
|
// If root is a directory, delete all children first
|
||||||
|
if (root.isDirectory()) {
|
||||||
|
try {
|
||||||
|
// Only delete recursively if the directory is not a symlink
|
||||||
|
if ((Os.lstat(root.getPath()).st_mode & S_IFLNK) != S_IFLNK) {
|
||||||
|
File[] files = root.listFiles();
|
||||||
|
if (files != null) {
|
||||||
|
for (File file : files) {
|
||||||
|
if (!deleteRecursively(file)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ErrnoException e) {
|
||||||
|
Log.e("InternalDocumentsProvider", "Failed to lstat " + root.getPath(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete file or empty directory
|
||||||
|
return root.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the MIME type of a file based on its extension.
|
||||||
|
*
|
||||||
|
* @param file The file to resolve the MIME type for.
|
||||||
|
* @return The MIME type of the file.
|
||||||
|
*/
|
||||||
|
private static String resolveMimeType(File file) {
|
||||||
|
if (file.isDirectory()) {
|
||||||
|
return DocumentsContract.Document.MIME_TYPE_DIR;
|
||||||
|
}
|
||||||
|
|
||||||
|
String name = file.getName();
|
||||||
|
int indexOfExtDot = name.lastIndexOf('.');
|
||||||
|
if (indexOfExtDot < 0) {
|
||||||
|
// No extension
|
||||||
|
return "application/octet-stream";
|
||||||
|
}
|
||||||
|
|
||||||
|
String extension = name.substring(indexOfExtDot + 1).toLowerCase();
|
||||||
|
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
||||||
|
return mimeType != null ? mimeType : "application/octet-stream";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final boolean onCreate() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void attachInfo(Context context, ProviderInfo providerInfo) {
|
||||||
|
super.attachInfo(context, providerInfo);
|
||||||
|
|
||||||
|
this.packageName = context.getPackageName();
|
||||||
|
this.dataDirectory = context.getFilesDir().getParentFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final String createDocument(String parentDocumentId, String mimeType, String displayName) throws FileNotFoundException {
|
||||||
|
File directory = resolveDocumentId(parentDocumentId);
|
||||||
|
File file = new File(directory, displayName);
|
||||||
|
|
||||||
|
// If file already exists, append a number to the name
|
||||||
|
int i = 2;
|
||||||
|
while (file.exists()) {
|
||||||
|
file = new File(directory, displayName + " (" + i + ")");
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create the file or directory
|
||||||
|
if (mimeType.equals(DocumentsContract.Document.MIME_TYPE_DIR) ? file.mkdir() : file.createNewFile()) {
|
||||||
|
// Return the document ID of the new entity
|
||||||
|
if (!parentDocumentId.endsWith("/")) {
|
||||||
|
parentDocumentId = parentDocumentId + "/";
|
||||||
|
}
|
||||||
|
return parentDocumentId + file.getName();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
// Do nothing. We are throwing a FileNotFoundException later if the file could not be created.
|
||||||
|
}
|
||||||
|
throw new FileNotFoundException("Failed to create document in " + parentDocumentId + " with name " + displayName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void deleteDocument(String documentId) throws FileNotFoundException {
|
||||||
|
File file = resolveDocumentId(documentId);
|
||||||
|
if (!deleteRecursively(file)) {
|
||||||
|
throw new FileNotFoundException("Failed to delete document " + documentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final String getDocumentType(String documentId) throws FileNotFoundException {
|
||||||
|
return resolveMimeType(resolveDocumentId(documentId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final boolean isChildDocument(String parentDocumentId, String documentId) {
|
||||||
|
return documentId.startsWith(parentDocumentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final String moveDocument(String sourceDocumentId, String sourceParentDocumentId, String targetParentDocumentId) throws FileNotFoundException {
|
||||||
|
File source = resolveDocumentId(sourceDocumentId);
|
||||||
|
File dest = resolveDocumentId(targetParentDocumentId);
|
||||||
|
|
||||||
|
File file = new File(dest, source.getName());
|
||||||
|
if (!file.exists() && source.renameTo(file)) {
|
||||||
|
// Return the new document ID
|
||||||
|
if (targetParentDocumentId.endsWith("/")) {
|
||||||
|
return targetParentDocumentId + file.getName();
|
||||||
|
}
|
||||||
|
return targetParentDocumentId + "/" + file.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new FileNotFoundException("Failed to move document from " + sourceDocumentId + " to " + targetParentDocumentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final ParcelFileDescriptor openDocument(String documentId, String mode, CancellationSignal signal) throws FileNotFoundException {
|
||||||
|
File file = resolveDocumentId(documentId);
|
||||||
|
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.parseMode(mode));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException {
|
||||||
|
if (parentDocumentId.endsWith("/")) {
|
||||||
|
parentDocumentId = parentDocumentId.substring(0, parentDocumentId.length() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projection == null) {
|
||||||
|
projection = directoryColumns;
|
||||||
|
}
|
||||||
|
|
||||||
|
MatrixCursor cursor = new MatrixCursor(projection);
|
||||||
|
File children = resolveDocumentId(parentDocumentId);
|
||||||
|
|
||||||
|
// Collect all children
|
||||||
|
File[] files = children.listFiles();
|
||||||
|
if (files != null) {
|
||||||
|
for (File file : files) {
|
||||||
|
addRowForDocument(cursor, parentDocumentId + "/" + file.getName(), file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException {
|
||||||
|
if (projection == null) {
|
||||||
|
projection = directoryColumns;
|
||||||
|
}
|
||||||
|
|
||||||
|
MatrixCursor cursor = new MatrixCursor(projection);
|
||||||
|
addRowForDocument(cursor, documentId, null);
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final Cursor queryRoots(String[] projection) {
|
||||||
|
ApplicationInfo info = Objects.requireNonNull(getContext()).getApplicationInfo();
|
||||||
|
String appName = info.loadLabel(getContext().getPackageManager()).toString();
|
||||||
|
|
||||||
|
if (projection == null) {
|
||||||
|
projection = rootColumns;
|
||||||
|
}
|
||||||
|
|
||||||
|
MatrixCursor cursor = new MatrixCursor(projection);
|
||||||
|
MatrixCursor.RowBuilder row = cursor.newRow();
|
||||||
|
row.add(DocumentsContract.Root.COLUMN_ROOT_ID, this.packageName);
|
||||||
|
row.add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, this.packageName);
|
||||||
|
row.add(DocumentsContract.Root.COLUMN_SUMMARY, this.packageName);
|
||||||
|
row.add(DocumentsContract.Root.COLUMN_FLAGS,
|
||||||
|
DocumentsContract.Root.FLAG_LOCAL_ONLY |
|
||||||
|
DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD);
|
||||||
|
row.add(DocumentsContract.Root.COLUMN_TITLE, appName);
|
||||||
|
row.add(DocumentsContract.Root.COLUMN_MIME_TYPES, "*/*");
|
||||||
|
row.add(DocumentsContract.Root.COLUMN_ICON, info.icon);
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void removeDocument(String documentId, String parentDocumentId) throws FileNotFoundException {
|
||||||
|
deleteDocument(documentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final String renameDocument(String documentId, String displayName) throws FileNotFoundException {
|
||||||
|
File file = resolveDocumentId(documentId);
|
||||||
|
if (!file.renameTo(new File(file.getParentFile(), displayName))) {
|
||||||
|
throw new FileNotFoundException("Failed to rename document from " + documentId + " to " + displayName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the new document ID
|
||||||
|
return documentId.substring(0, documentId.lastIndexOf('/', documentId.length() - 2)) + "/" + displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a file instance for a given document ID.
|
||||||
|
*
|
||||||
|
* @param fullContentPath The document ID to resolve.
|
||||||
|
* @return File object for the given document ID.
|
||||||
|
* @throws FileNotFoundException If the document ID is invalid or the file does not exist.
|
||||||
|
*/
|
||||||
|
private File resolveDocumentId(String fullContentPath) throws FileNotFoundException {
|
||||||
|
if (!fullContentPath.startsWith(this.packageName)) {
|
||||||
|
throw new FileNotFoundException(fullContentPath + " not found");
|
||||||
|
}
|
||||||
|
String path = fullContentPath.substring(this.packageName.length());
|
||||||
|
|
||||||
|
// Resolve the relative path within /data/data/{PKG}
|
||||||
|
File file;
|
||||||
|
if (path.equals("/") || path.isEmpty()) {
|
||||||
|
file = this.dataDirectory;
|
||||||
|
} else {
|
||||||
|
// Remove leading slash
|
||||||
|
String relativePath = path.substring(1);
|
||||||
|
file = new File(this.dataDirectory, relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file.exists()) {
|
||||||
|
throw new FileNotFoundException(fullContentPath + " not found");
|
||||||
|
}
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a row containing all file properties to a MatrixCursor for a given document ID.
|
||||||
|
*
|
||||||
|
* @param cursor The cursor to add the row to.
|
||||||
|
* @param documentId The document ID to add the row for.
|
||||||
|
* @param file The file to add the row for. If null, the file will be resolved from the document ID.
|
||||||
|
* @throws FileNotFoundException If the file does not exist.
|
||||||
|
*/
|
||||||
|
private void addRowForDocument(MatrixCursor cursor, String documentId, File file) throws FileNotFoundException {
|
||||||
|
if (file == null) {
|
||||||
|
file = resolveDocumentId(documentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
int flags = 0;
|
||||||
|
if (file.isDirectory()) {
|
||||||
|
// Prefer list view for directories
|
||||||
|
flags = flags | DocumentsContract.Document.FLAG_DIR_PREFERS_LAST_MODIFIED;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.canWrite()) {
|
||||||
|
if (file.isDirectory()) {
|
||||||
|
flags = flags | DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
flags = flags | DocumentsContract.Document.FLAG_SUPPORTS_WRITE |
|
||||||
|
DocumentsContract.Document.FLAG_SUPPORTS_DELETE |
|
||||||
|
DocumentsContract.Document.FLAG_SUPPORTS_RENAME |
|
||||||
|
DocumentsContract.Document.FLAG_SUPPORTS_MOVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
MatrixCursor.RowBuilder row = cursor.newRow();
|
||||||
|
row.add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, documentId);
|
||||||
|
row.add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, file.getName());
|
||||||
|
row.add(DocumentsContract.Document.COLUMN_SIZE, file.length());
|
||||||
|
row.add(DocumentsContract.Document.COLUMN_MIME_TYPE, resolveMimeType(file));
|
||||||
|
row.add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, file.lastModified());
|
||||||
|
row.add(DocumentsContract.Document.COLUMN_FLAGS, flags);
|
||||||
|
|
||||||
|
// Custom columns
|
||||||
|
row.add("full_path", file.getAbsolutePath());
|
||||||
|
|
||||||
|
// Add lstat column
|
||||||
|
String path = file.getPath();
|
||||||
|
try {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
StructStat lstat = Os.lstat(path);
|
||||||
|
sb.append(lstat.st_mode);
|
||||||
|
sb.append(";");
|
||||||
|
sb.append(lstat.st_uid);
|
||||||
|
sb.append(";");
|
||||||
|
sb.append(lstat.st_gid);
|
||||||
|
// Append symlink target if it is a symlink
|
||||||
|
if ((lstat.st_mode & S_IFLNK) == S_IFLNK) {
|
||||||
|
sb.append(";");
|
||||||
|
sb.append(Os.readlink(path));
|
||||||
|
}
|
||||||
|
row.add("lstat_info", sb.toString());
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Log.e("InternalDocumentsProvider", "Failed to get lstat info for " + path, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
android.namespace = "app.revanced.extension"
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly(libs.annotation)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
android.namespace = "app.revanced.extension"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<manifest/>
|
||||||
4
extensions/boostforreddit/build.gradle.kts
Normal file
4
extensions/boostforreddit/build.gradle.kts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
dependencies {
|
||||||
|
compileOnly(project(":extensions:shared:library"))
|
||||||
|
compileOnly(project(":extensions:boostforreddit:stub"))
|
||||||
|
}
|
||||||
1
extensions/boostforreddit/src/main/AndroidManifest.xml
Normal file
1
extensions/boostforreddit/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest/>
|
||||||
@@ -4,7 +4,9 @@ import com.rubenmayayo.reddit.ui.activities.WebViewActivity;
|
|||||||
|
|
||||||
import app.revanced.extension.shared.fixes.slink.BaseFixSLinksPatch;
|
import app.revanced.extension.shared.fixes.slink.BaseFixSLinksPatch;
|
||||||
|
|
||||||
/** @noinspection unused*/
|
/**
|
||||||
|
* @noinspection unused
|
||||||
|
*/
|
||||||
public class FixSLinksPatch extends BaseFixSLinksPatch {
|
public class FixSLinksPatch extends BaseFixSLinksPatch {
|
||||||
static {
|
static {
|
||||||
INSTANCE = new FixSLinksPatch();
|
INSTANCE = new FixSLinksPatch();
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<manifest/>
|
||||||
1
extensions/music/build.gradle.kts
Normal file
1
extensions/music/build.gradle.kts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
// Do not remove. Necessary for the extension plugin to be applied to the project.
|
||||||
1
extensions/music/src/main/AndroidManifest.xml
Normal file
1
extensions/music/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest/>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package app.revanced.extension.music.spoof;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @noinspection unused
|
||||||
|
*/
|
||||||
|
public class SpoofClientPatch {
|
||||||
|
private static final int CLIENT_TYPE_ID = 26;
|
||||||
|
private static final String CLIENT_VERSION = "6.21";
|
||||||
|
private static final String DEVICE_MODEL = "iPhone16,2";
|
||||||
|
private static final String OS_VERSION = "17.7.2.21H221";
|
||||||
|
|
||||||
|
public static int getClientId() {
|
||||||
|
return CLIENT_TYPE_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getClientVersion() {
|
||||||
|
return CLIENT_VERSION;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getClientModel() {
|
||||||
|
return DEVICE_MODEL;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getOsVersion() {
|
||||||
|
return OS_VERSION;
|
||||||
|
}
|
||||||
|
}
|
||||||
3
extensions/reddit/build.gradle.kts
Normal file
3
extensions/reddit/build.gradle.kts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
dependencies {
|
||||||
|
compileOnly(project(":extensions:reddit:stub"))
|
||||||
|
}
|
||||||
1
extensions/reddit/src/main/AndroidManifest.xml
Normal file
1
extensions/reddit/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest/>
|
||||||
@@ -5,8 +5,12 @@ import com.reddit.domain.model.ILink;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
public final class FilterPromotedLinksPatch {
|
public final class FilterPromotedLinksPatch {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*
|
||||||
* Filters list from promoted links.
|
* Filters list from promoted links.
|
||||||
**/
|
**/
|
||||||
public static List<?> filterChildren(final Iterable<?> links) {
|
public static List<?> filterChildren(final Iterable<?> links) {
|
||||||
17
extensions/reddit/stub/build.gradle.kts
Normal file
17
extensions/reddit/stub/build.gradle.kts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
plugins {
|
||||||
|
id(libs.plugins.android.library.get().pluginId)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "app.revanced.extension"
|
||||||
|
compileSdk = 33
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 24
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
|
}
|
||||||
|
}
|
||||||
1
extensions/reddit/stub/src/main/AndroidManifest.xml
Normal file
1
extensions/reddit/stub/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest/>
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
extension {
|
|
||||||
name = "extensions/all/screencapture/remove-screen-capture-restriction.rve"
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "app.revanced.extension"
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
compileOnly(libs.annotation)
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
extension {
|
|
||||||
name = "extensions/all/screenshot/remove-screenshot-restriction.rve"
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "app.revanced.extension"
|
|
||||||
}
|
|
||||||
@@ -1,22 +1,3 @@
|
|||||||
extension {
|
|
||||||
name = "extensions/shared.rve"
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "app.revanced.extension"
|
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
release {
|
|
||||||
isMinifyEnabled = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly(libs.appcompat)
|
implementation(project(":extensions:shared:library"))
|
||||||
compileOnly(libs.annotation)
|
|
||||||
compileOnly(libs.okhttp)
|
|
||||||
compileOnly(libs.retrofit)
|
|
||||||
|
|
||||||
compileOnly(project(":extensions:shared:stub"))
|
|
||||||
}
|
}
|
||||||
|
|||||||
21
extensions/shared/library/build.gradle.kts
Normal file
21
extensions/shared/library/build.gradle.kts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.library")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "app.revanced.extension"
|
||||||
|
compileSdk = 34
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 23
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly(libs.annotation)
|
||||||
|
}
|
||||||
@@ -24,7 +24,9 @@ import java.net.URL;
|
|||||||
* @noinspection unused
|
* @noinspection unused
|
||||||
*/
|
*/
|
||||||
public class GmsCoreSupport {
|
public class GmsCoreSupport {
|
||||||
public static final String ORIGINAL_UNPATCHED_PACKAGE_NAME = "com.google.android.youtube";
|
private static final String PACKAGE_NAME_YOUTUBE = "com.google.android.youtube";
|
||||||
|
private static final String PACKAGE_NAME_YOUTUBE_MUSIC = "com.google.android.apps.youtube.music";
|
||||||
|
|
||||||
private static final String 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
|
||||||
@@ -52,17 +54,20 @@ public class GmsCoreSupport {
|
|||||||
|
|
||||||
private static void showBatteryOptimizationDialog(Activity context,
|
private static void showBatteryOptimizationDialog(Activity context,
|
||||||
String dialogMessageRef,
|
String dialogMessageRef,
|
||||||
String positiveButtonStringRef,
|
String positiveButtonTextRef,
|
||||||
DialogInterface.OnClickListener onPositiveClickListener) {
|
DialogInterface.OnClickListener onPositiveClickListener) {
|
||||||
// Do not set cancelable to false, to allow using back button to skip the action,
|
// Use a delay to allow the activity to finish initializing.
|
||||||
// just in case the check can never be satisfied.
|
// Otherwise, if device is in dark mode the dialog is shown with wrong color scheme.
|
||||||
var dialog = new AlertDialog.Builder(context)
|
Utils.runOnMainThreadDelayed(() -> {
|
||||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
// Do not set cancelable to false, to allow using back button to skip the action,
|
||||||
.setTitle(str("gms_core_dialog_title"))
|
// just in case the battery change can never be satisfied.
|
||||||
.setMessage(str(dialogMessageRef))
|
var dialog = new AlertDialog.Builder(context)
|
||||||
.setPositiveButton(str(positiveButtonStringRef), onPositiveClickListener)
|
.setTitle(str("gms_core_dialog_title"))
|
||||||
.create();
|
.setMessage(str(dialogMessageRef))
|
||||||
Utils.showDialog(context, dialog);
|
.setPositiveButton(str(positiveButtonTextRef), onPositiveClickListener)
|
||||||
|
.create();
|
||||||
|
Utils.showDialog(context, dialog);
|
||||||
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -74,7 +79,8 @@ 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.
|
||||||
if (context.getPackageName().equals(ORIGINAL_UNPATCHED_PACKAGE_NAME)) {
|
String packageName = context.getPackageName();
|
||||||
|
if (packageName.equals(PACKAGE_NAME_YOUTUBE) || packageName.equals(PACKAGE_NAME_YOUTUBE_MUSIC)) {
|
||||||
Logger.printInfo(() -> "App is mounted with root, but GmsCore patch was included");
|
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 from the unpatched app and all patch strings are missing.
|
// resources from the unpatched app and all patch strings are missing.
|
||||||
@@ -99,7 +105,22 @@ public class GmsCoreSupport {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if GmsCore is running in the background.
|
// Check if GmsCore is whitelisted from battery optimizations.
|
||||||
|
if (isAndroidAutomotive(context)) {
|
||||||
|
// Ignore Android Automotive devices (Google built-in),
|
||||||
|
// as there is no way to disable battery optimizations.
|
||||||
|
Logger.printDebug(() -> "Device is Android Automotive");
|
||||||
|
} else if (batteryOptimizationsEnabled(context)) {
|
||||||
|
Logger.printInfo(() -> "GmsCore is not whitelisted from battery optimizations");
|
||||||
|
|
||||||
|
showBatteryOptimizationDialog(context,
|
||||||
|
"gms_core_dialog_not_whitelisted_using_battery_optimizations_message",
|
||||||
|
"gms_core_dialog_continue_text",
|
||||||
|
(dialog, id) -> openGmsCoreDisableBatteryOptimizationsIntent(context));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if GmsCore is currently running in the background.
|
||||||
try (var client = context.getContentResolver().acquireContentProviderClient(GMS_CORE_PROVIDER)) {
|
try (var client = context.getContentResolver().acquireContentProviderClient(GMS_CORE_PROVIDER)) {
|
||||||
if (client == null) {
|
if (client == null) {
|
||||||
Logger.printInfo(() -> "GmsCore is not running in the background");
|
Logger.printInfo(() -> "GmsCore is not running in the background");
|
||||||
@@ -108,18 +129,8 @@ public class GmsCoreSupport {
|
|||||||
"gms_core_dialog_not_whitelisted_not_allowed_in_background_message",
|
"gms_core_dialog_not_whitelisted_not_allowed_in_background_message",
|
||||||
"gms_core_dialog_open_website_text",
|
"gms_core_dialog_open_website_text",
|
||||||
(dialog, id) -> open(DONT_KILL_MY_APP_LINK));
|
(dialog, id) -> open(DONT_KILL_MY_APP_LINK));
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if GmsCore is whitelisted from battery optimizations.
|
|
||||||
if (batteryOptimizationsEnabled(context)) {
|
|
||||||
Logger.printInfo(() -> "GmsCore is not whitelisted from battery optimizations");
|
|
||||||
showBatteryOptimizationDialog(context,
|
|
||||||
"gms_core_dialog_not_whitelisted_using_battery_optimizations_message",
|
|
||||||
"gms_core_dialog_continue_text",
|
|
||||||
(dialog, id) -> openGmsCoreDisableBatteryOptimizationsIntent(context));
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "checkGmsCore failure", ex);
|
Logger.printException(() -> "checkGmsCore failure", ex);
|
||||||
}
|
}
|
||||||
@@ -140,15 +151,17 @@ public class GmsCoreSupport {
|
|||||||
return !powerManager.isIgnoringBatteryOptimizations(GMS_CORE_PACKAGE_NAME);
|
return !powerManager.isIgnoringBatteryOptimizations(GMS_CORE_PACKAGE_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isAndroidAutomotive(Context context) {
|
||||||
|
return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
|
||||||
|
}
|
||||||
|
|
||||||
private static String getGmsCoreDownload() {
|
private static String getGmsCoreDownload() {
|
||||||
final var vendorGroupId = getGmsCoreVendorGroupId();
|
final var vendorGroupId = getGmsCoreVendorGroupId();
|
||||||
//noinspection SwitchStatementWithTooFewBranches
|
//noinspection SwitchStatementWithTooFewBranches
|
||||||
switch (vendorGroupId) {
|
return switch (vendorGroupId) {
|
||||||
case "app.revanced":
|
case "app.revanced" -> "https://github.com/revanced/gmscore/releases/latest";
|
||||||
return "https://github.com/revanced/gmscore/releases/latest";
|
default -> vendorGroupId + ".android.gms";
|
||||||
default:
|
};
|
||||||
return vendorGroupId + ".android.gms";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modified by a patch. Do not touch.
|
// Modified by a patch. Do not touch.
|
||||||
@@ -4,8 +4,10 @@ import android.annotation.SuppressLint;
|
|||||||
import android.app.*;
|
import android.app.*;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.content.pm.ApplicationInfo;
|
||||||
import android.content.pm.PackageInfo;
|
import android.content.pm.PackageInfo;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
|
import android.content.res.Configuration;
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
import android.net.ConnectivityManager;
|
import android.net.ConnectivityManager;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
@@ -38,15 +40,18 @@ import java.util.concurrent.SynchronousQueue;
|
|||||||
import java.util.concurrent.ThreadPoolExecutor;
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.settings.AppLanguage;
|
||||||
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||||
import app.revanced.extension.shared.settings.preference.ReVancedAboutPreference;
|
import app.revanced.extension.shared.settings.preference.ReVancedAboutPreference;
|
||||||
|
|
||||||
public class Utils {
|
public class Utils {
|
||||||
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
@SuppressLint("StaticFieldLeak")
|
||||||
private static Context context;
|
private static volatile Context context;
|
||||||
|
|
||||||
private static String versionName;
|
private static String versionName;
|
||||||
|
private static String applicationLabel;
|
||||||
|
|
||||||
private Utils() {
|
private Utils() {
|
||||||
} // utility class
|
} // utility class
|
||||||
@@ -61,28 +66,30 @@ public class Utils {
|
|||||||
return ""; // Value is replaced during patching.
|
return ""; // Value is replaced during patching.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static PackageInfo getPackageInfo() throws PackageManager.NameNotFoundException {
|
||||||
|
final var packageName = Objects.requireNonNull(getContext()).getPackageName();
|
||||||
|
|
||||||
|
PackageManager packageManager = context.getPackageManager();
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
return packageManager.getPackageInfo(
|
||||||
|
packageName,
|
||||||
|
PackageManager.PackageInfoFlags.of(0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return packageManager.getPackageInfo(
|
||||||
|
packageName,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return The version name of the app, such as 19.11.43
|
* @return The version name of the app, such as 19.11.43
|
||||||
*/
|
*/
|
||||||
public static String getAppVersionName() {
|
public static String getAppVersionName() {
|
||||||
if (versionName == null) {
|
if (versionName == null) {
|
||||||
try {
|
try {
|
||||||
final var packageName = Objects.requireNonNull(getContext()).getPackageName();
|
versionName = getPackageInfo().versionName;
|
||||||
|
|
||||||
PackageManager packageManager = context.getPackageManager();
|
|
||||||
PackageInfo packageInfo;
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
packageInfo = packageManager.getPackageInfo(
|
|
||||||
packageName,
|
|
||||||
PackageManager.PackageInfoFlags.of(0)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
packageInfo = packageManager.getPackageInfo(
|
|
||||||
packageName,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
versionName = packageInfo.versionName;
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "Failed to get package info", ex);
|
Logger.printException(() -> "Failed to get package info", ex);
|
||||||
versionName = "Unknown";
|
versionName = "Unknown";
|
||||||
@@ -92,6 +99,19 @@ public class Utils {
|
|||||||
return versionName;
|
return versionName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String getApplicationName() {
|
||||||
|
if (applicationLabel == null) {
|
||||||
|
try {
|
||||||
|
ApplicationInfo applicationInfo = getPackageInfo().applicationInfo;
|
||||||
|
applicationLabel = (String) applicationInfo.loadLabel(context.getPackageManager());
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "Failed to get application name", ex);
|
||||||
|
applicationLabel = "Unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return applicationLabel;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hide a view by setting its layout height and width to 1dp.
|
* Hide a view by setting its layout height and width to 1dp.
|
||||||
@@ -325,7 +345,7 @@ public class Utils {
|
|||||||
|
|
||||||
public static void restartApp(@NonNull Context context) {
|
public static void restartApp(@NonNull Context context) {
|
||||||
String packageName = context.getPackageName();
|
String packageName = context.getPackageName();
|
||||||
Intent intent = context.getPackageManager().getLaunchIntentForPackage(packageName);
|
Intent intent = Objects.requireNonNull(context.getPackageManager().getLaunchIntentForPackage(packageName));
|
||||||
Intent mainIntent = Intent.makeRestartActivityTask(intent.getComponent());
|
Intent mainIntent = Intent.makeRestartActivityTask(intent.getComponent());
|
||||||
// Required for API 34 and later
|
// Required for API 34 and later
|
||||||
// Ref: https://developer.android.com/about/versions/14/behavior-changes-14#safer-intents
|
// Ref: https://developer.android.com/about/versions/14/behavior-changes-14#safer-intents
|
||||||
@@ -342,7 +362,17 @@ public class Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void setContext(Context appContext) {
|
public static void setContext(Context appContext) {
|
||||||
|
// Must initially set context as the language settings needs it.
|
||||||
context = appContext;
|
context = appContext;
|
||||||
|
|
||||||
|
AppLanguage language = BaseSettings.REVANCED_LANGUAGE.get();
|
||||||
|
if (language != AppLanguage.DEFAULT) {
|
||||||
|
// Create a new context with the desired language.
|
||||||
|
Configuration config = appContext.getResources().getConfiguration();
|
||||||
|
config.setLocale(language.getLocale());
|
||||||
|
context = appContext.createConfigurationContext(config);
|
||||||
|
}
|
||||||
|
|
||||||
// In some apps like TikTok, the Setting classes can load in weird orders due to cyclic class dependencies.
|
// In some apps like TikTok, the Setting classes can load in weird orders due to cyclic class dependencies.
|
||||||
// Calling the regular printDebug method here can cause a Settings context null pointer exception,
|
// Calling the regular printDebug method here can cause a Settings context null pointer exception,
|
||||||
// even though the context is already set before the call.
|
// even though the context is already set before the call.
|
||||||
@@ -499,6 +529,17 @@ public class Utils {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isDarkModeEnabled(Context context) {
|
||||||
|
Configuration config = context.getResources().getConfiguration();
|
||||||
|
final int currentNightMode = config.uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||||
|
return currentNightMode == Configuration.UI_MODE_NIGHT_YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isLandscapeOrientation() {
|
||||||
|
final int orientation = context.getResources().getConfiguration().orientation;
|
||||||
|
return orientation == Configuration.ORIENTATION_LANDSCAPE;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Automatically logs any exceptions the runnable throws.
|
* Automatically logs any exceptions the runnable throws.
|
||||||
*
|
*
|
||||||
@@ -571,7 +612,7 @@ public class Utils {
|
|||||||
|| networkType == NetworkType.OTHER;
|
|| networkType == NetworkType.OTHER;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("MissingPermission") // permission already included in YouTube
|
@SuppressLint({"MissingPermission", "deprecation"}) // Permission already included in YouTube.
|
||||||
public static NetworkType getNetworkType() {
|
public static NetworkType getNetworkType() {
|
||||||
Context networkContext = getContext();
|
Context networkContext = getContext();
|
||||||
if (networkContext == null) {
|
if (networkContext == null) {
|
||||||
@@ -681,8 +722,8 @@ public class Utils {
|
|||||||
Preference preference = group.getPreference(i);
|
Preference preference = group.getPreference(i);
|
||||||
|
|
||||||
final Sort preferenceSort;
|
final Sort preferenceSort;
|
||||||
if (preference instanceof PreferenceGroup) {
|
if (preference instanceof PreferenceGroup subGroup) {
|
||||||
sortPreferenceGroups((PreferenceGroup) preference);
|
sortPreferenceGroups(subGroup);
|
||||||
preferenceSort = groupSort; // Sort value for groups is for it's content, not itself.
|
preferenceSort = groupSort; // Sort value for groups is for it's content, not itself.
|
||||||
} else {
|
} else {
|
||||||
// Allow individual preferences to set a key sorting.
|
// Allow individual preferences to set a key sorting.
|
||||||
@@ -736,8 +777,8 @@ public class Utils {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String deviceLanguage = Utils.getContext().getResources().getConfiguration().locale.getLanguage();
|
String revancedLocale = Utils.getContext().getResources().getConfiguration().locale.getLanguage();
|
||||||
if (deviceLanguage.equals("en")) {
|
if (revancedLocale.equals(Locale.ENGLISH.getLanguage())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -745,8 +786,8 @@ public class Utils {
|
|||||||
Preference pref = group.getPreference(i);
|
Preference pref = group.getPreference(i);
|
||||||
pref.setSingleLineTitle(false);
|
pref.setSingleLineTitle(false);
|
||||||
|
|
||||||
if (pref instanceof PreferenceGroup) {
|
if (pref instanceof PreferenceGroup subGroup) {
|
||||||
setPreferenceTitlesToMultiLineIfNeeded((PreferenceGroup) pref);
|
setPreferenceTitlesToMultiLineIfNeeded(subGroup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,7 @@ import java.util.Collection;
|
|||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
|
|
||||||
abstract class Check {
|
abstract class Check {
|
||||||
private static final int NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING = 2;
|
private static final int NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING = 2;
|
||||||
@@ -46,11 +46,11 @@ abstract class Check {
|
|||||||
/**
|
/**
|
||||||
* For debugging and development only.
|
* For debugging and development only.
|
||||||
* Forces all checks to be performed and the check failed dialog to be shown.
|
* Forces all checks to be performed and the check failed dialog to be shown.
|
||||||
* Can be enabled by importing settings text with {@link Settings#CHECK_ENVIRONMENT_WARNINGS_ISSUED}
|
* Can be enabled by importing settings text with {@link BaseSettings#CHECK_ENVIRONMENT_WARNINGS_ISSUED}
|
||||||
* set to -1.
|
* set to -1.
|
||||||
*/
|
*/
|
||||||
static boolean debugAlwaysShowWarning() {
|
static boolean debugAlwaysShowWarning() {
|
||||||
final boolean alwaysShowWarning = Settings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get() < 0;
|
final boolean alwaysShowWarning = BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get() < 0;
|
||||||
if (alwaysShowWarning) {
|
if (alwaysShowWarning) {
|
||||||
Logger.printInfo(() -> "Debug forcing environment check warning to show");
|
Logger.printInfo(() -> "Debug forcing environment check warning to show");
|
||||||
}
|
}
|
||||||
@@ -59,14 +59,14 @@ abstract class Check {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static boolean shouldRun() {
|
static boolean shouldRun() {
|
||||||
return Settings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get()
|
return BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get()
|
||||||
< NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING;
|
< NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void disableForever() {
|
static void disableForever() {
|
||||||
Logger.printInfo(() -> "Environment checks disabled forever");
|
Logger.printInfo(() -> "Environment checks disabled forever");
|
||||||
|
|
||||||
Settings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(Integer.MAX_VALUE);
|
BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(Integer.MAX_VALUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("NewApi")
|
@SuppressLint("NewApi")
|
||||||
@@ -107,8 +107,8 @@ abstract class Check {
|
|||||||
" ",
|
" ",
|
||||||
(dialog, which) -> {
|
(dialog, which) -> {
|
||||||
// Cleanup data if the user incorrectly imported a huge negative number.
|
// Cleanup data if the user incorrectly imported a huge negative number.
|
||||||
final int current = Math.max(0, Settings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get());
|
final int current = Math.max(0, BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get());
|
||||||
Settings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(current + 1);
|
BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(current + 1);
|
||||||
|
|
||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
package app.revanced.extension.shared.checks;
|
package app.revanced.extension.shared.checks;
|
||||||
|
|
||||||
// Fields are set by the patch. Do not modify.
|
/**
|
||||||
// Fields are not final, because the compiler is inlining them.
|
* Fields are set by the patch. Do not modify.
|
||||||
|
* Fields are not final, because the compiler is inlining them.
|
||||||
|
*
|
||||||
|
* @noinspection CanBeFinal
|
||||||
|
*/
|
||||||
final class PatchInfo {
|
final class PatchInfo {
|
||||||
static long PATCH_TIME = 0L;
|
static long PATCH_TIME = 0L;
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package app.revanced.extension.youtube.requests;
|
package app.revanced.extension.shared.requests;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package app.revanced.extension.youtube.requests;
|
package app.revanced.extension.shared.requests;
|
||||||
|
|
||||||
public class Route {
|
public class Route {
|
||||||
private final String route;
|
private final String route;
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
package app.revanced.extension.shared.settings;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
public enum AppLanguage {
|
||||||
|
/**
|
||||||
|
* The current app language.
|
||||||
|
*/
|
||||||
|
DEFAULT,
|
||||||
|
|
||||||
|
// Language codes found in locale_config.xml
|
||||||
|
// All region specific variants have been removed.
|
||||||
|
AF,
|
||||||
|
AM,
|
||||||
|
AR,
|
||||||
|
AS,
|
||||||
|
AZ,
|
||||||
|
BE,
|
||||||
|
BG,
|
||||||
|
BN,
|
||||||
|
BS,
|
||||||
|
CA,
|
||||||
|
CS,
|
||||||
|
DA,
|
||||||
|
DE,
|
||||||
|
EL,
|
||||||
|
EN,
|
||||||
|
ES,
|
||||||
|
ET,
|
||||||
|
EU,
|
||||||
|
FA,
|
||||||
|
FI,
|
||||||
|
FR,
|
||||||
|
GL,
|
||||||
|
GU,
|
||||||
|
HI,
|
||||||
|
HE, // App uses obsolete 'IW' and not the modern 'HE' ISO code.
|
||||||
|
HR,
|
||||||
|
HU,
|
||||||
|
HY,
|
||||||
|
ID,
|
||||||
|
IS,
|
||||||
|
IT,
|
||||||
|
JA,
|
||||||
|
KA,
|
||||||
|
KK,
|
||||||
|
KM,
|
||||||
|
KN,
|
||||||
|
KO,
|
||||||
|
KY,
|
||||||
|
LO,
|
||||||
|
LT,
|
||||||
|
LV,
|
||||||
|
MK,
|
||||||
|
ML,
|
||||||
|
MN,
|
||||||
|
MR,
|
||||||
|
MS,
|
||||||
|
MY,
|
||||||
|
NE,
|
||||||
|
NL,
|
||||||
|
NB,
|
||||||
|
OR,
|
||||||
|
PA,
|
||||||
|
PL,
|
||||||
|
PT,
|
||||||
|
RO,
|
||||||
|
RU,
|
||||||
|
SI,
|
||||||
|
SK,
|
||||||
|
SL,
|
||||||
|
SQ,
|
||||||
|
SR,
|
||||||
|
SV,
|
||||||
|
SW,
|
||||||
|
TA,
|
||||||
|
TE,
|
||||||
|
TH,
|
||||||
|
TL,
|
||||||
|
TR,
|
||||||
|
UK,
|
||||||
|
UR,
|
||||||
|
UZ,
|
||||||
|
VI,
|
||||||
|
ZH,
|
||||||
|
ZU;
|
||||||
|
|
||||||
|
private final String language;
|
||||||
|
|
||||||
|
AppLanguage() {
|
||||||
|
language = name().toLowerCase(Locale.US);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The 2 letter ISO 639_1 language code.
|
||||||
|
*/
|
||||||
|
public String getLanguage() {
|
||||||
|
// Changing the app language does not force the app to completely restart,
|
||||||
|
// so the default needs to be the current language and not a static field.
|
||||||
|
if (this == DEFAULT) {
|
||||||
|
return Locale.getDefault().getLanguage();
|
||||||
|
}
|
||||||
|
|
||||||
|
return language;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Locale getLocale() {
|
||||||
|
if (this == DEFAULT) {
|
||||||
|
return Locale.getDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Locale.forLanguageTag(language);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package app.revanced.extension.shared.settings;
|
||||||
|
|
||||||
|
import static java.lang.Boolean.FALSE;
|
||||||
|
import static java.lang.Boolean.TRUE;
|
||||||
|
import static app.revanced.extension.shared.settings.Setting.parent;
|
||||||
|
import static app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch.AudioStreamLanguageOverrideAvailability;
|
||||||
|
import static app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch.SpoofiOSAvailability;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.spoof.ClientType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settings shared across multiple apps.
|
||||||
|
* <p>
|
||||||
|
* To ensure this class is loaded when the UI is created, app specific setting bundles should extend
|
||||||
|
* or reference this class.
|
||||||
|
*/
|
||||||
|
public class BaseSettings {
|
||||||
|
public static final BooleanSetting DEBUG = new BooleanSetting("revanced_debug", FALSE);
|
||||||
|
public static final BooleanSetting DEBUG_STACKTRACE = new BooleanSetting("revanced_debug_stacktrace", FALSE, parent(DEBUG));
|
||||||
|
public static final BooleanSetting DEBUG_TOAST_ON_ERROR = new BooleanSetting("revanced_debug_toast_on_error", TRUE, "revanced_debug_toast_on_error_user_dialog_message");
|
||||||
|
|
||||||
|
public static final IntegerSetting CHECK_ENVIRONMENT_WARNINGS_ISSUED = new IntegerSetting("revanced_check_environment_warnings_issued", 0, true, false);
|
||||||
|
|
||||||
|
public static final EnumSetting<AppLanguage> REVANCED_LANGUAGE = new EnumSetting<>("revanced_language", AppLanguage.DEFAULT, true, "revanced_language_user_dialog_message");
|
||||||
|
|
||||||
|
public static final BooleanSetting SPOOF_VIDEO_STREAMS = new BooleanSetting("revanced_spoof_video_streams", TRUE, true, "revanced_spoof_video_streams_user_dialog_message");
|
||||||
|
public static final EnumSetting<AppLanguage> SPOOF_VIDEO_STREAMS_LANGUAGE = new EnumSetting<>("revanced_spoof_video_streams_language", AppLanguage.DEFAULT, new AudioStreamLanguageOverrideAvailability());
|
||||||
|
public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE, parent(SPOOF_VIDEO_STREAMS));
|
||||||
|
public static final BooleanSetting SPOOF_VIDEO_STREAMS_IOS_FORCE_AVC = new BooleanSetting("revanced_spoof_video_streams_ios_force_avc", FALSE, true,
|
||||||
|
"revanced_spoof_video_streams_ios_force_avc_user_dialog_message", new SpoofiOSAvailability());
|
||||||
|
// Client type must be last spoof setting due to cyclic references.
|
||||||
|
public static final EnumSetting<ClientType> SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_video_streams_client_type", ClientType.ANDROID_VR, true, parent(SPOOF_VIDEO_STREAMS));
|
||||||
|
|
||||||
|
}
|
||||||
@@ -7,8 +7,6 @@ import app.revanced.extension.shared.Logger;
|
|||||||
import app.revanced.extension.shared.StringRef;
|
import app.revanced.extension.shared.StringRef;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.preference.SharedPrefCategory;
|
import app.revanced.extension.shared.settings.preference.SharedPrefCategory;
|
||||||
import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
@@ -62,6 +60,30 @@ public abstract class Setting<T> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for importing/exporting settings.
|
||||||
|
*/
|
||||||
|
public interface ImportExportCallback {
|
||||||
|
/**
|
||||||
|
* Called after all settings have been imported.
|
||||||
|
*/
|
||||||
|
void settingsImported(@Nullable Context context);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called after all settings have been exported.
|
||||||
|
*/
|
||||||
|
void settingsExported(@Nullable Context context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final List<ImportExportCallback> importExportCallbacks = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a callback for {@link #importFromJSON(Context, String)} and {@link #exportToJson(Context)}.
|
||||||
|
*/
|
||||||
|
public static void addImportExportCallback(@NonNull ImportExportCallback callback) {
|
||||||
|
importExportCallbacks.add(Objects.requireNonNull(callback));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All settings that were instantiated.
|
* All settings that were instantiated.
|
||||||
* When a new setting is created, it is automatically added to this list.
|
* When a new setting is created, it is automatically added to this list.
|
||||||
@@ -131,7 +153,6 @@ public abstract class Setting<T> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Confirmation message to display, if the user tries to change the setting from the default value.
|
* Confirmation message to display, if the user tries to change the setting from the default value.
|
||||||
* Currently this works only for Boolean setting types.
|
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
public final StringRef userDialogMessage;
|
public final StringRef userDialogMessage;
|
||||||
@@ -222,6 +243,7 @@ public abstract class Setting<T> {
|
|||||||
*
|
*
|
||||||
* This method will be deleted in the future.
|
* This method will be deleted in the future.
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
public static void migrateFromOldPreferences(@NonNull SharedPrefCategory oldPrefs, @NonNull Setting setting, String settingKey) {
|
public static void migrateFromOldPreferences(@NonNull SharedPrefCategory oldPrefs, @NonNull Setting setting, String settingKey) {
|
||||||
if (!oldPrefs.preferences.contains(settingKey)) {
|
if (!oldPrefs.preferences.contains(settingKey)) {
|
||||||
return; // Nothing to do.
|
return; // Nothing to do.
|
||||||
@@ -307,7 +329,7 @@ public abstract class Setting<T> {
|
|||||||
return value.equals(defaultValue);
|
return value.equals(defaultValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return key + "=" + get();
|
return key + "=" + get();
|
||||||
@@ -365,7 +387,10 @@ public abstract class Setting<T> {
|
|||||||
setting.writeToJSON(json, importExportKey);
|
setting.writeToJSON(json, importExportKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SponsorBlockSettings.showExportWarningIfNeeded(alertDialogContext);
|
|
||||||
|
for (ImportExportCallback callback : importExportCallbacks) {
|
||||||
|
callback.settingsExported(alertDialogContext);
|
||||||
|
}
|
||||||
|
|
||||||
if (json.length() == 0) {
|
if (json.length() == 0) {
|
||||||
return "";
|
return "";
|
||||||
@@ -385,7 +410,7 @@ public abstract class Setting<T> {
|
|||||||
/**
|
/**
|
||||||
* @return if any settings that require a reboot were changed.
|
* @return if any settings that require a reboot were changed.
|
||||||
*/
|
*/
|
||||||
public static boolean importFromJSON(@NonNull String settingsJsonString) {
|
public static boolean importFromJSON(@NonNull Context alertDialogContext, @NonNull String settingsJsonString) {
|
||||||
try {
|
try {
|
||||||
if (!settingsJsonString.matches("[\\s\\S]*\\{")) {
|
if (!settingsJsonString.matches("[\\s\\S]*\\{")) {
|
||||||
settingsJsonString = '{' + settingsJsonString + '}'; // Restore outer JSON braces
|
settingsJsonString = '{' + settingsJsonString + '}'; // Restore outer JSON braces
|
||||||
@@ -394,6 +419,7 @@ public abstract class Setting<T> {
|
|||||||
|
|
||||||
boolean rebootSettingChanged = false;
|
boolean rebootSettingChanged = false;
|
||||||
int numberOfSettingsImported = 0;
|
int numberOfSettingsImported = 0;
|
||||||
|
//noinspection rawtypes
|
||||||
for (Setting setting : SETTINGS) {
|
for (Setting setting : SETTINGS) {
|
||||||
String key = setting.getImportExportKey();
|
String key = setting.getImportExportKey();
|
||||||
if (json.has(key)) {
|
if (json.has(key)) {
|
||||||
@@ -411,12 +437,9 @@ public abstract class Setting<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SB Enum categories are saved using StringSettings.
|
for (ImportExportCallback callback : importExportCallbacks) {
|
||||||
// Which means they need to reload again if changed by other code (such as here).
|
callback.settingsImported(alertDialogContext);
|
||||||
// This call could be removed by creating a custom Setting class that manages the
|
}
|
||||||
// "String <-> Enum" logic or by adding an event hook of when settings are imported.
|
|
||||||
// But for now this is simple and works.
|
|
||||||
SponsorBlockSettings.updateFromImportedSettings();
|
|
||||||
|
|
||||||
Utils.showToastLong(numberOfSettingsImported == 0
|
Utils.showToastLong(numberOfSettingsImported == 0
|
||||||
? str("revanced_settings_import_reset")
|
? str("revanced_settings_import_reset")
|
||||||
@@ -1,22 +1,25 @@
|
|||||||
package app.revanced.extension.shared.settings.preference;
|
package app.revanced.extension.shared.settings.preference;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.*;
|
import android.preference.*;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||||
import app.revanced.extension.shared.settings.Setting;
|
import app.revanced.extension.shared.settings.Setting;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
||||||
/**
|
/**
|
||||||
@@ -39,7 +42,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
|||||||
|
|
||||||
private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
|
private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
|
||||||
try {
|
try {
|
||||||
Setting<?> setting = Setting.getSettingFromPath(str);
|
Setting<?> setting = Setting.getSettingFromPath(Objects.requireNonNull(str));
|
||||||
if (setting == null) {
|
if (setting == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -49,28 +52,27 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
|||||||
}
|
}
|
||||||
Logger.printDebug(() -> "Preference changed: " + setting.key);
|
Logger.printDebug(() -> "Preference changed: " + setting.key);
|
||||||
|
|
||||||
// Apply 'Setting <- Preference', unless during importing when it needs to be 'Setting -> Preference'.
|
if (!settingImportInProgress && !showingUserDialogMessage) {
|
||||||
updatePreference(pref, setting, true, settingImportInProgress);
|
if (setting.userDialogMessage != null && !prefIsSetToDefault(pref, setting)) {
|
||||||
// Update any other preference availability that may now be different.
|
// Do not change the setting yet, to allow preserving whatever
|
||||||
updateUIAvailability();
|
// list/text value was previously set if it needs to be reverted.
|
||||||
|
showSettingUserDialogConfirmation(pref, setting);
|
||||||
if (settingImportInProgress) {
|
return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!showingUserDialogMessage) {
|
|
||||||
if (setting.userDialogMessage != null && ((SwitchPreference) pref).isChecked() != (Boolean) setting.defaultValue) {
|
|
||||||
showSettingUserDialogConfirmation((SwitchPreference) pref, (BooleanSetting) setting);
|
|
||||||
} else if (setting.rebootApp) {
|
} else if (setting.rebootApp) {
|
||||||
showRestartDialog(getContext());
|
showRestartDialog(getContext());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply 'Setting <- Preference', unless during importing when it needs to be 'Setting -> Preference'.
|
||||||
|
updatePreference(pref, setting, true, settingImportInProgress);
|
||||||
|
// Update any other preference availability that may now be different.
|
||||||
|
updateUIAvailability();
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "OnSharedPreferenceChangeListener failure", ex);
|
Logger.printException(() -> "OnSharedPreferenceChangeListener failure", ex);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize this instance, and do any custom behavior.
|
* Initialize this instance, and do any custom behavior.
|
||||||
* <p>
|
* <p>
|
||||||
@@ -88,7 +90,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
|||||||
Utils.setPreferenceTitlesToMultiLineIfNeeded(screen);
|
Utils.setPreferenceTitlesToMultiLineIfNeeded(screen);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showSettingUserDialogConfirmation(SwitchPreference switchPref, BooleanSetting setting) {
|
private void showSettingUserDialogConfirmation(Preference pref, Setting<?> setting) {
|
||||||
Utils.verifyOnMainThread();
|
Utils.verifyOnMainThread();
|
||||||
|
|
||||||
final var context = getContext();
|
final var context = getContext();
|
||||||
@@ -98,14 +100,21 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
|||||||
showingUserDialogMessage = true;
|
showingUserDialogMessage = true;
|
||||||
new AlertDialog.Builder(context)
|
new AlertDialog.Builder(context)
|
||||||
.setTitle(confirmDialogTitle)
|
.setTitle(confirmDialogTitle)
|
||||||
.setMessage(setting.userDialogMessage.toString())
|
.setMessage(Objects.requireNonNull(setting.userDialogMessage).toString())
|
||||||
.setPositiveButton(android.R.string.ok, (dialog, id) -> {
|
.setPositiveButton(android.R.string.ok, (dialog, id) -> {
|
||||||
|
// User confirmed, save to the Setting.
|
||||||
|
updatePreference(pref, setting, true, false);
|
||||||
|
|
||||||
|
// Update availability of other preferences that may be changed.
|
||||||
|
updateUIAvailability();
|
||||||
|
|
||||||
if (setting.rebootApp) {
|
if (setting.rebootApp) {
|
||||||
showRestartDialog(context);
|
showRestartDialog(context);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.setNegativeButton(android.R.string.cancel, (dialog, id) -> {
|
.setNegativeButton(android.R.string.cancel, (dialog, id) -> {
|
||||||
switchPref.setChecked(setting.defaultValue); // Recursive call that resets the Setting value.
|
// Restore whatever the setting was before the change.
|
||||||
|
updatePreference(pref, setting, true, true);
|
||||||
})
|
})
|
||||||
.setOnDismissListener(dialog -> {
|
.setOnDismissListener(dialog -> {
|
||||||
showingUserDialogMessage = false;
|
showingUserDialogMessage = false;
|
||||||
@@ -128,6 +137,24 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
|||||||
updatePreferenceScreen(getPreferenceScreen(), false, false);
|
updatePreferenceScreen(getPreferenceScreen(), false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return If the preference is currently set to the default value of the Setting.
|
||||||
|
*/
|
||||||
|
protected boolean prefIsSetToDefault(Preference pref, Setting<?> setting) {
|
||||||
|
if (pref instanceof SwitchPreference switchPref) {
|
||||||
|
return switchPref.isChecked() == (Boolean) setting.defaultValue;
|
||||||
|
}
|
||||||
|
if (pref instanceof EditTextPreference editPreference) {
|
||||||
|
return editPreference.getText().equals(setting.defaultValue.toString());
|
||||||
|
}
|
||||||
|
if (pref instanceof ListPreference listPref) {
|
||||||
|
return listPref.getValue().equals(setting.defaultValue.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalStateException("Must override method to handle "
|
||||||
|
+ "preference type: " + pref.getClass());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Syncs all UI Preferences to any {@link Setting} they represent.
|
* Syncs all UI Preferences to any {@link Setting} they represent.
|
||||||
*/
|
*/
|
||||||
@@ -166,23 +193,20 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
|||||||
protected void syncSettingWithPreference(@NonNull Preference pref,
|
protected void syncSettingWithPreference(@NonNull Preference pref,
|
||||||
@NonNull Setting<?> setting,
|
@NonNull Setting<?> setting,
|
||||||
boolean applySettingToPreference) {
|
boolean applySettingToPreference) {
|
||||||
if (pref instanceof SwitchPreference) {
|
if (pref instanceof SwitchPreference switchPref) {
|
||||||
SwitchPreference switchPref = (SwitchPreference) pref;
|
|
||||||
BooleanSetting boolSetting = (BooleanSetting) setting;
|
BooleanSetting boolSetting = (BooleanSetting) setting;
|
||||||
if (applySettingToPreference) {
|
if (applySettingToPreference) {
|
||||||
switchPref.setChecked(boolSetting.get());
|
switchPref.setChecked(boolSetting.get());
|
||||||
} else {
|
} else {
|
||||||
BooleanSetting.privateSetValue(boolSetting, switchPref.isChecked());
|
BooleanSetting.privateSetValue(boolSetting, switchPref.isChecked());
|
||||||
}
|
}
|
||||||
} else if (pref instanceof EditTextPreference) {
|
} else if (pref instanceof EditTextPreference editPreference) {
|
||||||
EditTextPreference editPreference = (EditTextPreference) pref;
|
|
||||||
if (applySettingToPreference) {
|
if (applySettingToPreference) {
|
||||||
editPreference.setText(setting.get().toString());
|
editPreference.setText(setting.get().toString());
|
||||||
} else {
|
} else {
|
||||||
Setting.privateSetValueFromString(setting, editPreference.getText());
|
Setting.privateSetValueFromString(setting, editPreference.getText());
|
||||||
}
|
}
|
||||||
} else if (pref instanceof ListPreference) {
|
} else if (pref instanceof ListPreference listPref) {
|
||||||
ListPreference listPref = (ListPreference) pref;
|
|
||||||
if (applySettingToPreference) {
|
if (applySettingToPreference) {
|
||||||
listPref.setValue(setting.get().toString());
|
listPref.setValue(setting.get().toString());
|
||||||
} else {
|
} else {
|
||||||
@@ -72,20 +72,21 @@ public class ImportExportPreference extends EditTextPreference implements Prefer
|
|||||||
builder.setNeutralButton(str("revanced_settings_import_copy"), (dialog, which) -> {
|
builder.setNeutralButton(str("revanced_settings_import_copy"), (dialog, which) -> {
|
||||||
Utils.setClipboard(getEditText().getText().toString());
|
Utils.setClipboard(getEditText().getText().toString());
|
||||||
}).setPositiveButton(str("revanced_settings_import"), (dialog, which) -> {
|
}).setPositiveButton(str("revanced_settings_import"), (dialog, which) -> {
|
||||||
importSettings(getEditText().getText().toString());
|
importSettings(builder.getContext(), getEditText().getText().toString());
|
||||||
});
|
});
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "onPrepareDialogBuilder failure", ex);
|
Logger.printException(() -> "onPrepareDialogBuilder failure", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void importSettings(String replacementSettings) {
|
private void importSettings(Context context, String replacementSettings) {
|
||||||
try {
|
try {
|
||||||
if (replacementSettings.equals(existingSettings)) {
|
if (replacementSettings.equals(existingSettings)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
AbstractPreferenceFragment.settingImportInProgress = true;
|
AbstractPreferenceFragment.settingImportInProgress = true;
|
||||||
final boolean rebootNeeded = Setting.importFromJSON(replacementSettings);
|
|
||||||
|
final boolean rebootNeeded = Setting.importFromJSON(context, replacementSettings);
|
||||||
if (rebootNeeded) {
|
if (rebootNeeded) {
|
||||||
AbstractPreferenceFragment.showRestartDialog(getContext());
|
AbstractPreferenceFragment.showRestartDialog(getContext());
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
package app.revanced.extension.shared.settings.preference;
|
package app.revanced.extension.shared.settings.preference;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.sf;
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
import static app.revanced.extension.youtube.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.Dialog;
|
import android.app.Dialog;
|
||||||
@@ -13,6 +12,8 @@ import android.content.res.Configuration;
|
|||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
import android.preference.Preference;
|
import android.preference.Preference;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.view.Window;
|
import android.view.Window;
|
||||||
@@ -33,11 +34,11 @@ import java.util.List;
|
|||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.youtube.requests.Requester;
|
import app.revanced.extension.shared.requests.Requester;
|
||||||
import app.revanced.extension.youtube.requests.Route;
|
import app.revanced.extension.shared.requests.Route;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a dialog showing the links from {@link SocialLinksRoutes}.
|
* Opens a dialog showing official links.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings({"unused", "deprecation"})
|
@SuppressWarnings({"unused", "deprecation"})
|
||||||
public class ReVancedAboutPreference extends Preference {
|
public class ReVancedAboutPreference extends Preference {
|
||||||
@@ -53,9 +54,7 @@ public class ReVancedAboutPreference extends Preference {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected boolean isDarkModeEnabled() {
|
protected boolean isDarkModeEnabled() {
|
||||||
Configuration config = getContext().getResources().getConfiguration();
|
return Utils.isDarkModeEnabled(getContext());
|
||||||
final int currentNightMode = config.uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
|
||||||
return currentNightMode == Configuration.UI_MODE_NIGHT_YES;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -72,7 +71,16 @@ public class ReVancedAboutPreference extends Preference {
|
|||||||
return Color.BLACK;
|
return Color.BLACK;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String createDialogHtml(WebLink[] socialLinks) {
|
/**
|
||||||
|
* Apps that do not support bundling resources must override this.
|
||||||
|
*
|
||||||
|
* @return A localized string to display for the key.
|
||||||
|
*/
|
||||||
|
protected String getString(String key, Object ... args) {
|
||||||
|
return str(key, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String createDialogHtml(WebLink[] aboutLinks) {
|
||||||
final boolean isNetworkConnected = Utils.isNetworkConnected();
|
final boolean isNetworkConnected = Utils.isNetworkConnected();
|
||||||
|
|
||||||
StringBuilder builder = new StringBuilder();
|
StringBuilder builder = new StringBuilder();
|
||||||
@@ -91,7 +99,7 @@ public class ReVancedAboutPreference extends Preference {
|
|||||||
builder.append("<img style=\"width: 100px; height: 100px;\" "
|
builder.append("<img style=\"width: 100px; height: 100px;\" "
|
||||||
// Hide the image if it does not load.
|
// Hide the image if it does not load.
|
||||||
+ "onerror=\"this.style.display='none';\" "
|
+ "onerror=\"this.style.display='none';\" "
|
||||||
+ "src=\"https://revanced.app/favicon.ico\" />");
|
+ "src=\"").append(AboutLinksRoutes.aboutLogoUrl).append("\" />");
|
||||||
}
|
}
|
||||||
|
|
||||||
String patchesVersion = Utils.getPatchesReleaseVersion();
|
String patchesVersion = Utils.getPatchesReleaseVersion();
|
||||||
@@ -103,29 +111,29 @@ public class ReVancedAboutPreference extends Preference {
|
|||||||
|
|
||||||
builder.append("<p>")
|
builder.append("<p>")
|
||||||
// Replace hyphens with non breaking dashes so the version number does not break lines.
|
// Replace hyphens with non breaking dashes so the version number does not break lines.
|
||||||
.append(useNonBreakingHyphens(str("revanced_settings_about_links_body", patchesVersion)))
|
.append(useNonBreakingHyphens(getString("revanced_settings_about_links_body", patchesVersion)))
|
||||||
.append("</p>");
|
.append("</p>");
|
||||||
|
|
||||||
// Add a disclaimer if using a dev release.
|
// Add a disclaimer if using a dev release.
|
||||||
if (patchesVersion.contains("dev")) {
|
if (patchesVersion.contains("dev")) {
|
||||||
builder.append("<h3>")
|
builder.append("<h3>")
|
||||||
// English text 'Pre-release' can break lines.
|
// English text 'Pre-release' can break lines.
|
||||||
.append(useNonBreakingHyphens(str("revanced_settings_about_links_dev_header")))
|
.append(useNonBreakingHyphens(getString("revanced_settings_about_links_dev_header")))
|
||||||
.append("</h3>");
|
.append("</h3>");
|
||||||
|
|
||||||
builder.append("<p>")
|
builder.append("<p>")
|
||||||
.append(str("revanced_settings_about_links_dev_body"))
|
.append(getString("revanced_settings_about_links_dev_body"))
|
||||||
.append("</p>");
|
.append("</p>");
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.append("<h2 style=\"margin-top: 30px;\">")
|
builder.append("<h2 style=\"margin-top: 30px;\">")
|
||||||
.append(str("revanced_settings_about_links_header"))
|
.append(getString("revanced_settings_about_links_header"))
|
||||||
.append("</h2>");
|
.append("</h2>");
|
||||||
|
|
||||||
builder.append("<div>");
|
builder.append("<div>");
|
||||||
for (WebLink social : socialLinks) {
|
for (WebLink link : aboutLinks) {
|
||||||
builder.append("<div style=\"margin-bottom: 20px;\">");
|
builder.append("<div style=\"margin-bottom: 20px;\">");
|
||||||
builder.append(String.format("<a href=\"%s\">%s</a>", social.url, social.name));
|
builder.append(String.format("<a href=\"%s\">%s</a>", link.url, link.name));
|
||||||
builder.append("</div>");
|
builder.append("</div>");
|
||||||
}
|
}
|
||||||
builder.append("</div>");
|
builder.append("</div>");
|
||||||
@@ -137,25 +145,44 @@ public class ReVancedAboutPreference extends Preference {
|
|||||||
{
|
{
|
||||||
setOnPreferenceClickListener(pref -> {
|
setOnPreferenceClickListener(pref -> {
|
||||||
// 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 (!SocialLinksRoutes.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.
|
||||||
|
final long delayToShowProgressSpinner = 500;
|
||||||
ProgressDialog progress = new ProgressDialog(getContext());
|
ProgressDialog progress = new ProgressDialog(getContext());
|
||||||
progress.setProgressStyle(ProgressDialog.STYLE_SPINNER);
|
progress.setProgressStyle(ProgressDialog.STYLE_SPINNER);
|
||||||
progress.show();
|
|
||||||
Utils.runOnBackgroundThread(() -> fetchLinksAndShowDialog(progress));
|
Handler handler = new Handler(Looper.getMainLooper());
|
||||||
|
Runnable showDialogRunnable = progress::show;
|
||||||
|
handler.postDelayed(showDialogRunnable, delayToShowProgressSpinner);
|
||||||
|
|
||||||
|
Utils.runOnBackgroundThread(() ->
|
||||||
|
fetchLinksAndShowDialog(handler, showDialogRunnable, progress));
|
||||||
} else {
|
} else {
|
||||||
// No network call required and can run now.
|
// No network call required and can run now.
|
||||||
fetchLinksAndShowDialog(null);
|
fetchLinksAndShowDialog(null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void fetchLinksAndShowDialog(@Nullable ProgressDialog progress) {
|
private void fetchLinksAndShowDialog(@Nullable Handler handler,
|
||||||
WebLink[] socialLinks = SocialLinksRoutes.fetchSocialLinks();
|
Runnable showDialogRunnable,
|
||||||
String htmlDialog = createDialogHtml(socialLinks);
|
@Nullable ProgressDialog progress) {
|
||||||
|
WebLink[] links = AboutLinksRoutes.fetchAboutLinks();
|
||||||
|
String htmlDialog = createDialogHtml(links);
|
||||||
|
|
||||||
|
// Enable to randomly force a delay to debug the spinner logic.
|
||||||
|
final boolean debugSpinnerDelayLogic = false;
|
||||||
|
//noinspection ConstantConditions
|
||||||
|
if (debugSpinnerDelayLogic && handler != null && Math.random() < 0.5f) {
|
||||||
|
Utils.doNothingForDuration((long) (Math.random() * 4000));
|
||||||
|
}
|
||||||
|
|
||||||
Utils.runOnMainThreadNowOrLater(() -> {
|
Utils.runOnMainThreadNowOrLater(() -> {
|
||||||
|
if (handler != null) {
|
||||||
|
handler.removeCallbacks(showDialogRunnable);
|
||||||
|
}
|
||||||
if (progress != null) {
|
if (progress != null) {
|
||||||
progress.dismiss();
|
progress.dismiss();
|
||||||
}
|
}
|
||||||
@@ -224,7 +251,7 @@ class WebViewDialog extends Dialog {
|
|||||||
|
|
||||||
class WebLink {
|
class WebLink {
|
||||||
final boolean preferred;
|
final boolean preferred;
|
||||||
final String name;
|
String name;
|
||||||
final String url;
|
final String url;
|
||||||
|
|
||||||
WebLink(JSONObject json) throws JSONException {
|
WebLink(JSONObject json) throws JSONException {
|
||||||
@@ -243,7 +270,7 @@ class WebLink {
|
|||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "ReVancedSocialLink{" +
|
return "WebLink{" +
|
||||||
"preferred=" + preferred +
|
"preferred=" + preferred +
|
||||||
", name='" + name + '\'' +
|
", name='" + name + '\'' +
|
||||||
", url='" + url + '\'' +
|
", url='" + url + '\'' +
|
||||||
@@ -251,25 +278,21 @@ class WebLink {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SocialLinksRoutes {
|
class AboutLinksRoutes {
|
||||||
/**
|
/**
|
||||||
* Simple link to the website donate page,
|
* Backup icon url if the API call fails.
|
||||||
* rather than fetching and parsing the donation links using the API.
|
|
||||||
*/
|
*/
|
||||||
public static final WebLink DONATE_LINK = new WebLink(true,
|
public static volatile String aboutLogoUrl = "https://revanced.app/favicon.ico";
|
||||||
sf("revanced_settings_about_links_donate").toString(),
|
|
||||||
"https://revanced.app/donate");
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Links to use if fetch links api call fails.
|
* Links to use if fetch links api call fails.
|
||||||
*/
|
*/
|
||||||
private static final WebLink[] NO_CONNECTION_STATIC_LINKS = {
|
private static final WebLink[] NO_CONNECTION_STATIC_LINKS = {
|
||||||
new WebLink(true, "ReVanced.app", "https://revanced.app"),
|
new WebLink(true, "ReVanced.app", "https://revanced.app")
|
||||||
DONATE_LINK,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private static final String SOCIAL_LINKS_PROVIDER = "https://api.revanced.app/v2";
|
private static final String SOCIAL_LINKS_PROVIDER = "https://api.revanced.app/v4";
|
||||||
private static final Route.CompiledRoute GET_SOCIAL = new Route(GET, "/socials").compile();
|
private static final Route.CompiledRoute GET_SOCIAL = new Route(GET, "/about").compile();
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private static volatile WebLink[] fetchedLinks;
|
private static volatile WebLink[] fetchedLinks;
|
||||||
@@ -278,7 +301,7 @@ class SocialLinksRoutes {
|
|||||||
return fetchedLinks != null;
|
return fetchedLinks != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static WebLink[] fetchSocialLinks() {
|
static WebLink[] fetchAboutLinks() {
|
||||||
try {
|
try {
|
||||||
if (hasFetchedLinks()) return fetchedLinks;
|
if (hasFetchedLinks()) return fetchedLinks;
|
||||||
|
|
||||||
@@ -298,11 +321,22 @@ class SocialLinksRoutes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
JSONObject json = Requester.parseJSONObjectAndDisconnect(connection);
|
JSONObject json = Requester.parseJSONObjectAndDisconnect(connection);
|
||||||
JSONArray socials = json.getJSONArray("socials");
|
aboutLogoUrl = json.getJSONObject("branding").getString("logo");
|
||||||
|
|
||||||
List<WebLink> links = new ArrayList<>();
|
List<WebLink> links = new ArrayList<>();
|
||||||
|
|
||||||
links.add(DONATE_LINK); // Show donate link first.
|
JSONArray donations = json.getJSONObject("donations").getJSONArray("links");
|
||||||
|
for (int i = 0, length = donations.length(); i < length; i++) {
|
||||||
|
WebLink link = new WebLink(donations.getJSONObject(i));
|
||||||
|
if (link.preferred) {
|
||||||
|
// This could be localized, but TikTok does not support localized resources.
|
||||||
|
// All link names returned by the api are also non localized.
|
||||||
|
link.name = "Donate";
|
||||||
|
links.add(link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONArray socials = json.getJSONArray("socials");
|
||||||
for (int i = 0, length = socials.length(); i < length; i++) {
|
for (int i = 0, length = socials.length(); i < length; i++) {
|
||||||
WebLink link = new WebLink(socials.getJSONObject(i));
|
WebLink link = new WebLink(socials.getJSONObject(i));
|
||||||
links.add(link);
|
links.add(link);
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
package app.revanced.extension.shared.spoof;
|
||||||
|
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
|
|
||||||
|
public enum ClientType {
|
||||||
|
// https://dumps.tadiphone.dev/dumps/oculus/eureka
|
||||||
|
ANDROID_VR_NO_AUTH(
|
||||||
|
28,
|
||||||
|
"ANDROID_VR",
|
||||||
|
"com.google.android.apps.youtube.vr.oculus",
|
||||||
|
"Oculus",
|
||||||
|
"Quest 3",
|
||||||
|
"Android",
|
||||||
|
"12",
|
||||||
|
// Android 12.1
|
||||||
|
"32",
|
||||||
|
"SQ3A.220605.009.A1",
|
||||||
|
"132.0.6808.3",
|
||||||
|
"1.61.48",
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
"Android VR No auth"
|
||||||
|
),
|
||||||
|
// Chromecast with Google TV 4K.
|
||||||
|
// https://dumps.tadiphone.dev/dumps/google/kirkwood
|
||||||
|
ANDROID_UNPLUGGED(
|
||||||
|
29,
|
||||||
|
"ANDROID_UNPLUGGED",
|
||||||
|
"com.google.android.apps.youtube.unplugged",
|
||||||
|
"Google",
|
||||||
|
"Google TV Streamer",
|
||||||
|
"Android",
|
||||||
|
"14",
|
||||||
|
"34",
|
||||||
|
"UTT3.240625.001.K5",
|
||||||
|
"132.0.6808.3",
|
||||||
|
"8.49.0",
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
"Android TV"
|
||||||
|
),
|
||||||
|
// Cannot play livestreams and lacks HDR, but can play videos with music and labeled "for children".
|
||||||
|
// Google Pixel 9 Pro Fold
|
||||||
|
// https://dumps.tadiphone.dev/dumps/google/barbet
|
||||||
|
ANDROID_CREATOR(
|
||||||
|
14,
|
||||||
|
"ANDROID_CREATOR",
|
||||||
|
"com.google.android.apps.youtube.creator",
|
||||||
|
"Google",
|
||||||
|
"Pixel 9 Pro Fold",
|
||||||
|
"Android",
|
||||||
|
"15",
|
||||||
|
"35",
|
||||||
|
"AP3A.241005.015.A2",
|
||||||
|
"132.0.6779.0",
|
||||||
|
"23.47.101",
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
"Android Creator"
|
||||||
|
),
|
||||||
|
ANDROID_VR(
|
||||||
|
ANDROID_VR_NO_AUTH.id,
|
||||||
|
ANDROID_VR_NO_AUTH.clientName,
|
||||||
|
ANDROID_VR_NO_AUTH.packageName,
|
||||||
|
ANDROID_VR_NO_AUTH.deviceMake,
|
||||||
|
ANDROID_VR_NO_AUTH.deviceModel,
|
||||||
|
ANDROID_VR_NO_AUTH.osName,
|
||||||
|
ANDROID_VR_NO_AUTH.osVersion,
|
||||||
|
ANDROID_VR_NO_AUTH.androidSdkVersion,
|
||||||
|
ANDROID_VR_NO_AUTH.buildId,
|
||||||
|
ANDROID_VR_NO_AUTH.cronetVersion,
|
||||||
|
ANDROID_VR_NO_AUTH.clientVersion,
|
||||||
|
ANDROID_VR_NO_AUTH.requiresAuth,
|
||||||
|
true,
|
||||||
|
"Android VR"
|
||||||
|
),
|
||||||
|
IOS_UNPLUGGED(
|
||||||
|
33,
|
||||||
|
"IOS_UNPLUGGED",
|
||||||
|
"com.google.ios.youtubeunplugged",
|
||||||
|
"Apple",
|
||||||
|
forceAVC()
|
||||||
|
// 11 Pro Max (last device with iOS 13)
|
||||||
|
? "iPhone12,5"
|
||||||
|
// 15 Pro Max
|
||||||
|
: "iPhone16,2",
|
||||||
|
"iOS",
|
||||||
|
forceAVC()
|
||||||
|
// iOS 13 and earlier uses only AVC. 14+ adds VP9 and AV1.
|
||||||
|
? "13.7.17H35"
|
||||||
|
: "18.2.22C152",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
// Version number should be a valid iOS release.
|
||||||
|
// https://www.ipa4fun.com/history/152043/
|
||||||
|
forceAVC()
|
||||||
|
// Some newer versions can also force AVC,
|
||||||
|
// but 6.45 is the last version that supports iOS 13.
|
||||||
|
? "6.45"
|
||||||
|
: "8.49",
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
forceAVC()
|
||||||
|
? "iOS TV Force AVC"
|
||||||
|
: "iOS TV"
|
||||||
|
);
|
||||||
|
|
||||||
|
private static boolean forceAVC() {
|
||||||
|
return BaseSettings.SPOOF_VIDEO_STREAMS_IOS_FORCE_AVC.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* YouTube
|
||||||
|
* <a href="https://github.com/zerodytrash/YouTube-Internal-Clients?tab=readme-ov-file#clients">client type</a>
|
||||||
|
*/
|
||||||
|
public final int id;
|
||||||
|
|
||||||
|
public final String clientName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* App package name.
|
||||||
|
*/
|
||||||
|
private final String packageName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Player user-agent.
|
||||||
|
*/
|
||||||
|
public final String userAgent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Device model, equivalent to {@link Build#MANUFACTURER} (System property: ro.product.vendor.manufacturer)
|
||||||
|
*/
|
||||||
|
public final String deviceMake;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Device model, equivalent to {@link Build#MODEL} (System property: ro.product.vendor.model)
|
||||||
|
*/
|
||||||
|
public final String deviceModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Device OS name.
|
||||||
|
*/
|
||||||
|
public final String osName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Device OS version.
|
||||||
|
*/
|
||||||
|
public final String osVersion;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Android SDK version, equivalent to {@link Build.VERSION#SDK} (System property: ro.build.version.sdk)
|
||||||
|
* Field is null if not applicable.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public final String androidSdkVersion;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Android build id, equivalent to {@link Build#ID}.
|
||||||
|
* Field is null if not applicable.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
private final String buildId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cronet release version, as found in decompiled client apk.
|
||||||
|
* Field is null if not applicable.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
private final String cronetVersion;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* App version.
|
||||||
|
*/
|
||||||
|
public final String clientVersion;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this client requires authentication and does not work
|
||||||
|
* if logged out or in incognito mode.
|
||||||
|
*/
|
||||||
|
public final boolean requiresAuth;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the client should use authentication if available.
|
||||||
|
*/
|
||||||
|
public final boolean useAuth;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Friendly name displayed in stats for nerds.
|
||||||
|
*/
|
||||||
|
public final String friendlyName;
|
||||||
|
|
||||||
|
@SuppressWarnings("ConstantLocale")
|
||||||
|
ClientType(int id,
|
||||||
|
String clientName,
|
||||||
|
String packageName,
|
||||||
|
String deviceMake,
|
||||||
|
String deviceModel,
|
||||||
|
String osName,
|
||||||
|
String osVersion,
|
||||||
|
@Nullable String androidSdkVersion,
|
||||||
|
@Nullable String buildId,
|
||||||
|
@Nullable String cronetVersion,
|
||||||
|
String clientVersion,
|
||||||
|
boolean requiresAuth,
|
||||||
|
boolean useAuth,
|
||||||
|
String friendlyName) {
|
||||||
|
this.id = id;
|
||||||
|
this.clientName = clientName;
|
||||||
|
this.packageName = packageName;
|
||||||
|
this.deviceMake = deviceMake;
|
||||||
|
this.deviceModel = deviceModel;
|
||||||
|
this.osName = osName;
|
||||||
|
this.osVersion = osVersion;
|
||||||
|
this.androidSdkVersion = androidSdkVersion;
|
||||||
|
this.buildId = buildId;
|
||||||
|
this.cronetVersion = cronetVersion;
|
||||||
|
this.clientVersion = clientVersion;
|
||||||
|
this.requiresAuth = requiresAuth;
|
||||||
|
this.useAuth = useAuth;
|
||||||
|
this.friendlyName = friendlyName;
|
||||||
|
|
||||||
|
Locale defaultLocale = Locale.getDefault();
|
||||||
|
if (androidSdkVersion == null) {
|
||||||
|
// Convert version from '18.2.22C152' into '18_2_22'
|
||||||
|
String userAgentOsVersion = osVersion
|
||||||
|
.replaceAll("(\\d+\\.\\d+\\.\\d+).*", "$1")
|
||||||
|
.replace(".", "_");
|
||||||
|
// https://github.com/mitmproxy/mitmproxy/issues/4836
|
||||||
|
this.userAgent = String.format("%s/%s (%s; U; CPU iOS %s like Mac OS X; %s)",
|
||||||
|
packageName,
|
||||||
|
clientVersion,
|
||||||
|
deviceModel,
|
||||||
|
userAgentOsVersion,
|
||||||
|
defaultLocale
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.userAgent = String.format("%s/%s (Linux; U; Android %s; %s; %s; Build/%s; Cronet/%s)",
|
||||||
|
packageName,
|
||||||
|
clientVersion,
|
||||||
|
osVersion,
|
||||||
|
defaultLocale,
|
||||||
|
deviceModel,
|
||||||
|
Objects.requireNonNull(buildId),
|
||||||
|
Objects.requireNonNull(cronetVersion)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Logger.printDebug(() -> "userAgent: " + this.userAgent);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,30 +1,25 @@
|
|||||||
package app.revanced.extension.youtube.patches.spoof;
|
package app.revanced.extension.shared.spoof;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
import app.revanced.extension.shared.settings.Setting;
|
import app.revanced.extension.shared.settings.Setting;
|
||||||
import app.revanced.extension.youtube.patches.spoof.requests.StreamingDataRequest;
|
import app.revanced.extension.shared.spoof.requests.StreamingDataRequest;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public class SpoofVideoStreamsPatch {
|
public class SpoofVideoStreamsPatch {
|
||||||
public static final class ForceiOSAVCAvailability implements Setting.Availability {
|
private static final boolean SPOOF_STREAMING_DATA = BaseSettings.SPOOF_VIDEO_STREAMS.get();
|
||||||
@Override
|
|
||||||
public boolean isAvailable() {
|
|
||||||
return Settings.SPOOF_VIDEO_STREAMS.get() && Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ClientType.IOS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final boolean SPOOF_STREAMING_DATA = Settings.SPOOF_VIDEO_STREAMS.get();
|
private static final boolean FIX_HLS_CURRENT_TIME = SPOOF_STREAMING_DATA
|
||||||
|
&& BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ClientType.IOS_UNPLUGGED;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Any unreachable ip address. Used to intentionally fail requests.
|
* Any unreachable ip address. Used to intentionally fail requests.
|
||||||
@@ -32,6 +27,19 @@ public class SpoofVideoStreamsPatch {
|
|||||||
private static final String UNREACHABLE_HOST_URI_STRING = "https://127.0.0.0";
|
private static final String UNREACHABLE_HOST_URI_STRING = "https://127.0.0.0";
|
||||||
private static final Uri UNREACHABLE_HOST_URI = Uri.parse(UNREACHABLE_HOST_URI_STRING);
|
private static final Uri UNREACHABLE_HOST_URI = Uri.parse(UNREACHABLE_HOST_URI_STRING);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return If this patch was included during patching.
|
||||||
|
*/
|
||||||
|
private static boolean isPatchIncluded() {
|
||||||
|
return false; // Modified during patching.
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean notSpoofingToAndroid() {
|
||||||
|
return !isPatchIncluded()
|
||||||
|
|| !BaseSettings.SPOOF_VIDEO_STREAMS.get()
|
||||||
|
|| BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ClientType.IOS_UNPLUGGED;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
* Blocks /get_watch requests by returning an unreachable URI.
|
* Blocks /get_watch requests by returning an unreachable URI.
|
||||||
@@ -69,9 +77,9 @@ public class SpoofVideoStreamsPatch {
|
|||||||
String path = originalUri.getPath();
|
String path = originalUri.getPath();
|
||||||
|
|
||||||
if (path != null && path.contains("initplayback")) {
|
if (path != null && path.contains("initplayback")) {
|
||||||
Logger.printDebug(() -> "Blocking 'initplayback' by returning unreachable url");
|
Logger.printDebug(() -> "Blocking 'initplayback' by clearing query");
|
||||||
|
|
||||||
return UNREACHABLE_HOST_URI_STRING;
|
return originalUri.buildUpon().clearQuery().build().toString();
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "blockInitPlaybackRequest failure", ex);
|
Logger.printException(() -> "blockInitPlaybackRequest failure", ex);
|
||||||
@@ -88,6 +96,17 @@ public class SpoofVideoStreamsPatch {
|
|||||||
return SPOOF_STREAMING_DATA;
|
return SPOOF_STREAMING_DATA;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
* Only invoked when playing a livestream on an iOS client.
|
||||||
|
*/
|
||||||
|
public static boolean fixHLSCurrentTime(boolean original) {
|
||||||
|
if (!SPOOF_STREAMING_DATA) {
|
||||||
|
return original;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
@@ -96,11 +115,27 @@ public class SpoofVideoStreamsPatch {
|
|||||||
try {
|
try {
|
||||||
Uri uri = Uri.parse(url);
|
Uri uri = Uri.parse(url);
|
||||||
String path = uri.getPath();
|
String path = uri.getPath();
|
||||||
// 'heartbeat' has no video id and appears to be only after playback has started.
|
if (path == null || !path.contains("player")) {
|
||||||
if (path != null && path.contains("player") && !path.contains("heartbeat")) {
|
return;
|
||||||
String videoId = Objects.requireNonNull(uri.getQueryParameter("id"));
|
|
||||||
StreamingDataRequest.fetchRequest(videoId, requestHeaders);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 'get_drm_license' has no video id and appears to happen when waiting for a paid video to start.
|
||||||
|
// 'heartbeat' has no video id and appears to be only after playback has started.
|
||||||
|
// 'refresh' has no video id and appears to happen when waiting for a livestream to start.
|
||||||
|
// 'ad_break' has no video id.
|
||||||
|
if (path.contains("get_drm_license") || path.contains("heartbeat")
|
||||||
|
|| path.contains("refresh") || path.contains("ad_break")) {
|
||||||
|
Logger.printDebug(() -> "Ignoring path: " + path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String id = uri.getQueryParameter("id");
|
||||||
|
if (id == null) {
|
||||||
|
Logger.printException(() -> "Ignoring request with no id: " + url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
StreamingDataRequest.fetchRequest(id, requestHeaders);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "buildRequest failure", ex);
|
Logger.printException(() -> "buildRequest failure", ex);
|
||||||
}
|
}
|
||||||
@@ -165,4 +200,38 @@ public class SpoofVideoStreamsPatch {
|
|||||||
|
|
||||||
return postData;
|
return postData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static String appendSpoofedClient(String videoFormat) {
|
||||||
|
try {
|
||||||
|
if (SPOOF_STREAMING_DATA && BaseSettings.SPOOF_STREAMING_DATA_STATS_FOR_NERDS.get()
|
||||||
|
&& !TextUtils.isEmpty(videoFormat)) {
|
||||||
|
// Force LTR layout, to match the same LTR video time/length layout YouTube uses for all languages.
|
||||||
|
return "\u202D" + videoFormat + "\u2009(" // u202D = left to right override
|
||||||
|
+ StreamingDataRequest.getLastSpoofedClientName() + ")";
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "appendSpoofedClient failure", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return videoFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class AudioStreamLanguageOverrideAvailability implements Setting.Availability {
|
||||||
|
@Override
|
||||||
|
public boolean isAvailable() {
|
||||||
|
return BaseSettings.SPOOF_VIDEO_STREAMS.get()
|
||||||
|
&& BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ClientType.ANDROID_VR_NO_AUTH;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class SpoofiOSAvailability implements Setting.Availability {
|
||||||
|
@Override
|
||||||
|
public boolean isAvailable() {
|
||||||
|
return BaseSettings.SPOOF_VIDEO_STREAMS.get()
|
||||||
|
&& BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ClientType.IOS_UNPLUGGED;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,19 +1,19 @@
|
|||||||
package app.revanced.extension.youtube.patches.spoof.requests;
|
package app.revanced.extension.shared.spoof.requests;
|
||||||
|
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.youtube.patches.spoof.ClientType;
|
import app.revanced.extension.shared.requests.Requester;
|
||||||
import app.revanced.extension.youtube.requests.Requester;
|
import app.revanced.extension.shared.requests.Route;
|
||||||
import app.revanced.extension.youtube.requests.Route;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
|
import app.revanced.extension.shared.spoof.ClientType;
|
||||||
|
|
||||||
final class PlayerRoutes {
|
final class PlayerRoutes {
|
||||||
private static final String YT_API_URL = "https://youtubei.googleapis.com/youtubei/v1/";
|
|
||||||
|
|
||||||
static final Route.CompiledRoute GET_STREAMING_DATA = new Route(
|
static final Route.CompiledRoute GET_STREAMING_DATA = new Route(
|
||||||
Route.Method.POST,
|
Route.Method.POST,
|
||||||
"player" +
|
"player" +
|
||||||
@@ -21,6 +21,8 @@ final class PlayerRoutes {
|
|||||||
"&alt=proto"
|
"&alt=proto"
|
||||||
).compile();
|
).compile();
|
||||||
|
|
||||||
|
private static final String YT_API_URL = "https://youtubei.googleapis.com/youtubei/v1/";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TCP connection and HTTP read timeout
|
* TCP connection and HTTP read timeout
|
||||||
*/
|
*/
|
||||||
@@ -29,27 +31,39 @@ final class PlayerRoutes {
|
|||||||
private PlayerRoutes() {
|
private PlayerRoutes() {
|
||||||
}
|
}
|
||||||
|
|
||||||
static String createInnertubeBody(ClientType clientType) {
|
static String createInnertubeBody(ClientType clientType, String videoId) {
|
||||||
JSONObject innerTubeBody = new JSONObject();
|
JSONObject innerTubeBody = new JSONObject();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
JSONObject context = new JSONObject();
|
JSONObject context = new JSONObject();
|
||||||
|
|
||||||
|
// Can override default language only if no login is used.
|
||||||
|
// Could use preferred audio for all clients that do not login,
|
||||||
|
// but if this is a fall over client it will set the language even though
|
||||||
|
// the audio language is not selectable in the UI.
|
||||||
|
ClientType userSelectedClient = BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get();
|
||||||
|
Locale streamLocale = userSelectedClient == ClientType.ANDROID_VR_NO_AUTH
|
||||||
|
? BaseSettings.SPOOF_VIDEO_STREAMS_LANGUAGE.get().getLocale()
|
||||||
|
: Locale.getDefault();
|
||||||
|
|
||||||
JSONObject client = new JSONObject();
|
JSONObject client = new JSONObject();
|
||||||
client.put("clientName", clientType.name());
|
client.put("deviceMake", clientType.deviceMake);
|
||||||
client.put("clientVersion", clientType.appVersion);
|
client.put("deviceModel", clientType.deviceModel);
|
||||||
client.put("deviceModel", clientType.model);
|
client.put("clientName", clientType.clientName);
|
||||||
|
client.put("clientVersion", clientType.clientVersion);
|
||||||
|
client.put("osName", clientType.osName);
|
||||||
client.put("osVersion", clientType.osVersion);
|
client.put("osVersion", clientType.osVersion);
|
||||||
if (clientType.androidSdkVersion != null) {
|
if (clientType.androidSdkVersion != null) {
|
||||||
client.put("androidSdkVersion", clientType.androidSdkVersion);
|
client.put("androidSdkVersion", clientType.androidSdkVersion);
|
||||||
}
|
}
|
||||||
|
client.put("hl", streamLocale.getLanguage());
|
||||||
|
client.put("gl", streamLocale.getCountry());
|
||||||
context.put("client", client);
|
context.put("client", client);
|
||||||
|
|
||||||
innerTubeBody.put("context", context);
|
innerTubeBody.put("context", context);
|
||||||
innerTubeBody.put("contentCheckOk", true);
|
innerTubeBody.put("contentCheckOk", true);
|
||||||
innerTubeBody.put("racyCheckOk", true);
|
innerTubeBody.put("racyCheckOk", true);
|
||||||
innerTubeBody.put("videoId", "%s");
|
innerTubeBody.put("videoId", videoId);
|
||||||
} catch (JSONException e) {
|
} catch (JSONException e) {
|
||||||
Logger.printException(() -> "Failed to create innerTubeBody", e);
|
Logger.printException(() -> "Failed to create innerTubeBody", e);
|
||||||
}
|
}
|
||||||
@@ -57,12 +71,17 @@ final class PlayerRoutes {
|
|||||||
return innerTubeBody.toString();
|
return innerTubeBody.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @noinspection SameParameterValue*/
|
/**
|
||||||
|
* @noinspection SameParameterValue
|
||||||
|
*/
|
||||||
static HttpURLConnection getPlayerResponseConnectionFromRoute(Route.CompiledRoute route, ClientType clientType) throws IOException {
|
static HttpURLConnection getPlayerResponseConnectionFromRoute(Route.CompiledRoute route, ClientType clientType) throws IOException {
|
||||||
var connection = Requester.getConnectionFromCompiledRoute(YT_API_URL, route);
|
var connection = Requester.getConnectionFromCompiledRoute(YT_API_URL, route);
|
||||||
|
|
||||||
connection.setRequestProperty("Content-Type", "application/json");
|
connection.setRequestProperty("Content-Type", "application/json");
|
||||||
connection.setRequestProperty("User-Agent", clientType.userAgent);
|
connection.setRequestProperty("User-Agent", clientType.userAgent);
|
||||||
|
// Not a typo. "Client-Name" uses the client type id.
|
||||||
|
connection.setRequestProperty("X-YouTube-Client-Name", String.valueOf(clientType.id));
|
||||||
|
connection.setRequestProperty("X-YouTube-Client-Version", clientType.clientVersion);
|
||||||
|
|
||||||
connection.setUseCaches(false);
|
connection.setUseCaches(false);
|
||||||
connection.setDoOutput(true);
|
connection.setDoOutput(true);
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package app.revanced.extension.youtube.patches.spoof.requests;
|
package app.revanced.extension.shared.spoof.requests;
|
||||||
|
|
||||||
import static app.revanced.extension.youtube.patches.spoof.requests.PlayerRoutes.GET_STREAMING_DATA;
|
import static app.revanced.extension.shared.spoof.requests.PlayerRoutes.GET_STREAMING_DATA;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
@@ -22,13 +22,12 @@ import java.util.concurrent.TimeoutException;
|
|||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
import app.revanced.extension.youtube.patches.spoof.ClientType;
|
import app.revanced.extension.shared.spoof.ClientType;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Video streaming data. Fetching is tied to the behavior YT uses,
|
* Video streaming data. Fetching is tied to the behavior YT uses,
|
||||||
* where this class fetches the streams only when YT fetches.
|
* where this class fetches the streams only when YT fetches.
|
||||||
*
|
* <p>
|
||||||
* Effectively the cache expiration of these fetches is the same as the stock app,
|
* Effectively the cache expiration of these fetches is the same as the stock app,
|
||||||
* since the stock app would not use expired streams and therefor
|
* since the stock app would not use expired streams and therefor
|
||||||
* the extension replace stream hook is called only if YT
|
* the extension replace stream hook is called only if YT
|
||||||
@@ -40,7 +39,7 @@ public class StreamingDataRequest {
|
|||||||
|
|
||||||
static {
|
static {
|
||||||
ClientType[] allClientTypes = ClientType.values();
|
ClientType[] allClientTypes = ClientType.values();
|
||||||
ClientType preferredClient = Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get();
|
ClientType preferredClient = BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get();
|
||||||
|
|
||||||
CLIENT_ORDER_TO_USE = new ClientType[allClientTypes.length];
|
CLIENT_ORDER_TO_USE = new ClientType[allClientTypes.length];
|
||||||
CLIENT_ORDER_TO_USE[0] = preferredClient;
|
CLIENT_ORDER_TO_USE[0] = preferredClient;
|
||||||
@@ -53,8 +52,10 @@ public class StreamingDataRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final String AUTHORIZATION_HEADER = "Authorization";
|
||||||
|
|
||||||
private static final String[] REQUEST_HEADER_KEYS = {
|
private static final String[] REQUEST_HEADER_KEYS = {
|
||||||
"Authorization", // Available only to logged in users.
|
AUTHORIZATION_HEADER, // Available only to logged-in users.
|
||||||
"X-GOOG-API-FORMAT-VERSION",
|
"X-GOOG-API-FORMAT-VERSION",
|
||||||
"X-Goog-Visitor-Id"
|
"X-Goog-Visitor-Id"
|
||||||
};
|
};
|
||||||
@@ -86,8 +87,25 @@ public class StreamingDataRequest {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
private static volatile ClientType lastSpoofedClientType;
|
||||||
|
|
||||||
|
public static String getLastSpoofedClientName() {
|
||||||
|
ClientType client = lastSpoofedClientType;
|
||||||
|
return client == null ? "Unknown" : client.friendlyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final String videoId;
|
||||||
|
|
||||||
|
private final Future<ByteBuffer> future;
|
||||||
|
|
||||||
|
private StreamingDataRequest(String videoId, Map<String, String> playerHeaders) {
|
||||||
|
Objects.requireNonNull(playerHeaders);
|
||||||
|
this.videoId = videoId;
|
||||||
|
this.future = Utils.submitOnBackgroundThread(() -> fetch(videoId, playerHeaders));
|
||||||
|
}
|
||||||
|
|
||||||
public static void fetchRequest(String videoId, Map<String, String> fetchHeaders) {
|
public static void fetchRequest(String videoId, Map<String, String> fetchHeaders) {
|
||||||
// Always fetch, even if there is a existing request for the same video.
|
// Always fetch, even if there is an existing request for the same video.
|
||||||
cache.put(videoId, new StreamingDataRequest(videoId, fetchHeaders));
|
cache.put(videoId, new StreamingDataRequest(videoId, fetchHeaders));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +120,8 @@ public class StreamingDataRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private static HttpURLConnection send(ClientType clientType, String videoId,
|
private static HttpURLConnection send(ClientType clientType,
|
||||||
|
String videoId,
|
||||||
Map<String, String> playerHeaders,
|
Map<String, String> playerHeaders,
|
||||||
boolean showErrorToasts) {
|
boolean showErrorToasts) {
|
||||||
Objects.requireNonNull(clientType);
|
Objects.requireNonNull(clientType);
|
||||||
@@ -110,22 +129,40 @@ public class StreamingDataRequest {
|
|||||||
Objects.requireNonNull(playerHeaders);
|
Objects.requireNonNull(playerHeaders);
|
||||||
|
|
||||||
final long startTime = System.currentTimeMillis();
|
final long startTime = System.currentTimeMillis();
|
||||||
String clientTypeName = clientType.name();
|
|
||||||
Logger.printDebug(() -> "Fetching video streams for: " + videoId + " using client: " + clientType.name());
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType);
|
HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType);
|
||||||
connection.setConnectTimeout(HTTP_TIMEOUT_MILLISECONDS);
|
connection.setConnectTimeout(HTTP_TIMEOUT_MILLISECONDS);
|
||||||
connection.setReadTimeout(HTTP_TIMEOUT_MILLISECONDS);
|
connection.setReadTimeout(HTTP_TIMEOUT_MILLISECONDS);
|
||||||
|
|
||||||
|
boolean authHeadersIncludes = false;
|
||||||
|
|
||||||
for (String key : REQUEST_HEADER_KEYS) {
|
for (String key : REQUEST_HEADER_KEYS) {
|
||||||
String value = playerHeaders.get(key);
|
String value = playerHeaders.get(key);
|
||||||
|
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
|
if (key.equals(AUTHORIZATION_HEADER)) {
|
||||||
|
if (!clientType.useAuth) {
|
||||||
|
Logger.printDebug(() -> "Not including request header: " + key);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
authHeadersIncludes = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.printDebug(() -> "Including request header: " + key);
|
||||||
connection.setRequestProperty(key, value);
|
connection.setRequestProperty(key, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String innerTubeBody = String.format(PlayerRoutes.createInnertubeBody(clientType), videoId);
|
if (!authHeadersIncludes && clientType.requiresAuth) {
|
||||||
|
Logger.printDebug(() -> "Skipping client since user is not logged in: " + clientType
|
||||||
|
+ " videoId: " + videoId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.printDebug(() -> "Fetching video streams for: " + videoId + " using client: " + clientType);
|
||||||
|
|
||||||
|
String innerTubeBody = PlayerRoutes.createInnertubeBody(clientType, videoId);
|
||||||
byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8);
|
byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8);
|
||||||
connection.setFixedLengthStreamingMode(requestBody.length);
|
connection.setFixedLengthStreamingMode(requestBody.length);
|
||||||
connection.getOutputStream().write(requestBody);
|
connection.getOutputStream().write(requestBody);
|
||||||
@@ -133,8 +170,10 @@ public class StreamingDataRequest {
|
|||||||
final int responseCode = connection.getResponseCode();
|
final int responseCode = connection.getResponseCode();
|
||||||
if (responseCode == 200) return connection;
|
if (responseCode == 200) return connection;
|
||||||
|
|
||||||
handleConnectionError(clientTypeName + " not available with response code: "
|
// This situation likely means the patches are outdated.
|
||||||
+ responseCode + " message: " + connection.getResponseMessage(),
|
// Use a toast message that suggests updating.
|
||||||
|
handleConnectionError("Playback error (App is outdated?) " + clientType + ": "
|
||||||
|
+ responseCode + " response: " + connection.getResponseMessage(),
|
||||||
null, showErrorToasts);
|
null, showErrorToasts);
|
||||||
} catch (SocketTimeoutException ex) {
|
} catch (SocketTimeoutException ex) {
|
||||||
handleConnectionError("Connection timeout", ex, showErrorToasts);
|
handleConnectionError("Connection timeout", ex, showErrorToasts);
|
||||||
@@ -155,7 +194,7 @@ public class StreamingDataRequest {
|
|||||||
// Retry with different client if empty response body is received.
|
// Retry with different client if empty response body is received.
|
||||||
int i = 0;
|
int i = 0;
|
||||||
for (ClientType clientType : CLIENT_ORDER_TO_USE) {
|
for (ClientType clientType : CLIENT_ORDER_TO_USE) {
|
||||||
// Show an error if the last client type fails, or if the debug is enabled then show for all attempts.
|
// Show an error if the last client type fails, or if debug is enabled then show for all attempts.
|
||||||
final boolean showErrorToast = (++i == CLIENT_ORDER_TO_USE.length) || debugEnabled;
|
final boolean showErrorToast = (++i == CLIENT_ORDER_TO_USE.length) || debugEnabled;
|
||||||
|
|
||||||
HttpURLConnection connection = send(clientType, videoId, playerHeaders, showErrorToast);
|
HttpURLConnection connection = send(clientType, videoId, playerHeaders, showErrorToast);
|
||||||
@@ -163,17 +202,22 @@ public class StreamingDataRequest {
|
|||||||
try {
|
try {
|
||||||
// gzip encoding doesn't response with content length (-1),
|
// gzip encoding doesn't response with content length (-1),
|
||||||
// but empty response body does.
|
// but empty response body does.
|
||||||
if (connection.getContentLength() != 0) {
|
if (connection.getContentLength() == 0) {
|
||||||
try (InputStream inputStream = new BufferedInputStream(connection.getInputStream())) {
|
if (BaseSettings.DEBUG.get() && BaseSettings.DEBUG_TOAST_ON_ERROR.get()) {
|
||||||
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
Utils.showToastShort("Ignoring empty spoof stream client: " + clientType);
|
||||||
byte[] buffer = new byte[2048];
|
}
|
||||||
int bytesRead;
|
} else {
|
||||||
while ((bytesRead = inputStream.read(buffer)) >= 0) {
|
try (InputStream inputStream = new BufferedInputStream(connection.getInputStream());
|
||||||
baos.write(buffer, 0, bytesRead);
|
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
||||||
}
|
|
||||||
|
|
||||||
return ByteBuffer.wrap(baos.toByteArray());
|
byte[] buffer = new byte[2048];
|
||||||
|
int bytesRead;
|
||||||
|
while ((bytesRead = inputStream.read(buffer)) >= 0) {
|
||||||
|
baos.write(buffer, 0, bytesRead);
|
||||||
}
|
}
|
||||||
|
lastSpoofedClientType = clientType;
|
||||||
|
|
||||||
|
return ByteBuffer.wrap(baos.toByteArray());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
@@ -182,19 +226,11 @@ public class StreamingDataRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleConnectionError("Could not fetch any client streams", null, debugEnabled);
|
lastSpoofedClientType = null;
|
||||||
|
handleConnectionError("Could not fetch any client streams", null, true);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private final String videoId;
|
|
||||||
private final Future<ByteBuffer> future;
|
|
||||||
|
|
||||||
private StreamingDataRequest(String videoId, Map<String, String> playerHeaders) {
|
|
||||||
Objects.requireNonNull(playerHeaders);
|
|
||||||
this.videoId = videoId;
|
|
||||||
this.future = Utils.submitOnBackgroundThread(() -> fetch(videoId, playerHeaders));
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean fetchCompleted() {
|
public boolean fetchCompleted() {
|
||||||
return future.isDone();
|
return future.isDone();
|
||||||
}
|
}
|
||||||
@@ -1,4 +1 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<manifest/>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
|
||||||
</manifest>
|
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
package app.revanced.extension.shared.settings;
|
|
||||||
|
|
||||||
import static java.lang.Boolean.FALSE;
|
|
||||||
import static java.lang.Boolean.TRUE;
|
|
||||||
import static app.revanced.extension.shared.settings.Setting.parent;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Settings shared across multiple apps.
|
|
||||||
*
|
|
||||||
* To ensure this class is loaded when the UI is created, app specific setting bundles should extend
|
|
||||||
* or reference this class.
|
|
||||||
*/
|
|
||||||
public class BaseSettings {
|
|
||||||
public static final BooleanSetting DEBUG = new BooleanSetting("revanced_debug", FALSE);
|
|
||||||
public static final BooleanSetting DEBUG_STACKTRACE = new BooleanSetting("revanced_debug_stacktrace", FALSE, parent(DEBUG));
|
|
||||||
public static final BooleanSetting DEBUG_TOAST_ON_ERROR = new BooleanSetting("revanced_debug_toast_on_error", TRUE, "revanced_debug_toast_on_error_user_dialog_message");
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package app.revanced.extension.tiktok;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.settings.StringSetting;
|
|
||||||
|
|
||||||
public class Utils {
|
|
||||||
|
|
||||||
// Edit: This could be handled using a custom Setting<Long[]> class
|
|
||||||
// that saves its value to preferences and JSON using the formatted String created here.
|
|
||||||
public static long[] parseMinMax(StringSetting setting) {
|
|
||||||
final String[] minMax = setting.get().split("-");
|
|
||||||
if (minMax.length == 2) {
|
|
||||||
try {
|
|
||||||
final long min = Long.parseLong(minMax[0]);
|
|
||||||
final long max = Long.parseLong(minMax[1]);
|
|
||||||
|
|
||||||
if (min <= max && min >= 0) return new long[]{min, max};
|
|
||||||
|
|
||||||
} catch (NumberFormatException ignored) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setting.save("0-" + Long.MAX_VALUE);
|
|
||||||
return new long[]{0L, Long.MAX_VALUE};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package app.revanced.extension.youtube.patches;
|
|
||||||
|
|
||||||
import android.view.View;
|
|
||||||
|
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public class FullscreenPanelsRemoverPatch {
|
|
||||||
public static int getFullscreenPanelsVisibility() {
|
|
||||||
return Settings.HIDE_FULLSCREEN_PANELS.get() ? View.GONE : View.VISIBLE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package app.revanced.extension.youtube.patches;
|
|
||||||
|
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public final class TabletLayoutPatch {
|
|
||||||
|
|
||||||
private static final boolean TABLET_LAYOUT_ENABLED = Settings.TABLET_LAYOUT.get();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point.
|
|
||||||
*/
|
|
||||||
public static boolean getTabletLayoutEnabled() {
|
|
||||||
return TABLET_LAYOUT_ENABLED;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package app.revanced.extension.youtube.patches;
|
|
||||||
|
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public class VideoAdsPatch {
|
|
||||||
|
|
||||||
// Used by app.revanced.patches.youtube.ad.general.video.patch.VideoAdsPatch
|
|
||||||
// depends on Whitelist patch (still needs to be written)
|
|
||||||
public static boolean shouldShowAds() {
|
|
||||||
return !Settings.HIDE_VIDEO_ADS.get(); // TODO && Whitelist.shouldShowAds();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
package app.revanced.extension.youtube.patches.playback.speed;
|
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
|
||||||
import app.revanced.extension.shared.Utils;
|
|
||||||
import app.revanced.extension.youtube.patches.VideoInformation;
|
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public final class RememberPlaybackSpeedPatch {
|
|
||||||
|
|
||||||
private static final long TOAST_DELAY_MILLISECONDS = 750;
|
|
||||||
|
|
||||||
private static long lastTimeSpeedChanged;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point.
|
|
||||||
*/
|
|
||||||
public static void newVideoStarted(VideoInformation.PlaybackController ignoredPlayerController) {
|
|
||||||
Logger.printDebug(() -> "newVideoStarted");
|
|
||||||
VideoInformation.overridePlaybackSpeed(Settings.PLAYBACK_SPEED_DEFAULT.get());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point.
|
|
||||||
* Called when user selects a playback speed.
|
|
||||||
*
|
|
||||||
* @param playbackSpeed The playback speed the user selected
|
|
||||||
*/
|
|
||||||
public static void userSelectedPlaybackSpeed(float playbackSpeed) {
|
|
||||||
if (Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED.get()) {
|
|
||||||
// With the 0.05x menu, if the speed is set by integrations to higher than 2.0x
|
|
||||||
// then the menu will allow increasing without bounds but the max speed is
|
|
||||||
// still capped to under 8.0x.
|
|
||||||
playbackSpeed = Math.min(playbackSpeed, CustomPlaybackSpeedPatch.MAXIMUM_PLAYBACK_SPEED - 0.05f);
|
|
||||||
|
|
||||||
// Prevent toast spamming if using the 0.05x adjustments.
|
|
||||||
// Show exactly one toast after the user stops interacting with the speed menu.
|
|
||||||
final long now = System.currentTimeMillis();
|
|
||||||
lastTimeSpeedChanged = now;
|
|
||||||
|
|
||||||
final float finalPlaybackSpeed = playbackSpeed;
|
|
||||||
Utils.runOnMainThreadDelayed(() -> {
|
|
||||||
if (lastTimeSpeedChanged != now) {
|
|
||||||
// The user made additional speed adjustments and this call is outdated.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Settings.PLAYBACK_SPEED_DEFAULT.get() == finalPlaybackSpeed) {
|
|
||||||
// User changed to a different speed and immediately changed back.
|
|
||||||
// Or the user is going past 8.0x in the glitched out 0.05x menu.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Settings.PLAYBACK_SPEED_DEFAULT.save(finalPlaybackSpeed);
|
|
||||||
|
|
||||||
Utils.showToastLong(str("revanced_remember_playback_speed_toast", (finalPlaybackSpeed + "x")));
|
|
||||||
}, TOAST_DELAY_MILLISECONDS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point.
|
|
||||||
* Overrides the video speed. Called after video loads, and immediately after user selects a different playback speed
|
|
||||||
*/
|
|
||||||
public static float getPlaybackSpeedOverride() {
|
|
||||||
return VideoInformation.getPlaybackSpeed();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
package app.revanced.extension.youtube.patches.spoof;
|
|
||||||
|
|
||||||
import static app.revanced.extension.youtube.patches.spoof.DeviceHardwareSupport.allowAV1;
|
|
||||||
import static app.revanced.extension.youtube.patches.spoof.DeviceHardwareSupport.allowVP9;
|
|
||||||
|
|
||||||
import android.os.Build;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
public enum ClientType {
|
|
||||||
// https://dumps.tadiphone.dev/dumps/oculus/eureka
|
|
||||||
IOS(5,
|
|
||||||
// iPhone 15 supports AV1 hardware decoding.
|
|
||||||
// Only use if this Android device also has hardware decoding.
|
|
||||||
allowAV1()
|
|
||||||
? "iPhone16,2" // 15 Pro Max
|
|
||||||
: "iPhone11,4", // XS Max
|
|
||||||
// iOS 14+ forces VP9.
|
|
||||||
allowVP9()
|
|
||||||
? "17.5.1.21F90"
|
|
||||||
: "13.7.17H35",
|
|
||||||
allowVP9()
|
|
||||||
? "com.google.ios.youtube/19.10.7 (iPhone; U; CPU iOS 17_5_1 like Mac OS X)"
|
|
||||||
: "com.google.ios.youtube/19.10.7 (iPhone; U; CPU iOS 13_7 like Mac OS X)",
|
|
||||||
null,
|
|
||||||
// Version number should be a valid iOS release.
|
|
||||||
// https://www.ipa4fun.com/history/185230
|
|
||||||
"19.10.7"
|
|
||||||
),
|
|
||||||
ANDROID_VR(28,
|
|
||||||
"Quest 3",
|
|
||||||
"12",
|
|
||||||
"com.google.android.apps.youtube.vr.oculus/1.56.21 (Linux; U; Android 12; GB) gzip",
|
|
||||||
"32", // Android 12.1
|
|
||||||
"1.56.21"
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* YouTube
|
|
||||||
* <a href="https://github.com/zerodytrash/YouTube-Internal-Clients?tab=readme-ov-file#clients">client type</a>
|
|
||||||
*/
|
|
||||||
public final int id;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Device model, equivalent to {@link Build#MODEL} (System property: ro.product.model)
|
|
||||||
*/
|
|
||||||
public final String model;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Device OS version.
|
|
||||||
*/
|
|
||||||
public final String osVersion;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Player user-agent.
|
|
||||||
*/
|
|
||||||
public final String userAgent;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Android SDK version, equivalent to {@link Build.VERSION#SDK} (System property: ro.build.version.sdk)
|
|
||||||
* Field is null if not applicable.
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
public final String androidSdkVersion;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* App version.
|
|
||||||
*/
|
|
||||||
public final String appVersion;
|
|
||||||
|
|
||||||
ClientType(int id, String model, String osVersion, String userAgent, @Nullable String androidSdkVersion, String appVersion) {
|
|
||||||
this.id = id;
|
|
||||||
this.model = model;
|
|
||||||
this.osVersion = osVersion;
|
|
||||||
this.userAgent = userAgent;
|
|
||||||
this.androidSdkVersion = androidSdkVersion;
|
|
||||||
this.appVersion = appVersion;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
package app.revanced.extension.youtube.patches.spoof;
|
|
||||||
|
|
||||||
import android.media.MediaCodecInfo;
|
|
||||||
import android.media.MediaCodecList;
|
|
||||||
import android.os.Build;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
|
||||||
|
|
||||||
public class DeviceHardwareSupport {
|
|
||||||
public static final boolean DEVICE_HAS_HARDWARE_DECODING_VP9;
|
|
||||||
public static final boolean DEVICE_HAS_HARDWARE_DECODING_AV1;
|
|
||||||
|
|
||||||
static {
|
|
||||||
boolean vp9found = false;
|
|
||||||
boolean av1found = false;
|
|
||||||
MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS);
|
|
||||||
final boolean deviceIsAndroidTenOrLater = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
|
|
||||||
|
|
||||||
for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) {
|
|
||||||
final boolean isHardwareAccelerated = deviceIsAndroidTenOrLater
|
|
||||||
? codecInfo.isHardwareAccelerated()
|
|
||||||
: !codecInfo.getName().startsWith("OMX.google"); // Software decoder.
|
|
||||||
if (isHardwareAccelerated && !codecInfo.isEncoder()) {
|
|
||||||
for (String type : codecInfo.getSupportedTypes()) {
|
|
||||||
if (type.equalsIgnoreCase("video/x-vnd.on2.vp9")) {
|
|
||||||
vp9found = true;
|
|
||||||
} else if (type.equalsIgnoreCase("video/av01")) {
|
|
||||||
av1found = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DEVICE_HAS_HARDWARE_DECODING_VP9 = vp9found;
|
|
||||||
DEVICE_HAS_HARDWARE_DECODING_AV1 = av1found;
|
|
||||||
|
|
||||||
Logger.printDebug(() -> DEVICE_HAS_HARDWARE_DECODING_AV1
|
|
||||||
? "Device supports AV1 hardware decoding\n"
|
|
||||||
: "Device does not support AV1 hardware decoding\n"
|
|
||||||
+ (DEVICE_HAS_HARDWARE_DECODING_VP9
|
|
||||||
? "Device supports VP9 hardware decoding"
|
|
||||||
: "Device does not support VP9 hardware decoding"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean allowVP9() {
|
|
||||||
return DEVICE_HAS_HARDWARE_DECODING_VP9 && !Settings.SPOOF_VIDEO_STREAMS_IOS_FORCE_AVC.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean allowAV1() {
|
|
||||||
return allowVP9() && DEVICE_HAS_HARDWARE_DECODING_AV1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
package app.revanced.extension.youtube.patches.theme;
|
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
|
||||||
|
|
||||||
import android.graphics.Color;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
|
||||||
import app.revanced.extension.shared.Utils;
|
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public final class SeekbarColorPatch {
|
|
||||||
|
|
||||||
private static final boolean SEEKBAR_CUSTOM_COLOR_ENABLED = Settings.SEEKBAR_CUSTOM_COLOR.get();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default color of the seekbar.
|
|
||||||
*/
|
|
||||||
private static final int ORIGINAL_SEEKBAR_COLOR = 0xFFFF0000;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default colors of the gradient seekbar.
|
|
||||||
*/
|
|
||||||
private static final int[] ORIGINAL_SEEKBAR_GRADIENT_COLORS = { 0xFFFF0033, 0xFFFF2791 };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default positions of the gradient seekbar.
|
|
||||||
*/
|
|
||||||
private static final float[] ORIGINAL_SEEKBAR_GRADIENT_POSITIONS = { 0.8f, 1.0f };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default YouTube seekbar color brightness.
|
|
||||||
*/
|
|
||||||
private static final float ORIGINAL_SEEKBAR_COLOR_BRIGHTNESS;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If {@link Settings#SEEKBAR_CUSTOM_COLOR} is enabled,
|
|
||||||
* this is the color value of {@link Settings#SEEKBAR_CUSTOM_COLOR_VALUE}.
|
|
||||||
* Otherwise this is {@link #ORIGINAL_SEEKBAR_COLOR}.
|
|
||||||
*/
|
|
||||||
private static int seekbarColor = ORIGINAL_SEEKBAR_COLOR;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom seekbar hue, saturation, and brightness values.
|
|
||||||
*/
|
|
||||||
private static final float[] customSeekbarColorHSV = new float[3];
|
|
||||||
|
|
||||||
static {
|
|
||||||
float[] hsv = new float[3];
|
|
||||||
Color.colorToHSV(ORIGINAL_SEEKBAR_COLOR, hsv);
|
|
||||||
ORIGINAL_SEEKBAR_COLOR_BRIGHTNESS = hsv[2];
|
|
||||||
|
|
||||||
if (SEEKBAR_CUSTOM_COLOR_ENABLED) {
|
|
||||||
loadCustomSeekbarColor();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void loadCustomSeekbarColor() {
|
|
||||||
try {
|
|
||||||
seekbarColor = Color.parseColor(Settings.SEEKBAR_CUSTOM_COLOR_VALUE.get());
|
|
||||||
Color.colorToHSV(seekbarColor, customSeekbarColorHSV);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Utils.showToastShort(str("revanced_seekbar_custom_color_invalid"));
|
|
||||||
Settings.SEEKBAR_CUSTOM_COLOR_VALUE.resetToDefault();
|
|
||||||
loadCustomSeekbarColor();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int getSeekbarColor() {
|
|
||||||
return seekbarColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean playerSeekbarGradientEnabled(boolean original) {
|
|
||||||
if (SEEKBAR_CUSTOM_COLOR_ENABLED) return false;
|
|
||||||
|
|
||||||
return original;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point.
|
|
||||||
*
|
|
||||||
* Overrides all Litho components that use the YouTube seekbar color.
|
|
||||||
* Used only for the video thumbnails seekbar.
|
|
||||||
*
|
|
||||||
* If {@link Settings#HIDE_SEEKBAR_THUMBNAIL} is enabled, this returns a fully transparent color.
|
|
||||||
*/
|
|
||||||
public static int getLithoColor(int colorValue) {
|
|
||||||
if (colorValue == ORIGINAL_SEEKBAR_COLOR) {
|
|
||||||
if (Settings.HIDE_SEEKBAR_THUMBNAIL.get()) {
|
|
||||||
return 0x00000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
return getSeekbarColorValue(ORIGINAL_SEEKBAR_COLOR);
|
|
||||||
}
|
|
||||||
return colorValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point.
|
|
||||||
*/
|
|
||||||
public static void setLinearGradient(int[] colors, float[] positions) {
|
|
||||||
final boolean hideSeekbar = Settings.HIDE_SEEKBAR_THUMBNAIL.get();
|
|
||||||
|
|
||||||
if (SEEKBAR_CUSTOM_COLOR_ENABLED || hideSeekbar) {
|
|
||||||
// Most litho usage of linear gradients is hooked here,
|
|
||||||
// so must only change if the values are those for the seekbar.
|
|
||||||
if (Arrays.equals(ORIGINAL_SEEKBAR_GRADIENT_COLORS, colors)
|
|
||||||
&& Arrays.equals(ORIGINAL_SEEKBAR_GRADIENT_POSITIONS, positions)) {
|
|
||||||
Arrays.fill(colors, hideSeekbar
|
|
||||||
? 0x00000000
|
|
||||||
: seekbarColor);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.printDebug(() -> "Ignoring gradient colors: " + Arrays.toString(colors)
|
|
||||||
+ " positions: " + Arrays.toString(positions));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point.
|
|
||||||
*
|
|
||||||
* Overrides color when video player seekbar is clicked.
|
|
||||||
*/
|
|
||||||
public static int getVideoPlayerSeekbarClickedColor(int colorValue) {
|
|
||||||
if (!SEEKBAR_CUSTOM_COLOR_ENABLED) {
|
|
||||||
return colorValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return colorValue == ORIGINAL_SEEKBAR_COLOR
|
|
||||||
? getSeekbarColorValue(ORIGINAL_SEEKBAR_COLOR)
|
|
||||||
: colorValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point.
|
|
||||||
*
|
|
||||||
* Overrides color used for the video player seekbar.
|
|
||||||
*/
|
|
||||||
public static int getVideoPlayerSeekbarColor(int originalColor) {
|
|
||||||
if (!SEEKBAR_CUSTOM_COLOR_ENABLED) {
|
|
||||||
return originalColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
return getSeekbarColorValue(originalColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Color parameter is changed to the custom seekbar color, while retaining
|
|
||||||
* the brightness and alpha changes of the parameter value compared to the original seekbar color.
|
|
||||||
*/
|
|
||||||
private static int getSeekbarColorValue(int originalColor) {
|
|
||||||
try {
|
|
||||||
if (!SEEKBAR_CUSTOM_COLOR_ENABLED || originalColor == seekbarColor) {
|
|
||||||
return originalColor; // nothing to do
|
|
||||||
}
|
|
||||||
|
|
||||||
final int alphaDifference = Color.alpha(originalColor) - Color.alpha(ORIGINAL_SEEKBAR_COLOR);
|
|
||||||
|
|
||||||
// The seekbar uses the same color but different brightness for different situations.
|
|
||||||
float[] hsv = new float[3];
|
|
||||||
Color.colorToHSV(originalColor, hsv);
|
|
||||||
final float brightnessDifference = hsv[2] - ORIGINAL_SEEKBAR_COLOR_BRIGHTNESS;
|
|
||||||
|
|
||||||
// Apply the brightness difference to the custom seekbar color.
|
|
||||||
hsv[0] = customSeekbarColorHSV[0];
|
|
||||||
hsv[1] = customSeekbarColorHSV[1];
|
|
||||||
hsv[2] = clamp(customSeekbarColorHSV[2] + brightnessDifference, 0, 1);
|
|
||||||
|
|
||||||
final int replacementAlpha = clamp(Color.alpha(seekbarColor) + alphaDifference, 0, 255);
|
|
||||||
final int replacementColor = Color.HSVToColor(replacementAlpha, hsv);
|
|
||||||
Logger.printDebug(() -> String.format("Original color: #%08X replacement color: #%08X",
|
|
||||||
originalColor, replacementColor));
|
|
||||||
return replacementColor;
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "getSeekbarColorValue failure", ex);
|
|
||||||
return originalColor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @noinspection SameParameterValue */
|
|
||||||
private static int clamp(int value, int lower, int upper) {
|
|
||||||
return Math.max(lower, Math.min(value, upper));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @noinspection SameParameterValue */
|
|
||||||
private static float clamp(float value, float lower, float upper) {
|
|
||||||
return Math.max(lower, Math.min(value, upper));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
package app.revanced.extension.youtube.settings;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.preference.PreferenceFragment;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ImageButton;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import app.revanced.extension.shared.Logger;
|
|
||||||
import app.revanced.extension.youtube.ThemeHelper;
|
|
||||||
import app.revanced.extension.youtube.settings.preference.ReVancedPreferenceFragment;
|
|
||||||
import app.revanced.extension.youtube.settings.preference.ReturnYouTubeDislikePreferenceFragment;
|
|
||||||
import app.revanced.extension.youtube.settings.preference.SponsorBlockPreferenceFragment;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
import static app.revanced.extension.shared.Utils.getChildView;
|
|
||||||
import static app.revanced.extension.shared.Utils.getResourceIdentifier;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hooks LicenseActivity.
|
|
||||||
* <p>
|
|
||||||
* This class is responsible for injecting our own fragment by replacing the LicenseActivity.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public class LicenseActivityHook {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point.
|
|
||||||
* <p>
|
|
||||||
* Hooks LicenseActivity#onCreate in order to inject our own fragment.
|
|
||||||
*/
|
|
||||||
public static void initialize(Activity licenseActivity) {
|
|
||||||
try {
|
|
||||||
ThemeHelper.setActivityTheme(licenseActivity);
|
|
||||||
licenseActivity.setContentView(
|
|
||||||
getResourceIdentifier("revanced_settings_with_toolbar", "layout"));
|
|
||||||
setBackButton(licenseActivity);
|
|
||||||
|
|
||||||
PreferenceFragment fragment;
|
|
||||||
String toolbarTitleResourceName;
|
|
||||||
String dataString = licenseActivity.getIntent().getDataString();
|
|
||||||
switch (dataString) {
|
|
||||||
case "revanced_sb_settings_intent":
|
|
||||||
toolbarTitleResourceName = "revanced_sb_settings_title";
|
|
||||||
fragment = new SponsorBlockPreferenceFragment();
|
|
||||||
break;
|
|
||||||
case "revanced_ryd_settings_intent":
|
|
||||||
toolbarTitleResourceName = "revanced_ryd_settings_title";
|
|
||||||
fragment = new ReturnYouTubeDislikePreferenceFragment();
|
|
||||||
break;
|
|
||||||
case "revanced_settings_intent":
|
|
||||||
toolbarTitleResourceName = "revanced_settings_title";
|
|
||||||
fragment = new ReVancedPreferenceFragment();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
Logger.printException(() -> "Unknown setting: " + dataString);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setToolbarTitle(licenseActivity, toolbarTitleResourceName);
|
|
||||||
licenseActivity.getFragmentManager()
|
|
||||||
.beginTransaction()
|
|
||||||
.replace(getResourceIdentifier("revanced_settings_fragments", "id"), fragment)
|
|
||||||
.commit();
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "onCreate failure", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void setToolbarTitle(Activity activity, String toolbarTitleResourceName) {
|
|
||||||
ViewGroup toolbar = activity.findViewById(getToolbarResourceId());
|
|
||||||
TextView toolbarTextView = Objects.requireNonNull(getChildView(toolbar, false,
|
|
||||||
view -> view instanceof TextView));
|
|
||||||
toolbarTextView.setText(getResourceIdentifier(toolbarTitleResourceName, "string"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("UseCompatLoadingForDrawables")
|
|
||||||
private static void setBackButton(Activity activity) {
|
|
||||||
ViewGroup toolbar = activity.findViewById(getToolbarResourceId());
|
|
||||||
ImageButton imageButton = Objects.requireNonNull(getChildView(toolbar, false,
|
|
||||||
view -> view instanceof ImageButton));
|
|
||||||
final int backButtonResource = getResourceIdentifier(ThemeHelper.isDarkTheme()
|
|
||||||
? "yt_outline_arrow_left_white_24"
|
|
||||||
: "yt_outline_arrow_left_black_24",
|
|
||||||
"drawable");
|
|
||||||
imageButton.setImageDrawable(activity.getResources().getDrawable(backButtonResource));
|
|
||||||
imageButton.setOnClickListener(view -> activity.onBackPressed());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int getToolbarResourceId() {
|
|
||||||
final int toolbarResourceId = getResourceIdentifier("revanced_toolbar", "id");
|
|
||||||
if (toolbarResourceId == 0) {
|
|
||||||
throw new IllegalStateException("Could not find back button resource");
|
|
||||||
}
|
|
||||||
return toolbarResourceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
package app.revanced.extension.youtube.settings.preference;
|
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
|
||||||
import static app.revanced.extension.youtube.patches.spoof.DeviceHardwareSupport.DEVICE_HAS_HARDWARE_DECODING_VP9;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.preference.SwitchPreference;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
|
|
||||||
@SuppressWarnings({"unused", "deprecation"})
|
|
||||||
public class ForceAVCSpoofingPreference extends SwitchPreference {
|
|
||||||
{
|
|
||||||
if (!DEVICE_HAS_HARDWARE_DECODING_VP9) {
|
|
||||||
setSummaryOn(str("revanced_spoof_video_streams_ios_force_avc_no_hardware_vp9_summary_on"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ForceAVCSpoofingPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
|
||||||
super(context, attrs, defStyleAttr, defStyleRes);
|
|
||||||
}
|
|
||||||
public ForceAVCSpoofingPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
|
||||||
super(context, attrs, defStyleAttr);
|
|
||||||
}
|
|
||||||
public ForceAVCSpoofingPreference(Context context, AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
}
|
|
||||||
public ForceAVCSpoofingPreference(Context context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateUI() {
|
|
||||||
if (DEVICE_HAS_HARDWARE_DECODING_VP9) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Temporarily remove the preference key to allow changing this preference without
|
|
||||||
// causing the settings UI listeners from showing reboot dialogs by the changes made here.
|
|
||||||
String key = getKey();
|
|
||||||
setKey(null);
|
|
||||||
|
|
||||||
// This setting cannot be changed by the user.
|
|
||||||
super.setEnabled(false);
|
|
||||||
super.setChecked(true);
|
|
||||||
|
|
||||||
setKey(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setEnabled(boolean enabled) {
|
|
||||||
super.setEnabled(enabled);
|
|
||||||
|
|
||||||
updateUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setChecked(boolean checked) {
|
|
||||||
super.setChecked(checked);
|
|
||||||
|
|
||||||
updateUI();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package app.revanced.extension.youtube.settings.preference;
|
|
||||||
|
|
||||||
import android.os.Build;
|
|
||||||
import android.preference.ListPreference;
|
|
||||||
import android.preference.Preference;
|
|
||||||
|
|
||||||
import androidx.annotation.RequiresApi;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
|
||||||
import app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment;
|
|
||||||
import app.revanced.extension.youtube.patches.playback.speed.CustomPlaybackSpeedPatch;
|
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Preference fragment for ReVanced settings.
|
|
||||||
*
|
|
||||||
* @noinspection deprecation
|
|
||||||
*/
|
|
||||||
public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
|
||||||
@Override
|
|
||||||
protected void initialize() {
|
|
||||||
super.initialize();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// If the preference was included, then initialize it based on the available playback speed.
|
|
||||||
Preference defaultSpeedPreference = findPreference(Settings.PLAYBACK_SPEED_DEFAULT.key);
|
|
||||||
if (defaultSpeedPreference instanceof ListPreference) {
|
|
||||||
CustomPlaybackSpeedPatch.initializeListPreference((ListPreference) defaultSpeedPreference);
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "initialize failure", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest />
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package com.google.protos.youtube.api.innertube;
|
|
||||||
|
|
||||||
public class InnertubeContext$ClientInfo {
|
|
||||||
public int r;
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
extension {
|
|
||||||
name = "extensions/all/connectivity/wifi/spoof/spoof-wifi.rve"
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "app.revanced.extension"
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
compileOnly(libs.annotation)
|
|
||||||
}
|
|
||||||
5
extensions/syncforreddit/build.gradle.kts
Normal file
5
extensions/syncforreddit/build.gradle.kts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
dependencies {
|
||||||
|
compileOnly(project(":extensions:shared:library"))
|
||||||
|
compileOnly(project(":extensions:syncforreddit:stub"))
|
||||||
|
compileOnly(libs.annotation)
|
||||||
|
}
|
||||||
1
extensions/syncforreddit/src/main/AndroidManifest.xml
Normal file
1
extensions/syncforreddit/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest/>
|
||||||
17
extensions/syncforreddit/stub/build.gradle.kts
Normal file
17
extensions/syncforreddit/stub/build.gradle.kts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
plugins {
|
||||||
|
id(libs.plugins.android.library.get().pluginId)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "app.revanced.extension"
|
||||||
|
compileSdk = 33
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 24
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<manifest/>
|
||||||
5
extensions/tiktok/build.gradle.kts
Normal file
5
extensions/tiktok/build.gradle.kts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
dependencies {
|
||||||
|
compileOnly(project(":extensions:shared:library"))
|
||||||
|
compileOnly(project(":extensions:tiktok:stub"))
|
||||||
|
compileOnly(libs.annotation)
|
||||||
|
}
|
||||||
1
extensions/tiktok/src/main/AndroidManifest.xml
Normal file
1
extensions/tiktok/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest/>
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package app.revanced.extension.tiktok;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.Utils.isDarkModeEnabled;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.ColorInt;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.settings.StringSetting;
|
||||||
|
|
||||||
|
public class Utils {
|
||||||
|
|
||||||
|
private static final long[] DEFAULT_MIN_MAX_VALUES = {0L, Long.MAX_VALUE};
|
||||||
|
|
||||||
|
// Edit: This could be handled using a custom Setting<Long[]> class
|
||||||
|
// that saves its value to preferences and JSON using the formatted String created here.
|
||||||
|
public static long[] parseMinMax(StringSetting setting) {
|
||||||
|
final String[] minMax = setting.get().split("-");
|
||||||
|
if (minMax.length == 2) {
|
||||||
|
try {
|
||||||
|
final long min = Long.parseLong(minMax[0]);
|
||||||
|
final long max = Long.parseLong(minMax[1]);
|
||||||
|
|
||||||
|
if (min <= max && min >= 0) return new long[]{min, max};
|
||||||
|
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setting.save("0-" + Long.MAX_VALUE);
|
||||||
|
return DEFAULT_MIN_MAX_VALUES;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Colors picked by hand. These should be replaced with the styled resources TikTok uses.
|
||||||
|
private static final @ColorInt int TEXT_DARK_MODE_TITLE = Color.WHITE;
|
||||||
|
private static final @ColorInt int TEXT_DARK_MODE_SUMMARY
|
||||||
|
= Color.argb(255, 170, 170, 170);
|
||||||
|
|
||||||
|
private static final @ColorInt int TEXT_LIGHT_MODE_TITLE = Color.BLACK;
|
||||||
|
private static final @ColorInt int TEXT_LIGHT_MODE_SUMMARY
|
||||||
|
= Color.argb(255, 80, 80, 80);
|
||||||
|
|
||||||
|
public static void setTitleAndSummaryColor(Context context, View view) {
|
||||||
|
final boolean darkModeEnabled = isDarkModeEnabled(context);
|
||||||
|
|
||||||
|
TextView title = view.findViewById(android.R.id.title);
|
||||||
|
title.setTextColor(darkModeEnabled
|
||||||
|
? TEXT_DARK_MODE_TITLE
|
||||||
|
: TEXT_LIGHT_MODE_TITLE);
|
||||||
|
|
||||||
|
TextView summary = view.findViewById(android.R.id.summary);
|
||||||
|
summary.setTextColor(darkModeEnabled
|
||||||
|
? TEXT_DARK_MODE_SUMMARY
|
||||||
|
: TEXT_LIGHT_MODE_SUMMARY);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,18 @@
|
|||||||
package app.revanced.extension.tiktok.feedfilter;
|
package app.revanced.extension.tiktok.feedfilter;
|
||||||
|
|
||||||
import app.revanced.extension.tiktok.settings.Settings;
|
|
||||||
import com.ss.android.ugc.aweme.feed.model.Aweme;
|
import com.ss.android.ugc.aweme.feed.model.Aweme;
|
||||||
import com.ss.android.ugc.aweme.feed.model.AwemeStatistics;
|
import com.ss.android.ugc.aweme.feed.model.AwemeStatistics;
|
||||||
|
|
||||||
import static app.revanced.extension.tiktok.Utils.parseMinMax;
|
import app.revanced.extension.tiktok.Utils;
|
||||||
|
import app.revanced.extension.tiktok.settings.Settings;
|
||||||
|
|
||||||
public final class LikeCountFilter implements IFilter {
|
public final class LikeCountFilter implements IFilter {
|
||||||
|
|
||||||
final long minLike;
|
final long minLike;
|
||||||
final long maxLike;
|
final long maxLike;
|
||||||
|
|
||||||
LikeCountFilter() {
|
LikeCountFilter() {
|
||||||
long[] minMax = parseMinMax(Settings.MIN_MAX_LIKES);
|
long[] minMax = Utils.parseMinMax(Settings.MIN_MAX_LIKES);
|
||||||
minLike = minMax[0];
|
minLike = minMax[0];
|
||||||
maxLike = minMax[1];
|
maxLike = minMax[1];
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user