mirror of
https://github.com/revanced/revanced-patches.git
synced 2025-12-07 09:53:55 +01:00
Compare commits
356 Commits
v5.2.4-dev
...
v5.14.0-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a88b1f895 | ||
|
|
ee5c830df8 | ||
|
|
e63a4b31f3 | ||
|
|
8d0bca3b03 | ||
|
|
c162d65d5b | ||
|
|
67dcd091c4 | ||
|
|
ac5ce2d67f | ||
|
|
4b78d056fd | ||
|
|
f8c901b2c1 | ||
|
|
2a67c312e1 | ||
|
|
a7eed30f46 | ||
|
|
e2de2d8d44 | ||
|
|
7ebbf356c0 | ||
|
|
2ced5c6e2a | ||
|
|
4a090ba659 | ||
|
|
cb609a6d9d | ||
|
|
42e6de9e8f | ||
|
|
c4a5b9a28c | ||
|
|
c86c85947f | ||
|
|
cbbf474c50 | ||
|
|
f147b7b73d | ||
|
|
fb8dbb4723 | ||
|
|
1e0d27e689 | ||
|
|
a2185bce09 | ||
|
|
1b60a72ede | ||
|
|
12b4ee04ad | ||
|
|
f9a6cc96de | ||
|
|
93ea250bf3 | ||
|
|
fdb946a2cc | ||
|
|
7cc939ab03 | ||
|
|
228d72428d | ||
|
|
4db7ab4207 | ||
|
|
329f993024 | ||
|
|
7cd1fb22d8 | ||
|
|
ae111bc0b9 | ||
|
|
79f1dfd3e8 | ||
|
|
f5dd902915 | ||
|
|
10e2b08eb2 | ||
|
|
4ae1155e51 | ||
|
|
69fbfaea19 | ||
|
|
f44fede67c | ||
|
|
3c52ab8017 | ||
|
|
d1641a6e3d | ||
|
|
09773e8934 | ||
|
|
d77d5bfbdd | ||
|
|
a84bded9e7 | ||
|
|
e664a24f73 | ||
|
|
5bf964fff6 | ||
|
|
0c0bbb8713 | ||
|
|
8afe48cd92 | ||
|
|
dde8ea31cb | ||
|
|
d3abbe3e93 | ||
|
|
c8179776ed | ||
|
|
c6c6516b12 | ||
|
|
d6eae01e12 | ||
|
|
ba88603f4b | ||
|
|
d5aab3d464 | ||
|
|
fca2f70c0e | ||
|
|
348f7e12cb | ||
|
|
b6b7208eeb | ||
|
|
a2c79f1349 | ||
|
|
4f5bb3c915 | ||
|
|
4b77d27c77 | ||
|
|
7991c80129 | ||
|
|
6baf4ea2ac | ||
|
|
c89538c8f5 | ||
|
|
94fb367618 | ||
|
|
354835966d | ||
|
|
168f9b769e | ||
|
|
e4c4b3a73a | ||
|
|
fce98b4960 | ||
|
|
839aa81e9c | ||
|
|
905bb0ea5f | ||
|
|
a94a663859 | ||
|
|
04b37dd55a | ||
|
|
2382e9d09e | ||
|
|
97f504976a | ||
|
|
0a6c5158e0 | ||
|
|
a959d798e8 | ||
|
|
39a0b9bda6 | ||
|
|
92c38b2cb4 | ||
|
|
4732210d4b | ||
|
|
f30a49f1cb | ||
|
|
bcd157dd2b | ||
|
|
d299ea5973 | ||
|
|
a20021e290 | ||
|
|
373ca966f3 | ||
|
|
12de922afa | ||
|
|
580bb3cf6c | ||
|
|
421af92f4c | ||
|
|
4d03e1b5a1 | ||
|
|
24d68df6cd | ||
|
|
e9aee17746 | ||
|
|
7c4285e3e6 | ||
|
|
e3110271a7 | ||
|
|
0079eceb87 | ||
|
|
af2a97cb16 | ||
|
|
aeb552e8f2 | ||
|
|
6e936fea42 | ||
|
|
f63769f39f | ||
|
|
1c9ab20a63 | ||
|
|
cdeccad908 | ||
|
|
399889c6fa | ||
|
|
ec77861410 | ||
|
|
b5afc6d827 | ||
|
|
b7ebfddf65 | ||
|
|
2742aca48b | ||
|
|
14ca4d3288 | ||
|
|
a06c0318bf | ||
|
|
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 |
8
.github/workflows/build_pull_request.yml
vendored
8
.github/workflows/build_pull_request.yml
vendored
@@ -28,4 +28,10 @@ jobs:
|
||||
- name: Build
|
||||
env:
|
||||
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
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 */8 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -14,23 +16,29 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: dev
|
||||
fetch-depth: 0
|
||||
clean: true
|
||||
|
||||
- name: Pull strings
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
config: crowdin.yml
|
||||
upload_sources: false
|
||||
download_translations: true
|
||||
skip_ref_checkout: true
|
||||
localization_branch_name: feat/translations
|
||||
create_pull_request: true
|
||||
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
|
||||
create_pull_request: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
|
||||
- name: Open pull request
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
uses: repo-sync/pull-request@v2
|
||||
with:
|
||||
source_branch: feat/translations
|
||||
destination_branch: dev
|
||||
pr_title: "chore: Sync translations"
|
||||
pr_body: "Sync translations from [crowdin.com/project/revanced](https://crowdin.com/project/revanced)"
|
||||
|
||||
5
.github/workflows/push_strings.yml
vendored
5
.github/workflows/push_strings.yml
vendored
@@ -18,6 +18,11 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Preprocess strings
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./gradlew clean preprocessCrowdinStrings
|
||||
|
||||
- name: Push strings
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
- name: Build
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./gradlew build clean
|
||||
run: ./gradlew :patches:buildAndroid clean
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
1055
CHANGELOG.md
1055
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,11 @@
|
||||
android.namespace = "app.revanced.extension"
|
||||
android {
|
||||
namespace = "app.revanced.extension"
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(libs.annotation)
|
||||
|
||||
@@ -12,7 +12,7 @@ import android.os.Handler;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
/** @noinspection deprecation, unused */
|
||||
@SuppressWarnings({"deprecation", "unused"})
|
||||
public class SpoofWifiPatch {
|
||||
|
||||
// Used to check what the (real or fake) active network is (take a look at `hasTransport`).
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
android {
|
||||
namespace = "app.revanced.extension"
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 21
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(libs.annotation)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
@@ -0,0 +1,336 @@
|
||||
package app.revanced.extension.all.misc.directory.documentsprovider;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.ProviderInfo;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.os.CancellationSignal;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.DocumentsProvider;
|
||||
import android.system.ErrnoException;
|
||||
import android.system.Os;
|
||||
import android.system.StructStat;
|
||||
import android.util.Log;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A DocumentsProvider that allows access to the app's internal data directory.
|
||||
*/
|
||||
@SuppressLint("LongLogTag")
|
||||
public class InternalDataDocumentsProvider extends DocumentsProvider {
|
||||
private static final String[] rootColumns =
|
||||
{"root_id", "mime_types", "flags", "icon", "title", "summary", "document_id"};
|
||||
private static final String[] directoryColumns =
|
||||
{"document_id", "mime_type", "_display_name", "last_modified", "flags",
|
||||
"_size", "full_path", "lstat_info"};
|
||||
private static final int S_IFLNK = 0x8000;
|
||||
|
||||
private String packageName;
|
||||
private File dataDirectory;
|
||||
|
||||
/**
|
||||
* Recursively delete a file or directory and all its children.
|
||||
*
|
||||
* @param root The file or directory to delete.
|
||||
* @return True if the file or directory and all its children were successfully deleted.
|
||||
*/
|
||||
private static boolean deleteRecursively(File root) {
|
||||
// If root is a directory, delete all children first
|
||||
if (root.isDirectory()) {
|
||||
try {
|
||||
// Only delete recursively if the directory is not a symlink
|
||||
if ((Os.lstat(root.getPath()).st_mode & S_IFLNK) != S_IFLNK) {
|
||||
File[] files = root.listFiles();
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
if (!deleteRecursively(file)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (ErrnoException e) {
|
||||
Log.e("InternalDocumentsProvider", "Failed to lstat " + root.getPath(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete file or empty directory
|
||||
return root.delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the MIME type of a file based on its extension.
|
||||
*
|
||||
* @param file The file to resolve the MIME type for.
|
||||
* @return The MIME type of the file.
|
||||
*/
|
||||
private static String resolveMimeType(File file) {
|
||||
if (file.isDirectory()) {
|
||||
return DocumentsContract.Document.MIME_TYPE_DIR;
|
||||
}
|
||||
|
||||
String name = file.getName();
|
||||
int indexOfExtDot = name.lastIndexOf('.');
|
||||
if (indexOfExtDot < 0) {
|
||||
// No extension
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
String extension = name.substring(indexOfExtDot + 1).toLowerCase();
|
||||
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
||||
return mimeType != null ? mimeType : "application/octet-stream";
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean onCreate() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void attachInfo(Context context, ProviderInfo providerInfo) {
|
||||
super.attachInfo(context, providerInfo);
|
||||
|
||||
this.packageName = context.getPackageName();
|
||||
this.dataDirectory = context.getFilesDir().getParentFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String createDocument(String parentDocumentId, String mimeType, String displayName) throws FileNotFoundException {
|
||||
File directory = resolveDocumentId(parentDocumentId);
|
||||
File file = new File(directory, displayName);
|
||||
|
||||
// If file already exists, append a number to the name
|
||||
int i = 2;
|
||||
while (file.exists()) {
|
||||
file = new File(directory, displayName + " (" + i + ")");
|
||||
i++;
|
||||
}
|
||||
|
||||
try {
|
||||
// Create the file or directory
|
||||
if (mimeType.equals(DocumentsContract.Document.MIME_TYPE_DIR) ? file.mkdir() : file.createNewFile()) {
|
||||
// Return the document ID of the new entity
|
||||
if (!parentDocumentId.endsWith("/")) {
|
||||
parentDocumentId = parentDocumentId + "/";
|
||||
}
|
||||
return parentDocumentId + file.getName();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Do nothing. We are throwing a FileNotFoundException later if the file could not be created.
|
||||
}
|
||||
throw new FileNotFoundException("Failed to create document in " + parentDocumentId + " with name " + displayName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void deleteDocument(String documentId) throws FileNotFoundException {
|
||||
File file = resolveDocumentId(documentId);
|
||||
if (!deleteRecursively(file)) {
|
||||
throw new FileNotFoundException("Failed to delete document " + documentId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String getDocumentType(String documentId) throws FileNotFoundException {
|
||||
return resolveMimeType(resolveDocumentId(documentId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean isChildDocument(String parentDocumentId, String documentId) {
|
||||
return documentId.startsWith(parentDocumentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String moveDocument(String sourceDocumentId, String sourceParentDocumentId, String targetParentDocumentId) throws FileNotFoundException {
|
||||
File source = resolveDocumentId(sourceDocumentId);
|
||||
File dest = resolveDocumentId(targetParentDocumentId);
|
||||
|
||||
File file = new File(dest, source.getName());
|
||||
if (!file.exists() && source.renameTo(file)) {
|
||||
// Return the new document ID
|
||||
if (targetParentDocumentId.endsWith("/")) {
|
||||
return targetParentDocumentId + file.getName();
|
||||
}
|
||||
return targetParentDocumentId + "/" + file.getName();
|
||||
}
|
||||
|
||||
throw new FileNotFoundException("Failed to move document from " + sourceDocumentId + " to " + targetParentDocumentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final ParcelFileDescriptor openDocument(String documentId, String mode, CancellationSignal signal) throws FileNotFoundException {
|
||||
File file = resolveDocumentId(documentId);
|
||||
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.parseMode(mode));
|
||||
}
|
||||
|
||||
@Override
|
||||
public final Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException {
|
||||
if (parentDocumentId.endsWith("/")) {
|
||||
parentDocumentId = parentDocumentId.substring(0, parentDocumentId.length() - 1);
|
||||
}
|
||||
|
||||
if (projection == null) {
|
||||
projection = directoryColumns;
|
||||
}
|
||||
|
||||
MatrixCursor cursor = new MatrixCursor(projection);
|
||||
File children = resolveDocumentId(parentDocumentId);
|
||||
|
||||
// Collect all children
|
||||
File[] files = children.listFiles();
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
addRowForDocument(cursor, parentDocumentId + "/" + file.getName(), file);
|
||||
}
|
||||
}
|
||||
return cursor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException {
|
||||
if (projection == null) {
|
||||
projection = directoryColumns;
|
||||
}
|
||||
|
||||
MatrixCursor cursor = new MatrixCursor(projection);
|
||||
addRowForDocument(cursor, documentId, null);
|
||||
return cursor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final Cursor queryRoots(String[] projection) {
|
||||
ApplicationInfo info = Objects.requireNonNull(getContext()).getApplicationInfo();
|
||||
String appName = info.loadLabel(getContext().getPackageManager()).toString();
|
||||
|
||||
if (projection == null) {
|
||||
projection = rootColumns;
|
||||
}
|
||||
|
||||
MatrixCursor cursor = new MatrixCursor(projection);
|
||||
MatrixCursor.RowBuilder row = cursor.newRow();
|
||||
row.add(DocumentsContract.Root.COLUMN_ROOT_ID, this.packageName);
|
||||
row.add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, this.packageName);
|
||||
row.add(DocumentsContract.Root.COLUMN_SUMMARY, this.packageName);
|
||||
row.add(DocumentsContract.Root.COLUMN_FLAGS,
|
||||
DocumentsContract.Root.FLAG_LOCAL_ONLY |
|
||||
DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD);
|
||||
row.add(DocumentsContract.Root.COLUMN_TITLE, appName);
|
||||
row.add(DocumentsContract.Root.COLUMN_MIME_TYPES, "*/*");
|
||||
row.add(DocumentsContract.Root.COLUMN_ICON, info.icon);
|
||||
return cursor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void removeDocument(String documentId, String parentDocumentId) throws FileNotFoundException {
|
||||
deleteDocument(documentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String renameDocument(String documentId, String displayName) throws FileNotFoundException {
|
||||
File file = resolveDocumentId(documentId);
|
||||
if (!file.renameTo(new File(file.getParentFile(), displayName))) {
|
||||
throw new FileNotFoundException("Failed to rename document from " + documentId + " to " + displayName);
|
||||
}
|
||||
|
||||
// Return the new document ID
|
||||
return documentId.substring(0, documentId.lastIndexOf('/', documentId.length() - 2)) + "/" + displayName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a file instance for a given document ID.
|
||||
*
|
||||
* @param fullContentPath The document ID to resolve.
|
||||
* @return File object for the given document ID.
|
||||
* @throws FileNotFoundException If the document ID is invalid or the file does not exist.
|
||||
*/
|
||||
private File resolveDocumentId(String fullContentPath) throws FileNotFoundException {
|
||||
if (!fullContentPath.startsWith(this.packageName)) {
|
||||
throw new FileNotFoundException(fullContentPath + " not found");
|
||||
}
|
||||
String path = fullContentPath.substring(this.packageName.length());
|
||||
|
||||
// Resolve the relative path within /data/data/{PKG}
|
||||
File file;
|
||||
if (path.equals("/") || path.isEmpty()) {
|
||||
file = this.dataDirectory;
|
||||
} else {
|
||||
// Remove leading slash
|
||||
String relativePath = path.substring(1);
|
||||
file = new File(this.dataDirectory, relativePath);
|
||||
}
|
||||
|
||||
if (!file.exists()) {
|
||||
throw new FileNotFoundException(fullContentPath + " not found");
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a row containing all file properties to a MatrixCursor for a given document ID.
|
||||
*
|
||||
* @param cursor The cursor to add the row to.
|
||||
* @param documentId The document ID to add the row for.
|
||||
* @param file The file to add the row for. If null, the file will be resolved from the document ID.
|
||||
* @throws FileNotFoundException If the file does not exist.
|
||||
*/
|
||||
private void addRowForDocument(MatrixCursor cursor, String documentId, File file) throws FileNotFoundException {
|
||||
if (file == null) {
|
||||
file = resolveDocumentId(documentId);
|
||||
}
|
||||
|
||||
int flags = 0;
|
||||
if (file.isDirectory()) {
|
||||
// Prefer list view for directories
|
||||
flags = flags | DocumentsContract.Document.FLAG_DIR_PREFERS_LAST_MODIFIED;
|
||||
}
|
||||
|
||||
if (file.canWrite()) {
|
||||
if (file.isDirectory()) {
|
||||
flags = flags | DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE;
|
||||
}
|
||||
|
||||
flags = flags | DocumentsContract.Document.FLAG_SUPPORTS_WRITE |
|
||||
DocumentsContract.Document.FLAG_SUPPORTS_DELETE |
|
||||
DocumentsContract.Document.FLAG_SUPPORTS_RENAME |
|
||||
DocumentsContract.Document.FLAG_SUPPORTS_MOVE;
|
||||
}
|
||||
|
||||
MatrixCursor.RowBuilder row = cursor.newRow();
|
||||
row.add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, documentId);
|
||||
row.add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, file.getName());
|
||||
row.add(DocumentsContract.Document.COLUMN_SIZE, file.length());
|
||||
row.add(DocumentsContract.Document.COLUMN_MIME_TYPE, resolveMimeType(file));
|
||||
row.add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, file.lastModified());
|
||||
row.add(DocumentsContract.Document.COLUMN_FLAGS, flags);
|
||||
|
||||
// Custom columns
|
||||
row.add("full_path", file.getAbsolutePath());
|
||||
|
||||
// Add lstat column
|
||||
String path = file.getPath();
|
||||
try {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
StructStat lstat = Os.lstat(path);
|
||||
sb.append(lstat.st_mode);
|
||||
sb.append(";");
|
||||
sb.append(lstat.st_uid);
|
||||
sb.append(";");
|
||||
sb.append(lstat.st_gid);
|
||||
// Append symlink target if it is a symlink
|
||||
if ((lstat.st_mode & S_IFLNK) == S_IFLNK) {
|
||||
sb.append(";");
|
||||
sb.append(Os.readlink(path));
|
||||
}
|
||||
row.add("lstat_info", sb.toString());
|
||||
} catch (Exception ex) {
|
||||
Log.e("InternalDocumentsProvider", "Failed to get lstat info for " + path, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,15 @@
|
||||
android.namespace = "app.revanced.extension"
|
||||
android {
|
||||
namespace = "app.revanced.extension"
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 21
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(libs.annotation)
|
||||
|
||||
@@ -5,7 +5,8 @@ import android.os.Build;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
public final class RemoveScreencaptureRestrictionPatch {
|
||||
@SuppressWarnings("unused")
|
||||
public final class RemoveScreenCaptureRestrictionPatch {
|
||||
// Member of AudioAttributes.Builder
|
||||
@RequiresApi(api = Build.VERSION_CODES.Q)
|
||||
public static AudioAttributes.Builder setAllowedCapturePolicy(final AudioAttributes.Builder builder, final int capturePolicy) {
|
||||
@@ -1 +1,16 @@
|
||||
android.namespace = "app.revanced.extension"
|
||||
android {
|
||||
namespace = "app.revanced.extension"
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 21
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(libs.annotation)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package app.revanced.extension.all.screenshot.removerestriction;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class RemoveScreenshotRestrictionPatch {
|
||||
|
||||
public static void addFlags(Window window, int flags) {
|
||||
|
||||
@@ -4,7 +4,7 @@ plugins {
|
||||
|
||||
android {
|
||||
namespace = "app.revanced.extension"
|
||||
compileSdk = 33
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
|
||||
5
extensions/music/build.gradle.kts
Normal file
5
extensions/music/build.gradle.kts
Normal file
@@ -0,0 +1,5 @@
|
||||
android {
|
||||
defaultConfig {
|
||||
minSdk = 26
|
||||
}
|
||||
}
|
||||
1
extensions/music/src/main/AndroidManifest.xml
Normal file
1
extensions/music/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
@@ -0,0 +1,27 @@
|
||||
package app.revanced.extension.music.spoof;
|
||||
|
||||
/**
|
||||
* @noinspection unused
|
||||
*/
|
||||
public class SpoofClientPatch {
|
||||
private static final int CLIENT_TYPE_ID = 26;
|
||||
private static final String CLIENT_VERSION = "6.21";
|
||||
private static final String DEVICE_MODEL = "iPhone16,2";
|
||||
private static final String OS_VERSION = "17.7.2.21H221";
|
||||
|
||||
public static int getClientId() {
|
||||
return CLIENT_TYPE_ID;
|
||||
}
|
||||
|
||||
public static String getClientVersion() {
|
||||
return CLIENT_VERSION;
|
||||
}
|
||||
|
||||
public static String getClientModel() {
|
||||
return DEVICE_MODEL;
|
||||
}
|
||||
|
||||
public static String getOsVersion() {
|
||||
return OS_VERSION;
|
||||
}
|
||||
}
|
||||
4
extensions/nunl/build.gradle.kts
Normal file
4
extensions/nunl/build.gradle.kts
Normal file
@@ -0,0 +1,4 @@
|
||||
dependencies {
|
||||
compileOnly(project(":extensions:shared:library"))
|
||||
compileOnly(project(":extensions:nunl:stub"))
|
||||
}
|
||||
1
extensions/nunl/src/main/AndroidManifest.xml
Normal file
1
extensions/nunl/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
@@ -0,0 +1,114 @@
|
||||
package app.revanced.extension.nunl.ads;
|
||||
|
||||
import nl.nu.performance.api.client.interfaces.Block;
|
||||
import nl.nu.performance.api.client.unions.SmallArticleLinkFlavor;
|
||||
import nl.nu.performance.api.client.objects.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class HideAdsPatch {
|
||||
private static final String[] blockedHeaderBlocks = {
|
||||
"Aanbiedingen (Adverteerders)",
|
||||
"Aangeboden door NUshop"
|
||||
};
|
||||
|
||||
// "Rubrieken" menu links to ads.
|
||||
private static final String[] blockedLinkBlocks = {
|
||||
"Van onze adverteerders"
|
||||
};
|
||||
|
||||
public static void filterAds(List<Block> blocks) {
|
||||
try {
|
||||
ArrayList<Block> cleanedList = new ArrayList<>();
|
||||
|
||||
boolean skipFullHeader = false;
|
||||
boolean skipUntilDivider = false;
|
||||
|
||||
int index = 0;
|
||||
while (index < blocks.size()) {
|
||||
Block currentBlock = blocks.get(index);
|
||||
|
||||
// Because of pagination, we might not see the Divider in front of it.
|
||||
// Just remove it as is and leave potential extra spacing visible on the screen.
|
||||
if (currentBlock instanceof DpgBannerBlock) {
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (index + 1 < blocks.size()) {
|
||||
// Filter Divider -> DpgMediaBanner -> Divider.
|
||||
if (currentBlock instanceof DividerBlock
|
||||
&& blocks.get(index + 1) instanceof DpgBannerBlock) {
|
||||
index += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Filter Divider -> LinkBlock (... -> LinkBlock -> LinkBlock-> LinkBlock -> Divider).
|
||||
if (currentBlock instanceof DividerBlock
|
||||
&& blocks.get(index + 1) instanceof LinkBlock linkBlock) {
|
||||
Link link = linkBlock.getLink();
|
||||
if (link != null && link.getTitle() != null) {
|
||||
for (String blockedLinkBlock : blockedLinkBlocks) {
|
||||
if (blockedLinkBlock.equals(link.getTitle().getText())) {
|
||||
skipUntilDivider = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (skipUntilDivider) {
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Skip LinkBlocks with a "flavor" claiming to be "isPartner" (sponsored inline ads).
|
||||
if (currentBlock instanceof LinkBlock linkBlock
|
||||
&& linkBlock.getLink() != null
|
||||
&& linkBlock.getLink().getLinkFlavor() instanceof SmallArticleLinkFlavor smallArticleLinkFlavor
|
||||
&& smallArticleLinkFlavor.isPartner() != null
|
||||
&& smallArticleLinkFlavor.isPartner()) {
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentBlock instanceof DividerBlock) {
|
||||
skipUntilDivider = false;
|
||||
}
|
||||
|
||||
// Filter HeaderBlock with known ads until next HeaderBlock.
|
||||
if (currentBlock instanceof HeaderBlock headerBlock) {
|
||||
StyledText headerText = headerBlock.component20();
|
||||
if (headerText != null) {
|
||||
skipFullHeader = false;
|
||||
for (String blockedHeaderBlock : blockedHeaderBlocks) {
|
||||
if (blockedHeaderBlock.equals(headerText.getText())) {
|
||||
skipFullHeader = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (skipFullHeader) {
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!skipFullHeader && !skipUntilDivider) {
|
||||
cleanedList.add(currentBlock);
|
||||
}
|
||||
index++;
|
||||
}
|
||||
|
||||
// Replace list in-place to not deal with moving the result to the correct register in smali.
|
||||
blocks.clear();
|
||||
blocks.addAll(cleanedList);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "filterAds failure", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
17
extensions/nunl/stub/build.gradle.kts
Normal file
17
extensions/nunl/stub/build.gradle.kts
Normal file
@@ -0,0 +1,17 @@
|
||||
plugins {
|
||||
id(libs.plugins.android.library.get().pluginId)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "app.revanced.extension"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 26
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
}
|
||||
1
extensions/nunl/stub/src/main/AndroidManifest.xml
Normal file
1
extensions/nunl/stub/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
@@ -0,0 +1,5 @@
|
||||
package nl.nu.performance.api.client.interfaces;
|
||||
|
||||
public class Block {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package nl.nu.performance.api.client.objects;
|
||||
|
||||
import nl.nu.performance.api.client.interfaces.Block;
|
||||
|
||||
public class DividerBlock extends Block {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package nl.nu.performance.api.client.objects;
|
||||
|
||||
import nl.nu.performance.api.client.interfaces.Block;
|
||||
|
||||
public class DpgBannerBlock extends Block {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package nl.nu.performance.api.client.objects;
|
||||
|
||||
import nl.nu.performance.api.client.interfaces.Block;
|
||||
|
||||
public class HeaderBlock extends Block {
|
||||
// returns title
|
||||
public final StyledText component20() {
|
||||
throw new UnsupportedOperationException("Stub");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package nl.nu.performance.api.client.objects;
|
||||
|
||||
import nl.nu.performance.api.client.unions.LinkFlavor;
|
||||
|
||||
public class Link {
|
||||
public final StyledText getTitle() {
|
||||
throw new UnsupportedOperationException("Stub");
|
||||
}
|
||||
|
||||
public final LinkFlavor getLinkFlavor() {
|
||||
throw new UnsupportedOperationException("Stub");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package nl.nu.performance.api.client.objects;
|
||||
|
||||
import android.os.Parcelable;
|
||||
import nl.nu.performance.api.client.interfaces.Block;
|
||||
|
||||
public abstract class LinkBlock extends Block implements Parcelable {
|
||||
public final Link getLink() {
|
||||
throw new UnsupportedOperationException("Stub");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package nl.nu.performance.api.client.objects;
|
||||
|
||||
public class StyledText {
|
||||
public final String getText() {
|
||||
throw new UnsupportedOperationException("Stub");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package nl.nu.performance.api.client.unions;
|
||||
|
||||
public interface LinkFlavor {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package nl.nu.performance.api.client.unions;
|
||||
|
||||
public class SmallArticleLinkFlavor implements LinkFlavor {
|
||||
public final Boolean isPartner() {
|
||||
throw new UnsupportedOperationException("Stub");
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
package app.revanced.extension.patches;
|
||||
package app.revanced.extension.reddit.patches;
|
||||
|
||||
import com.reddit.domain.model.ILink;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class FilterPromotedLinksPatch {
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* Filters list from promoted links.
|
||||
**/
|
||||
public static List<?> filterChildren(final Iterable<?> links) {
|
||||
@@ -4,7 +4,7 @@ plugins {
|
||||
|
||||
android {
|
||||
namespace = "app.revanced.extension"
|
||||
compileSdk = 33
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
|
||||
@@ -24,7 +24,9 @@ import java.net.URL;
|
||||
* @noinspection unused
|
||||
*/
|
||||
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
|
||||
= getGmsCoreVendorGroupId() + ".android.gms";
|
||||
private static final Uri GMS_CORE_PROVIDER
|
||||
@@ -52,17 +54,20 @@ public class GmsCoreSupport {
|
||||
|
||||
private static void showBatteryOptimizationDialog(Activity context,
|
||||
String dialogMessageRef,
|
||||
String positiveButtonStringRef,
|
||||
String positiveButtonTextRef,
|
||||
DialogInterface.OnClickListener onPositiveClickListener) {
|
||||
// Use a delay to allow the activity to finish initializing.
|
||||
// Otherwise, if device is in dark mode the dialog is shown with wrong color scheme.
|
||||
Utils.runOnMainThreadDelayed(() -> {
|
||||
// Do not set cancelable to false, to allow using back button to skip the action,
|
||||
// just in case the check can never be satisfied.
|
||||
// just in case the battery change can never be satisfied.
|
||||
var dialog = new AlertDialog.Builder(context)
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setTitle(str("gms_core_dialog_title"))
|
||||
.setMessage(str(dialogMessageRef))
|
||||
.setPositiveButton(str(positiveButtonStringRef), onPositiveClickListener)
|
||||
.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.
|
||||
// GmsCore Support changes the package name, but with a mounted installation
|
||||
// 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");
|
||||
// Cannot use localize text here, since the app will load
|
||||
// resources from the unpatched app and all patch strings are missing.
|
||||
@@ -99,7 +105,22 @@ public class GmsCoreSupport {
|
||||
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)) {
|
||||
if (client == null) {
|
||||
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_open_website_text",
|
||||
(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) {
|
||||
Logger.printException(() -> "checkGmsCore failure", ex);
|
||||
}
|
||||
@@ -140,15 +151,17 @@ public class GmsCoreSupport {
|
||||
return !powerManager.isIgnoringBatteryOptimizations(GMS_CORE_PACKAGE_NAME);
|
||||
}
|
||||
|
||||
private static boolean isAndroidAutomotive(Context context) {
|
||||
return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
|
||||
}
|
||||
|
||||
private static String getGmsCoreDownload() {
|
||||
final var vendorGroupId = getGmsCoreVendorGroupId();
|
||||
//noinspection SwitchStatementWithTooFewBranches
|
||||
switch (vendorGroupId) {
|
||||
case "app.revanced":
|
||||
return "https://github.com/revanced/gmscore/releases/latest";
|
||||
default:
|
||||
return vendorGroupId + ".android.gms";
|
||||
}
|
||||
return switch (vendorGroupId) {
|
||||
case "app.revanced" -> "https://github.com/revanced/gmscore/releases/latest";
|
||||
default -> vendorGroupId + ".android.gms";
|
||||
};
|
||||
}
|
||||
|
||||
// Modified by a patch. Do not touch.
|
||||
|
||||
@@ -4,8 +4,10 @@ import android.annotation.SuppressLint;
|
||||
import android.app.*;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.os.Build;
|
||||
@@ -38,15 +40,18 @@ import java.util.concurrent.SynchronousQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import app.revanced.extension.shared.settings.AppLanguage;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||
import app.revanced.extension.shared.settings.preference.ReVancedAboutPreference;
|
||||
|
||||
public class Utils {
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private static Context context;
|
||||
private static volatile Context context;
|
||||
|
||||
private static String versionName;
|
||||
private static String applicationLabel;
|
||||
|
||||
private Utils() {
|
||||
} // utility class
|
||||
@@ -61,28 +66,30 @@ public class Utils {
|
||||
return ""; // Value is replaced during patching.
|
||||
}
|
||||
|
||||
private static PackageInfo getPackageInfo() throws PackageManager.NameNotFoundException {
|
||||
final var packageName = Objects.requireNonNull(getContext()).getPackageName();
|
||||
|
||||
PackageManager packageManager = context.getPackageManager();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
return packageManager.getPackageInfo(
|
||||
packageName,
|
||||
PackageManager.PackageInfoFlags.of(0)
|
||||
);
|
||||
}
|
||||
|
||||
return packageManager.getPackageInfo(
|
||||
packageName,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The version name of the app, such as 19.11.43
|
||||
*/
|
||||
public static String getAppVersionName() {
|
||||
if (versionName == null) {
|
||||
try {
|
||||
final var packageName = Objects.requireNonNull(getContext()).getPackageName();
|
||||
|
||||
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;
|
||||
versionName = getPackageInfo().versionName;
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Failed to get package info", ex);
|
||||
versionName = "Unknown";
|
||||
@@ -92,6 +99,19 @@ public class Utils {
|
||||
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.
|
||||
@@ -325,7 +345,7 @@ public class Utils {
|
||||
|
||||
public static void restartApp(@NonNull Context context) {
|
||||
String packageName = context.getPackageName();
|
||||
Intent intent = context.getPackageManager().getLaunchIntentForPackage(packageName);
|
||||
Intent intent = Objects.requireNonNull(context.getPackageManager().getLaunchIntentForPackage(packageName));
|
||||
Intent mainIntent = Intent.makeRestartActivityTask(intent.getComponent());
|
||||
// Required for API 34 and later
|
||||
// Ref: https://developer.android.com/about/versions/14/behavior-changes-14#safer-intents
|
||||
@@ -336,23 +356,24 @@ public class Utils {
|
||||
|
||||
public static Context getContext() {
|
||||
if (context == null) {
|
||||
Logger.initializationException(Utils.class, "Context is null, returning null!", null);
|
||||
Logger.initializationException(Utils.class, "Context is not set by extension hook, returning null", null);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
public static void setContext(Context appContext) {
|
||||
// Must initially set context to check the app language.
|
||||
context = appContext;
|
||||
// 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,
|
||||
// even though the context is already set before the call.
|
||||
//
|
||||
// The initialization logger methods do not directly or indirectly
|
||||
// reference the Context or any Settings and are unaffected by this problem.
|
||||
//
|
||||
// Info level also helps debug if a patch hook is called before
|
||||
// the context is set since debug logging is off by default.
|
||||
Logger.initializationInfo(Utils.class, "Set context: " + appContext);
|
||||
|
||||
AppLanguage language = BaseSettings.REVANCED_LANGUAGE.get();
|
||||
if (language != AppLanguage.DEFAULT) {
|
||||
// Create a new context with the desired language.
|
||||
Logger.printDebug(() -> "Using app language: " + language);
|
||||
Configuration config = appContext.getResources().getConfiguration();
|
||||
config.setLocale(language.getLocale());
|
||||
context = appContext.createConfigurationContext(config);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setClipboard(@NonNull String text) {
|
||||
@@ -499,6 +520,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.
|
||||
*
|
||||
@@ -571,7 +603,7 @@ public class Utils {
|
||||
|| networkType == NetworkType.OTHER;
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission") // permission already included in YouTube
|
||||
@SuppressLint({"MissingPermission", "deprecation"}) // Permission already included in YouTube.
|
||||
public static NetworkType getNetworkType() {
|
||||
Context networkContext = getContext();
|
||||
if (networkContext == null) {
|
||||
@@ -681,8 +713,8 @@ public class Utils {
|
||||
Preference preference = group.getPreference(i);
|
||||
|
||||
final Sort preferenceSort;
|
||||
if (preference instanceof PreferenceGroup) {
|
||||
sortPreferenceGroups((PreferenceGroup) preference);
|
||||
if (preference instanceof PreferenceGroup subGroup) {
|
||||
sortPreferenceGroups(subGroup);
|
||||
preferenceSort = groupSort; // Sort value for groups is for it's content, not itself.
|
||||
} else {
|
||||
// Allow individual preferences to set a key sorting.
|
||||
@@ -736,8 +768,8 @@ public class Utils {
|
||||
return;
|
||||
}
|
||||
|
||||
String deviceLanguage = Utils.getContext().getResources().getConfiguration().locale.getLanguage();
|
||||
if (deviceLanguage.equals("en")) {
|
||||
String revancedLocale = Utils.getContext().getResources().getConfiguration().locale.getLanguage();
|
||||
if (revancedLocale.equals(Locale.ENGLISH.getLanguage())) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -745,8 +777,8 @@ public class Utils {
|
||||
Preference pref = group.getPreference(i);
|
||||
pref.setSingleLineTitle(false);
|
||||
|
||||
if (pref instanceof PreferenceGroup) {
|
||||
setPreferenceTitlesToMultiLineIfNeeded((PreferenceGroup) pref);
|
||||
if (pref instanceof PreferenceGroup subGroup) {
|
||||
setPreferenceTitlesToMultiLineIfNeeded(subGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,14 @@ 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.
|
||||
*/
|
||||
@@ -16,4 +20,15 @@ public class BaseSettings {
|
||||
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_UNPLUGGED, true, parent(SPOOF_VIDEO_STREAMS));
|
||||
|
||||
}
|
||||
|
||||
@@ -47,6 +47,10 @@ public class BooleanSetting extends Setting<Boolean> {
|
||||
*/
|
||||
public static void privateSetValue(@NonNull BooleanSetting setting, @NonNull Boolean newValue) {
|
||||
setting.value = Objects.requireNonNull(newValue);
|
||||
|
||||
if (setting.isSetToDefault()) {
|
||||
setting.removeFromPreferences();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -65,10 +69,8 @@ public class BooleanSetting extends Setting<Boolean> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(@NonNull Boolean newValue) {
|
||||
// Must set before saving to preferences (otherwise importing fails to update UI correctly).
|
||||
value = Objects.requireNonNull(newValue);
|
||||
preferences.saveBoolean(key, newValue);
|
||||
public void saveToPreferences() {
|
||||
preferences.saveBoolean(key, value);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
||||
@@ -89,10 +89,8 @@ public class EnumSetting<T extends Enum<?>> extends Setting<T> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(@NonNull T newValue) {
|
||||
// Must set before saving to preferences (otherwise importing fails to update UI correctly).
|
||||
value = Objects.requireNonNull(newValue);
|
||||
preferences.saveEnumAsString(key, newValue);
|
||||
public void saveToPreferences() {
|
||||
preferences.saveEnumAsString(key, value);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
||||
@@ -55,10 +55,8 @@ public class FloatSetting extends Setting<Float> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(@NonNull Float newValue) {
|
||||
// Must set before saving to preferences (otherwise importing fails to update UI correctly).
|
||||
value = Objects.requireNonNull(newValue);
|
||||
preferences.saveFloatString(key, newValue);
|
||||
public void saveToPreferences() {
|
||||
preferences.saveFloatString(key, value);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
||||
@@ -55,10 +55,8 @@ public class IntegerSetting extends Setting<Integer> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(@NonNull Integer newValue) {
|
||||
// Must set before saving to preferences (otherwise importing fails to update UI correctly).
|
||||
value = Objects.requireNonNull(newValue);
|
||||
preferences.saveIntegerString(key, newValue);
|
||||
public void saveToPreferences() {
|
||||
preferences.saveIntegerString(key, value);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
||||
@@ -55,10 +55,8 @@ public class LongSetting extends Setting<Long> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(@NonNull Long newValue) {
|
||||
// Must set before saving to preferences (otherwise importing fails to update UI correctly).
|
||||
value = Objects.requireNonNull(newValue);
|
||||
preferences.saveLongString(key, newValue);
|
||||
public void saveToPreferences() {
|
||||
preferences.saveLongString(key, value);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
||||
@@ -7,7 +7,6 @@ import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.StringRef;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.settings.preference.SharedPrefCategory;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
@@ -15,7 +14,6 @@ import java.util.*;
|
||||
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public abstract class Setting<T> {
|
||||
|
||||
/**
|
||||
@@ -154,7 +152,6 @@ public abstract class Setting<T> {
|
||||
|
||||
/**
|
||||
* 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
|
||||
public final StringRef userDialogMessage;
|
||||
@@ -245,6 +242,7 @@ public abstract class Setting<T> {
|
||||
*
|
||||
* This method will be deleted in the future.
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
public static void migrateFromOldPreferences(@NonNull SharedPrefCategory oldPrefs, @NonNull Setting setting, String settingKey) {
|
||||
if (!oldPrefs.preferences.contains(settingKey)) {
|
||||
return; // Nothing to do.
|
||||
@@ -289,6 +287,13 @@ public abstract class Setting<T> {
|
||||
*/
|
||||
public static void privateSetValueFromString(@NonNull Setting<?> setting, @NonNull String newValue) {
|
||||
setting.setValueFromString(newValue);
|
||||
|
||||
// Clear the preference value since default is used, to allow changing
|
||||
// the changing the default for a future release. Without this after upgrading
|
||||
// the saved value will be whatever was the default when the app was first installed.
|
||||
if (setting.isSetToDefault()) {
|
||||
setting.removeFromPreferences();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -304,7 +309,33 @@ public abstract class Setting<T> {
|
||||
/**
|
||||
* Persistently saves the value.
|
||||
*/
|
||||
public abstract void save(@NonNull T newValue);
|
||||
public final void save(@NonNull T newValue) {
|
||||
if (value.equals(newValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Must set before saving to preferences (otherwise importing fails to update UI correctly).
|
||||
value = Objects.requireNonNull(newValue);
|
||||
|
||||
if (defaultValue.equals(newValue)) {
|
||||
removeFromPreferences();
|
||||
} else {
|
||||
saveToPreferences();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save {@link #value} to {@link #preferences}.
|
||||
*/
|
||||
protected abstract void saveToPreferences();
|
||||
|
||||
/**
|
||||
* Remove {@link #value} from {@link #preferences}.
|
||||
*/
|
||||
protected final void removeFromPreferences() {
|
||||
Logger.printDebug(() -> "Clearing stored preference value (reset to default): " + key);
|
||||
preferences.removeKey(key);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public abstract T get();
|
||||
@@ -330,7 +361,7 @@ public abstract class Setting<T> {
|
||||
return value.equals(defaultValue);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return key + "=" + get();
|
||||
@@ -420,6 +451,7 @@ public abstract class Setting<T> {
|
||||
|
||||
boolean rebootSettingChanged = false;
|
||||
int numberOfSettingsImported = 0;
|
||||
//noinspection rawtypes
|
||||
for (Setting setting : SETTINGS) {
|
||||
String key = setting.getImportExportKey();
|
||||
if (json.has(key)) {
|
||||
|
||||
@@ -55,10 +55,8 @@ public class StringSetting extends Setting<String> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(@NonNull String newValue) {
|
||||
// Must set before saving to preferences (otherwise importing fails to update UI correctly).
|
||||
value = Objects.requireNonNull(newValue);
|
||||
preferences.saveString(key, newValue);
|
||||
public void saveToPreferences() {
|
||||
preferences.saveString(key, value);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
||||
@@ -42,7 +42,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
||||
|
||||
private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
|
||||
try {
|
||||
Setting<?> setting = Setting.getSettingFromPath(str);
|
||||
Setting<?> setting = Setting.getSettingFromPath(Objects.requireNonNull(str));
|
||||
if (setting == null) {
|
||||
return;
|
||||
}
|
||||
@@ -52,23 +52,21 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
||||
}
|
||||
Logger.printDebug(() -> "Preference changed: " + setting.key);
|
||||
|
||||
// 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();
|
||||
|
||||
if (settingImportInProgress) {
|
||||
if (!settingImportInProgress && !showingUserDialogMessage) {
|
||||
if (setting.userDialogMessage != null && !prefIsSetToDefault(pref, setting)) {
|
||||
// Do not change the setting yet, to allow preserving whatever
|
||||
// list/text value was previously set if it needs to be reverted.
|
||||
showSettingUserDialogConfirmation(pref, setting);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!showingUserDialogMessage) {
|
||||
if (setting.userDialogMessage != null && ((SwitchPreference) pref).isChecked() != (Boolean) setting.defaultValue) {
|
||||
showSettingUserDialogConfirmation((SwitchPreference) pref, (BooleanSetting) setting);
|
||||
} else if (setting.rebootApp) {
|
||||
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) {
|
||||
Logger.printException(() -> "OnSharedPreferenceChangeListener failure", ex);
|
||||
}
|
||||
@@ -92,7 +90,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
||||
Utils.setPreferenceTitlesToMultiLineIfNeeded(screen);
|
||||
}
|
||||
|
||||
private void showSettingUserDialogConfirmation(SwitchPreference switchPref, BooleanSetting setting) {
|
||||
private void showSettingUserDialogConfirmation(Preference pref, Setting<?> setting) {
|
||||
Utils.verifyOnMainThread();
|
||||
|
||||
final var context = getContext();
|
||||
@@ -104,12 +102,19 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
||||
.setTitle(confirmDialogTitle)
|
||||
.setMessage(Objects.requireNonNull(setting.userDialogMessage).toString())
|
||||
.setPositiveButton(android.R.string.ok, (dialog, id) -> {
|
||||
// User confirmed, save to the Setting.
|
||||
updatePreference(pref, setting, true, false);
|
||||
|
||||
// Update availability of other preferences that may be changed.
|
||||
updateUIAvailability();
|
||||
|
||||
if (setting.rebootApp) {
|
||||
showRestartDialog(context);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, (dialog, id) -> {
|
||||
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 -> {
|
||||
showingUserDialogMessage = false;
|
||||
@@ -132,19 +137,37 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
||||
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.
|
||||
*/
|
||||
private void updatePreferenceScreen(@NonNull PreferenceScreen screen,
|
||||
private void updatePreferenceScreen(@NonNull PreferenceGroup group,
|
||||
boolean syncSettingValue,
|
||||
boolean applySettingToPreference) {
|
||||
// Alternatively this could iterate thru all Settings and check for any matching Preferences,
|
||||
// but there are many more Settings than UI preferences so it's more efficient to only check
|
||||
// the Preferences.
|
||||
for (int i = 0, prefCount = screen.getPreferenceCount(); i < prefCount; i++) {
|
||||
Preference pref = screen.getPreference(i);
|
||||
if (pref instanceof PreferenceScreen) {
|
||||
updatePreferenceScreen((PreferenceScreen) pref, syncSettingValue, applySettingToPreference);
|
||||
for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) {
|
||||
Preference pref = group.getPreference(i);
|
||||
if (pref instanceof PreferenceGroup subGroup) {
|
||||
updatePreferenceScreen(subGroup, syncSettingValue, applySettingToPreference);
|
||||
} else if (pref.hasKey()) {
|
||||
String key = pref.getKey();
|
||||
Setting<?> setting = Setting.getSettingFromPath(key);
|
||||
@@ -170,23 +193,20 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
||||
protected void syncSettingWithPreference(@NonNull Preference pref,
|
||||
@NonNull Setting<?> setting,
|
||||
boolean applySettingToPreference) {
|
||||
if (pref instanceof SwitchPreference) {
|
||||
SwitchPreference switchPref = (SwitchPreference) pref;
|
||||
if (pref instanceof SwitchPreference switchPref) {
|
||||
BooleanSetting boolSetting = (BooleanSetting) setting;
|
||||
if (applySettingToPreference) {
|
||||
switchPref.setChecked(boolSetting.get());
|
||||
} else {
|
||||
BooleanSetting.privateSetValue(boolSetting, switchPref.isChecked());
|
||||
}
|
||||
} else if (pref instanceof EditTextPreference) {
|
||||
EditTextPreference editPreference = (EditTextPreference) pref;
|
||||
} else if (pref instanceof EditTextPreference editPreference) {
|
||||
if (applySettingToPreference) {
|
||||
editPreference.setText(setting.get().toString());
|
||||
} else {
|
||||
Setting.privateSetValueFromString(setting, editPreference.getText());
|
||||
}
|
||||
} else if (pref instanceof ListPreference) {
|
||||
ListPreference listPref = (ListPreference) pref;
|
||||
} else if (pref instanceof ListPreference listPref) {
|
||||
if (applySettingToPreference) {
|
||||
listPref.setValue(setting.get().toString());
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package app.revanced.extension.shared.settings.preference;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.preference.PreferenceCategory;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
/**
|
||||
* Empty preference category with no title, used to organize and group related preferences together.
|
||||
*/
|
||||
@SuppressWarnings({"unused", "deprecation"})
|
||||
public class NoTitlePreferenceCategory extends PreferenceCategory {
|
||||
public NoTitlePreferenceCategory(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public NoTitlePreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public NoTitlePreferenceCategory(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressLint("MissingSuperCall")
|
||||
protected View onCreateView(ViewGroup parent) {
|
||||
// Return an empty, zero-height view to eliminate spacing
|
||||
return new View(getContext());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,9 +54,7 @@ public class ReVancedAboutPreference extends Preference {
|
||||
}
|
||||
|
||||
protected boolean isDarkModeEnabled() {
|
||||
Configuration config = getContext().getResources().getConfiguration();
|
||||
final int currentNightMode = config.uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||
return currentNightMode == Configuration.UI_MODE_NIGHT_YES;
|
||||
return Utils.isDarkModeEnabled(getContext());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
package app.revanced.extension.shared.spoof;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
|
||||
public enum ClientType {
|
||||
// https://dumps.tadiphone.dev/dumps/oculus/eureka
|
||||
ANDROID_VR_NO_AUTH(
|
||||
28,
|
||||
"ANDROID_VR",
|
||||
"com.google.android.apps.youtube.vr.oculus",
|
||||
"Oculus",
|
||||
"Quest 3",
|
||||
"Android",
|
||||
"12",
|
||||
// Android 12.1
|
||||
"32",
|
||||
"SQ3A.220605.009.A1",
|
||||
"132.0.6808.3",
|
||||
"1.61.48",
|
||||
false,
|
||||
false,
|
||||
"Android VR No auth"
|
||||
),
|
||||
// Chromecast with Google TV 4K.
|
||||
// https://dumps.tadiphone.dev/dumps/google/kirkwood
|
||||
ANDROID_UNPLUGGED(
|
||||
29,
|
||||
"ANDROID_UNPLUGGED",
|
||||
"com.google.android.apps.youtube.unplugged",
|
||||
"Google",
|
||||
"Google TV Streamer",
|
||||
"Android",
|
||||
"14",
|
||||
"34",
|
||||
"UTT3.240625.001.K5",
|
||||
"132.0.6808.3",
|
||||
"8.49.0",
|
||||
true,
|
||||
true,
|
||||
"Android TV"
|
||||
),
|
||||
// Cannot play livestreams and lacks HDR, but can play videos with music and labeled "for children".
|
||||
// Google Pixel 9 Pro Fold
|
||||
// https://dumps.tadiphone.dev/dumps/google/barbet
|
||||
ANDROID_CREATOR(
|
||||
14,
|
||||
"ANDROID_CREATOR",
|
||||
"com.google.android.apps.youtube.creator",
|
||||
"Google",
|
||||
"Pixel 9 Pro Fold",
|
||||
"Android",
|
||||
"15",
|
||||
"35",
|
||||
"AP3A.241005.015.A2",
|
||||
"132.0.6779.0",
|
||||
"23.47.101",
|
||||
true,
|
||||
true,
|
||||
"Android Creator"
|
||||
),
|
||||
IOS_UNPLUGGED(
|
||||
33,
|
||||
"IOS_UNPLUGGED",
|
||||
"com.google.ios.youtubeunplugged",
|
||||
"Apple",
|
||||
forceAVC()
|
||||
// 11 Pro Max (last device with iOS 13)
|
||||
? "iPhone12,5"
|
||||
// 15 Pro Max
|
||||
: "iPhone16,2",
|
||||
"iOS",
|
||||
forceAVC()
|
||||
// iOS 13 and earlier uses only AVC. 14+ adds VP9 and AV1.
|
||||
? "13.7.17H35"
|
||||
: "18.2.22C152",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
// Version number should be a valid iOS release.
|
||||
// https://www.ipa4fun.com/history/152043/
|
||||
forceAVC()
|
||||
// Some newer versions can also force AVC,
|
||||
// but 6.45 is the last version that supports iOS 13.
|
||||
? "6.45"
|
||||
: "8.49",
|
||||
true,
|
||||
true,
|
||||
forceAVC()
|
||||
? "iOS TV Force AVC"
|
||||
: "iOS TV"
|
||||
),
|
||||
ANDROID_VR_AUTH(
|
||||
ANDROID_VR_NO_AUTH.id,
|
||||
ANDROID_VR_NO_AUTH.clientName,
|
||||
ANDROID_VR_NO_AUTH.packageName,
|
||||
ANDROID_VR_NO_AUTH.deviceMake,
|
||||
ANDROID_VR_NO_AUTH.deviceModel,
|
||||
ANDROID_VR_NO_AUTH.osName,
|
||||
ANDROID_VR_NO_AUTH.osVersion,
|
||||
ANDROID_VR_NO_AUTH.androidSdkVersion,
|
||||
ANDROID_VR_NO_AUTH.buildId,
|
||||
ANDROID_VR_NO_AUTH.cronetVersion,
|
||||
ANDROID_VR_NO_AUTH.clientVersion,
|
||||
ANDROID_VR_NO_AUTH.requiresAuth,
|
||||
true,
|
||||
"Android VR Auth"
|
||||
);
|
||||
|
||||
private static boolean forceAVC() {
|
||||
return BaseSettings.SPOOF_VIDEO_STREAMS_IOS_FORCE_AVC.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* YouTube
|
||||
* <a href="https://github.com/zerodytrash/YouTube-Internal-Clients?tab=readme-ov-file#clients">client type</a>
|
||||
*/
|
||||
public final int id;
|
||||
|
||||
public final String clientName;
|
||||
|
||||
/**
|
||||
* App package name.
|
||||
*/
|
||||
private final String packageName;
|
||||
|
||||
/**
|
||||
* Player user-agent.
|
||||
*/
|
||||
public final String userAgent;
|
||||
|
||||
/**
|
||||
* Device model, equivalent to {@link Build#MANUFACTURER} (System property: ro.product.vendor.manufacturer)
|
||||
*/
|
||||
public final String deviceMake;
|
||||
|
||||
/**
|
||||
* Device model, equivalent to {@link Build#MODEL} (System property: ro.product.vendor.model)
|
||||
*/
|
||||
public final String deviceModel;
|
||||
|
||||
/**
|
||||
* Device OS name.
|
||||
*/
|
||||
public final String osName;
|
||||
|
||||
/**
|
||||
* Device OS version.
|
||||
*/
|
||||
public final String osVersion;
|
||||
|
||||
/**
|
||||
* Android SDK version, equivalent to {@link Build.VERSION#SDK} (System property: ro.build.version.sdk)
|
||||
* Field is null if not applicable.
|
||||
*/
|
||||
@Nullable
|
||||
public final String androidSdkVersion;
|
||||
|
||||
/**
|
||||
* Android build id, equivalent to {@link Build#ID}.
|
||||
* Field is null if not applicable.
|
||||
*/
|
||||
@Nullable
|
||||
private final String buildId;
|
||||
|
||||
/**
|
||||
* Cronet release version, as found in decompiled client apk.
|
||||
* Field is null if not applicable.
|
||||
*/
|
||||
@Nullable
|
||||
private final String cronetVersion;
|
||||
|
||||
/**
|
||||
* App version.
|
||||
*/
|
||||
public final String clientVersion;
|
||||
|
||||
/**
|
||||
* If this client requires authentication and does not work
|
||||
* if logged out or in incognito mode.
|
||||
*/
|
||||
public final boolean requiresAuth;
|
||||
|
||||
/**
|
||||
* If the client should use authentication if available.
|
||||
*/
|
||||
public final boolean useAuth;
|
||||
|
||||
/**
|
||||
* Friendly name displayed in stats for nerds.
|
||||
*/
|
||||
public final String friendlyName;
|
||||
|
||||
@SuppressWarnings("ConstantLocale")
|
||||
ClientType(int id,
|
||||
String clientName,
|
||||
String packageName,
|
||||
String deviceMake,
|
||||
String deviceModel,
|
||||
String osName,
|
||||
String osVersion,
|
||||
@Nullable String androidSdkVersion,
|
||||
@Nullable String buildId,
|
||||
@Nullable String cronetVersion,
|
||||
String clientVersion,
|
||||
boolean requiresAuth,
|
||||
boolean useAuth,
|
||||
String friendlyName) {
|
||||
this.id = id;
|
||||
this.clientName = clientName;
|
||||
this.packageName = packageName;
|
||||
this.deviceMake = deviceMake;
|
||||
this.deviceModel = deviceModel;
|
||||
this.osName = osName;
|
||||
this.osVersion = osVersion;
|
||||
this.androidSdkVersion = androidSdkVersion;
|
||||
this.buildId = buildId;
|
||||
this.cronetVersion = cronetVersion;
|
||||
this.clientVersion = clientVersion;
|
||||
this.requiresAuth = requiresAuth;
|
||||
this.useAuth = useAuth;
|
||||
this.friendlyName = friendlyName;
|
||||
|
||||
Locale defaultLocale = Locale.getDefault();
|
||||
if (androidSdkVersion == null) {
|
||||
// Convert version from '18.2.22C152' into '18_2_22'
|
||||
String userAgentOsVersion = osVersion
|
||||
.replaceAll("(\\d+\\.\\d+\\.\\d+).*", "$1")
|
||||
.replace(".", "_");
|
||||
// https://github.com/mitmproxy/mitmproxy/issues/4836
|
||||
this.userAgent = String.format("%s/%s (%s; U; CPU iOS %s like Mac OS X; %s)",
|
||||
packageName,
|
||||
clientVersion,
|
||||
deviceModel,
|
||||
userAgentOsVersion,
|
||||
defaultLocale
|
||||
);
|
||||
} else {
|
||||
this.userAgent = String.format("%s/%s (Linux; U; Android %s; %s; %s; Build/%s; Cronet/%s)",
|
||||
packageName,
|
||||
clientVersion,
|
||||
osVersion,
|
||||
defaultLocale,
|
||||
deviceModel,
|
||||
Objects.requireNonNull(buildId),
|
||||
Objects.requireNonNull(cronetVersion)
|
||||
);
|
||||
}
|
||||
Logger.printDebug(() -> "userAgent: " + this.userAgent);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,30 +1,25 @@
|
||||
package app.revanced.extension.youtube.patches.spoof;
|
||||
package app.revanced.extension.shared.spoof;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
import app.revanced.extension.shared.settings.Setting;
|
||||
import app.revanced.extension.youtube.patches.spoof.requests.StreamingDataRequest;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.shared.spoof.requests.StreamingDataRequest;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class SpoofVideoStreamsPatch {
|
||||
public static final class ForceiOSAVCAvailability implements Setting.Availability {
|
||||
@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 = BaseSettings.SPOOF_VIDEO_STREAMS.get();
|
||||
|
||||
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.
|
||||
@@ -32,6 +27,19 @@ public class SpoofVideoStreamsPatch {
|
||||
private static final String UNREACHABLE_HOST_URI_STRING = "https://127.0.0.0";
|
||||
private static final Uri UNREACHABLE_HOST_URI = Uri.parse(UNREACHABLE_HOST_URI_STRING);
|
||||
|
||||
/**
|
||||
* @return If this patch was included during patching.
|
||||
*/
|
||||
private static boolean isPatchIncluded() {
|
||||
return false; // Modified during patching.
|
||||
}
|
||||
|
||||
public static boolean notSpoofingToAndroid() {
|
||||
return !isPatchIncluded()
|
||||
|| !BaseSettings.SPOOF_VIDEO_STREAMS.get()
|
||||
|| BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ClientType.IOS_UNPLUGGED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* Blocks /get_watch requests by returning an unreachable URI.
|
||||
@@ -69,9 +77,9 @@ public class SpoofVideoStreamsPatch {
|
||||
String path = originalUri.getPath();
|
||||
|
||||
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) {
|
||||
Logger.printException(() -> "blockInitPlaybackRequest failure", ex);
|
||||
@@ -88,6 +96,32 @@ public class SpoofVideoStreamsPatch {
|
||||
return SPOOF_STREAMING_DATA;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* Only invoked when playing a livestream on an iOS client.
|
||||
*/
|
||||
public static boolean fixHLSCurrentTime(boolean original) {
|
||||
if (!SPOOF_STREAMING_DATA) {
|
||||
return original;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* Turns off a feature flag that interferes with spoofing.
|
||||
*/
|
||||
public static boolean useMediaFetchHotConfigReplacement(boolean original) {
|
||||
if (original) {
|
||||
Logger.printDebug(() -> "useMediaFetchHotConfigReplacement is set on");
|
||||
}
|
||||
|
||||
if (!SPOOF_STREAMING_DATA) {
|
||||
return original;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
@@ -96,11 +130,27 @@ public class SpoofVideoStreamsPatch {
|
||||
try {
|
||||
Uri uri = Uri.parse(url);
|
||||
String path = uri.getPath();
|
||||
// 'heartbeat' has no video id and appears to be only after playback has started.
|
||||
if (path != null && path.contains("player") && !path.contains("heartbeat")) {
|
||||
String videoId = Objects.requireNonNull(uri.getQueryParameter("id"));
|
||||
StreamingDataRequest.fetchRequest(videoId, requestHeaders);
|
||||
if (path == null || !path.contains("player")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 'get_drm_license' has no video id and appears to happen when waiting for a paid video to start.
|
||||
// 'heartbeat' has no video id and appears to be only after playback has started.
|
||||
// 'refresh' has no video id and appears to happen when waiting for a livestream to start.
|
||||
// 'ad_break' has no video id.
|
||||
if (path.contains("get_drm_license") || path.contains("heartbeat")
|
||||
|| path.contains("refresh") || path.contains("ad_break")) {
|
||||
Logger.printDebug(() -> "Ignoring path: " + path);
|
||||
return;
|
||||
}
|
||||
|
||||
String id = uri.getQueryParameter("id");
|
||||
if (id == null) {
|
||||
Logger.printException(() -> "Ignoring request with no id: " + url);
|
||||
return;
|
||||
}
|
||||
|
||||
StreamingDataRequest.fetchRequest(id, requestHeaders);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "buildRequest failure", ex);
|
||||
}
|
||||
@@ -165,4 +215,38 @@ public class SpoofVideoStreamsPatch {
|
||||
|
||||
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,16 +1,17 @@
|
||||
package app.revanced.extension.youtube.patches.spoof.requests;
|
||||
package app.revanced.extension.shared.spoof.requests;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.util.Locale;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.requests.Requester;
|
||||
import app.revanced.extension.shared.requests.Route;
|
||||
import app.revanced.extension.youtube.patches.spoof.ClientType;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
import app.revanced.extension.shared.spoof.ClientType;
|
||||
|
||||
final class PlayerRoutes {
|
||||
static final Route.CompiledRoute GET_STREAMING_DATA = new Route(
|
||||
@@ -19,40 +20,50 @@ final class PlayerRoutes {
|
||||
"?fields=streamingData" +
|
||||
"&alt=proto"
|
||||
).compile();
|
||||
|
||||
private static final String YT_API_URL = "https://youtubei.googleapis.com/youtubei/v1/";
|
||||
|
||||
/**
|
||||
* TCP connection and HTTP read timeout
|
||||
*/
|
||||
private static final int CONNECTION_TIMEOUT_MILLISECONDS = 10 * 1000; // 10 Seconds.
|
||||
|
||||
private static final String LOCALE_LANGUAGE = Utils.getContext().getResources()
|
||||
.getConfiguration().locale.getLanguage();
|
||||
|
||||
private PlayerRoutes() {
|
||||
}
|
||||
|
||||
static String createInnertubeBody(ClientType clientType) {
|
||||
static String createInnertubeBody(ClientType clientType, String videoId) {
|
||||
JSONObject innerTubeBody = new JSONObject();
|
||||
|
||||
try {
|
||||
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();
|
||||
// Required to use correct default audio channel with iOS.
|
||||
client.put("hl", LOCALE_LANGUAGE);
|
||||
client.put("clientName", clientType.name());
|
||||
client.put("clientVersion", clientType.clientVersion);
|
||||
client.put("deviceMake", clientType.deviceMake);
|
||||
client.put("deviceModel", clientType.deviceModel);
|
||||
client.put("clientName", clientType.clientName);
|
||||
client.put("clientVersion", clientType.clientVersion);
|
||||
client.put("osName", clientType.osName);
|
||||
client.put("osVersion", clientType.osVersion);
|
||||
if (clientType.androidSdkVersion != null) {
|
||||
client.put("androidSdkVersion", clientType.androidSdkVersion);
|
||||
}
|
||||
client.put("hl", streamLocale.getLanguage());
|
||||
client.put("gl", streamLocale.getCountry());
|
||||
context.put("client", client);
|
||||
|
||||
innerTubeBody.put("context", context);
|
||||
innerTubeBody.put("contentCheckOk", true);
|
||||
innerTubeBody.put("racyCheckOk", true);
|
||||
innerTubeBody.put("videoId", "%s");
|
||||
innerTubeBody.put("videoId", videoId);
|
||||
} catch (JSONException e) {
|
||||
Logger.printException(() -> "Failed to create innerTubeBody", e);
|
||||
}
|
||||
@@ -68,6 +79,9 @@ final class PlayerRoutes {
|
||||
|
||||
connection.setRequestProperty("Content-Type", "application/json");
|
||||
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.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.Nullable;
|
||||
@@ -22,8 +22,7 @@ import java.util.concurrent.TimeoutException;
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
import app.revanced.extension.youtube.patches.spoof.ClientType;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.shared.spoof.ClientType;
|
||||
|
||||
/**
|
||||
* Video streaming data. Fetching is tied to the behavior YT uses,
|
||||
@@ -37,20 +36,40 @@ import app.revanced.extension.youtube.settings.Settings;
|
||||
public class StreamingDataRequest {
|
||||
|
||||
private static final ClientType[] CLIENT_ORDER_TO_USE;
|
||||
|
||||
static {
|
||||
ClientType[] allClientTypes = ClientType.values();
|
||||
ClientType preferredClient = BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get();
|
||||
|
||||
CLIENT_ORDER_TO_USE = new ClientType[allClientTypes.length];
|
||||
CLIENT_ORDER_TO_USE[0] = preferredClient;
|
||||
|
||||
int i = 1;
|
||||
for (ClientType c : allClientTypes) {
|
||||
if (c != preferredClient) {
|
||||
CLIENT_ORDER_TO_USE[i++] = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final String AUTHORIZATION_HEADER = "Authorization";
|
||||
|
||||
private static final String[] REQUEST_HEADER_KEYS = {
|
||||
AUTHORIZATION_HEADER, // Available only to logged-in users.
|
||||
"X-GOOG-API-FORMAT-VERSION",
|
||||
"X-Goog-Visitor-Id"
|
||||
};
|
||||
|
||||
/**
|
||||
* TCP connection and HTTP read timeout.
|
||||
*/
|
||||
private static final int HTTP_TIMEOUT_MILLISECONDS = 10 * 1000;
|
||||
|
||||
/**
|
||||
* Any arbitrarily large value, but must be at least twice {@link #HTTP_TIMEOUT_MILLISECONDS}
|
||||
*/
|
||||
private static final int MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000;
|
||||
|
||||
private static final Map<String, StreamingDataRequest> cache = Collections.synchronizedMap(
|
||||
new LinkedHashMap<>(100) {
|
||||
/**
|
||||
@@ -68,22 +87,15 @@ public class StreamingDataRequest {
|
||||
}
|
||||
});
|
||||
|
||||
static {
|
||||
ClientType[] allClientTypes = ClientType.values();
|
||||
ClientType preferredClient = Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get();
|
||||
private static volatile ClientType lastSpoofedClientType;
|
||||
|
||||
CLIENT_ORDER_TO_USE = new ClientType[allClientTypes.length];
|
||||
CLIENT_ORDER_TO_USE[0] = preferredClient;
|
||||
|
||||
int i = 1;
|
||||
for (ClientType c : allClientTypes) {
|
||||
if (c != preferredClient) {
|
||||
CLIENT_ORDER_TO_USE[i++] = c;
|
||||
}
|
||||
}
|
||||
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) {
|
||||
@@ -108,7 +120,8 @@ public class StreamingDataRequest {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static HttpURLConnection send(ClientType clientType, String videoId,
|
||||
private static HttpURLConnection send(ClientType clientType,
|
||||
String videoId,
|
||||
Map<String, String> playerHeaders,
|
||||
boolean showErrorToasts) {
|
||||
Objects.requireNonNull(clientType);
|
||||
@@ -116,26 +129,40 @@ public class StreamingDataRequest {
|
||||
Objects.requireNonNull(playerHeaders);
|
||||
|
||||
final long startTime = System.currentTimeMillis();
|
||||
String clientTypeName = clientType.name();
|
||||
Logger.printDebug(() -> "Fetching video streams for: " + videoId + " using client: " + clientType.name());
|
||||
|
||||
try {
|
||||
HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType);
|
||||
connection.setConnectTimeout(HTTP_TIMEOUT_MILLISECONDS);
|
||||
connection.setReadTimeout(HTTP_TIMEOUT_MILLISECONDS);
|
||||
|
||||
boolean authHeadersIncludes = false;
|
||||
|
||||
for (String key : REQUEST_HEADER_KEYS) {
|
||||
if (!clientType.canLogin && key.equals(AUTHORIZATION_HEADER)) {
|
||||
String value = playerHeaders.get(key);
|
||||
|
||||
if (value != null) {
|
||||
if (key.equals(AUTHORIZATION_HEADER)) {
|
||||
if (!clientType.useAuth) {
|
||||
Logger.printDebug(() -> "Not including request header: " + key);
|
||||
continue;
|
||||
}
|
||||
authHeadersIncludes = true;
|
||||
}
|
||||
|
||||
String value = playerHeaders.get(key);
|
||||
if (value != null) {
|
||||
Logger.printDebug(() -> "Including request header: " + key);
|
||||
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);
|
||||
connection.setFixedLengthStreamingMode(requestBody.length);
|
||||
connection.getOutputStream().write(requestBody);
|
||||
@@ -143,8 +170,10 @@ public class StreamingDataRequest {
|
||||
final int responseCode = connection.getResponseCode();
|
||||
if (responseCode == 200) return connection;
|
||||
|
||||
handleConnectionError(clientTypeName + " not available with response code: "
|
||||
+ responseCode + " message: " + connection.getResponseMessage(),
|
||||
// This situation likely means the patches are outdated.
|
||||
// Use a toast message that suggests updating.
|
||||
handleConnectionError("Playback error (App is outdated?) " + clientType + ": "
|
||||
+ responseCode + " response: " + connection.getResponseMessage(),
|
||||
null, showErrorToasts);
|
||||
} catch (SocketTimeoutException ex) {
|
||||
handleConnectionError("Connection timeout", ex, showErrorToasts);
|
||||
@@ -165,7 +194,7 @@ public class StreamingDataRequest {
|
||||
// Retry with different client if empty response body is received.
|
||||
int i = 0;
|
||||
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;
|
||||
|
||||
HttpURLConnection connection = send(clientType, videoId, playerHeaders, showErrorToast);
|
||||
@@ -173,26 +202,32 @@ public class StreamingDataRequest {
|
||||
try {
|
||||
// gzip encoding doesn't response with content length (-1),
|
||||
// but empty response body does.
|
||||
if (connection.getContentLength() != 0) {
|
||||
try (InputStream inputStream = new BufferedInputStream(connection.getInputStream())) {
|
||||
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
||||
if (connection.getContentLength() == 0) {
|
||||
if (BaseSettings.DEBUG.get() && BaseSettings.DEBUG_TOAST_ON_ERROR.get()) {
|
||||
Utils.showToastShort("Ignoring empty spoof stream client: " + clientType);
|
||||
}
|
||||
} else {
|
||||
try (InputStream inputStream = new BufferedInputStream(connection.getInputStream());
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
||||
|
||||
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) {
|
||||
Logger.printException(() -> "Fetch failed while processing response data", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleConnectionError("Could not fetch any client streams", null, debugEnabled);
|
||||
lastSpoofedClientType = null;
|
||||
handleConnectionError("Could not fetch any client streams", null, true);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ plugins {
|
||||
|
||||
android {
|
||||
namespace = "app.revanced.extension"
|
||||
compileSdk = 33
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
|
||||
@@ -3,3 +3,14 @@ dependencies {
|
||||
compileOnly(project(":extensions:tiktok:stub"))
|
||||
compileOnly(libs.annotation)
|
||||
}
|
||||
|
||||
android {
|
||||
defaultConfig {
|
||||
minSdk = 22
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
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) {
|
||||
@@ -20,6 +31,29 @@ public class Utils {
|
||||
}
|
||||
|
||||
setting.save("0-" + Long.MAX_VALUE);
|
||||
return new long[]{0L, 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;
|
||||
|
||||
import app.revanced.extension.tiktok.settings.Settings;
|
||||
import com.ss.android.ugc.aweme.feed.model.Aweme;
|
||||
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 {
|
||||
|
||||
final long minLike;
|
||||
final long maxLike;
|
||||
|
||||
LikeCountFilter() {
|
||||
long[] minMax = parseMinMax(Settings.MIN_MAX_LIKES);
|
||||
long[] minMax = Utils.parseMinMax(Settings.MIN_MAX_LIKES);
|
||||
minLike = minMax[0];
|
||||
maxLike = minMax[1];
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
package app.revanced.extension.tiktok.feedfilter;
|
||||
|
||||
import app.revanced.extension.tiktok.Utils;
|
||||
import app.revanced.extension.tiktok.settings.Settings;
|
||||
import com.ss.android.ugc.aweme.feed.model.Aweme;
|
||||
import com.ss.android.ugc.aweme.feed.model.AwemeStatistics;
|
||||
|
||||
import static app.revanced.extension.tiktok.Utils.parseMinMax;
|
||||
|
||||
public class ViewCountFilter implements IFilter {
|
||||
final long minView;
|
||||
final long maxView;
|
||||
|
||||
ViewCountFilter() {
|
||||
long[] minMax = parseMinMax(Settings.MIN_MAX_VIEWS);
|
||||
long[] minMax = Utils.parseMinMax(Settings.MIN_MAX_VIEWS);
|
||||
minView = minMax[0];
|
||||
maxView = minMax[1];
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ public class Settings extends BaseSettings {
|
||||
public static final BooleanSetting DOWNLOAD_WATERMARK = new BooleanSetting("down_watermark", TRUE);
|
||||
public static final BooleanSetting CLEAR_DISPLAY = new BooleanSetting("clear_display", FALSE);
|
||||
public static final FloatSetting REMEMBERED_SPEED = new FloatSetting("REMEMBERED_SPEED", 1.0f);
|
||||
public static final BooleanSetting SIM_SPOOF = new BooleanSetting("simspoof", TRUE, true);
|
||||
public static final BooleanSetting SIM_SPOOF = new BooleanSetting("simspoof", FALSE, true);
|
||||
public static final StringSetting SIM_SPOOF_ISO = new StringSetting("simspoof_iso", "us");
|
||||
public static final StringSetting SIMSPOOF_MCCMNC = new StringSetting("simspoof_mccmnc", "310160");
|
||||
public static final StringSetting SIMSPOOF_OP_NAME = new StringSetting("simspoof_op_name", "T-Mobile");
|
||||
|
||||
@@ -16,10 +16,10 @@ import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
|
||||
import app.revanced.extension.shared.settings.StringSetting;
|
||||
import app.revanced.extension.tiktok.Utils;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class DownloadPathPreference extends DialogPreference {
|
||||
private final Context context;
|
||||
private final String[] entryValues = {"DCIM", "Movies", "Pictures"};
|
||||
private String mValue;
|
||||
|
||||
@@ -29,11 +29,10 @@ public class DownloadPathPreference extends DialogPreference {
|
||||
|
||||
public DownloadPathPreference(Context context, String title, StringSetting setting) {
|
||||
super(context);
|
||||
this.context = context;
|
||||
this.setTitle(title);
|
||||
this.setSummary(Environment.getExternalStorageDirectory().getPath() + "/" + setting.get());
|
||||
this.setKey(setting.key);
|
||||
this.setValue(setting.get());
|
||||
setTitle(title);
|
||||
setSummary(Environment.getExternalStorageDirectory().getPath() + "/" + setting.get());
|
||||
setKey(setting.key);
|
||||
setValue(setting.get());
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
@@ -59,6 +58,7 @@ public class DownloadPathPreference extends DialogPreference {
|
||||
childDownloadPath = getValue().substring(getValue().indexOf("/") + 1);
|
||||
mediaPathIndex = findIndexOf(currentMedia);
|
||||
|
||||
Context context = getContext();
|
||||
LinearLayout dialogView = new LinearLayout(context);
|
||||
RadioGroup mediaPath = new RadioGroup(context);
|
||||
mediaPath.setLayoutParams(new RadioGroup.LayoutParams(-1, -2));
|
||||
@@ -79,12 +79,10 @@ public class DownloadPathPreference extends DialogPreference {
|
||||
downloadPath.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -99,6 +97,13 @@ public class DownloadPathPreference extends DialogPreference {
|
||||
return dialogView;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBindView(View view) {
|
||||
super.onBindView(view);
|
||||
|
||||
Utils.setTitleAndSummaryColor(getContext(), view);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
||||
builder.setTitle("Download Path");
|
||||
|
||||
@@ -2,16 +2,26 @@ package app.revanced.extension.tiktok.settings.preference;
|
||||
|
||||
import android.content.Context;
|
||||
import android.preference.EditTextPreference;
|
||||
import android.view.View;
|
||||
|
||||
import app.revanced.extension.shared.settings.StringSetting;
|
||||
import app.revanced.extension.tiktok.Utils;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class InputTextPreference extends EditTextPreference {
|
||||
|
||||
public InputTextPreference(Context context, String title, String summary, StringSetting setting) {
|
||||
super(context);
|
||||
this.setTitle(title);
|
||||
this.setSummary(summary);
|
||||
this.setKey(setting.key);
|
||||
this.setText(setting.get());
|
||||
setTitle(title);
|
||||
setSummary(summary);
|
||||
setKey(setting.key);
|
||||
setText(setting.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBindView(View view) {
|
||||
super.onBindView(view);
|
||||
|
||||
Utils.setTitleAndSummaryColor(getContext(), view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package app.revanced.extension.tiktok.settings.preference;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
@@ -14,11 +15,10 @@ import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import app.revanced.extension.shared.settings.StringSetting;
|
||||
import app.revanced.extension.tiktok.Utils;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class RangeValuePreference extends DialogPreference {
|
||||
private final Context context;
|
||||
|
||||
private String minValue;
|
||||
|
||||
private String maxValue;
|
||||
@@ -29,7 +29,6 @@ public class RangeValuePreference extends DialogPreference {
|
||||
|
||||
public RangeValuePreference(Context context, String title, String summary, StringSetting setting) {
|
||||
super(context);
|
||||
this.context = context;
|
||||
setTitle(title);
|
||||
setSummary(summary);
|
||||
setKey(setting.key);
|
||||
@@ -53,41 +52,52 @@ public class RangeValuePreference extends DialogPreference {
|
||||
return mValue;
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
@Override
|
||||
protected View onCreateDialogView() {
|
||||
minValue = getValue().split("-")[0];
|
||||
maxValue = getValue().split("-")[1];
|
||||
|
||||
Context context = getContext();
|
||||
|
||||
LinearLayout dialogView = new LinearLayout(context);
|
||||
dialogView.setOrientation(LinearLayout.VERTICAL);
|
||||
|
||||
// Min view
|
||||
LinearLayout minView = new LinearLayout(context);
|
||||
minView.setOrientation(LinearLayout.HORIZONTAL);
|
||||
dialogView.addView(minView);
|
||||
|
||||
TextView min = new TextView(context);
|
||||
min.setText("Min: ");
|
||||
minView.addView(min);
|
||||
|
||||
EditText minEditText = new EditText(context);
|
||||
minEditText.setInputType(InputType.TYPE_CLASS_NUMBER);
|
||||
minEditText.setText(minValue);
|
||||
minView.addView(minEditText);
|
||||
dialogView.addView(minView);
|
||||
|
||||
// Max view
|
||||
LinearLayout maxView = new LinearLayout(context);
|
||||
maxView.setOrientation(LinearLayout.HORIZONTAL);
|
||||
dialogView.addView(maxView);
|
||||
|
||||
TextView max = new TextView(context);
|
||||
max.setText("Max: ");
|
||||
maxView.addView(max);
|
||||
|
||||
EditText maxEditText = new EditText(context);
|
||||
maxEditText.setInputType(InputType.TYPE_CLASS_NUMBER);
|
||||
maxEditText.setText(maxValue);
|
||||
maxView.addView(maxEditText);
|
||||
dialogView.addView(maxView);
|
||||
|
||||
minEditText.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -98,12 +108,10 @@ public class RangeValuePreference extends DialogPreference {
|
||||
maxEditText.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -111,12 +119,21 @@ public class RangeValuePreference extends DialogPreference {
|
||||
maxValue = editable.toString();
|
||||
}
|
||||
});
|
||||
|
||||
return dialogView;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBindView(View view) {
|
||||
super.onBindView(view);
|
||||
|
||||
Utils.setTitleAndSummaryColor(getContext(), view);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
||||
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> this.onClick(dialog, DialogInterface.BUTTON_POSITIVE));
|
||||
builder.setPositiveButton(android.R.string.ok, (dialog, which)
|
||||
-> this.onClick(dialog, DialogInterface.BUTTON_POSITIVE));
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@ import org.jetbrains.annotations.NotNull;
|
||||
public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
|
||||
|
||||
@Override
|
||||
protected void syncSettingWithPreference(@NonNull @NotNull Preference pref,
|
||||
@NonNull @NotNull Setting<?> setting,
|
||||
protected void syncSettingWithPreference(@NonNull Preference pref,
|
||||
@NonNull Setting<?> setting,
|
||||
boolean applySettingToPreference) {
|
||||
if (pref instanceof RangeValuePreference) {
|
||||
RangeValuePreference rangeValuePref = (RangeValuePreference) pref;
|
||||
@@ -34,7 +34,7 @@ public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
|
||||
|
||||
@Override
|
||||
protected void initialize() {
|
||||
final var context = getContext();
|
||||
final var context = getActivity();
|
||||
|
||||
// Currently no resources can be compiled for TikTok (fails with aapt error).
|
||||
// So all TikTok Strings are hard coded in the extension.
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
package app.revanced.extension.tiktok.settings.preference;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.settings.preference.ReVancedAboutPreference;
|
||||
import app.revanced.extension.tiktok.Utils;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class ReVancedTikTokAboutPreference extends ReVancedAboutPreference {
|
||||
|
||||
/**
|
||||
@@ -23,22 +25,11 @@ public class ReVancedTikTokAboutPreference extends ReVancedAboutPreference {
|
||||
"revanced_settings_about_links_header", "Official links"
|
||||
);
|
||||
|
||||
{
|
||||
//noinspection deprecation
|
||||
setTitle("About");
|
||||
}
|
||||
|
||||
public ReVancedTikTokAboutPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
public ReVancedTikTokAboutPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
public ReVancedTikTokAboutPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
public ReVancedTikTokAboutPreference(Context context) {
|
||||
super(context);
|
||||
|
||||
setTitle("About");
|
||||
setSummary("About ReVanced");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -52,4 +43,11 @@ public class ReVancedTikTokAboutPreference extends ReVancedAboutPreference {
|
||||
|
||||
return String.format(format, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBindView(View view) {
|
||||
super.onBindView(view);
|
||||
|
||||
Utils.setTitleAndSummaryColor(getContext(), view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,26 @@ package app.revanced.extension.tiktok.settings.preference;
|
||||
|
||||
import android.content.Context;
|
||||
import android.preference.SwitchPreference;
|
||||
import android.view.View;
|
||||
|
||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||
import app.revanced.extension.tiktok.Utils;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class TogglePreference extends SwitchPreference {
|
||||
|
||||
public TogglePreference(Context context, String title, String summary, BooleanSetting setting) {
|
||||
super(context);
|
||||
this.setTitle(title);
|
||||
this.setSummary(summary);
|
||||
this.setKey(setting.key);
|
||||
this.setChecked(setting.get());
|
||||
setTitle(title);
|
||||
setSummary(summary);
|
||||
setKey(setting.key);
|
||||
setChecked(setting.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBindView(View view) {
|
||||
super.onBindView(view);
|
||||
|
||||
Utils.setTitleAndSummaryColor(getContext(), view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,60 @@
|
||||
package app.revanced.extension.tiktok.spoof.sim;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.tiktok.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class SpoofSimPatch {
|
||||
|
||||
private static final boolean ENABLED = Settings.SIM_SPOOF.get();
|
||||
/**
|
||||
* During app startup native code can be called with no obvious way to set the context.
|
||||
* Cannot check if sim spoofing is enabled or the app will crash since no context is set.
|
||||
*/
|
||||
private static boolean isContextNotSet(String fieldSpoofed) {
|
||||
if (Utils.getContext() != null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Logger.initializationException(SpoofSimPatch.class,
|
||||
"Context is not yet set, cannot spoof: " + fieldSpoofed, null);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static String getCountryIso(String value) {
|
||||
if (ENABLED) {
|
||||
if (isContextNotSet("countryIso")) return value;
|
||||
|
||||
if (Settings.SIM_SPOOF.get()) {
|
||||
String iso = Settings.SIM_SPOOF_ISO.get();
|
||||
Logger.printDebug(() -> "Spoofing sim ISO from: " + value + " to: " + iso);
|
||||
Logger.printDebug(() -> "Spoofing countryIso from: " + value + " to: " + iso);
|
||||
return iso;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public static String getOperator(String value) {
|
||||
if (ENABLED) {
|
||||
if (isContextNotSet("MCC-MNC")) return value;
|
||||
|
||||
if (Settings.SIM_SPOOF.get()) {
|
||||
String mcc_mnc = Settings.SIMSPOOF_MCCMNC.get();
|
||||
Logger.printDebug(() -> "Spoofing sim MCC-MNC from: " + value + " to: " + mcc_mnc);
|
||||
return mcc_mnc;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public static String getOperatorName(String value) {
|
||||
if (ENABLED) {
|
||||
if (isContextNotSet("operatorName")) return value;
|
||||
|
||||
if (Settings.SIM_SPOOF.get()) {
|
||||
String operator = Settings.SIMSPOOF_OP_NAME.get();
|
||||
Logger.printDebug(() -> "Spoofing sim operator from: " + value + " to: " + operator);
|
||||
Logger.printDebug(() -> "Spoofing sim operatorName from: " + value + " to: " + operator);
|
||||
return operator;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,9 @@ plugins {
|
||||
|
||||
android {
|
||||
namespace = "app.revanced.extension"
|
||||
compileSdk = 33
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
minSdk = 22
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
dependencies {
|
||||
compileOnly(project(":extensions:tumblr:stub"))
|
||||
}
|
||||
|
||||
android {
|
||||
defaultConfig {
|
||||
minSdk = 26
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
android.namespace = "app.revanced.extension"
|
||||
|
||||
plugins {
|
||||
id(libs.plugins.android.library.get().pluginId)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "app.revanced.extension"
|
||||
compileSdk = 33
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
minSdk = 26
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,3 +6,14 @@ dependencies {
|
||||
compileOnly(libs.annotation)
|
||||
compileOnly(libs.appcompat)
|
||||
}
|
||||
|
||||
android {
|
||||
defaultConfig {
|
||||
minSdk = 21
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,9 @@ plugins {
|
||||
|
||||
android {
|
||||
namespace = "app.revanced.extension"
|
||||
compileSdk = 33
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
minSdk = 21
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
//noinspection GradleDependency
|
||||
android.compileSdk = 33
|
||||
|
||||
dependencies {
|
||||
compileOnly(project(":extensions:shared:library"))
|
||||
compileOnly(project(":extensions:youtube:stub"))
|
||||
compileOnly(libs.annotation)
|
||||
}
|
||||
|
||||
android {
|
||||
defaultConfig {
|
||||
minSdk = 26
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,4 +90,12 @@ public class ThemeHelper {
|
||||
public static int getForegroundColor() {
|
||||
return isDarkTheme() ? getLightThemeColor() : getDarkThemeColor();
|
||||
}
|
||||
|
||||
public static int getToolbarBackgroundColor() {
|
||||
final String colorName = isDarkTheme()
|
||||
? "yt_black3"
|
||||
: "yt_white1";
|
||||
|
||||
return getColorInt(colorName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,14 +176,13 @@ public final class AlternativeThumbnailsPatch {
|
||||
// Unknown tab, treat as the home tab;
|
||||
return homeOption;
|
||||
}
|
||||
if (selectedNavButton == NavigationButton.HOME) {
|
||||
return homeOption;
|
||||
}
|
||||
if (selectedNavButton == NavigationButton.SUBSCRIPTIONS || selectedNavButton == NavigationButton.NOTIFICATIONS) {
|
||||
return subscriptionsOption;
|
||||
}
|
||||
// A library tab variant is active.
|
||||
return libraryOption;
|
||||
|
||||
return switch (selectedNavButton) {
|
||||
case SUBSCRIPTIONS, NOTIFICATIONS -> subscriptionsOption;
|
||||
case LIBRARY -> libraryOption;
|
||||
// Home or explore tab.
|
||||
default -> homeOption;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.PlayerType;
|
||||
import app.revanced.extension.youtube.shared.ShortsPlayerState;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class BackgroundPlaybackPatch {
|
||||
@@ -23,16 +23,7 @@ public class BackgroundPlaybackPatch {
|
||||
// 7. Close the Short
|
||||
// 8. Resume playing the regular video
|
||||
// 9. Minimize the app (PIP should appear)
|
||||
if (!VideoInformation.lastVideoIdIsShort()) {
|
||||
return true; // Definitely is not a Short.
|
||||
}
|
||||
|
||||
// TODO: Add better hook.
|
||||
// Might be a Shorts, or might be a prior regular video on screen again after a Shorts was closed.
|
||||
// This incorrectly prevents PIP if player is in WATCH_WHILE_MINIMIZED after closing a Shorts,
|
||||
// But there's no way around this unless an additional hook is added to definitively detect
|
||||
// the Shorts player is on screen. This use case is unusual anyways so it's not a huge concern.
|
||||
return !PlayerType.getCurrent().isNoneHiddenOrMinimized();
|
||||
return !ShortsPlayerState.isOpen();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.NavigationBar;
|
||||
import app.revanced.extension.youtube.shared.PlayerType;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class ChangeFormFactorPatch {
|
||||
|
||||
public enum FormFactor {
|
||||
/**
|
||||
* Unmodified, and same as un-patched.
|
||||
*/
|
||||
DEFAULT(null),
|
||||
/**
|
||||
* <pre>
|
||||
* Some changes include:
|
||||
* - Explore tab is present.
|
||||
* - watch history is missing.
|
||||
* - feed thumbnails fade in.
|
||||
*/
|
||||
UNKNOWN(0),
|
||||
SMALL(1),
|
||||
LARGE(2),
|
||||
/**
|
||||
* Cars with 'Google built-in'.
|
||||
* Layout seems identical to {@link #UNKNOWN}
|
||||
* even when using an Android Automotive device.
|
||||
*/
|
||||
AUTOMOTIVE(3),
|
||||
WEARABLE(4);
|
||||
|
||||
@Nullable
|
||||
final Integer formFactorType;
|
||||
|
||||
FormFactor(@Nullable Integer formFactorType) {
|
||||
this.formFactorType = formFactorType;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static final Integer FORM_FACTOR_TYPE = Settings.CHANGE_FORM_FACTOR.get().formFactorType;
|
||||
private static final boolean USING_AUTOMOTIVE_TYPE = Objects.requireNonNull(
|
||||
FormFactor.AUTOMOTIVE.formFactorType).equals(FORM_FACTOR_TYPE);
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static int getFormFactor(int original) {
|
||||
if (FORM_FACTOR_TYPE == null) return original;
|
||||
|
||||
if (USING_AUTOMOTIVE_TYPE) {
|
||||
// Do not change if the player is opening or is opened,
|
||||
// otherwise the video description cannot be opened.
|
||||
PlayerType current = PlayerType.getCurrent();
|
||||
if (current.isMaximizedOrFullscreen() || current == PlayerType.WATCH_WHILE_SLIDING_MINIMIZED_MAXIMIZED) {
|
||||
Logger.printDebug(() -> "Using original form factor for player");
|
||||
return original;
|
||||
}
|
||||
|
||||
if (!NavigationBar.isSearchBarActive()) {
|
||||
// Automotive type shows error 400 when opening a channel page and using some explore tab.
|
||||
// This is a bug in unpatched YouTube that occurs on actual Android Automotive devices.
|
||||
// Work around the issue by using the original form factor if not in search and the
|
||||
// navigation back button is present.
|
||||
if (NavigationBar.isBackButtonVisible()) {
|
||||
Logger.printDebug(() -> "Using original form factor, as back button is visible without search present");
|
||||
return original;
|
||||
}
|
||||
|
||||
// Do not change library tab otherwise watch history is hidden.
|
||||
// Do this check last since the current navigation button is required.
|
||||
if (NavigationButton.getSelectedNavigationButton() == NavigationButton.LIBRARY) {
|
||||
return original;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return FORM_FACTOR_TYPE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void navigationTabCreated(NavigationButton button, View tabView) {
|
||||
// On first startup of the app the navigation buttons are fetched and updated.
|
||||
// If the user immediately opens the 'You' or opens a video, then the call to
|
||||
// update the navigtation buttons will use the non automotive form factor
|
||||
// and the explore tab is missing.
|
||||
// Fixing this is not so simple because of the concurrent calls for the player and You tab.
|
||||
// For now, always hide the explore tab.
|
||||
if (USING_AUTOMOTIVE_TYPE && button == NavigationButton.EXPLORE) {
|
||||
tabView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,26 +18,35 @@ public final class ChangeStartPagePatch {
|
||||
/**
|
||||
* Unmodified type, and same as un-patched.
|
||||
*/
|
||||
ORIGINAL("", null),
|
||||
DEFAULT("", null),
|
||||
|
||||
/**
|
||||
* Browse id.
|
||||
*/
|
||||
ALL_SUBSCRIPTIONS("FEchannels", TRUE),
|
||||
BROWSE("FEguide_builder", TRUE),
|
||||
EXPLORE("FEexplore", TRUE),
|
||||
HISTORY("FEhistory", TRUE),
|
||||
LIBRARY("FElibrary", TRUE),
|
||||
MOVIE("FEstorefront", TRUE),
|
||||
NOTIFICATIONS("FEactivity", TRUE),
|
||||
PLAYLISTS("FEplaylist_aggregation", TRUE),
|
||||
SUBSCRIPTIONS("FEsubscriptions", TRUE),
|
||||
TRENDING("FEtrending", TRUE),
|
||||
YOUR_CLIPS("FEclips", TRUE),
|
||||
|
||||
/**
|
||||
* Channel id, this can be used as a browseId.
|
||||
*/
|
||||
COURSES("UCtFRv9O2AHqOZjjynzrv-xg", TRUE),
|
||||
FASHION("UCrpQ4p1Ql_hG8rKXIKM1MOQ", TRUE),
|
||||
GAMING("UCOpNcN46UbXVtpKMrmU4Abg", TRUE),
|
||||
LIVE("UC4R8DWoMoI7CAwX8_LjQHig", TRUE),
|
||||
MUSIC("UC-9-kyTW8ZkZNDHQJ6FgpwQ", TRUE),
|
||||
NEWS("UCYfdidRxbB8Qhf0Nx7ioOYw", TRUE),
|
||||
SHOPPING("UCkYQyvc_i9hXEo4xic9Hh2g", TRUE),
|
||||
SPORTS("UCEgdi0XIXXZ-qJOFPf4JSKw", TRUE),
|
||||
VIRTUAL_REALITY("UCzuqhhs6NWbgTzMuM09WKDQ", TRUE),
|
||||
|
||||
/**
|
||||
* Playlist id, this can be used as a browseId.
|
||||
@@ -51,12 +60,12 @@ public final class ChangeStartPagePatch {
|
||||
SEARCH("com.google.android.youtube.action.open.search", FALSE),
|
||||
SHORTS("com.google.android.youtube.action.open.shorts", FALSE);
|
||||
|
||||
@Nullable
|
||||
final Boolean isBrowseId;
|
||||
|
||||
@NonNull
|
||||
final String id;
|
||||
|
||||
@Nullable
|
||||
final Boolean isBrowseId;
|
||||
|
||||
StartPage(@NonNull String id, @Nullable Boolean isBrowseId) {
|
||||
this.id = id;
|
||||
this.isBrowseId = isBrowseId;
|
||||
@@ -122,7 +131,7 @@ public final class ChangeStartPagePatch {
|
||||
}
|
||||
appLaunched = true;
|
||||
|
||||
final String intentAction = START_PAGE.id;
|
||||
String intentAction = START_PAGE.id;
|
||||
Logger.printDebug(() -> "Changing intent action to " + intentAction);
|
||||
intent.setAction(intentAction);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.PlayerType;
|
||||
import app.revanced.extension.youtube.shared.ShortsPlayerState;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class DisableAutoCaptionsPatch {
|
||||
@@ -14,7 +14,7 @@ public class DisableAutoCaptionsPatch {
|
||||
public static boolean autoCaptionsEnabled() {
|
||||
return Settings.AUTO_CAPTIONS.get()
|
||||
// Do not use auto captions for Shorts.
|
||||
&& !PlayerType.getCurrent().isNoneHiddenOrSlidingMinimized();
|
||||
&& ShortsPlayerState.isOpen();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class DisableHdrPatch {
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean disableHDRVideo() {
|
||||
return !Settings.DISABLE_HDR_VIDEO.get();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
/** @noinspection unused*/
|
||||
public final class DisableSuggestedVideoEndScreenPatch {
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private static ImageView lastView;
|
||||
|
||||
public static void closeEndScreen(final ImageView imageView) {
|
||||
if (!Settings.DISABLE_SUGGESTED_VIDEO_END_SCREEN.get()) return;
|
||||
|
||||
// Prevent adding the listener multiple times.
|
||||
if (lastView == imageView) return;
|
||||
lastView = imageView;
|
||||
|
||||
imageView.getViewTreeObserver().addOnGlobalLayoutListener(() -> {
|
||||
if (imageView.isShown()) imageView.callOnClick();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -9,14 +9,23 @@ import app.revanced.extension.shared.settings.BaseSettings;
|
||||
@SuppressWarnings("unused")
|
||||
public final class EnableDebuggingPatch {
|
||||
|
||||
private static final ConcurrentMap<Long, Boolean> featureFlags
|
||||
= new ConcurrentHashMap<>(300, 0.75f, 1);
|
||||
/**
|
||||
* Only log if debugging is enabled on startup.
|
||||
* This prevents enabling debugging
|
||||
* while the app is running then failing to restart
|
||||
* resulting in an incomplete log.
|
||||
*/
|
||||
private static final boolean LOG_FEATURE_FLAGS = BaseSettings.DEBUG.get();
|
||||
|
||||
private static final ConcurrentMap<Long, Boolean> featureFlags = LOG_FEATURE_FLAGS
|
||||
? new ConcurrentHashMap<>(800, 0.5f, 1)
|
||||
: null;
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean isBooleanFeatureFlagEnabled(boolean value, long flag) {
|
||||
if (value && BaseSettings.DEBUG.get()) {
|
||||
public static boolean isBooleanFeatureFlagEnabled(boolean value, Long flag) {
|
||||
if (LOG_FEATURE_FLAGS && value) {
|
||||
if (featureFlags.putIfAbsent(flag, true) == null) {
|
||||
Logger.printDebug(() -> "boolean feature is enabled: " + flag);
|
||||
}
|
||||
@@ -29,7 +38,7 @@ public final class EnableDebuggingPatch {
|
||||
* Injection point.
|
||||
*/
|
||||
public static double isDoubleFeatureFlagEnabled(double value, long flag, double defaultValue) {
|
||||
if (defaultValue != value && BaseSettings.DEBUG.get()) {
|
||||
if (LOG_FEATURE_FLAGS && defaultValue != value) {
|
||||
if (featureFlags.putIfAbsent(flag, true) == null) {
|
||||
// Align the log outputs to make post processing easier.
|
||||
Logger.printDebug(() -> " double feature is enabled: " + flag
|
||||
@@ -44,7 +53,7 @@ public final class EnableDebuggingPatch {
|
||||
* Injection point.
|
||||
*/
|
||||
public static long isLongFeatureFlagEnabled(long value, long flag, long defaultValue) {
|
||||
if (defaultValue != value && BaseSettings.DEBUG.get()) {
|
||||
if (LOG_FEATURE_FLAGS && defaultValue != value) {
|
||||
if (featureFlags.putIfAbsent(flag, true) == null) {
|
||||
Logger.printDebug(() -> " long feature is enabled: " + flag
|
||||
+ " value: " + value + (defaultValue == 0 ? "" : " default: " + defaultValue));
|
||||
@@ -58,7 +67,7 @@ public final class EnableDebuggingPatch {
|
||||
* Injection point.
|
||||
*/
|
||||
public static String isStringFeatureFlagEnabled(String value, long flag, String defaultValue) {
|
||||
if (BaseSettings.DEBUG.get() && !defaultValue.equals(value)) {
|
||||
if (LOG_FEATURE_FLAGS && !defaultValue.equals(value)) {
|
||||
if (featureFlags.putIfAbsent(flag, true) == null) {
|
||||
Logger.printDebug(() -> " string feature is enabled: " + flag
|
||||
+ " value: " + value + (defaultValue.isEmpty() ? "" : " default: " + defaultValue));
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import android.widget.ImageView;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.PlayerType;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class ExitFullscreenPatch {
|
||||
|
||||
public enum FullscreenMode {
|
||||
DISABLED,
|
||||
PORTRAIT,
|
||||
LANDSCAPE,
|
||||
PORTRAIT_LANDSCAPE,
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void endOfVideoReached() {
|
||||
try {
|
||||
FullscreenMode mode = Settings.EXIT_FULLSCREEN.get();
|
||||
if (mode == FullscreenMode.DISABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (PlayerType.getCurrent() == PlayerType.WATCH_WHILE_FULLSCREEN) {
|
||||
if (mode != FullscreenMode.PORTRAIT_LANDSCAPE) {
|
||||
if (Utils.isLandscapeOrientation()) {
|
||||
if (mode == FullscreenMode.PORTRAIT) {
|
||||
return;
|
||||
}
|
||||
} else if (mode == FullscreenMode.LANDSCAPE) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If the user cold launches the app and plays a video but does not
|
||||
// tap to show the overlay controls, the fullscreen button is not
|
||||
// set because the overlay controls are not attached.
|
||||
// To fix this, push the perform click to the back fo the main thread,
|
||||
// and by then the overlay controls will be visible since the video is now finished.
|
||||
Utils.runOnMainThread(() -> {
|
||||
ImageView button = PlayerControlsPatch.fullscreenButtonRef.get();
|
||||
if (button == null) {
|
||||
Logger.printDebug(() -> "Fullscreen button is null, cannot click");
|
||||
} else {
|
||||
Logger.printDebug(() -> "Clicking fullscreen button");
|
||||
final boolean soundEffectsEnabled = button.isSoundEffectsEnabled();
|
||||
button.setSoundEffectsEnabled(false);
|
||||
button.performClick();
|
||||
button.setSoundEffectsEnabled(soundEffectsEnabled);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "endOfVideoReached failure", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.youtube.shared.PlayerType;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class FixPlaybackSpeedWhilePlayingPatch {
|
||||
|
||||
private static final float DEFAULT_YOUTUBE_PLAYBACK_SPEED = 1.0f;
|
||||
|
||||
public static boolean playbackSpeedChanged(float playbackSpeed) {
|
||||
if (playbackSpeed == DEFAULT_YOUTUBE_PLAYBACK_SPEED &&
|
||||
PlayerType.getCurrent().isMaximizedOrFullscreen()) {
|
||||
|
||||
Logger.printDebug(() -> "Blocking call to change playback speed to 1.0x");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.settings.Setting;
|
||||
import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class ForceOriginalAudioPatch {
|
||||
|
||||
private static final String DEFAULT_AUDIO_TRACKS_SUFFIX = ".4";
|
||||
|
||||
/**
|
||||
* If the conditions to use this patch were present when the app launched.
|
||||
*/
|
||||
public static boolean PATCH_AVAILABLE = SpoofVideoStreamsPatch.notSpoofingToAndroid();
|
||||
|
||||
public static final class ForceOriginalAudioAvailability implements Setting.Availability {
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
// Check conditions of launch and now. Otherwise if spoofing is changed
|
||||
// without a restart the setting will show as available when it's not.
|
||||
return PATCH_AVAILABLE && SpoofVideoStreamsPatch.notSpoofingToAndroid();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean isDefaultAudioStream(boolean isDefault, String audioTrackId, String audioTrackDisplayName) {
|
||||
try {
|
||||
if (!Settings.FORCE_ORIGINAL_AUDIO.get()) {
|
||||
return isDefault;
|
||||
}
|
||||
|
||||
if (audioTrackId.isEmpty()) {
|
||||
// Older app targets can have empty audio tracks and these might be placeholders.
|
||||
// The real audio tracks are called after these.
|
||||
return isDefault;
|
||||
}
|
||||
|
||||
Logger.printDebug(() -> "default: " + String.format("%-5s", isDefault) + " id: "
|
||||
+ String.format("%-8s", audioTrackId) + " name:" + audioTrackDisplayName);
|
||||
|
||||
final boolean isOriginal = audioTrackId.endsWith(DEFAULT_AUDIO_TRACKS_SUFFIX);
|
||||
if (isOriginal) {
|
||||
Logger.printDebug(() -> "Using audio: " + audioTrackId);
|
||||
}
|
||||
|
||||
return isOriginal;
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "isDefaultAudioStream failure", ex);
|
||||
|
||||
return isDefault;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class HideEndScreenSuggestedVideoPatch {
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean hideEndScreenSuggestedVideo() {
|
||||
return Settings.HIDE_END_SCREEN_SUGGESTED_VIDEO.get();
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ public final class MiniplayerPatch {
|
||||
*/
|
||||
DISABLED(false, null),
|
||||
/** Unmodified type, and same as un-patched. */
|
||||
ORIGINAL(null, null),
|
||||
DEFAULT(null, null),
|
||||
/**
|
||||
* Exactly the same as MINIMAL and only here for migration of user settings.
|
||||
* Eventually this should be deleted.
|
||||
@@ -82,7 +82,13 @@ public final class MiniplayerPatch {
|
||||
final int WIDTH_DIP_MIN = 170; // Seems to be the smallest that works.
|
||||
final int HORIZONTAL_PADDING_DIP = 15; // Estimated padding.
|
||||
// Round down to the nearest 5 pixels, to keep any error toasts easier to read.
|
||||
final int WIDTH_DIP_MAX = 5 * ((deviceDipWidth - HORIZONTAL_PADDING_DIP) / 5);
|
||||
final int estimatedWidthDipMax = 5 * ((deviceDipWidth - HORIZONTAL_PADDING_DIP) / 5);
|
||||
// On some ultra low end devices the pixel width and density are the same number,
|
||||
// which causes the estimate to always give a value of 1.
|
||||
// Fix this by using a fixed size of double the min width.
|
||||
final int WIDTH_DIP_MAX = estimatedWidthDipMax <= WIDTH_DIP_MIN
|
||||
? 2 * WIDTH_DIP_MIN
|
||||
: estimatedWidthDipMax;
|
||||
Logger.printDebug(() -> "Screen dip width: " + deviceDipWidth + " maxWidth: " + WIDTH_DIP_MAX);
|
||||
|
||||
int dipWidth = Settings.MINIPLAYER_WIDTH_DIP.get();
|
||||
@@ -127,8 +133,10 @@ public final class MiniplayerPatch {
|
||||
private static final boolean HIDE_SUBTEXT_ENABLED =
|
||||
(CURRENT_TYPE == MODERN_1 || CURRENT_TYPE == MODERN_3) && Settings.MINIPLAYER_HIDE_SUBTEXT.get();
|
||||
|
||||
private static final boolean HIDE_REWIND_FORWARD_ENABLED =
|
||||
CURRENT_TYPE == MODERN_1 && Settings.MINIPLAYER_HIDE_REWIND_FORWARD.get();
|
||||
// 19.25 is last version that has forward/back buttons for phones,
|
||||
// but buttons still show for tablets/foldable devices and they don't work well so always hide.
|
||||
private static final boolean HIDE_REWIND_FORWARD_ENABLED = CURRENT_TYPE == MODERN_1
|
||||
&& (VersionCheckPatch.IS_19_34_OR_GREATER || Settings.MINIPLAYER_HIDE_REWIND_FORWARD.get());
|
||||
|
||||
private static final boolean MINIPLAYER_ROUNDED_CORNERS_ENABLED =
|
||||
Settings.MINIPLAYER_ROUNDED_CORNERS.get();
|
||||
@@ -145,6 +153,18 @@ public final class MiniplayerPatch {
|
||||
|
||||
private static final int OPACITY_LEVEL;
|
||||
|
||||
static {
|
||||
int opacity = Settings.MINIPLAYER_OPACITY.get();
|
||||
|
||||
if (opacity < 0 || opacity > 100) {
|
||||
Utils.showToastLong(str("revanced_miniplayer_opacity_invalid_toast"));
|
||||
Settings.MINIPLAYER_OPACITY.resetToDefault();
|
||||
opacity = Settings.MINIPLAYER_OPACITY.defaultValue;
|
||||
}
|
||||
|
||||
OPACITY_LEVEL = (opacity * 255) / 100;
|
||||
}
|
||||
|
||||
public static final class MiniplayerHorizontalDragAvailability implements Setting.Availability {
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
@@ -163,18 +183,6 @@ public final class MiniplayerPatch {
|
||||
}
|
||||
}
|
||||
|
||||
static {
|
||||
int opacity = Settings.MINIPLAYER_OPACITY.get();
|
||||
|
||||
if (opacity < 0 || opacity > 100) {
|
||||
Utils.showToastLong(str("revanced_miniplayer_opacity_invalid_toast"));
|
||||
Settings.MINIPLAYER_OPACITY.resetToDefault();
|
||||
opacity = Settings.MINIPLAYER_OPACITY.defaultValue;
|
||||
}
|
||||
|
||||
OPACITY_LEVEL = (opacity * 255) / 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
@@ -182,7 +190,7 @@ public final class MiniplayerPatch {
|
||||
* effectively disabling the miniplayer.
|
||||
*/
|
||||
public static boolean getMiniplayerOnCloseHandler(boolean original) {
|
||||
return CURRENT_TYPE == ORIGINAL
|
||||
return CURRENT_TYPE == DEFAULT
|
||||
? original
|
||||
: CURRENT_TYPE == DISABLED;
|
||||
}
|
||||
@@ -201,7 +209,7 @@ public final class MiniplayerPatch {
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean getModernMiniplayerOverride(boolean original) {
|
||||
return CURRENT_TYPE == ORIGINAL
|
||||
return CURRENT_TYPE == DEFAULT
|
||||
? original
|
||||
: CURRENT_TYPE.isModern();
|
||||
}
|
||||
@@ -229,7 +237,7 @@ public final class MiniplayerPatch {
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean getModernFeatureFlagsActiveOverride(boolean original) {
|
||||
if (CURRENT_TYPE == ORIGINAL) {
|
||||
if (CURRENT_TYPE == DEFAULT) {
|
||||
return original;
|
||||
}
|
||||
|
||||
@@ -240,7 +248,7 @@ public final class MiniplayerPatch {
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean enableMiniplayerDoubleTapAction(boolean original) {
|
||||
if (CURRENT_TYPE == ORIGINAL) {
|
||||
if (CURRENT_TYPE == DEFAULT) {
|
||||
return original;
|
||||
}
|
||||
|
||||
@@ -251,7 +259,7 @@ public final class MiniplayerPatch {
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean enableMiniplayerDragAndDrop(boolean original) {
|
||||
if (CURRENT_TYPE == ORIGINAL) {
|
||||
if (CURRENT_TYPE == DEFAULT) {
|
||||
return original;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,15 @@ package app.revanced.extension.youtube.patches;
|
||||
import static app.revanced.extension.shared.Utils.hideViewUnderCondition;
|
||||
import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton;
|
||||
|
||||
import android.os.Build;
|
||||
import android.view.View;
|
||||
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
|
||||
import android.widget.TextView;
|
||||
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@@ -18,6 +21,7 @@ public final class NavigationButtonsPatch {
|
||||
{
|
||||
put(NavigationButton.HOME, Settings.HIDE_HOME_BUTTON.get());
|
||||
put(NavigationButton.CREATE, Settings.HIDE_CREATE_BUTTON.get());
|
||||
put(NavigationButton.NOTIFICATIONS, Settings.HIDE_NOTIFICATIONS_BUTTON.get());
|
||||
put(NavigationButton.SHORTS, Settings.HIDE_SHORTS_BUTTON.get());
|
||||
put(NavigationButton.SUBSCRIPTIONS, Settings.HIDE_SUBSCRIPTIONS_BUTTON.get());
|
||||
}
|
||||
@@ -26,6 +30,15 @@ public final class NavigationButtonsPatch {
|
||||
private static final boolean SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON
|
||||
= Settings.SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON.get();
|
||||
|
||||
private static final Boolean DISABLE_TRANSLUCENT_STATUS_BAR
|
||||
= Settings.DISABLE_TRANSLUCENT_STATUS_BAR.get();
|
||||
|
||||
private static final Boolean DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT
|
||||
= Settings.DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT.get();
|
||||
|
||||
private static final Boolean DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK
|
||||
= Settings.DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK.get();
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
@@ -48,4 +61,42 @@ public final class NavigationButtonsPatch {
|
||||
public static void hideNavigationButtonLabels(TextView navigationLabelsView) {
|
||||
hideViewUnderCondition(Settings.HIDE_NAVIGATION_BUTTON_LABELS, navigationLabelsView);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean useTranslucentNavigationStatusBar(boolean original) {
|
||||
// Must check Android version, as forcing this on Android 11 or lower causes app hang and crash.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||
return original;
|
||||
}
|
||||
|
||||
if (DISABLE_TRANSLUCENT_STATUS_BAR) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return original;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean useTranslucentNavigationButtons(boolean original) {
|
||||
// Feature requires Android 13+
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
return original;
|
||||
}
|
||||
|
||||
if (!DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK && !DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT) {
|
||||
return original;
|
||||
}
|
||||
|
||||
if (DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK && DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Utils.isDarkModeEnabled(Utils.getContext())
|
||||
? !DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK
|
||||
: !DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class OpenShortsInRegularPlayerPatch {
|
||||
|
||||
public enum ShortsPlayerType {
|
||||
SHORTS_PLAYER,
|
||||
REGULAR_PLAYER,
|
||||
REGULAR_PLAYER_FULLSCREEN
|
||||
}
|
||||
|
||||
static {
|
||||
if (!VersionCheckPatch.IS_19_46_OR_GREATER
|
||||
&& Settings.SHORTS_PLAYER_TYPE.get() == ShortsPlayerType.REGULAR_PLAYER_FULLSCREEN) {
|
||||
// User imported newer settings to an older app target.
|
||||
Logger.printInfo(() -> "Resetting " + Settings.SHORTS_PLAYER_TYPE);
|
||||
Settings.SHORTS_PLAYER_TYPE.resetToDefault();
|
||||
}
|
||||
}
|
||||
|
||||
private static WeakReference<Activity> mainActivityRef = new WeakReference<>(null);
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void setMainActivity(Activity activity) {
|
||||
mainActivityRef = new WeakReference<>(activity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean openShort(String videoID) {
|
||||
try {
|
||||
ShortsPlayerType type = Settings.SHORTS_PLAYER_TYPE.get();
|
||||
if (type == ShortsPlayerType.SHORTS_PLAYER) {
|
||||
return false; // Default unpatched behavior.
|
||||
}
|
||||
|
||||
if (videoID.isEmpty()) {
|
||||
// Shorts was opened using launcher app shortcut.
|
||||
//
|
||||
// This check will not detect if the Shorts app shortcut is used
|
||||
// while the app is running in the background (instead the regular player is opened).
|
||||
// To detect that the hooked method map parameter can be checked
|
||||
// if integer key 'com.google.android.apps.youtube.app.endpoint.flags'
|
||||
// has bitmask 16 set.
|
||||
//
|
||||
// This use case seems unlikely if the user has the Shorts
|
||||
// set to open in the regular player, so it's ignored as
|
||||
// checking the map makes the patch more complicated.
|
||||
Logger.printDebug(() -> "Ignoring Short with no videoId");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (NavigationButton.getSelectedNavigationButton() == NavigationButton.SHORTS) {
|
||||
return false; // Always use Shorts player for the Shorts nav button.
|
||||
}
|
||||
|
||||
final boolean forceFullScreen = (type == ShortsPlayerType.REGULAR_PLAYER_FULLSCREEN);
|
||||
OpenVideosFullscreenHookPatch.setOpenNextVideoFullscreen(forceFullScreen);
|
||||
|
||||
// Can use the application context and add intent flags of
|
||||
// FLAG_ACTIVITY_NEW_TASK and FLAG_ACTIVITY_CLEAR_TOP
|
||||
// But the activity context seems to fix random app crashes
|
||||
// if Shorts urls are opened outside the app.
|
||||
var context = mainActivityRef.get();
|
||||
|
||||
Intent videoPlayerIntent = new Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse("https://youtube.com/watch?v=" + videoID)
|
||||
);
|
||||
videoPlayerIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
videoPlayerIntent.setPackage(context.getPackageName());
|
||||
|
||||
context.startActivity(videoPlayerIntent);
|
||||
return true;
|
||||
} catch (Exception ex) {
|
||||
OpenVideosFullscreenHookPatch.setOpenNextVideoFullscreen(null);
|
||||
Logger.printException(() -> "openShort failure", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class OpenVideosFullscreenHookPatch {
|
||||
|
||||
@Nullable
|
||||
private static volatile Boolean openNextVideoFullscreen;
|
||||
|
||||
public static void setOpenNextVideoFullscreen(@Nullable Boolean forceFullScreen) {
|
||||
openNextVideoFullscreen = forceFullScreen;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changed during patching since this class is also
|
||||
* used by {@link OpenVideosFullscreenHookPatch}.
|
||||
*/
|
||||
private static boolean isFullScreenPatchIncluded() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean openVideoFullscreenPortrait(boolean original) {
|
||||
Boolean openFullscreen = openNextVideoFullscreen;
|
||||
if (openFullscreen != null) {
|
||||
openNextVideoFullscreen = null;
|
||||
return openFullscreen;
|
||||
}
|
||||
|
||||
if (!isFullScreenPatchIncluded()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Settings.OPEN_VIDEOS_FULLSCREEN_PORTRAIT.get();
|
||||
}
|
||||
}
|
||||
@@ -4,15 +4,30 @@ import android.view.View;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class PlayerControlsPatch {
|
||||
|
||||
public static WeakReference<ImageView> fullscreenButtonRef = new WeakReference<>(null);
|
||||
|
||||
private static boolean fullscreenButtonVisibilityCallbacksExist() {
|
||||
return false; // Modified during patching if needed.
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void setFullscreenCloseButton(ImageView imageButton) {
|
||||
fullscreenButtonRef = new WeakReference<>(imageButton);
|
||||
Logger.printDebug(() -> "Fullscreen button set");
|
||||
|
||||
if (!fullscreenButtonVisibilityCallbacksExist()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add a global listener, since the protected method
|
||||
// View#onVisibilityChanged() does not have any call backs.
|
||||
imageButton.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||
@@ -39,7 +54,7 @@ public class PlayerControlsPatch {
|
||||
}
|
||||
|
||||
// noinspection EmptyMethod
|
||||
public static void fullscreenButtonVisibilityChanged(boolean isVisible) {
|
||||
private static void fullscreenButtonVisibilityChanged(boolean isVisible) {
|
||||
// Code added during patching.
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.youtube.shared.PlayerType;
|
||||
import app.revanced.extension.youtube.shared.ShortsPlayerState;
|
||||
import app.revanced.extension.youtube.shared.VideoState;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@@ -24,4 +27,26 @@ public class PlayerTypeHookPatch {
|
||||
|
||||
VideoState.setFromString(youTubeVideoState.name());
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* Add a listener to the shorts player overlay View.
|
||||
* Triggered when a shorts player is attached or detached to Windows.
|
||||
*
|
||||
* @param view shorts player overlay (R.id.reel_watch_player).
|
||||
*/
|
||||
public static void onShortsCreate(View view) {
|
||||
view.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
|
||||
@Override
|
||||
public void onViewAttachedToWindow(@Nullable View v) {
|
||||
ShortsPlayerState.setOpen(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewDetachedFromWindow(@Nullable View v) {
|
||||
ShortsPlayerState.setOpen(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import static app.revanced.extension.youtube.returnyoutubedislike.ReturnYouTubeD
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.ShapeDrawable;
|
||||
import android.os.Build;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
@@ -366,10 +365,8 @@ public class ReturnYouTubeDislikePatch {
|
||||
private static final List<WeakReference<TextView>> shortsTextViewRefs = new ArrayList<>();
|
||||
|
||||
private static void clearRemovedShortsTextViews() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { // YouTube requires Android N or greater
|
||||
shortsTextViewRefs.removeIf(ref -> ref.get() == null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point. Called when a Shorts dislike is updated. Always on main thread.
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Objects;
|
||||
@@ -58,7 +55,6 @@ public class ShortsAutoplayPatch {
|
||||
/**
|
||||
* @return If the app is currently in background PiP mode.
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
private static boolean isAppInBackgroundPiPMode() {
|
||||
Activity activity = mainActivityRef.get();
|
||||
return activity != null && activity.isInPictureInPictureMode();
|
||||
@@ -80,7 +76,6 @@ public class ShortsAutoplayPatch {
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
public static Enum<?> changeShortsRepeatBehavior(Enum<?> original) {
|
||||
try {
|
||||
final boolean autoplay;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -9,4 +9,5 @@ public class VersionCheckPatch {
|
||||
public static final boolean IS_19_26_OR_GREATER = Utils.getAppVersionName().compareTo("19.26.00") >= 0;
|
||||
public static final boolean IS_19_29_OR_GREATER = Utils.getAppVersionName().compareTo("19.29.00") >= 0;
|
||||
public static final boolean IS_19_34_OR_GREATER = Utils.getAppVersionName().compareTo("19.34.00") >= 0;
|
||||
public static final boolean IS_19_46_OR_GREATER = Utils.getAppVersionName().compareTo("19.46.00") >= 0;
|
||||
}
|
||||
|
||||
@@ -5,10 +5,13 @@ 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)
|
||||
private static final boolean SHOW_VIDEO_ADS = !Settings.HIDE_VIDEO_ADS.get();
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean shouldShowAds() {
|
||||
return !Settings.HIDE_VIDEO_ADS.get(); // TODO && Whitelist.shouldShowAds();
|
||||
return SHOW_VIDEO_ADS;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,13 +7,10 @@ import static app.revanced.extension.youtube.patches.announcements.requests.Anno
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.os.Build;
|
||||
import android.text.Html;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import org.json.JSONArray;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -31,7 +28,6 @@ public final class AnnouncementsPatch {
|
||||
private AnnouncementsPatch() {
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
private static boolean isLatestAlready() throws IOException {
|
||||
HttpURLConnection connection =
|
||||
AnnouncementsRoutes.getAnnouncementsConnectionFromRoute(GET_LATEST_ANNOUNCEMENT_IDS);
|
||||
@@ -70,7 +66,6 @@ public final class AnnouncementsPatch {
|
||||
return Settings.ANNOUNCEMENT_LAST_ID.get() == id;
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
public static void showAnnouncement(final Activity context) {
|
||||
if (!Settings.ANNOUNCEMENTS.get()) return;
|
||||
|
||||
|
||||
@@ -8,10 +8,12 @@ import android.view.View;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import java.util.List;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.youtube.StringTrieSearch;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class AdsFilter extends Filter {
|
||||
@@ -22,6 +24,11 @@ public final class AdsFilter extends Filter {
|
||||
|
||||
// endregion
|
||||
|
||||
// https://encrypted-tbn0.gstatic.com/shopping?q=abc
|
||||
private static final String STORE_BANNER_DOMAIN = "gstatic.com/shopping";
|
||||
private static final boolean HIDE_END_SCREEN_STORE_BANNER =
|
||||
Settings.HIDE_END_SCREEN_STORE_BANNER.get();
|
||||
|
||||
private final StringTrieSearch exceptions = new StringTrieSearch();
|
||||
|
||||
private final StringFilterGroup playerShoppingShelf;
|
||||
@@ -66,7 +73,10 @@ public final class AdsFilter extends Filter {
|
||||
"full_width_square_image_layout",
|
||||
"video_display_button_group_layout",
|
||||
"landscape_image_wide_button_layout",
|
||||
"video_display_carousel_button_group_layout"
|
||||
"video_display_carousel_button_group_layout",
|
||||
"video_display_full_buttoned_short_dr_layout",
|
||||
"compact_landscape_image_layout", // Tablet layout search results.
|
||||
"text_image_no_button_layout" // Tablet layout search results.
|
||||
);
|
||||
|
||||
final var generalAds = new StringFilterGroup(
|
||||
@@ -85,7 +95,8 @@ public final class AdsFilter extends Filter {
|
||||
"composite_concurrent_carousel_layout",
|
||||
"carousel_headered_layout",
|
||||
"full_width_portrait_image_layout",
|
||||
"brand_video_shelf"
|
||||
"brand_video_shelf",
|
||||
"brand_video_singleton"
|
||||
);
|
||||
|
||||
final var movieAds = new StringFilterGroup(
|
||||
@@ -111,23 +122,24 @@ public final class AdsFilter extends Filter {
|
||||
"expandable_list"
|
||||
);
|
||||
|
||||
channelProfile = new StringFilterGroup(
|
||||
null,
|
||||
"channel_profile.eml"
|
||||
);
|
||||
|
||||
playerShoppingShelf = new StringFilterGroup(
|
||||
null,
|
||||
Settings.HIDE_PLAYER_STORE_SHELF,
|
||||
"horizontal_shelf.eml"
|
||||
);
|
||||
|
||||
playerShoppingShelfBuffer = new ByteArrayFilterGroup(
|
||||
Settings.HIDE_PLAYER_STORE_SHELF,
|
||||
null,
|
||||
"shopping_item_card_list.eml"
|
||||
);
|
||||
|
||||
visitStoreButton = new ByteArrayFilterGroup(
|
||||
channelProfile = new StringFilterGroup(
|
||||
Settings.HIDE_VISIT_STORE_BUTTON,
|
||||
"channel_profile.eml",
|
||||
"page_header.eml"
|
||||
);
|
||||
|
||||
visitStoreButton = new ByteArrayFilterGroup(
|
||||
null,
|
||||
"header_store_button"
|
||||
);
|
||||
|
||||
@@ -171,6 +183,11 @@ public final class AdsFilter extends Filter {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for the index because of likelihood of false positives.
|
||||
if (matchedGroup == shoppingLinks && contentIndex != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (exceptions.matches(path))
|
||||
return false;
|
||||
|
||||
@@ -187,13 +204,25 @@ public final class AdsFilter extends Filter {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for the index because of likelihood of false positives.
|
||||
if (matchedGroup == shoppingLinks && contentIndex != 0)
|
||||
return false;
|
||||
|
||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* @param elementsList List of components of the end screen container.
|
||||
* @param protobufList Component (ProtobufList).
|
||||
*/
|
||||
public static void hideEndScreenStoreBanner(List<Object> elementsList, Object protobufList) {
|
||||
if (HIDE_END_SCREEN_STORE_BANNER && protobufList.toString().contains(STORE_BANNER_DOMAIN)) {
|
||||
Logger.printDebug(() -> "Hiding store banner");
|
||||
return;
|
||||
}
|
||||
|
||||
elementsList.add(protobufList);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Hide the view, which shows ads in the homepage.
|
||||
*
|
||||
|
||||
@@ -2,20 +2,20 @@ package app.revanced.extension.youtube.patches.components;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.youtube.patches.playback.quality.RestoreOldVideoQualityMenuPatch;
|
||||
import app.revanced.extension.youtube.patches.playback.quality.AdvancedVideoQualityMenuPatch;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
/**
|
||||
* Abuse LithoFilter for {@link RestoreOldVideoQualityMenuPatch}.
|
||||
* Abuse LithoFilter for {@link AdvancedVideoQualityMenuPatch}.
|
||||
*/
|
||||
public final class VideoQualityMenuFilterPatch extends Filter {
|
||||
public final class AdvancedVideoQualityMenuFilter extends Filter {
|
||||
// Must be volatile or synchronized, as litho filtering runs off main thread
|
||||
// and this field is then access from the main thread.
|
||||
public static volatile boolean isVideoQualityMenuVisible;
|
||||
|
||||
public VideoQualityMenuFilterPatch() {
|
||||
public AdvancedVideoQualityMenuFilter() {
|
||||
addPathCallbacks(new StringFilterGroup(
|
||||
Settings.RESTORE_OLD_VIDEO_QUALITY_MENU,
|
||||
Settings.ADVANCED_VIDEO_QUALITY_MENU,
|
||||
"quick_quality_sheet_content.eml-js"
|
||||
));
|
||||
}
|
||||
@@ -6,8 +6,12 @@ import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
final class ButtonsFilter extends Filter {
|
||||
private static final String COMPACT_CHANNEL_BAR_PATH_PREFIX = "compact_channel_bar.eml";
|
||||
private static final String VIDEO_ACTION_BAR_PATH_PREFIX = "video_action_bar.eml";
|
||||
private static final String VIDEO_ACTION_BAR_PATH = "video_action_bar.eml";
|
||||
private static final String ANIMATED_VECTOR_TYPE_PATH = "AnimatedVectorType";
|
||||
|
||||
private final StringFilterGroup likeSubscribeGlow;
|
||||
private final StringFilterGroup actionBarGroup;
|
||||
private final StringFilterGroup bufferFilterPathGroup;
|
||||
private final ByteArrayFilterGroupList bufferButtonsGroupList = new ByteArrayFilterGroupList();
|
||||
@@ -20,11 +24,19 @@ final class ButtonsFilter extends Filter {
|
||||
addIdentifierCallbacks(actionBarGroup);
|
||||
|
||||
|
||||
likeSubscribeGlow = new StringFilterGroup(
|
||||
Settings.DISABLE_LIKE_SUBSCRIBE_GLOW,
|
||||
"animated_button_border.eml"
|
||||
);
|
||||
|
||||
bufferFilterPathGroup = new StringFilterGroup(
|
||||
null,
|
||||
"|ContainerType|button.eml|"
|
||||
);
|
||||
|
||||
addPathCallbacks(
|
||||
likeSubscribeGlow,
|
||||
bufferFilterPathGroup,
|
||||
new StringFilterGroup(
|
||||
Settings.HIDE_LIKE_DISLIKE_BUTTON,
|
||||
"|segmented_like_dislike_button"
|
||||
@@ -40,8 +52,7 @@ final class ButtonsFilter extends Filter {
|
||||
new StringFilterGroup(
|
||||
Settings.HIDE_CLIP_BUTTON,
|
||||
"|clip_button.eml|"
|
||||
),
|
||||
bufferFilterPathGroup
|
||||
)
|
||||
);
|
||||
|
||||
bufferButtonsGroupList.addAll(
|
||||
@@ -83,6 +94,15 @@ final class ButtonsFilter extends Filter {
|
||||
@Override
|
||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
if (matchedGroup == likeSubscribeGlow) {
|
||||
if ((path.startsWith(VIDEO_ACTION_BAR_PATH_PREFIX) || path.startsWith(COMPACT_CHANNEL_BAR_PATH_PREFIX))
|
||||
&& path.contains(ANIMATED_VECTOR_TYPE_PATH)) {
|
||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the current matched group is the action bar group,
|
||||
// in case every filter group is enabled, hide the action bar.
|
||||
if (matchedGroup == actionBarGroup) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user