Compare commits
1 commit
main
...
github/for
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc29c18c88 |
1313 changed files with 94711 additions and 136133 deletions
3
.editorconfig
Normal file
3
.editorconfig
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[*.{kt,kts}]
|
||||
ktlint_code_style = intellij_idea
|
||||
ktlint_standard_no-wildcard-imports = disabled
|
||||
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -72,7 +72,6 @@ body:
|
|||
|
||||
- **Do not submit a duplicate bug report**: Search for existing bug reports [here](https://github.com/ReVanced/revanced-patches/issues?q=label%3A%22Bug+report%22).
|
||||
- **Review the contribution guidelines**: Make sure your bug report adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-patches/blob/main/CONTRIBUTING.md).
|
||||
- **Check the troubleshooting guide**: A solution to your issue might be found in the [FAQ](https://github.com/ReVanced/revanced-documentation/blob/main/docs/revanced-resources/questions.md) or the [troubleshooting guide](https://github.com/ReVanced/revanced-documentation/blob/main/docs/revanced-resources/troubleshooting.md).
|
||||
- **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
|
||||
- type: textarea
|
||||
attributes:
|
||||
|
|
|
|||
1
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
1
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
|
|
@ -72,7 +72,6 @@ body:
|
|||
|
||||
- **Do not submit a duplicate feature request**: Search for existing feature requests [here](https://github.com/ReVanced/revanced-patches/issues?q=label%3A%22Feature+request%22).
|
||||
- **Review the contribution guidelines**: Make sure your feature request adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-patches/blob/main/CONTRIBUTING.md).
|
||||
- **Check the troubleshooting guide**: Information about your issue might be found in the [FAQ](https://github.com/ReVanced/revanced-documentation/blob/main/docs/revanced-resources/questions.md) or the [troubleshooting guide](https://github.com/ReVanced/revanced-documentation/blob/main/docs/revanced-resources/troubleshooting.md).
|
||||
- **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
|
||||
- type: textarea
|
||||
attributes:
|
||||
|
|
|
|||
19
.github/workflows/build_pull_request.yml
vendored
19
.github/workflows/build_pull_request.yml
vendored
|
|
@ -2,10 +2,6 @@ name: Build pull request
|
|||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr:
|
||||
description: "PR to build"
|
||||
required: true
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
|
|
@ -14,17 +10,12 @@ jobs:
|
|||
release:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.pr && format('refs/pull/{0}/merge', inputs.pr) || github.ref }}
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v5
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
|
@ -34,13 +25,11 @@ jobs:
|
|||
|
||||
- name: Build
|
||||
env:
|
||||
ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ github.actor }}
|
||||
ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./gradlew :patches:buildAndroid --no-daemon
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: revanced-patches
|
||||
path: patches/build/libs
|
||||
archive: false
|
||||
|
|
|
|||
2
.github/workflows/open_pull_request.yml
vendored
2
.github/workflows/open_pull_request.yml
vendored
|
|
@ -15,7 +15,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Open pull request
|
||||
uses: repo-sync/pull-request@v2
|
||||
|
|
|
|||
25
.github/workflows/pull_strings.yml
vendored
25
.github/workflows/pull_strings.yml
vendored
|
|
@ -1,6 +1,8 @@
|
|||
name: Pull strings
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 */12 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
|
@ -12,10 +14,10 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: dev
|
||||
persist-credentials: true
|
||||
clean: true
|
||||
|
||||
- name: Pull strings
|
||||
uses: crowdin/github-action@v2
|
||||
|
|
@ -23,29 +25,16 @@ jobs:
|
|||
config: crowdin.yml
|
||||
upload_sources: false
|
||||
download_translations: true
|
||||
push_translations: false
|
||||
skip_ref_checkout: true
|
||||
localization_branch_name: feat/translations
|
||||
create_pull_request: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
|
||||
- name: Process strings
|
||||
run: |
|
||||
chmod -R 777 patches/src/main/resources
|
||||
./gradlew processStringsFromCrowdin
|
||||
env:
|
||||
ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ github.actor }}
|
||||
ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Commit changes
|
||||
uses: stefanzweifel/git-auto-commit-action@v7
|
||||
with:
|
||||
commit_message: "chore: Sync translations from Crowdin"
|
||||
push_options: '--force'
|
||||
branch: feat/translations
|
||||
|
||||
- name: Open pull request
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
uses: repo-sync/pull-request@v2
|
||||
with:
|
||||
source_branch: feat/translations
|
||||
|
|
|
|||
9
.github/workflows/push_strings.yml
vendored
9
.github/workflows/push_strings.yml
vendored
|
|
@ -14,13 +14,12 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Process strings
|
||||
- name: Preprocess strings
|
||||
env:
|
||||
ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ github.actor }}
|
||||
ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./gradlew processStringsForCrowdin
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./gradlew clean preprocessCrowdinStrings
|
||||
|
||||
- name: Push strings
|
||||
uses: crowdin/github-action@v2
|
||||
|
|
|
|||
16
.github/workflows/release.yml
vendored
16
.github/workflows/release.yml
vendored
|
|
@ -15,14 +15,13 @@ jobs:
|
|||
packages: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
artifact-metadata: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v5
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
|
@ -32,12 +31,11 @@ jobs:
|
|||
|
||||
- name: Build
|
||||
env:
|
||||
ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ github.actor }}
|
||||
ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./gradlew :patches:buildAndroid clean
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
cache: 'npm'
|
||||
|
|
@ -53,16 +51,14 @@ jobs:
|
|||
fingerprint: ${{ vars.GPG_FINGERPRINT }}
|
||||
|
||||
- name: Release
|
||||
uses: cycjimmy/semantic-release-action@v5
|
||||
uses: cycjimmy/semantic-release-action@v4
|
||||
id: release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ github.actor }}
|
||||
ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Attest
|
||||
if: steps.release.outputs.new_release_published == 'true'
|
||||
uses: actions/attest@v4
|
||||
uses: actions/attest-build-provenance@v2
|
||||
with:
|
||||
subject-name: 'ReVanced Patches ${{ steps.release.outputs.new_release_git_tag }}'
|
||||
subject-path: patches/build/libs/patches-*.rvp
|
||||
|
|
|
|||
4
.github/workflows/update-gradle-wrapper.yml
vendored
4
.github/workflows/update-gradle-wrapper.yml
vendored
|
|
@ -10,9 +10,9 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Update Gradle Wrapper
|
||||
uses: gradle-update/update-gradle-wrapper-action@v2
|
||||
uses: gradle-update/update-gradle-wrapper-action@v1
|
||||
with:
|
||||
target-branch: dev
|
||||
|
|
|
|||
2083
CHANGELOG.md
2083
CHANGELOG.md
File diff suppressed because it is too large
Load diff
|
|
@ -97,9 +97,9 @@ Thank you for considering contributing to ReVanced Patches. You can find the con
|
|||
|
||||
To build ReVanced Patches, you can follow the [ReVanced documentation](https://github.com/ReVanced/revanced-documentation).
|
||||
|
||||
## 📜 License
|
||||
## 📜 Licence
|
||||
|
||||
ReVanced Patches is licensed under the GPLv3 license. Please see the [license file](LICENSE) for more information.
|
||||
[tl;dr](https://www.tldrlegal.com/license/gnu-general-public-license-v3-gpl-3) you may copy, distribute and modify ReVanced Patches as long as you track changes/dates in source files.
|
||||
Any modifications to ReVanced Patches must also be made available under the GPL,
|
||||
along with build & install instructions.
|
||||
along with build & install instructions.
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"info": "This is verification file for ads.fund project",
|
||||
"project": {
|
||||
"name": "Revanced Patches",
|
||||
"walletAddress": "0x7ab4091e00363654bf84B34151225742cd92FCE5",
|
||||
"tokenAddress": "0xadf325f255083a3f3d9a9d01ffb3db52a148d802"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
project_id_env: "CROWDIN_PROJECT_ID"
|
||||
api_token_env: "CROWDIN_PERSONAL_TOKEN"
|
||||
|
||||
preserve_hierarchy: true
|
||||
preserve_hierarchy: false
|
||||
files:
|
||||
- source: patches/src/main/resources/addresources/values/strings.xml
|
||||
dest: patches.xml
|
||||
translation: patches/src/main/resources/addresources/values-%android_code%/strings.xml
|
||||
skip_untranslated_strings: true
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
android {
|
||||
namespace = "app.revanced.extension"
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 21
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
android {
|
||||
defaultConfig {
|
||||
minSdk = 23
|
||||
namespace = "app.revanced.extension"
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
android {
|
||||
namespace = "app.revanced.extension"
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 21
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
|
|
|||
|
|
@ -31,10 +31,7 @@ public class InternalDataDocumentsProvider extends DocumentsProvider {
|
|||
private static final String[] directoryColumns =
|
||||
{"document_id", "mime_type", "_display_name", "last_modified", "flags",
|
||||
"_size", "full_path", "lstat_info"};
|
||||
@SuppressWarnings("OctalInteger")
|
||||
private static final int S_IFMT = 0170000;
|
||||
@SuppressWarnings("OctalInteger")
|
||||
private static final int S_IFLNK = 0120000;
|
||||
private static final int S_IFLNK = 0x8000;
|
||||
|
||||
private String packageName;
|
||||
private File dataDirectory;
|
||||
|
|
@ -50,7 +47,7 @@ public class InternalDataDocumentsProvider extends DocumentsProvider {
|
|||
if (root.isDirectory()) {
|
||||
try {
|
||||
// Only delete recursively if the directory is not a symlink
|
||||
if ((Os.lstat(root.getPath()).st_mode & S_IFMT) != S_IFLNK) {
|
||||
if ((Os.lstat(root.getPath()).st_mode & S_IFLNK) != S_IFLNK) {
|
||||
File[] files = root.listFiles();
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
|
|
@ -327,7 +324,7 @@ public class InternalDataDocumentsProvider extends DocumentsProvider {
|
|||
sb.append(";");
|
||||
sb.append(lstat.st_gid);
|
||||
// Append symlink target if it is a symlink
|
||||
if ((lstat.st_mode & S_IFMT) == S_IFLNK) {
|
||||
if ((lstat.st_mode & S_IFLNK) == S_IFLNK) {
|
||||
sb.append(";");
|
||||
sb.append(Os.readlink(path));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
android {
|
||||
defaultConfig {
|
||||
minSdk = 21
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
aidl = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(libs.annotation)
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
<manifest/>
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
package com.google.android.play.core.integrity.protocol;
|
||||
|
||||
import android.os.Bundle;
|
||||
import com.google.android.play.core.integrity.protocol.IExpressIntegrityServiceCallback;
|
||||
|
||||
interface IExpressIntegrityService {
|
||||
oneway void requestIntegrityToken(in Bundle request, IExpressIntegrityServiceCallback callback) = 2;
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
package com.google.android.play.core.integrity.protocol;
|
||||
|
||||
interface IExpressIntegrityServiceCallback {
|
||||
oneway void onRequestExpressIntegrityTokenResult(in Bundle result) = 2;
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
package com.google.android.play.core.integrity.protocol;
|
||||
|
||||
import android.os.Bundle;
|
||||
import com.google.android.play.core.integrity.protocol.IIntegrityServiceCallback;
|
||||
|
||||
interface IIntegrityService {
|
||||
oneway void requestIntegrityToken(in Bundle request, IIntegrityServiceCallback callback) = 1;
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
package com.google.android.play.core.integrity.protocol;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
interface IIntegrityServiceCallback {
|
||||
oneway void onResult(in Bundle result) = 1;
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
package android.ext;
|
||||
/** @hide */
|
||||
// Int values that are assigned to packages in this interface can be retrieved at runtime from
|
||||
// ApplicationInfo.ext().getPackageId() or from AndroidPackage.ext().getPackageId() (in system_server).
|
||||
//
|
||||
// PackageIds are assigned to parsed APKs only after they are verified, either by a certificate check
|
||||
// or by a check that the APK is stored on an immutable OS partition.
|
||||
public interface PackageId {
|
||||
String PLAY_STORE_NAME = "com.android.vending";
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
package android.os;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
|
||||
/** @hide */
|
||||
public class BinderWrapper implements IBinder {
|
||||
protected final IBinder base;
|
||||
|
||||
public BinderWrapper(IBinder base) {
|
||||
this.base = base;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean transact(int code, @NonNull Parcel data, @Nullable Parcel reply, int flags) throws RemoteException {
|
||||
return base.transact(code, data, reply, flags);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IInterface queryLocalInterface(@NonNull String descriptor) {
|
||||
return base.queryLocalInterface(descriptor);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getInterfaceDescriptor() throws RemoteException {
|
||||
return base.getInterfaceDescriptor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean pingBinder() {
|
||||
return base.pingBinder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBinderAlive() {
|
||||
return base.isBinderAlive();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dump(@NonNull FileDescriptor fd, @Nullable String[] args) throws RemoteException {
|
||||
base.dump(fd, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dumpAsync(@NonNull FileDescriptor fd, @Nullable String[] args) throws RemoteException {
|
||||
base.dumpAsync(fd, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void linkToDeath(@NonNull DeathRecipient recipient, int flags) throws RemoteException {
|
||||
base.linkToDeath(recipient, flags);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean unlinkToDeath(@NonNull DeathRecipient recipient, int flags) {
|
||||
return base.unlinkToDeath(recipient, flags);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
package app.grapheneos.gmscompat.lib.playintegrity;
|
||||
|
||||
import android.os.Binder;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.internal.os.FakeBackgroundHandler;
|
||||
import com.google.android.play.core.integrity.protocol.IIntegrityService;
|
||||
import com.google.android.play.core.integrity.protocol.IIntegrityServiceCallback;
|
||||
|
||||
class ClassicPlayIntegrityServiceWrapper extends PlayIntegrityServiceWrapper {
|
||||
|
||||
ClassicPlayIntegrityServiceWrapper(IBinder base) {
|
||||
super(base);
|
||||
requestIntegrityTokenTxnCode = 2; // IIntegrityService.Stub.TRANSACTION_requestIntegrityToken
|
||||
}
|
||||
|
||||
static class TokenRequestStub extends IIntegrityService.Stub {
|
||||
public void requestIntegrityToken(Bundle request, IIntegrityServiceCallback callback) {
|
||||
Runnable r = () -> {
|
||||
var result = new Bundle();
|
||||
// https://developer.android.com/google/play/integrity/reference/com/google/android/play/core/integrity/model/IntegrityErrorCode.html#API_NOT_AVAILABLE
|
||||
final int API_NOT_AVAILABLE = -1;
|
||||
result.putInt("error", API_NOT_AVAILABLE);
|
||||
try {
|
||||
callback.onResult(result);
|
||||
} catch (RemoteException e) {
|
||||
Log.e("IIntegrityService.Stub", "", e);
|
||||
}
|
||||
};
|
||||
FakeBackgroundHandler.getHandler().postDelayed(r, getTokenRequestResultDelay());
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected Binder createTokenRequestStub() {
|
||||
return new TokenRequestStub();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
package app.grapheneos.gmscompat.lib.playintegrity;
|
||||
|
||||
import android.os.Binder;
|
||||
import android.os.BinderWrapper;
|
||||
import android.os.IBinder;
|
||||
import android.os.Parcel;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
abstract class PlayIntegrityServiceWrapper extends BinderWrapper {
|
||||
final String TAG;
|
||||
protected int requestIntegrityTokenTxnCode;
|
||||
|
||||
public PlayIntegrityServiceWrapper(IBinder base) {
|
||||
super(base);
|
||||
TAG = getClass().getSimpleName();
|
||||
}
|
||||
|
||||
protected abstract Binder createTokenRequestStub();
|
||||
|
||||
@Override
|
||||
public boolean transact(int code, Parcel data, @Nullable Parcel reply, int flags) throws RemoteException {
|
||||
if (code == requestIntegrityTokenTxnCode) {
|
||||
if (maybeStubOutIntegrityTokenRequest(code, data, reply, flags)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return super.transact(code, data, reply, flags);
|
||||
}
|
||||
|
||||
private boolean maybeStubOutIntegrityTokenRequest(int code, Parcel data, @Nullable Parcel reply, int flags) {
|
||||
Log.d(TAG, "integrity token request detected");
|
||||
|
||||
try {
|
||||
createTokenRequestStub().transact(code, data, reply, flags);
|
||||
} catch (RemoteException e) {
|
||||
// this is a local call
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected static long getTokenRequestResultDelay() {
|
||||
return 500L;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
package app.grapheneos.gmscompat.lib.playintegrity;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.ext.PackageId;
|
||||
import android.os.IBinder;
|
||||
import androidx.annotation.Nullable;
|
||||
import app.grapheneos.gmscompat.lib.util.ServiceConnectionWrapper;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
public class PlayIntegrityUtils {
|
||||
|
||||
public static @Nullable ServiceConnection maybeReplaceServiceConnection(Intent service, ServiceConnection orig) {
|
||||
if (PackageId.PLAY_STORE_NAME.equals(service.getPackage())) {
|
||||
UnaryOperator<IBinder> binderOverride = null;
|
||||
|
||||
final String CLASSIC_SERVICE =
|
||||
"com.google.android.play.core.integrityservice.BIND_INTEGRITY_SERVICE";
|
||||
final String STANDARD_SERVICE =
|
||||
"com.google.android.play.core.expressintegrityservice.BIND_EXPRESS_INTEGRITY_SERVICE";
|
||||
|
||||
String action = service.getAction();
|
||||
if (STANDARD_SERVICE.equals(action)) {
|
||||
binderOverride = StandardPlayIntegrityServiceWrapper::new;
|
||||
} else if (CLASSIC_SERVICE.equals(action)) {
|
||||
binderOverride = ClassicPlayIntegrityServiceWrapper::new;
|
||||
}
|
||||
|
||||
if (binderOverride != null) {
|
||||
return new ServiceConnectionWrapper(orig, binderOverride);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
package app.grapheneos.gmscompat.lib.playintegrity;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.os.Binder;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
import com.android.internal.os.FakeBackgroundHandler;
|
||||
import com.google.android.play.core.integrity.protocol.IExpressIntegrityService;
|
||||
import com.google.android.play.core.integrity.protocol.IExpressIntegrityServiceCallback;
|
||||
|
||||
@SuppressLint("LongLogTag")
|
||||
class StandardPlayIntegrityServiceWrapper extends PlayIntegrityServiceWrapper {
|
||||
|
||||
StandardPlayIntegrityServiceWrapper(IBinder base) {
|
||||
super(base);
|
||||
requestIntegrityTokenTxnCode = 3; // IExpressIntegrityService.Stub.TRANSACTION_requestIntegrityToken
|
||||
}
|
||||
|
||||
static class TokenRequestStub extends IExpressIntegrityService.Stub {
|
||||
public void requestIntegrityToken(Bundle request, IExpressIntegrityServiceCallback callback) {
|
||||
Runnable r = () -> {
|
||||
var result = new Bundle();
|
||||
// https://developer.android.com/google/play/integrity/reference/com/google/android/play/core/integrity/model/StandardIntegrityErrorCode.html#API_NOT_AVAILABLE
|
||||
final int API_NOT_AVAILABLE = -1;
|
||||
result.putInt("error", API_NOT_AVAILABLE);
|
||||
try {
|
||||
callback.onRequestExpressIntegrityTokenResult(result);
|
||||
} catch (RemoteException e) {
|
||||
Log.e("IExpressIntegrityService.Stub", "", e);
|
||||
}
|
||||
};
|
||||
FakeBackgroundHandler.getHandler().postDelayed(r, getTokenRequestResultDelay());
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected Binder createTokenRequestStub() {
|
||||
return new TokenRequestStub();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
package app.grapheneos.gmscompat.lib.util;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.ServiceConnection;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
public class ServiceConnectionWrapper implements ServiceConnection {
|
||||
private final ServiceConnection base;
|
||||
private final UnaryOperator<IBinder> binderOverride;
|
||||
|
||||
public ServiceConnectionWrapper(ServiceConnection base, UnaryOperator<IBinder> binderOverride) {
|
||||
this.base = base;
|
||||
this.binderOverride = binderOverride;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
IBinder override = binderOverride.apply(service);
|
||||
if (override != null) {
|
||||
service = override;
|
||||
}
|
||||
}
|
||||
|
||||
base.onServiceConnected(name, service);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
base.onServiceDisconnected(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindingDied(ComponentName name) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
base.onBindingDied(name);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNullBinding(ComponentName name) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
base.onNullBinding(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
package app.revanced.extension.play;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import app.grapheneos.gmscompat.lib.playintegrity.PlayIntegrityUtils;
|
||||
|
||||
public class DisablePlayIntegrityPatch {
|
||||
public static boolean bindService(Context context, Intent service, ServiceConnection conn, int flags) {
|
||||
ServiceConnection override = PlayIntegrityUtils.maybeReplaceServiceConnection(service, conn);
|
||||
if (override != null) {
|
||||
conn = override;
|
||||
}
|
||||
|
||||
return context.bindService(service, conn, flags);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
package com.android.internal.os;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
public class FakeBackgroundHandler {
|
||||
|
||||
public static Handler getHandler() {
|
||||
return new Handler(Looper.getMainLooper());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,14 @@
|
|||
android {
|
||||
namespace = "app.revanced.extension"
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 21
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
android {
|
||||
namespace = "app.revanced.extension"
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 21
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
|
|
|||
|
|
@ -3,9 +3,3 @@ dependencies {
|
|||
compileOnly(libs.annotation)
|
||||
compileOnly(libs.okhttp)
|
||||
}
|
||||
|
||||
android {
|
||||
defaultConfig {
|
||||
minSdk = 22
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,3 @@ dependencies {
|
|||
compileOnly(libs.annotation)
|
||||
compileOnly(libs.okhttp)
|
||||
}
|
||||
|
||||
android {
|
||||
defaultConfig {
|
||||
minSdk = 21
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,4 @@
|
|||
dependencies {
|
||||
compileOnly(project(":extensions:shared:library"))
|
||||
compileOnly(project(":extensions:cricbuzz:stub"))
|
||||
}
|
||||
|
||||
android {
|
||||
defaultConfig {
|
||||
minSdk = 21
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
dependencies {
|
||||
compileOnly(project(":extensions:shared:library"))
|
||||
}
|
||||
|
||||
android {
|
||||
defaultConfig {
|
||||
minSdk = 26
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
<manifest/>
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
package app.revanced.extension.instagram.feed;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class LimitFeedToFollowedProfiles {
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static Map<String, String> setFollowingHeader(Map<String, String> requestHeaderMap) {
|
||||
String paginationHeaderName = "pagination_source";
|
||||
|
||||
// Patch the header only if it's trying to fetch the default feed
|
||||
String currentHeader = requestHeaderMap.get(paginationHeaderName);
|
||||
if (currentHeader != null && !currentHeader.equals("feed_recs")) {
|
||||
return requestHeaderMap;
|
||||
}
|
||||
|
||||
// Create new map as original is unmodifiable.
|
||||
Map<String, String> patchedRequestHeaderMap = new HashMap<>(requestHeaderMap);
|
||||
patchedRequestHeaderMap.put(paginationHeaderName, "following");
|
||||
return patchedRequestHeaderMap;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
package app.revanced.extension.instagram.hide.navigation;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class HideNavigationButtonsPatch {
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* @param navigationButtonsList the list of navigation buttons, as an (obfuscated) Enum type
|
||||
* @param buttonNameToRemove the name of the button we want to remove
|
||||
* @param enumNameField the field in the nav button enum class which contains the name of the button
|
||||
* @return the patched list of navigation buttons
|
||||
*/
|
||||
public static List<Object> removeNavigationButtonByName(
|
||||
List<Object> navigationButtonsList,
|
||||
String buttonNameToRemove,
|
||||
String enumNameField
|
||||
)
|
||||
throws IllegalAccessException, NoSuchFieldException {
|
||||
for (Object button : navigationButtonsList) {
|
||||
Field f = button.getClass().getDeclaredField(enumNameField);
|
||||
String currentButtonEnumName = (String) f.get(button);
|
||||
|
||||
if (buttonNameToRemove.equals(currentButtonEnumName)) {
|
||||
navigationButtonsList.remove(button);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return navigationButtonsList;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
package app.revanced.extension.instagram.misc.links;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class OpenLinksExternallyPatch {
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean openExternally(String url) {
|
||||
try {
|
||||
// The "url" parameter to this function will be of the form.
|
||||
// https://l.instagram.com/?u=<actual url>&e=<tracking id>
|
||||
String actualUrl = Uri.parse(url).getQueryParameter("u");
|
||||
if (actualUrl != null) {
|
||||
Utils.openLink(actualUrl);
|
||||
return true;
|
||||
}
|
||||
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "openExternally failure", ex);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
package app.revanced.extension.instagram.misc.privacy;
|
||||
|
||||
import app.revanced.extension.shared.privacy.LinkSanitizer;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class SanitizeSharingLinksPatch {
|
||||
private static final LinkSanitizer sanitizer = new LinkSanitizer("igsh");
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static String sanitizeSharingLink(String url) {
|
||||
return sanitizer.sanitizeURLString(url);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
package app.revanced.extension.instagram.misc.share.domain;
|
||||
|
||||
import android.net.Uri;
|
||||
import app.revanced.extension.shared.Logger;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class ChangeLinkSharingDomainPatch {
|
||||
|
||||
private static String getCustomShareDomain() {
|
||||
// Method is modified during patching.
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static String setCustomShareDomain(String url) {
|
||||
try {
|
||||
Uri uri = Uri.parse(url);
|
||||
Uri.Builder builder = uri
|
||||
.buildUpon()
|
||||
.authority(getCustomShareDomain())
|
||||
.clearQuery();
|
||||
|
||||
String patchedUrl = builder.build().toString();
|
||||
Logger.printInfo(() -> "Domain change from : " + url + " to: " + patchedUrl);
|
||||
return patchedUrl;
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "setCustomShareDomain failure with " + url, ex);
|
||||
return url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
package app.revanced.extension.instagram.misc.share.privacy;
|
||||
|
||||
import app.revanced.extension.shared.privacy.LinkSanitizer;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class SanitizeSharingLinksPatch {
|
||||
private static final LinkSanitizer sanitizer = new LinkSanitizer("igsh");
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static String sanitizeSharingLink(String url) {
|
||||
return sanitizer.sanitizeURLString(url);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,3 @@
|
|||
dependencies {
|
||||
compileOnly(project(":extensions:shared:library"))
|
||||
}
|
||||
|
||||
android {
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,3 @@
|
|||
dependencies {
|
||||
compileOnly(project(":extensions:shared:library"))
|
||||
compileOnly(project(":extensions:youtube:stub"))
|
||||
compileOnly(libs.annotation)
|
||||
}
|
||||
|
||||
android {
|
||||
defaultConfig {
|
||||
minSdk = 26
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
package app.revanced.extension.music;
|
||||
|
||||
import app.revanced.extension.shared.Utils;
|
||||
|
||||
public class VersionCheckUtils {
|
||||
private static boolean isVersionOrGreater(String version) {
|
||||
return Utils.getAppVersionName().compareTo(version) >= 0;
|
||||
}
|
||||
|
||||
public static final boolean IS_8_40_OR_GREATER = isVersionOrGreater("8.40.00");
|
||||
}
|
||||
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
package app.revanced.extension.music.patches;
|
||||
|
||||
import app.revanced.extension.music.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class ChangeMiniplayerColorPatch {
|
||||
|
||||
/**
|
||||
* Injection point
|
||||
*/
|
||||
public static boolean changeMiniplayerColor() {
|
||||
return Settings.CHANGE_MINIPLAYER_COLOR.get();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
package app.revanced.extension.music.patches;
|
||||
|
||||
import app.revanced.extension.music.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class ForceOriginalAudioPatch {
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void setEnabled() {
|
||||
app.revanced.extension.shared.patches.ForceOriginalAudioPatch.setEnabled(
|
||||
Settings.FORCE_ORIGINAL_AUDIO.get(),
|
||||
Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
package app.revanced.extension.music.patches;
|
||||
|
||||
import static app.revanced.extension.shared.Utils.hideViewBy0dpUnderCondition;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import app.revanced.extension.music.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class HideButtonsPatch {
|
||||
|
||||
/**
|
||||
* Injection point
|
||||
*/
|
||||
public static int hideCastButton(int original) {
|
||||
return Settings.HIDE_CAST_BUTTON.get() ? View.GONE : original;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point
|
||||
*/
|
||||
public static void hideCastButton(View view) {
|
||||
hideViewBy0dpUnderCondition(Settings.HIDE_CAST_BUTTON, view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point
|
||||
*/
|
||||
public static boolean hideHistoryButton(boolean original) {
|
||||
return original && !Settings.HIDE_HISTORY_BUTTON.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point
|
||||
*/
|
||||
public static void hideNotificationButton(View view) {
|
||||
if (view.getParent() instanceof ViewGroup viewGroup) {
|
||||
hideViewBy0dpUnderCondition(Settings.HIDE_NOTIFICATION_BUTTON, viewGroup);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point
|
||||
*/
|
||||
public static void hideSearchButton(View view) {
|
||||
hideViewBy0dpUnderCondition(Settings.HIDE_SEARCH_BUTTON, view);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
package app.revanced.extension.music.patches;
|
||||
|
||||
import static app.revanced.extension.shared.Utils.hideViewBy0dpUnderCondition;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import app.revanced.extension.music.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class HideCategoryBarPatch {
|
||||
|
||||
/**
|
||||
* Injection point
|
||||
*/
|
||||
public static void hideCategoryBar(View view) {
|
||||
hideViewBy0dpUnderCondition(Settings.HIDE_CATEGORY_BAR, view);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
package app.revanced.extension.music.patches;
|
||||
|
||||
import app.revanced.extension.music.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class HideGetPremiumPatch {
|
||||
|
||||
/**
|
||||
* Injection point
|
||||
*/
|
||||
public static boolean hideGetPremiumLabel() {
|
||||
return Settings.HIDE_GET_PREMIUM_LABEL.get();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
package app.revanced.extension.music.patches;
|
||||
|
||||
import app.revanced.extension.music.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class HideVideoAdsPatch {
|
||||
|
||||
/**
|
||||
* Injection point
|
||||
*/
|
||||
public static boolean showVideoAds(boolean original) {
|
||||
if (Settings.HIDE_VIDEO_ADS.get()) {
|
||||
return false;
|
||||
}
|
||||
return original;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
package app.revanced.extension.music.patches;
|
||||
|
||||
import static app.revanced.extension.shared.Utils.hideViewUnderCondition;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import app.revanced.extension.music.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class NavigationBarPatch {
|
||||
private static String lastYTNavigationEnumName = "";
|
||||
|
||||
public static void setLastAppNavigationEnum(@Nullable Enum<?> ytNavigationEnumName) {
|
||||
if (ytNavigationEnumName != null) {
|
||||
lastYTNavigationEnumName = ytNavigationEnumName.name();
|
||||
}
|
||||
}
|
||||
|
||||
public static void hideNavigationLabel(TextView textview) {
|
||||
hideViewUnderCondition(Settings.HIDE_NAVIGATION_BAR_LABEL.get(), textview);
|
||||
}
|
||||
|
||||
public static void hideNavigationButton(View view) {
|
||||
// Hide entire navigation bar.
|
||||
if (Settings.HIDE_NAVIGATION_BAR.get() && view.getParent() != null) {
|
||||
hideViewUnderCondition(true, (View) view.getParent());
|
||||
return;
|
||||
}
|
||||
|
||||
// Hide navigation buttons based on their type.
|
||||
for (NavigationButton button : NavigationButton.values()) {
|
||||
if (button.ytEnumNames.contains(lastYTNavigationEnumName)) {
|
||||
hideViewUnderCondition(button.hidden, view);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum NavigationButton {
|
||||
HOME(
|
||||
Arrays.asList(
|
||||
"TAB_HOME"
|
||||
),
|
||||
Settings.HIDE_NAVIGATION_BAR_HOME_BUTTON.get()
|
||||
),
|
||||
SAMPLES(
|
||||
Arrays.asList(
|
||||
"TAB_SAMPLES"
|
||||
),
|
||||
Settings.HIDE_NAVIGATION_BAR_SAMPLES_BUTTON.get()
|
||||
),
|
||||
EXPLORE(
|
||||
Arrays.asList(
|
||||
"TAB_EXPLORE"
|
||||
),
|
||||
Settings.HIDE_NAVIGATION_BAR_EXPLORE_BUTTON.get()
|
||||
),
|
||||
LIBRARY(
|
||||
Arrays.asList(
|
||||
"LIBRARY_MUSIC",
|
||||
"TAB_BOOKMARK" // YouTube Music 8.24+
|
||||
),
|
||||
Settings.HIDE_NAVIGATION_BAR_LIBRARY_BUTTON.get()
|
||||
),
|
||||
UPGRADE(
|
||||
Arrays.asList(
|
||||
"TAB_MUSIC_PREMIUM"
|
||||
),
|
||||
Settings.HIDE_NAVIGATION_BAR_UPGRADE_BUTTON.get()
|
||||
);
|
||||
|
||||
private final List<String> ytEnumNames;
|
||||
private final boolean hidden;
|
||||
|
||||
NavigationButton(List<String> ytEnumNames, boolean hidden) {
|
||||
this.ytEnumNames = ytEnumNames;
|
||||
this.hidden = hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
package app.revanced.extension.music.patches;
|
||||
|
||||
import app.revanced.extension.music.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class PermanentRepeatPatch {
|
||||
|
||||
/**
|
||||
* Injection point
|
||||
*/
|
||||
public static boolean permanentRepeat() {
|
||||
return Settings.PERMANENT_REPEAT.get();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
package app.revanced.extension.music.patches.spoof;
|
||||
|
||||
import static app.revanced.extension.music.settings.Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE;
|
||||
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_REEL;
|
||||
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_43_32;
|
||||
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_61_48;
|
||||
import static app.revanced.extension.shared.spoof.ClientType.VISIONOS;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import app.revanced.extension.shared.spoof.ClientType;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class SpoofVideoStreamsPatch {
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void setClientOrderToUse() {
|
||||
List<ClientType> availableClients = List.of(
|
||||
ANDROID_REEL,
|
||||
ANDROID_VR_1_43_32,
|
||||
VISIONOS,
|
||||
ANDROID_VR_1_61_48
|
||||
);
|
||||
|
||||
app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch.setClientsToUse(
|
||||
availableClients, SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
package app.revanced.extension.music.patches.theme;
|
||||
|
||||
import app.revanced.extension.shared.theme.BaseThemePatch;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class ThemePatch extends BaseThemePatch {
|
||||
|
||||
// Color constants used in relation with litho components.
|
||||
private static final int[] DARK_VALUES = {
|
||||
0xFF212121, // Comments box background.
|
||||
0xFF030303, // Button container background in album.
|
||||
0xFF000000, // Button container background in playlist.
|
||||
};
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* <p>
|
||||
* Change the color of Litho components.
|
||||
* If the color of the component matches one of the values, return the background color.
|
||||
*
|
||||
* @param originalValue The original color value.
|
||||
* @return The new or original color value.
|
||||
*/
|
||||
public static int getValue(int originalValue) {
|
||||
return processColorValue(originalValue, DARK_VALUES, null);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,148 +0,0 @@
|
|||
package app.revanced.extension.music.settings;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.preference.PreferenceFragment;
|
||||
import android.view.View;
|
||||
import android.widget.Toolbar;
|
||||
|
||||
import app.revanced.extension.music.VersionCheckUtils;
|
||||
import app.revanced.extension.music.settings.preference.MusicPreferenceFragment;
|
||||
import app.revanced.extension.music.settings.search.MusicSearchViewController;
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.ResourceType;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.settings.BaseActivityHook;
|
||||
|
||||
/**
|
||||
* Hooks GoogleApiActivity to inject a custom {@link MusicPreferenceFragment} with a toolbar and search.
|
||||
*/
|
||||
public class MusicActivityHook extends BaseActivityHook {
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public static MusicSearchViewController searchViewController;
|
||||
|
||||
/**
|
||||
* How much time has passed since the first launch of the app. Simple check to prevent
|
||||
* forcing bold icons on first launch where the settings menu is partially broken
|
||||
* due to missing icon resources the client has not yet received.
|
||||
*
|
||||
* @see app.revanced.extension.youtube.settings.YouTubeActivityHook#MINIMUM_TIME_AFTER_FIRST_LAUNCH_BEFORE_ALLOWING_BOLD_ICONS
|
||||
*/
|
||||
private static final long MINIMUM_TIME_AFTER_FIRST_LAUNCH_BEFORE_ALLOWING_BOLD_ICONS = 30 * 1000; // 30 seconds.
|
||||
|
||||
static {
|
||||
final boolean useBoldIcons = VersionCheckUtils.IS_8_40_OR_GREATER
|
||||
&& !Settings.SETTINGS_DISABLE_BOLD_ICONS.get()
|
||||
&& (System.currentTimeMillis() - Settings.FIRST_TIME_APP_LAUNCHED.get())
|
||||
> MINIMUM_TIME_AFTER_FIRST_LAUNCH_BEFORE_ALLOWING_BOLD_ICONS;
|
||||
|
||||
Utils.setAppIsUsingBoldIcons(useBoldIcons);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public static void initialize(Activity parentActivity) {
|
||||
// Must touch the Music settings to ensure the class is loaded and
|
||||
// the values can be found when setting the UI preferences.
|
||||
// Logging anything under non debug ensures this is set.
|
||||
Logger.printInfo(() -> "Permanent repeat enabled: " + Settings.PERMANENT_REPEAT.get());
|
||||
|
||||
// YT Music always uses dark mode.
|
||||
Utils.setIsDarkModeEnabled(true);
|
||||
|
||||
BaseActivityHook.initialize(new MusicActivityHook(), parentActivity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the fixed theme for the activity.
|
||||
*/
|
||||
@Override
|
||||
protected void customizeActivityTheme(Activity activity) {
|
||||
// Override the default YouTube Music theme to increase start padding of list items.
|
||||
// Custom style located in resources/music/values/style.xml
|
||||
activity.setTheme(Utils.getResourceIdentifierOrThrow(
|
||||
ResourceType.STYLE, "Theme.ReVanced.YouTubeMusic.Settings"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the fixed background color for the toolbar.
|
||||
*/
|
||||
@Override
|
||||
protected int getToolbarBackgroundColor() {
|
||||
return Utils.getResourceColor("ytm_color_black");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the navigation icon with a color filter applied.
|
||||
*/
|
||||
@Override
|
||||
protected Drawable getNavigationIcon() {
|
||||
Drawable navigationIcon = MusicPreferenceFragment.getBackButtonDrawable();
|
||||
navigationIcon.setColorFilter(Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN);
|
||||
return navigationIcon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the click listener that finishes the activity when the navigation icon is clicked.
|
||||
*/
|
||||
@Override
|
||||
protected View.OnClickListener getNavigationClickListener(Activity activity) {
|
||||
return view -> {
|
||||
if (searchViewController != null && searchViewController.isSearchActive()) {
|
||||
searchViewController.closeSearch();
|
||||
} else {
|
||||
activity.finish();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds search view components to the toolbar for {@link MusicPreferenceFragment}.
|
||||
*
|
||||
* @param activity The activity hosting the toolbar.
|
||||
* @param toolbar The configured toolbar.
|
||||
* @param fragment The PreferenceFragment associated with the activity.
|
||||
*/
|
||||
@Override
|
||||
protected void onPostToolbarSetup(Activity activity, Toolbar toolbar, PreferenceFragment fragment) {
|
||||
if (fragment instanceof MusicPreferenceFragment) {
|
||||
searchViewController = MusicSearchViewController.addSearchViewComponents(
|
||||
activity, toolbar, (MusicPreferenceFragment) fragment);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link MusicPreferenceFragment} for the activity.
|
||||
*/
|
||||
@Override
|
||||
protected PreferenceFragment createPreferenceFragment() {
|
||||
return new MusicPreferenceFragment();
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* <p>
|
||||
* Overrides {@link Activity#finish()} of the injection Activity.
|
||||
*
|
||||
* @return if the original activity finish method should be allowed to run.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public static boolean handleFinish() {
|
||||
return MusicSearchViewController.handleFinish(searchViewController);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* <p>
|
||||
* Decides whether to use bold icons.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public static boolean useBoldIcons(boolean original) {
|
||||
return Utils.appIsUsingBoldIcons();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
package app.revanced.extension.music.settings;
|
||||
|
||||
import static java.lang.Boolean.FALSE;
|
||||
import static java.lang.Boolean.TRUE;
|
||||
import static app.revanced.extension.shared.settings.Setting.parent;
|
||||
|
||||
import app.revanced.extension.shared.settings.YouTubeAndMusicSettings;
|
||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||
import app.revanced.extension.shared.settings.EnumSetting;
|
||||
import app.revanced.extension.shared.spoof.ClientType;
|
||||
|
||||
public class Settings extends YouTubeAndMusicSettings {
|
||||
|
||||
// Ads
|
||||
public static final BooleanSetting HIDE_VIDEO_ADS = new BooleanSetting("revanced_music_hide_video_ads", TRUE, true);
|
||||
public static final BooleanSetting HIDE_GET_PREMIUM_LABEL = new BooleanSetting("revanced_music_hide_get_premium_label", TRUE, true);
|
||||
|
||||
// General
|
||||
public static final BooleanSetting HIDE_CAST_BUTTON = new BooleanSetting("revanced_music_hide_cast_button", TRUE, true);
|
||||
public static final BooleanSetting HIDE_CATEGORY_BAR = new BooleanSetting("revanced_music_hide_category_bar", FALSE, true);
|
||||
public static final BooleanSetting HIDE_HISTORY_BUTTON = new BooleanSetting("revanced_music_hide_history_button", FALSE, true);
|
||||
public static final BooleanSetting HIDE_SEARCH_BUTTON = new BooleanSetting("revanced_music_hide_search_button", FALSE, true);
|
||||
public static final BooleanSetting HIDE_NOTIFICATION_BUTTON = new BooleanSetting("revanced_music_hide_notification_button", FALSE, true);
|
||||
public static final BooleanSetting HIDE_NAVIGATION_BAR_HOME_BUTTON = new BooleanSetting("revanced_music_hide_navigation_bar_home_button", FALSE, true);
|
||||
public static final BooleanSetting HIDE_NAVIGATION_BAR_SAMPLES_BUTTON = new BooleanSetting("revanced_music_hide_navigation_bar_samples_button", FALSE, true);
|
||||
public static final BooleanSetting HIDE_NAVIGATION_BAR_EXPLORE_BUTTON = new BooleanSetting("revanced_music_hide_navigation_bar_explore_button", FALSE, true);
|
||||
public static final BooleanSetting HIDE_NAVIGATION_BAR_LIBRARY_BUTTON = new BooleanSetting("revanced_music_hide_navigation_bar_library_button", FALSE, true);
|
||||
public static final BooleanSetting HIDE_NAVIGATION_BAR_UPGRADE_BUTTON = new BooleanSetting("revanced_music_hide_navigation_bar_upgrade_button", TRUE, true);
|
||||
public static final BooleanSetting HIDE_NAVIGATION_BAR = new BooleanSetting("revanced_music_hide_navigation_bar", FALSE, true);
|
||||
public static final BooleanSetting HIDE_NAVIGATION_BAR_LABEL = new BooleanSetting("revanced_music_hide_navigation_bar_labels", FALSE, true);
|
||||
|
||||
// Player
|
||||
public static final BooleanSetting CHANGE_MINIPLAYER_COLOR = new BooleanSetting("revanced_music_change_miniplayer_color", FALSE, true);
|
||||
public static final BooleanSetting PERMANENT_REPEAT = new BooleanSetting("revanced_music_play_permanent_repeat", FALSE, true);
|
||||
|
||||
// Miscellaneous
|
||||
public static final EnumSetting<ClientType> SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_video_streams_client_type",
|
||||
ClientType.ANDROID_REEL, true, parent(SPOOF_VIDEO_STREAMS));
|
||||
|
||||
public static final BooleanSetting FORCE_ORIGINAL_AUDIO = new BooleanSetting("revanced_force_original_audio", TRUE, true);
|
||||
}
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
package app.revanced.extension.music.settings.preference;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.preference.PreferenceScreen;
|
||||
import android.widget.Toolbar;
|
||||
|
||||
import app.revanced.extension.music.settings.MusicActivityHook;
|
||||
import app.revanced.extension.shared.GmsCoreSupport;
|
||||
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.preference.ToolbarPreferenceFragment;
|
||||
|
||||
/**
|
||||
* Preference fragment for ReVanced settings.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public class MusicPreferenceFragment extends ToolbarPreferenceFragment {
|
||||
/**
|
||||
* The main PreferenceScreen used to display the current set of preferences.
|
||||
*/
|
||||
private PreferenceScreen preferenceScreen;
|
||||
|
||||
/**
|
||||
* Initializes the preference fragment.
|
||||
*/
|
||||
@Override
|
||||
protected void initialize() {
|
||||
super.initialize();
|
||||
|
||||
try {
|
||||
preferenceScreen = getPreferenceScreen();
|
||||
Utils.sortPreferenceGroups(preferenceScreen);
|
||||
setPreferenceScreenToolbar(preferenceScreen);
|
||||
|
||||
// Clunky work around until preferences are custom classes that manage themselves.
|
||||
// Custom branding only works with non-root install. But the preferences must be
|
||||
// added during patched because of difficulties detecting during patching if it's
|
||||
// a root install. So instead the non-functional preferences are removed during
|
||||
// runtime if the app is mount (root) installation.
|
||||
if (GmsCoreSupport.isPackageNameOriginal()) {
|
||||
removePreferences(
|
||||
BaseSettings.CUSTOM_BRANDING_ICON.key,
|
||||
BaseSettings.CUSTOM_BRANDING_NAME.key);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "initialize failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the fragment starts.
|
||||
*/
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
try {
|
||||
// Initialize search controller if needed
|
||||
if (MusicActivityHook.searchViewController != null) {
|
||||
// Trigger search data collection after fragment is ready.
|
||||
MusicActivityHook.searchViewController.initializeSearchData();
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "onStart failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets toolbar for all nested preference screens.
|
||||
*/
|
||||
@Override
|
||||
protected void customizeToolbar(Toolbar toolbar) {
|
||||
MusicActivityHook.setToolbarLayoutParams(toolbar);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform actions after toolbar setup.
|
||||
*/
|
||||
@Override
|
||||
protected void onPostToolbarSetup(Toolbar toolbar, Dialog preferenceScreenDialog) {
|
||||
if (MusicActivityHook.searchViewController != null
|
||||
&& MusicActivityHook.searchViewController.isSearchActive()) {
|
||||
toolbar.post(() -> MusicActivityHook.searchViewController.closeSearch());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the preference screen for external access by SearchViewController.
|
||||
*/
|
||||
public PreferenceScreen getPreferenceScreenForSearch() {
|
||||
return preferenceScreen;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
package app.revanced.extension.music.settings.search;
|
||||
|
||||
import android.content.Context;
|
||||
import android.preference.PreferenceScreen;
|
||||
|
||||
import app.revanced.extension.shared.settings.search.BaseSearchResultsAdapter;
|
||||
import app.revanced.extension.shared.settings.search.BaseSearchViewController;
|
||||
import app.revanced.extension.shared.settings.search.BaseSearchResultItem;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Music-specific search results adapter.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public class MusicSearchResultsAdapter extends BaseSearchResultsAdapter {
|
||||
|
||||
public MusicSearchResultsAdapter(Context context, List<BaseSearchResultItem> items,
|
||||
BaseSearchViewController.BasePreferenceFragment fragment,
|
||||
BaseSearchViewController searchViewController) {
|
||||
super(context, items, fragment, searchViewController);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PreferenceScreen getMainPreferenceScreen() {
|
||||
return fragment.getPreferenceScreenForSearch();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
package app.revanced.extension.music.settings.search;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceScreen;
|
||||
import android.view.View;
|
||||
import android.widget.Toolbar;
|
||||
|
||||
import app.revanced.extension.music.settings.preference.MusicPreferenceFragment;
|
||||
import app.revanced.extension.shared.settings.search.*;
|
||||
|
||||
/**
|
||||
* Music-specific search view controller implementation.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public class MusicSearchViewController extends BaseSearchViewController {
|
||||
|
||||
public static MusicSearchViewController addSearchViewComponents(Activity activity, Toolbar toolbar,
|
||||
MusicPreferenceFragment fragment) {
|
||||
return new MusicSearchViewController(activity, toolbar, fragment);
|
||||
}
|
||||
|
||||
private MusicSearchViewController(Activity activity, Toolbar toolbar, MusicPreferenceFragment fragment) {
|
||||
super(activity, toolbar, new PreferenceFragmentAdapter(fragment));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BaseSearchResultsAdapter createSearchResultsAdapter() {
|
||||
return new MusicSearchResultsAdapter(activity, filteredSearchItems, fragment, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isSpecialPreferenceGroup(Preference preference) {
|
||||
// Music doesn't have SponsorBlock, so no special groups.
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setupSpecialPreferenceListeners(BaseSearchResultItem item) {
|
||||
// Music doesn't have special preferences.
|
||||
// This method can be empty or handle music-specific preferences if any.
|
||||
}
|
||||
|
||||
// Static method for handling Activity finish
|
||||
public static boolean handleFinish(MusicSearchViewController searchViewController) {
|
||||
if (searchViewController != null && searchViewController.isSearchActive()) {
|
||||
searchViewController.closeSearch();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Adapter to wrap MusicPreferenceFragment to BasePreferenceFragment interface.
|
||||
private record PreferenceFragmentAdapter(MusicPreferenceFragment fragment) implements BasePreferenceFragment {
|
||||
|
||||
@Override
|
||||
public PreferenceScreen getPreferenceScreenForSearch() {
|
||||
return fragment.getPreferenceScreenForSearch();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView() {
|
||||
return fragment.getView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return fragment.getActivity();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
dependencies {
|
||||
compileOnly(project(":extensions:shared:library"))
|
||||
compileOnly(project(":extensions:nothingx:stub"))
|
||||
}
|
||||
|
||||
android {
|
||||
defaultConfig {
|
||||
minSdk = 23
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
<manifest/>
|
||||
|
|
@ -1,590 +0,0 @@
|
|||
package app.revanced.extension.nothingx.patches;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Application;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Typeface;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.widget.Button;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Patches to expose the K1 token for Nothing X app to enable pairing with GadgetBridge.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class ShowK1TokensPatch {
|
||||
|
||||
private static final String TAG = "ReVanced";
|
||||
private static final String PACKAGE_NAME = "com.nothing.smartcenter";
|
||||
private static final String EMPTY_MD5 = "d41d8cd98f00b204e9800998ecf8427e";
|
||||
private static final String PREFS_NAME = "revanced_nothingx_prefs";
|
||||
private static final String KEY_DONT_SHOW_DIALOG = "dont_show_k1_dialog";
|
||||
|
||||
// Colors
|
||||
private static final int COLOR_BG = 0xFF1E1E1E;
|
||||
private static final int COLOR_CARD = 0xFF2D2D2D;
|
||||
private static final int COLOR_TEXT_PRIMARY = 0xFFFFFFFF;
|
||||
private static final int COLOR_TEXT_SECONDARY = 0xFFB0B0B0;
|
||||
private static final int COLOR_ACCENT = 0xFFFF9500;
|
||||
private static final int COLOR_TOKEN_BG = 0xFF3A3A3A;
|
||||
private static final int COLOR_BUTTON_POSITIVE = 0xFFFF9500;
|
||||
private static final int COLOR_BUTTON_NEGATIVE = 0xFFFF6B6B;
|
||||
|
||||
// Match standalone K1: k1:, K1:, k1>, etc.
|
||||
private static final Pattern K1_STANDALONE_PATTERN = Pattern.compile("(?i)(?:k1\\s*[:>]\\s*)([0-9a-f]{32})");
|
||||
// Match combined r3+k1: format (64 chars = r3(32) + k1(32))
|
||||
private static final Pattern K1_COMBINED_PATTERN = Pattern.compile("(?i)r3\\+k1\\s*:\\s*([0-9a-f]{64})");
|
||||
|
||||
private static volatile boolean k1Logged = false;
|
||||
private static volatile boolean lifecycleCallbacksRegistered = false;
|
||||
private static Context appContext;
|
||||
|
||||
/**
|
||||
* Get K1 tokens from database and log files.
|
||||
* Call this after the app initializes.
|
||||
*
|
||||
* @param context Application context
|
||||
*/
|
||||
public static void showK1Tokens(Context context) {
|
||||
if (k1Logged) {
|
||||
return;
|
||||
}
|
||||
|
||||
appContext = context.getApplicationContext();
|
||||
|
||||
Set<String> allTokens = new LinkedHashSet<>();
|
||||
|
||||
// First try to get from database.
|
||||
String dbToken = getK1TokensFromDatabase();
|
||||
if (dbToken != null) {
|
||||
allTokens.add(dbToken);
|
||||
}
|
||||
|
||||
// Then get from log files.
|
||||
Set<String> logTokens = getK1TokensFromLogFiles();
|
||||
allTokens.addAll(logTokens);
|
||||
|
||||
if (allTokens.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Log all found tokens.
|
||||
int index = 1;
|
||||
for (String token : allTokens) {
|
||||
Log.i(TAG, "#" + index++ + ": " + token.toUpperCase());
|
||||
}
|
||||
|
||||
// Register lifecycle callbacks to show dialog when an Activity is ready.
|
||||
registerLifecycleCallbacks(allTokens);
|
||||
|
||||
k1Logged = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register ActivityLifecycleCallbacks to show dialog when first Activity resumes.
|
||||
*
|
||||
* @param tokens Set of K1 tokens to display
|
||||
*/
|
||||
private static void registerLifecycleCallbacks(Set<String> tokens) {
|
||||
if (lifecycleCallbacksRegistered || !(appContext instanceof Application)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Application application = (Application) appContext;
|
||||
application.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {
|
||||
@Override
|
||||
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityStarted(Activity activity) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResumed(Activity activity) {
|
||||
// Check if user chose not to show dialog.
|
||||
SharedPreferences prefs = activity.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||||
if (prefs.getBoolean(KEY_DONT_SHOW_DIALOG, false)) {
|
||||
application.unregisterActivityLifecycleCallbacks(this);
|
||||
lifecycleCallbacksRegistered = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Show dialog on first Activity resume.
|
||||
if (tokens != null && !tokens.isEmpty()) {
|
||||
activity.runOnUiThread(() -> showK1TokensDialog(activity, tokens));
|
||||
// Unregister after showing
|
||||
application.unregisterActivityLifecycleCallbacks(this);
|
||||
lifecycleCallbacksRegistered = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityPaused(Activity activity) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityStopped(Activity activity) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityDestroyed(Activity activity) {
|
||||
}
|
||||
});
|
||||
|
||||
lifecycleCallbacksRegistered = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show dialog with K1 tokens.
|
||||
*
|
||||
* @param activity Activity context
|
||||
* @param tokens Set of K1 tokens
|
||||
*/
|
||||
private static void showK1TokensDialog(Activity activity, Set<String> tokens) {
|
||||
try {
|
||||
// Create main container.
|
||||
LinearLayout mainLayout = new LinearLayout(activity);
|
||||
mainLayout.setOrientation(LinearLayout.VERTICAL);
|
||||
mainLayout.setBackgroundColor(COLOR_BG);
|
||||
mainLayout.setPadding(dpToPx(activity, 24), dpToPx(activity, 16),
|
||||
dpToPx(activity, 24), dpToPx(activity, 16));
|
||||
|
||||
// Title.
|
||||
TextView titleView = new TextView(activity);
|
||||
titleView.setText("K1 Token(s) Found");
|
||||
titleView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20);
|
||||
titleView.setTypeface(Typeface.DEFAULT_BOLD);
|
||||
titleView.setTextColor(COLOR_TEXT_PRIMARY);
|
||||
titleView.setGravity(Gravity.CENTER);
|
||||
mainLayout.addView(titleView, new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
));
|
||||
|
||||
// Subtitle.
|
||||
TextView subtitleView = new TextView(activity);
|
||||
subtitleView.setText(tokens.size() == 1 ? "1 token found • Tap to copy" : tokens.size() + " tokens found • Tap to copy");
|
||||
subtitleView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
|
||||
subtitleView.setTextColor(COLOR_TEXT_SECONDARY);
|
||||
subtitleView.setGravity(Gravity.CENTER);
|
||||
LinearLayout.LayoutParams subtitleParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
);
|
||||
subtitleParams.topMargin = dpToPx(activity, 4);
|
||||
subtitleParams.bottomMargin = dpToPx(activity, 16);
|
||||
mainLayout.addView(subtitleView, subtitleParams);
|
||||
|
||||
// Scrollable content.
|
||||
ScrollView scrollView = new ScrollView(activity);
|
||||
scrollView.setVerticalScrollBarEnabled(false);
|
||||
LinearLayout.LayoutParams scrollParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
0,
|
||||
1.0f
|
||||
);
|
||||
scrollParams.topMargin = dpToPx(activity, 8);
|
||||
scrollParams.bottomMargin = dpToPx(activity, 16);
|
||||
mainLayout.addView(scrollView, scrollParams);
|
||||
|
||||
LinearLayout tokensContainer = new LinearLayout(activity);
|
||||
tokensContainer.setOrientation(LinearLayout.VERTICAL);
|
||||
scrollView.addView(tokensContainer);
|
||||
|
||||
// Add each token as a card.
|
||||
boolean singleToken = tokens.size() == 1;
|
||||
int index = 1;
|
||||
for (String token : tokens) {
|
||||
LinearLayout tokenCard = createTokenCard(activity, token, index++, singleToken);
|
||||
LinearLayout.LayoutParams cardParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
);
|
||||
cardParams.bottomMargin = dpToPx(activity, 12);
|
||||
tokensContainer.addView(tokenCard, cardParams);
|
||||
}
|
||||
|
||||
// Info text.
|
||||
TextView infoView = new TextView(activity);
|
||||
infoView.setText(tokens.size() == 1 ? "Tap the token to copy it" : "Tap any token to copy it");
|
||||
infoView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12);
|
||||
infoView.setTextColor(COLOR_TEXT_SECONDARY);
|
||||
infoView.setGravity(Gravity.CENTER);
|
||||
infoView.setAlpha(0.7f);
|
||||
LinearLayout.LayoutParams infoParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
);
|
||||
infoParams.topMargin = dpToPx(activity, 8);
|
||||
mainLayout.addView(infoView, infoParams);
|
||||
|
||||
// Button row.
|
||||
LinearLayout buttonRow = new LinearLayout(activity);
|
||||
buttonRow.setOrientation(LinearLayout.HORIZONTAL);
|
||||
buttonRow.setGravity(Gravity.END);
|
||||
LinearLayout.LayoutParams buttonRowParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
);
|
||||
buttonRowParams.topMargin = dpToPx(activity, 16);
|
||||
mainLayout.addView(buttonRow, buttonRowParams);
|
||||
|
||||
// "Don't show again" button.
|
||||
Button dontShowButton = new Button(activity);
|
||||
dontShowButton.setText("Don't show again");
|
||||
dontShowButton.setTextColor(Color.WHITE);
|
||||
dontShowButton.setBackgroundColor(Color.TRANSPARENT);
|
||||
dontShowButton.setAllCaps(false);
|
||||
dontShowButton.setTypeface(Typeface.DEFAULT);
|
||||
dontShowButton.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
|
||||
dontShowButton.setPadding(dpToPx(activity, 16), dpToPx(activity, 8),
|
||||
dpToPx(activity, 16), dpToPx(activity, 8));
|
||||
LinearLayout.LayoutParams dontShowParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
);
|
||||
dontShowParams.rightMargin = dpToPx(activity, 8);
|
||||
buttonRow.addView(dontShowButton, dontShowParams);
|
||||
|
||||
// "OK" button.
|
||||
Button okButton = new Button(activity);
|
||||
okButton.setText("OK");
|
||||
okButton.setTextColor(Color.BLACK);
|
||||
okButton.setBackgroundColor(COLOR_BUTTON_POSITIVE);
|
||||
okButton.setAllCaps(false);
|
||||
okButton.setTypeface(Typeface.DEFAULT_BOLD);
|
||||
okButton.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
|
||||
okButton.setPadding(dpToPx(activity, 24), dpToPx(activity, 12),
|
||||
dpToPx(activity, 24), dpToPx(activity, 12));
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
okButton.setElevation(dpToPx(activity, 4));
|
||||
}
|
||||
buttonRow.addView(okButton, new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
));
|
||||
|
||||
// Build dialog.
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setView(mainLayout);
|
||||
|
||||
final AlertDialog dialog = builder.create();
|
||||
|
||||
// Style the dialog with dark background.
|
||||
if (dialog.getWindow() != null) {
|
||||
dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
|
||||
}
|
||||
|
||||
dialog.show();
|
||||
|
||||
// Set button click listeners after dialog is created.
|
||||
dontShowButton.setOnClickListener(v -> {
|
||||
SharedPreferences prefs = activity.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||||
prefs.edit().putBoolean(KEY_DONT_SHOW_DIALOG, true).apply();
|
||||
Toast.makeText(activity, "Dialog disabled. Clear app data to re-enable.",
|
||||
Toast.LENGTH_SHORT).show();
|
||||
dialog.dismiss();
|
||||
});
|
||||
|
||||
okButton.setOnClickListener(v -> {
|
||||
dialog.dismiss();
|
||||
});
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Failed to show K1 dialog", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a card view for a single token.
|
||||
*/
|
||||
private static LinearLayout createTokenCard(Activity activity, String token, int index, boolean singleToken) {
|
||||
LinearLayout card = new LinearLayout(activity);
|
||||
card.setOrientation(LinearLayout.VERTICAL);
|
||||
card.setBackgroundColor(COLOR_TOKEN_BG);
|
||||
card.setPadding(dpToPx(activity, 16), dpToPx(activity, 12),
|
||||
dpToPx(activity, 16), dpToPx(activity, 12));
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
card.setElevation(dpToPx(activity, 2));
|
||||
}
|
||||
card.setClickable(true);
|
||||
card.setFocusable(true);
|
||||
|
||||
// Token label (only show if multiple tokens).
|
||||
if (!singleToken) {
|
||||
TextView labelView = new TextView(activity);
|
||||
labelView.setText("Token #" + index);
|
||||
labelView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12);
|
||||
labelView.setTextColor(COLOR_ACCENT);
|
||||
labelView.setTypeface(Typeface.DEFAULT_BOLD);
|
||||
card.addView(labelView);
|
||||
}
|
||||
|
||||
// Token value.
|
||||
TextView tokenView = new TextView(activity);
|
||||
tokenView.setText(token.toUpperCase());
|
||||
tokenView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
|
||||
tokenView.setTextColor(COLOR_TEXT_PRIMARY);
|
||||
tokenView.setTypeface(Typeface.MONOSPACE);
|
||||
tokenView.setLetterSpacing(0.05f);
|
||||
LinearLayout.LayoutParams tokenParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
);
|
||||
if (!singleToken) {
|
||||
tokenParams.topMargin = dpToPx(activity, 8);
|
||||
}
|
||||
card.addView(tokenView, tokenParams);
|
||||
|
||||
// Click to copy.
|
||||
card.setOnClickListener(v -> {
|
||||
ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
if (clipboard != null) {
|
||||
clipboard.setText(token.toUpperCase());
|
||||
Toast.makeText(activity, "Token copied!", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert dp to pixels.
|
||||
*/
|
||||
private static int dpToPx(Context context, float dp) {
|
||||
return (int) TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
dp,
|
||||
context.getResources().getDisplayMetrics()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get K1 tokens from log files.
|
||||
* Prioritizes pairing K1 tokens over reconnect tokens.
|
||||
*/
|
||||
private static Set<String> getK1TokensFromLogFiles() {
|
||||
Set<String> pairingTokens = new LinkedHashSet<>();
|
||||
Set<String> reconnectTokens = new LinkedHashSet<>();
|
||||
try {
|
||||
File logDir = new File("/data/data/" + PACKAGE_NAME + "/files/log");
|
||||
if (!logDir.exists() || !logDir.isDirectory()) {
|
||||
return pairingTokens;
|
||||
}
|
||||
|
||||
File[] logFiles = logDir.listFiles((dir, name) ->
|
||||
name.endsWith(".log") || name.endsWith(".log.") || name.matches(".*\\.log\\.\\d+"));
|
||||
|
||||
if (logFiles == null || logFiles.length == 0) {
|
||||
return pairingTokens;
|
||||
}
|
||||
|
||||
for (File logFile : logFiles) {
|
||||
try (BufferedReader reader = new BufferedReader(new FileReader(logFile))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
// Determine if this is a pairing or reconnect context.
|
||||
boolean isPairingContext = line.toLowerCase().contains("watchbind");
|
||||
boolean isReconnectContext = line.toLowerCase().contains("watchreconnect");
|
||||
|
||||
String k1Token = null;
|
||||
|
||||
// First check for combined r3+k1 format (priority).
|
||||
Matcher combinedMatcher = K1_COMBINED_PATTERN.matcher(line);
|
||||
if (combinedMatcher.find()) {
|
||||
String combined = combinedMatcher.group(1);
|
||||
if (combined.length() == 64) {
|
||||
// Second half is the actual K1
|
||||
k1Token = combined.substring(32).toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
// Then check for standalone K1 format (only if not found in combined).
|
||||
if (k1Token == null) {
|
||||
Matcher standaloneMatcher = K1_STANDALONE_PATTERN.matcher(line);
|
||||
if (standaloneMatcher.find()) {
|
||||
String token = standaloneMatcher.group(1);
|
||||
if (token != null && token.length() == 32) {
|
||||
k1Token = token.toLowerCase();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add to appropriate set.
|
||||
if (k1Token != null) {
|
||||
if (isPairingContext && !isReconnectContext) {
|
||||
pairingTokens.add(k1Token);
|
||||
} else {
|
||||
reconnectTokens.add(k1Token);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Skip unreadable files.
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
// Fail silently.
|
||||
}
|
||||
|
||||
// Return pairing tokens first, add reconnect tokens if no pairing tokens found.
|
||||
if (!pairingTokens.isEmpty()) {
|
||||
Log.i(TAG, "Found " + pairingTokens.size() + " pairing K1 token(s)");
|
||||
return pairingTokens;
|
||||
}
|
||||
|
||||
if (!reconnectTokens.isEmpty()) {
|
||||
Log.i(TAG, "Found " + reconnectTokens.size() + " reconnect K1 token(s) (may not work for initial pairing)");
|
||||
}
|
||||
return reconnectTokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to get K1 tokens from the database.
|
||||
*/
|
||||
private static String getK1TokensFromDatabase() {
|
||||
try {
|
||||
File dbDir = new File("/data/data/" + PACKAGE_NAME + "/databases");
|
||||
if (!dbDir.exists() || !dbDir.isDirectory()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
File[] dbFiles = dbDir.listFiles((dir, name) ->
|
||||
name.endsWith(".db") && !name.startsWith("google_app_measurement") && !name.contains("firebase"));
|
||||
|
||||
if (dbFiles == null || dbFiles.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (File dbFile : dbFiles) {
|
||||
String token = getK1TokensFromDatabase(dbFile);
|
||||
if (token != null) {
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (Exception ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract K1 tokens from a database file.
|
||||
*/
|
||||
private static String getK1TokensFromDatabase(File dbFile) {
|
||||
SQLiteDatabase db = null;
|
||||
try {
|
||||
db = SQLiteDatabase.openDatabase(dbFile.getPath(), null, SQLiteDatabase.OPEN_READONLY);
|
||||
|
||||
// Get all tables.
|
||||
Cursor cursor = db.rawQuery(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'",
|
||||
null
|
||||
);
|
||||
|
||||
List<String> tables = new ArrayList<>();
|
||||
while (cursor.moveToNext()) {
|
||||
tables.add(cursor.getString(0));
|
||||
}
|
||||
cursor.close();
|
||||
|
||||
// Scan all columns for 32-char hex strings.
|
||||
for (String table : tables) {
|
||||
Cursor schemaCursor = null;
|
||||
try {
|
||||
schemaCursor = db.rawQuery("PRAGMA table_info(" + table + ")", null);
|
||||
List<String> columns = new ArrayList<>();
|
||||
while (schemaCursor.moveToNext()) {
|
||||
columns.add(schemaCursor.getString(1));
|
||||
}
|
||||
schemaCursor.close();
|
||||
|
||||
for (String column : columns) {
|
||||
Cursor dataCursor = null;
|
||||
try {
|
||||
dataCursor = db.query(table, new String[]{column}, null, null, null, null, null);
|
||||
while (dataCursor.moveToNext()) {
|
||||
String value = dataCursor.getString(0);
|
||||
if (value != null && value.length() == 32 && value.matches("[0-9a-fA-F]{32}")) {
|
||||
// Skip obviously fake tokens (MD5 of empty string).
|
||||
if (!value.equalsIgnoreCase(EMPTY_MD5)) {
|
||||
dataCursor.close();
|
||||
db.close();
|
||||
return value.toLowerCase();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Skip non-string columns.
|
||||
} finally {
|
||||
if (dataCursor != null) {
|
||||
dataCursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Continue to next table.
|
||||
} finally {
|
||||
if (schemaCursor != null && !schemaCursor.isClosed()) {
|
||||
schemaCursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (Exception ex) {
|
||||
return null;
|
||||
} finally {
|
||||
if (db != null && db.isOpen()) {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the logged flag (useful for testing or re-pairing).
|
||||
*/
|
||||
public static void resetK1Logged() {
|
||||
k1Logged = false;
|
||||
lifecycleCallbacksRegistered = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the "don't show again" preference.
|
||||
*/
|
||||
public static void resetDontShowPreference() {
|
||||
if (appContext != null) {
|
||||
SharedPreferences prefs = appContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||||
prefs.edit().putBoolean(KEY_DONT_SHOW_DIALOG, false).apply();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "app.revanced.extension"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 26
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
<manifest/>
|
||||
|
|
@ -2,9 +2,3 @@ dependencies {
|
|||
compileOnly(project(":extensions:shared:library"))
|
||||
compileOnly(project(":extensions:nunl:stub"))
|
||||
}
|
||||
|
||||
android {
|
||||
defaultConfig {
|
||||
minSdk = 26
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,3 @@ dependencies {
|
|||
compileOnly(project(":extensions:shared:library"))
|
||||
compileOnly(project(":extensions:primevideo:stub"))
|
||||
}
|
||||
|
||||
android {
|
||||
defaultConfig {
|
||||
minSdk = 21
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import java.util.Arrays;
|
|||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.ui.Dim;
|
||||
|
||||
import com.amazon.video.sdk.player.Player;
|
||||
|
||||
|
|
@ -65,8 +64,9 @@ public class PlaybackSpeedPatch {
|
|||
SpeedIconDrawable speedIcon = new SpeedIconDrawable();
|
||||
speedButton.setImageDrawable(speedIcon);
|
||||
|
||||
speedButton.setMinimumWidth(Dim.dp48);
|
||||
speedButton.setMinimumHeight(Dim.dp48);
|
||||
int buttonSize = Utils.dipToPixels(48);
|
||||
speedButton.setMinimumWidth(buttonSize);
|
||||
speedButton.setMinimumHeight(buttonSize);
|
||||
|
||||
return speedButton;
|
||||
}
|
||||
|
|
@ -197,11 +197,11 @@ class SpeedIconDrawable extends Drawable {
|
|||
|
||||
@Override
|
||||
public int getIntrinsicWidth() {
|
||||
return Dim.dp32;
|
||||
return Utils.dipToPixels(32);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicHeight() {
|
||||
return Dim.dp32;
|
||||
return Utils.dipToPixels(32);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,3 @@
|
|||
dependencies {
|
||||
compileOnly(project(":extensions:reddit:stub"))
|
||||
}
|
||||
|
||||
android {
|
||||
defaultConfig {
|
||||
minSdk = 28
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
dependencies {
|
||||
compileOnly(project(":extensions:shared:library"))
|
||||
compileOnly(project(":extensions:samsung:radio:stub"))
|
||||
}
|
||||
|
||||
android {
|
||||
defaultConfig {
|
||||
minSdk = 26
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
<manifest/>
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
package app.revanced.extension.samsung.radio.misc.fix.crash;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class FixCrashPatch {
|
||||
/**
|
||||
* Injection point.
|
||||
* <p>
|
||||
* Add the required permissions to the request list to avoid crashes on API 34+.
|
||||
**/
|
||||
public static final String[] fixPermissionRequestList(String[] perms) {
|
||||
List<String> permsList = new ArrayList<>(Arrays.asList(perms));
|
||||
if (permsList.contains("android.permission.POST_NOTIFICATIONS")) {
|
||||
permsList.addAll(Arrays.asList("android.permission.RECORD_AUDIO", "android.permission.READ_PHONE_STATE", "android.permission.FOREGROUND_SERVICE_MICROPHONE"));
|
||||
}
|
||||
if (permsList.contains("android.permission.RECORD_AUDIO")) {
|
||||
permsList.add("android.permission.FOREGROUND_SERVICE_MICROPHONE");
|
||||
}
|
||||
return permsList.toArray(new String[0]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
package app.revanced.extension.samsung.radio.restrictions.device;
|
||||
|
||||
import android.os.SemSystemProperties;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class BypassDeviceChecksPatch {
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* <p>
|
||||
* Check if the device has the required hardware
|
||||
**/
|
||||
public static final boolean checkIfDeviceIsIncompatible(String[] deviceList) {
|
||||
String currentDevice = SemSystemProperties.getSalesCode();
|
||||
return Arrays.asList(deviceList).contains(currentDevice);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "app.revanced.extension"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
<manifest/>
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
package android.os;
|
||||
|
||||
public class SemSystemProperties {
|
||||
public static String getSalesCode() {
|
||||
throw new UnsupportedOperationException("Stub");
|
||||
}
|
||||
}
|
||||
|
|
@ -2,9 +2,3 @@ dependencies {
|
|||
implementation(project(":extensions:shared:library"))
|
||||
compileOnly(libs.okhttp)
|
||||
}
|
||||
|
||||
android {
|
||||
defaultConfig {
|
||||
minSdk = 23
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.android.library)
|
||||
}
|
||||
|
||||
android {
|
||||
|
|
@ -19,6 +19,4 @@ android {
|
|||
dependencies {
|
||||
compileOnly(libs.annotation)
|
||||
compileOnly(libs.okhttp)
|
||||
compileOnly(libs.protobuf.javalite)
|
||||
implementation(project(":extensions:shared:protobuf", configuration = "shadowRuntimeElements"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
package app.revanced.extension.shared;
|
||||
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
import static app.revanced.extension.shared.requests.Route.Method.GET;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
|
|
@ -17,396 +20,237 @@ import android.widget.LinearLayout;
|
|||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.shared.requests.Requester;
|
||||
import app.revanced.extension.shared.requests.Route;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
import app.revanced.extension.shared.ui.CustomDialog;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.Locale;
|
||||
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
import static app.revanced.extension.shared.requests.Route.Method.GET;
|
||||
import app.revanced.extension.shared.requests.Requester;
|
||||
import app.revanced.extension.shared.requests.Route;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class GmsCoreSupport {
|
||||
private static GmsCore gmsCore = GmsCore.UNKNOWN;
|
||||
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";
|
||||
|
||||
static {
|
||||
for (GmsCore core : GmsCore.values()) {
|
||||
if (core.getGroupId().equals(getGmsCoreVendorGroupId())) {
|
||||
GmsCoreSupport.gmsCore = core;
|
||||
break;
|
||||
}
|
||||
private static final String GMS_CORE_PACKAGE_NAME
|
||||
= getGmsCoreVendorGroupId() + ".android.gms";
|
||||
private static final Uri GMS_CORE_PROVIDER
|
||||
= Uri.parse("content://" + getGmsCoreVendorGroupId() + ".android.gsf.gservices/prefix");
|
||||
private static final String DONT_KILL_MY_APP_URL
|
||||
= "https://dontkillmyapp.com/";
|
||||
private static final Route DONT_KILL_MY_APP_MANUFACTURER_API
|
||||
= new Route(GET, "/api/v2/{manufacturer}.json");
|
||||
private static final String DONT_KILL_MY_APP_NAME_PARAMETER
|
||||
= "?app=MicroG";
|
||||
private static final String BUILD_MANUFACTURER
|
||||
= Build.MANUFACTURER.toLowerCase(Locale.ROOT).replace(" ", "-");
|
||||
|
||||
/**
|
||||
* If a manufacturer specific page exists on DontKillMyApp.
|
||||
*/
|
||||
@Nullable
|
||||
private static volatile Boolean DONT_KILL_MY_APP_MANUFACTURER_SUPPORTED;
|
||||
|
||||
private static void open(String queryOrLink) {
|
||||
Logger.printInfo(() -> "Opening link: " + queryOrLink);
|
||||
|
||||
Intent intent;
|
||||
try {
|
||||
// Check if queryOrLink is a valid URL.
|
||||
new URL(queryOrLink);
|
||||
|
||||
intent = new Intent(Intent.ACTION_VIEW, Uri.parse(queryOrLink));
|
||||
} catch (MalformedURLException e) {
|
||||
intent = new Intent(Intent.ACTION_WEB_SEARCH);
|
||||
intent.putExtra(SearchManager.QUERY, queryOrLink);
|
||||
}
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
Utils.getContext().startActivity(intent);
|
||||
|
||||
// Gracefully exit, otherwise the broken app will continue to run.
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
private static void showBatteryOptimizationDialog(Activity context,
|
||||
String dialogMessageRef,
|
||||
String positiveButtonTextRef,
|
||||
DialogInterface.OnClickListener onPositiveClickListener) {
|
||||
// Use a delay to allow the activity to finish initializing.
|
||||
// Otherwise, if device is in dark mode the dialog is shown with wrong color scheme.
|
||||
Utils.runOnMainThreadDelayed(() -> {
|
||||
// Create the custom dialog.
|
||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
||||
context,
|
||||
str("gms_core_dialog_title"), // Title.
|
||||
str(dialogMessageRef), // Message.
|
||||
null, // No EditText.
|
||||
str(positiveButtonTextRef), // OK button text.
|
||||
() -> onPositiveClickListener.onClick(null, 0), // Convert DialogInterface.OnClickListener to Runnable.
|
||||
null, // No Cancel button action.
|
||||
null, // No Neutral button text.
|
||||
null, // No Neutral button action.
|
||||
true // Dismiss dialog when onNeutralClick.
|
||||
);
|
||||
|
||||
Dialog dialog = dialogPair.first;
|
||||
|
||||
// Do not set cancelable to false, to allow using back button to skip the action,
|
||||
// just in case the battery change can never be satisfied.
|
||||
dialog.setCancelable(true);
|
||||
|
||||
// Show the dialog
|
||||
Utils.showDialog(context, dialog);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void checkGmsCore(Activity context) {
|
||||
gmsCore.check(context);
|
||||
}
|
||||
try {
|
||||
// Verify the user has not included GmsCore for a root installation.
|
||||
// GmsCore Support changes the package name, but with a mounted installation
|
||||
// all manifest changes are ignored and the original package name is used.
|
||||
String packageName = context.getPackageName();
|
||||
if (packageName.equals(PACKAGE_NAME_YOUTUBE) || packageName.equals(PACKAGE_NAME_YOUTUBE_MUSIC)) {
|
||||
Logger.printInfo(() -> "App is mounted with root, but GmsCore patch was included");
|
||||
// Cannot use localize text here, since the app will load
|
||||
// resources from the unpatched app and all patch strings are missing.
|
||||
Utils.showToastLong("The 'GmsCore support' patch breaks mount installations");
|
||||
|
||||
private static String getOriginalPackageName() {
|
||||
return null; // Modified during patching.
|
||||
}
|
||||
|
||||
private static String getGmsCoreVendorGroupId() {
|
||||
return "app.revanced"; // Modified during patching.
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return If the current package name is the same as the original unpatched app.
|
||||
* If `GmsCore support` was not included during patching, this returns true;
|
||||
*/
|
||||
public static boolean isPackageNameOriginal() {
|
||||
String originalPackageName = getOriginalPackageName();
|
||||
return originalPackageName == null
|
||||
|| originalPackageName.equals(Utils.getContext().getPackageName());
|
||||
}
|
||||
|
||||
private enum GmsCore {
|
||||
REVANCED("app.revanced", "https://github.com/revanced/gmscore/releases/latest", () -> {
|
||||
try {
|
||||
HttpURLConnection connection = Requester.getConnectionFromRoute(
|
||||
"https://api.github.com",
|
||||
new Route(GET, "/repos/revanced/gmscore/releases/latest")
|
||||
);
|
||||
connection.setConnectTimeout(5000);
|
||||
connection.setReadTimeout(5000);
|
||||
|
||||
int responseCode = connection.getResponseCode();
|
||||
if (responseCode != 200) {
|
||||
Logger.printDebug(() -> "GitHub API returned status code: " + responseCode);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Parse the response
|
||||
JSONObject releaseData = Requester.parseJSONObject(connection);
|
||||
String tagName = releaseData.optString("tag_name", "");
|
||||
connection.disconnect();
|
||||
|
||||
if (tagName.isEmpty()) {
|
||||
Logger.printDebug(() -> "No tag_name found in GitHub release data");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (tagName.startsWith("v")) tagName = tagName.substring(1);
|
||||
|
||||
return tagName;
|
||||
} catch (Exception ex) {
|
||||
Logger.printInfo(() -> "Failed to fetch latest GmsCore version from GitHub", ex);
|
||||
return null;
|
||||
// Do not exit. If the app exits before launch completes (and without
|
||||
// opening another activity), then on some devices such as Pixel phone Android 10
|
||||
// no toast will be shown and the app will continually relaunch
|
||||
// with the appearance of a hung app.
|
||||
}
|
||||
}),
|
||||
UNKNOWN(getGmsCoreVendorGroupId(), getGmsCoreVendorGroupId() + "android.gms", () -> null);
|
||||
|
||||
private static final String DONT_KILL_MY_APP_URL
|
||||
= "https://dontkillmyapp.com/";
|
||||
private static final Route DONT_KILL_MY_APP_MANUFACTURER_API
|
||||
= new Route(GET, "/api/v2/{manufacturer}.json");
|
||||
private static final String DONT_KILL_MY_APP_NAME_PARAMETER
|
||||
= "?app=MicroG";
|
||||
private static final String BUILD_MANUFACTURER
|
||||
= Build.MANUFACTURER.toLowerCase(Locale.ROOT).replace(" ", "-");
|
||||
|
||||
/**
|
||||
* If a manufacturer specific page exists on DontKillMyApp.
|
||||
*/
|
||||
@Nullable
|
||||
private volatile Boolean dontKillMyAppManufacturerSupported;
|
||||
|
||||
private final String groupId;
|
||||
private final String packageName;
|
||||
private final String downloadQuery;
|
||||
private final GetLatestVersion getLatestVersion;
|
||||
private final Uri gmsCoreProvider;
|
||||
|
||||
GmsCore(String groupId, String downloadQuery, GetLatestVersion getLatestVersion) {
|
||||
this.groupId = groupId;
|
||||
this.packageName = groupId + ".android.gms";
|
||||
this.gmsCoreProvider = Uri.parse("content://" + groupId + ".android.gsf.gservices/prefix");
|
||||
|
||||
this.downloadQuery = downloadQuery;
|
||||
this.getLatestVersion = getLatestVersion;
|
||||
}
|
||||
|
||||
String getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
void check(Activity context) {
|
||||
checkInstallation(context);
|
||||
checkUpdates(context);
|
||||
}
|
||||
|
||||
private void checkInstallation(Activity context) {
|
||||
// Verify GmsCore is installed.
|
||||
try {
|
||||
// Verify the user has not included GmsCore for a root installation.
|
||||
// GmsCore Support changes the package name, but with a mounted installation
|
||||
// all manifest changes are ignored and the original package name is used.
|
||||
if (isPackageNameOriginal()) {
|
||||
Logger.printInfo(() -> "App is mounted with root, but GmsCore patch was included");
|
||||
// Cannot use localize text here, since the app will load resources
|
||||
// from the unpatched app and all patch strings are missing.
|
||||
Utils.showToastLong("The 'GmsCore support' patch breaks mount installations");
|
||||
|
||||
// Do not exit. If the app exits before launch completes (and without
|
||||
// opening another activity), then on some devices such as Pixel phone Android 10
|
||||
// no toast will be shown and the app will continually relaunch
|
||||
// with the appearance of a hung app.
|
||||
}
|
||||
|
||||
// Verify GmsCore is installed.
|
||||
try {
|
||||
PackageManager manager = context.getPackageManager();
|
||||
manager.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES);
|
||||
} catch (PackageManager.NameNotFoundException exception) {
|
||||
Logger.printInfo(() -> "GmsCore was not found");
|
||||
// Cannot show a dialog and must show a toast,
|
||||
// because on some installations the app crashes before a dialog can be displayed.
|
||||
Utils.showToastLong(str("revanced_gms_core_toast_not_installed_message"));
|
||||
|
||||
open(downloadQuery);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if GmsCore is whitelisted from battery optimizations.
|
||||
if (isAndroidAutomotive(context)) {
|
||||
// Ignore Android Automotive devices (Google built-in),
|
||||
// as there is no way to disable battery optimizations.
|
||||
Logger.printDebug(() -> "Device is Android Automotive");
|
||||
} else if (batteryOptimizationsEnabled(context)) {
|
||||
Logger.printInfo(() -> "GmsCore is not whitelisted from battery optimizations");
|
||||
|
||||
showBatteryOptimizationDialog(context,
|
||||
"revanced_gms_core_dialog_not_whitelisted_using_battery_optimizations_message",
|
||||
"revanced_gms_core_dialog_continue_text",
|
||||
(dialog, id) -> openGmsCoreDisableBatteryOptimizationsIntent(context));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if GmsCore is currently running in the background.
|
||||
var client = context.getContentResolver().acquireContentProviderClient(gmsCoreProvider);
|
||||
//noinspection TryFinallyCanBeTryWithResources
|
||||
try {
|
||||
if (client == null) {
|
||||
Logger.printInfo(() -> "GmsCore is not running in the background");
|
||||
checkIfDontKillMyAppSupportsManufacturer();
|
||||
|
||||
showBatteryOptimizationDialog(context,
|
||||
"revanced_gms_core_dialog_not_whitelisted_not_allowed_in_background_message",
|
||||
"gmsrevanced_gms_core_log_open_website_text",
|
||||
(dialog, id) -> openDontKillMyApp());
|
||||
}
|
||||
} finally {
|
||||
if (client != null) client.close();
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "checkGmsCore failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkUpdates(Activity context) {
|
||||
if (!BaseSettings.GMS_CORE_CHECK_UPDATES.get()) {
|
||||
Logger.printDebug(() -> "GmsCore update check is disabled in settings");
|
||||
PackageManager manager = context.getPackageManager();
|
||||
manager.getPackageInfo(GMS_CORE_PACKAGE_NAME, PackageManager.GET_ACTIVITIES);
|
||||
} catch (PackageManager.NameNotFoundException exception) {
|
||||
Logger.printInfo(() -> "GmsCore was not found");
|
||||
// Cannot show a dialog and must show a toast,
|
||||
// because on some installations the app crashes before a dialog can be displayed.
|
||||
Utils.showToastLong(str("gms_core_toast_not_installed_message"));
|
||||
open(getGmsCoreDownload());
|
||||
return;
|
||||
}
|
||||
|
||||
Utils.runOnBackgroundThread(() -> {
|
||||
try {
|
||||
PackageManager manager = context.getPackageManager();
|
||||
var installedVersion = manager.getPackageInfo(packageName, 0).versionName;
|
||||
// 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");
|
||||
|
||||
// GmsCore adds suffixes for flavor builds. Remove the suffix for version comparison.
|
||||
int suffixIndex = installedVersion.indexOf('-');
|
||||
if (suffixIndex != -1)
|
||||
installedVersion = installedVersion.substring(0, suffixIndex);
|
||||
String finalInstalledVersion = installedVersion;
|
||||
showBatteryOptimizationDialog(context,
|
||||
"gms_core_dialog_not_whitelisted_using_battery_optimizations_message",
|
||||
"gms_core_dialog_continue_text",
|
||||
(dialog, id) -> openGmsCoreDisableBatteryOptimizationsIntent(context));
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.printDebug(() -> "Installed GmsCore version: " + finalInstalledVersion);
|
||||
|
||||
var latestVersion = getLatestVersion.get();
|
||||
|
||||
if (latestVersion == null || latestVersion.isEmpty()) {
|
||||
Logger.printDebug(() -> "Could not get latest GmsCore version");
|
||||
Utils.showToastLong(str("revanced_gms_core_toast_update_check_failed_message"));
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.printDebug(() -> "Latest GmsCore version on GitHub: " + latestVersion);
|
||||
|
||||
// Compare versions
|
||||
if (!installedVersion.equals(latestVersion)) {
|
||||
Logger.printInfo(() -> "GmsCore update available. Installed: " + finalInstalledVersion
|
||||
+ ", Latest: " + latestVersion);
|
||||
|
||||
showUpdateDialog(context, installedVersion, latestVersion);
|
||||
} else {
|
||||
Logger.printDebug(() -> "GmsCore is up to date");
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printInfo(() -> "Could not check GmsCore updates", ex);
|
||||
Utils.showToastLong(str("revanced_gms_core_toast_update_check_failed_message"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void open(String queryOrLink) {
|
||||
Logger.printInfo(() -> "Opening link: " + queryOrLink);
|
||||
|
||||
Intent intent;
|
||||
// Check if GmsCore is currently running in the background.
|
||||
var client = context.getContentResolver().acquireContentProviderClient(GMS_CORE_PROVIDER);
|
||||
//noinspection TryFinallyCanBeTryWithResources
|
||||
try {
|
||||
// Check if queryOrLink is a valid URL.
|
||||
new URL(queryOrLink);
|
||||
if (client == null) {
|
||||
Logger.printInfo(() -> "GmsCore is not running in the background");
|
||||
checkIfDontKillMyAppSupportsManufacturer();
|
||||
|
||||
intent = new Intent(Intent.ACTION_VIEW, Uri.parse(queryOrLink));
|
||||
} catch (MalformedURLException e) {
|
||||
intent = new Intent(Intent.ACTION_WEB_SEARCH);
|
||||
intent.putExtra(SearchManager.QUERY, queryOrLink);
|
||||
}
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
Utils.getContext().startActivity(intent);
|
||||
|
||||
// Gracefully exit, otherwise the broken app will continue to run.
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
private void showUpdateDialog(Activity context, String installedVersion, String latestVersion) {
|
||||
// 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(() -> {
|
||||
try {
|
||||
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
|
||||
context,
|
||||
str("revanced_gms_core_dialog_title"),
|
||||
String.format(str("revanced_gms_core_update_available_message"), latestVersion, installedVersion),
|
||||
null,
|
||||
str("revanced_gms_core_dialog_open_website_text"),
|
||||
() -> open(downloadQuery),
|
||||
() -> {
|
||||
},
|
||||
str("revanced_gms_core_dialog_cancel_text"),
|
||||
null,
|
||||
true
|
||||
);
|
||||
|
||||
Dialog dialog = dialogPair.first;
|
||||
dialog.setCancelable(true);
|
||||
Utils.showDialog(context, dialog);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Failed to show GmsCore update dialog", ex);
|
||||
showBatteryOptimizationDialog(context,
|
||||
"gms_core_dialog_not_whitelisted_not_allowed_in_background_message",
|
||||
"gms_core_dialog_open_website_text",
|
||||
(dialog, id) -> openDontKillMyApp());
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
private static void showBatteryOptimizationDialog(Activity context,
|
||||
String dialogMessageRef,
|
||||
String positiveButtonTextRef,
|
||||
DialogInterface.OnClickListener onPositiveClickListener) {
|
||||
// Use a delay to allow the activity to finish initializing.
|
||||
// Otherwise, if device is in dark mode the dialog is shown with wrong color scheme.
|
||||
Utils.runOnMainThreadDelayed(() -> {
|
||||
// Create the custom dialog.
|
||||
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
|
||||
context,
|
||||
str("revanced_gms_core_dialog_title"), // Title.
|
||||
str(dialogMessageRef), // Message.
|
||||
null, // No EditText.
|
||||
str(positiveButtonTextRef), // OK button text.
|
||||
() -> onPositiveClickListener.onClick(null, 0), // Convert DialogInterface.OnClickListener to Runnable.
|
||||
null, // No Cancel button action.
|
||||
null, // No Neutral button text.
|
||||
null, // No Neutral button action.
|
||||
true // Dismiss dialog when onNeutralClick.
|
||||
);
|
||||
|
||||
Dialog dialog = dialogPair.first;
|
||||
|
||||
// Do not set cancelable to false to allow using back button to skip the action,
|
||||
// just in case the battery change can never be satisfied.
|
||||
dialog.setCancelable(true);
|
||||
|
||||
// Show the dialog
|
||||
Utils.showDialog(context, dialog);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
@SuppressLint("BatteryLife") // Permission is part of GmsCore
|
||||
private void openGmsCoreDisableBatteryOptimizationsIntent(Activity activity) {
|
||||
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
|
||||
intent.setData(Uri.fromParts("package", packageName, null));
|
||||
activity.startActivityForResult(intent, 0);
|
||||
}
|
||||
|
||||
private void checkIfDontKillMyAppSupportsManufacturer() {
|
||||
Utils.runOnBackgroundThread(() -> {
|
||||
try {
|
||||
final long start = System.currentTimeMillis();
|
||||
HttpURLConnection connection = Requester.getConnectionFromRoute(
|
||||
DONT_KILL_MY_APP_URL, DONT_KILL_MY_APP_MANUFACTURER_API, BUILD_MANUFACTURER);
|
||||
connection.setConnectTimeout(5000);
|
||||
connection.setReadTimeout(5000);
|
||||
|
||||
final boolean supported = connection.getResponseCode() == 200;
|
||||
Logger.printInfo(() -> "Manufacturer is " + (supported ? "" : "NOT ")
|
||||
+ "listed on DontKillMyApp: " + BUILD_MANUFACTURER
|
||||
+ " fetch took: " + (System.currentTimeMillis() - start) + "ms");
|
||||
dontKillMyAppManufacturerSupported = supported;
|
||||
} catch (Exception ex) {
|
||||
Logger.printInfo(() -> "Could not check if manufacturer is listed on DontKillMyApp: "
|
||||
+ BUILD_MANUFACTURER, ex);
|
||||
dontKillMyAppManufacturerSupported = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void openDontKillMyApp() {
|
||||
final Boolean manufacturerSupported = dontKillMyAppManufacturerSupported;
|
||||
|
||||
String manufacturerPageToOpen;
|
||||
if (manufacturerSupported == null) {
|
||||
// Fetch has not completed yet. Only happens on extremely slow internet connections
|
||||
// and the user spends less than 1 second reading what's on screen.
|
||||
// Instead of waiting for the fetch (which may timeout),
|
||||
// open the website without a vendor.
|
||||
manufacturerPageToOpen = "";
|
||||
} else if (manufacturerSupported) {
|
||||
manufacturerPageToOpen = BUILD_MANUFACTURER;
|
||||
} else {
|
||||
// No manufacturer specific page exists. Open the general page.
|
||||
manufacturerPageToOpen = "general";
|
||||
} finally {
|
||||
if (client != null) client.close();
|
||||
}
|
||||
|
||||
open(DONT_KILL_MY_APP_URL + manufacturerPageToOpen + DONT_KILL_MY_APP_NAME_PARAMETER);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return If GmsCore is not whitelisted from battery optimizations.
|
||||
*/
|
||||
private boolean batteryOptimizationsEnabled(Context context) {
|
||||
//noinspection ObsoleteSdkInt
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
// Android 5.0 does not have battery optimization settings.
|
||||
return false;
|
||||
}
|
||||
var powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
|
||||
return !powerManager.isIgnoringBatteryOptimizations(packageName);
|
||||
}
|
||||
|
||||
private boolean isAndroidAutomotive(Context context) {
|
||||
return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "checkGmsCore failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface GetLatestVersion {
|
||||
String get();
|
||||
@SuppressLint("BatteryLife") // Permission is part of GmsCore
|
||||
private static void openGmsCoreDisableBatteryOptimizationsIntent(Activity activity) {
|
||||
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
|
||||
intent.setData(Uri.fromParts("package", GMS_CORE_PACKAGE_NAME, null));
|
||||
activity.startActivityForResult(intent, 0);
|
||||
}
|
||||
|
||||
private static void checkIfDontKillMyAppSupportsManufacturer() {
|
||||
Utils.runOnBackgroundThread(() -> {
|
||||
try {
|
||||
final long start = System.currentTimeMillis();
|
||||
HttpURLConnection connection = Requester.getConnectionFromRoute(
|
||||
DONT_KILL_MY_APP_URL, DONT_KILL_MY_APP_MANUFACTURER_API, BUILD_MANUFACTURER);
|
||||
connection.setConnectTimeout(5000);
|
||||
connection.setReadTimeout(5000);
|
||||
|
||||
final boolean supported = connection.getResponseCode() == 200;
|
||||
Logger.printInfo(() -> "Manufacturer is " + (supported ? "" : "NOT ")
|
||||
+ "listed on DontKillMyApp: " + BUILD_MANUFACTURER
|
||||
+ " fetch took: " + (System.currentTimeMillis() - start) + "ms");
|
||||
DONT_KILL_MY_APP_MANUFACTURER_SUPPORTED = supported;
|
||||
} catch (Exception ex) {
|
||||
Logger.printInfo(() -> "Could not check if manufacturer is listed on DontKillMyApp: "
|
||||
+ BUILD_MANUFACTURER, ex);
|
||||
DONT_KILL_MY_APP_MANUFACTURER_SUPPORTED = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void openDontKillMyApp() {
|
||||
final Boolean manufacturerSupported = DONT_KILL_MY_APP_MANUFACTURER_SUPPORTED;
|
||||
|
||||
String manufacturerPageToOpen;
|
||||
if (manufacturerSupported == null) {
|
||||
// Fetch has not completed yet. Only happens on extremely slow internet connections
|
||||
// and the user spends less than 1 second reading what's on screen.
|
||||
// Instead of waiting for the fetch (which may timeout),
|
||||
// open the website without a vendor.
|
||||
manufacturerPageToOpen = "";
|
||||
} else if (manufacturerSupported) {
|
||||
manufacturerPageToOpen = BUILD_MANUFACTURER;
|
||||
} else {
|
||||
// No manufacturer specific page exists. Open the general page.
|
||||
manufacturerPageToOpen = "general";
|
||||
}
|
||||
|
||||
open(DONT_KILL_MY_APP_URL + manufacturerPageToOpen + DONT_KILL_MY_APP_NAME_PARAMETER);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return If GmsCore is not whitelisted from battery optimizations.
|
||||
*/
|
||||
private static boolean batteryOptimizationsEnabled(Context context) {
|
||||
//noinspection ObsoleteSdkInt
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
// Android 5.0 does not have battery optimization settings.
|
||||
return false;
|
||||
}
|
||||
var powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
|
||||
return !powerManager.isIgnoringBatteryOptimizations(GMS_CORE_PACKAGE_NAME);
|
||||
}
|
||||
|
||||
private static boolean isAndroidAutomotive(Context context) {
|
||||
return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
|
||||
}
|
||||
|
||||
private static String getGmsCoreDownload() {
|
||||
final var vendorGroupId = getGmsCoreVendorGroupId();
|
||||
//noinspection SwitchStatementWithTooFewBranches
|
||||
return switch (vendorGroupId) {
|
||||
case "app.revanced" -> "https://github.com/revanced/gmscore/releases/latest";
|
||||
default -> vendorGroupId + ".android.gms";
|
||||
};
|
||||
}
|
||||
|
||||
// Modified by a patch. Do not touch.
|
||||
private static String getGmsCoreVendorGroupId() {
|
||||
return "app.revanced";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ import app.revanced.extension.shared.settings.BaseSettings;
|
|||
import app.revanced.extension.shared.settings.preference.LogBufferManager;
|
||||
|
||||
/**
|
||||
* ReVanced specific logger. Logging is done to standard device log (accessible through ADB),
|
||||
* and additionally accessible through {@link LogBufferManager}.
|
||||
* ReVanced specific logger. Logging is done to standard device log (accessible thru ADB),
|
||||
* and additionally accessible thru {@link LogBufferManager}.
|
||||
*
|
||||
* All methods are thread safe, and are safe to call even
|
||||
* if {@link Utils#getContext()} is not available.
|
||||
|
|
@ -202,7 +202,7 @@ public class Logger {
|
|||
/**
|
||||
* Logs exceptions under the outer class name of the code calling this method.
|
||||
* <p>
|
||||
* If the calling code is showing its own error toast,
|
||||
* If the calling code is showing it's own error toast,
|
||||
* instead use {@link #printInfo(LogMessage, Exception)}
|
||||
*
|
||||
* @param message log message
|
||||
|
|
|
|||
|
|
@ -1,57 +0,0 @@
|
|||
package app.revanced.extension.shared;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public enum ResourceType {
|
||||
ANIM("anim"),
|
||||
ANIMATOR("animator"),
|
||||
ARRAY("array"),
|
||||
ATTR("attr"),
|
||||
BOOL("bool"),
|
||||
COLOR("color"),
|
||||
DIMEN("dimen"),
|
||||
DRAWABLE("drawable"),
|
||||
FONT("font"),
|
||||
FRACTION("fraction"),
|
||||
ID("id"),
|
||||
INTEGER("integer"),
|
||||
INTERPOLATOR("interpolator"),
|
||||
LAYOUT("layout"),
|
||||
MENU("menu"),
|
||||
MIPMAP("mipmap"),
|
||||
NAVIGATION("navigation"),
|
||||
PLURALS("plurals"),
|
||||
RAW("raw"),
|
||||
STRING("string"),
|
||||
STYLE("style"),
|
||||
STYLEABLE("styleable"),
|
||||
TRANSITION("transition"),
|
||||
VALUES("values"),
|
||||
XML("xml");
|
||||
|
||||
private static final Map<String, ResourceType> VALUE_MAP;
|
||||
|
||||
static {
|
||||
ResourceType[] values = values();
|
||||
VALUE_MAP = new HashMap<>(2 * values.length);
|
||||
|
||||
for (ResourceType type : values) {
|
||||
VALUE_MAP.put(type.value, type);
|
||||
}
|
||||
}
|
||||
|
||||
public final String value;
|
||||
|
||||
public static ResourceType fromValue(String value) {
|
||||
ResourceType type = VALUE_MAP.get(value);
|
||||
if (type == null) {
|
||||
throw new IllegalArgumentException("Unknown resource type: " + value);
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
ResourceType(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
|
@ -70,7 +70,7 @@ public class StringRef {
|
|||
}
|
||||
|
||||
/**
|
||||
* Creates a StringRef object that'll not change its value
|
||||
* Creates a StringRef object that'll not change it's value
|
||||
*
|
||||
* @param value value which toString() method returns when invoked on returned object
|
||||
* @return Unique StringRef instance, its value will never change
|
||||
|
|
@ -102,7 +102,7 @@ public class StringRef {
|
|||
public String toString() {
|
||||
if (!resolved) {
|
||||
if (resources == null || packageName == null) {
|
||||
var context = Utils.getContext();
|
||||
Context context = Utils.getContext();
|
||||
resources = context.getResources();
|
||||
packageName = context.getPackageName();
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -3,13 +3,14 @@ package app.revanced.extension.shared.checks;
|
|||
import static android.text.Html.FROM_HTML_MODE_COMPACT;
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
import static app.revanced.extension.shared.Utils.DialogFragmentOnStartAction;
|
||||
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.Intent;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.text.Html;
|
||||
import android.util.Pair;
|
||||
import android.view.Gravity;
|
||||
|
|
@ -19,17 +20,13 @@ import android.widget.ImageView;
|
|||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.ResourceType;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
import app.revanced.extension.shared.ui.CustomDialog;
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
abstract class Check {
|
||||
private static final int NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING = 2;
|
||||
|
||||
|
|
@ -78,6 +75,7 @@ abstract class Check {
|
|||
BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
static void issueWarning(Activity activity, Collection<Check> failedChecks) {
|
||||
final var reasons = new StringBuilder();
|
||||
|
||||
|
|
@ -95,7 +93,7 @@ abstract class Check {
|
|||
|
||||
Utils.runOnMainThreadDelayed(() -> {
|
||||
// Create the custom dialog.
|
||||
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
|
||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
||||
activity,
|
||||
str("revanced_check_environment_failed_title"), // Title.
|
||||
message, // Message.
|
||||
|
|
@ -129,8 +127,7 @@ abstract class Check {
|
|||
|
||||
// Add icon to the dialog.
|
||||
ImageView iconView = new ImageView(activity);
|
||||
iconView.setImageResource(Utils.getResourceIdentifierOrThrow(
|
||||
ResourceType.DRAWABLE, "revanced_ic_dialog_alert"));
|
||||
iconView.setImageResource(Utils.getResourceIdentifier("revanced_ic_dialog_alert", "drawable"));
|
||||
iconView.setColorFilter(Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN);
|
||||
iconView.setPadding(0, 0, 0, 0);
|
||||
LinearLayout.LayoutParams iconParams = new LinearLayout.LayoutParams(
|
||||
|
|
@ -161,8 +158,8 @@ abstract class Check {
|
|||
Button ignoreButton;
|
||||
|
||||
// Check if buttons are in a single-row layout (buttonContainer has one child: rowContainer).
|
||||
if (buttonContainer.getChildCount() == 1
|
||||
&& buttonContainer.getChildAt(0) instanceof LinearLayout rowContainer) {
|
||||
if (buttonContainer.getChildCount() == 1 && buttonContainer.getChildAt(0) instanceof LinearLayout) {
|
||||
LinearLayout rowContainer = (LinearLayout) buttonContainer.getChildAt(0);
|
||||
// Neutral button is the first child (index 0).
|
||||
ignoreButton = (Button) rowContainer.getChildAt(0);
|
||||
// OK button is the last child.
|
||||
|
|
|
|||
|
|
@ -7,12 +7,8 @@ import android.content.pm.PackageInfo;
|
|||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.util.Base64;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
|
||||
|
|
@ -31,7 +27,6 @@ import static app.revanced.extension.shared.checks.PatchInfo.Build.*;
|
|||
* <br>
|
||||
* Various indicators help to detect if the app was patched by the user.
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
@SuppressWarnings("unused")
|
||||
public final class CheckEnvironmentPatch {
|
||||
private static final boolean DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG = debugAlwaysShowWarning();
|
||||
|
|
@ -44,7 +39,6 @@ public final class CheckEnvironmentPatch {
|
|||
ADB((String) null),
|
||||
ROOT_MOUNT_ON_APP_STORE("com.android.vending"),
|
||||
MANAGER("app.revanced.manager.flutter",
|
||||
"app.revanced.manager.flutter.debug",
|
||||
"app.revanced.manager",
|
||||
"app.revanced.manager.debug");
|
||||
|
||||
|
|
@ -124,7 +118,7 @@ public final class CheckEnvironmentPatch {
|
|||
* If the build properties are different, the app was likely downloaded pre-patched or patched on another device.
|
||||
*/
|
||||
private static class CheckWasPatchedOnSameDevice extends Check {
|
||||
@SuppressLint("HardwareIds")
|
||||
@SuppressLint({"NewApi", "HardwareIds"})
|
||||
@Override
|
||||
protected Boolean check() {
|
||||
if (PATCH_BOARD.isEmpty()) {
|
||||
|
|
@ -198,7 +192,7 @@ public final class CheckEnvironmentPatch {
|
|||
PackageManager packageManager = context.getPackageManager();
|
||||
PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0);
|
||||
|
||||
// Duration since initial install or last update, whichever is sooner.
|
||||
// Duration since initial install or last update, which ever is sooner.
|
||||
durationBetweenPatchingAndInstallation = packageInfo.lastUpdateTime - PatchInfo.PATCH_TIME;
|
||||
Logger.printInfo(() -> "App was installed/updated: "
|
||||
+ (durationBetweenPatchingAndInstallation / (60 * 1000) + " minutes after patching"));
|
||||
|
|
@ -294,8 +288,8 @@ public final class CheckEnvironmentPatch {
|
|||
CheckIsNearPatchTime nearPatchTime = new CheckIsNearPatchTime();
|
||||
Boolean timeCheckPassed = nearPatchTime.check();
|
||||
if (timeCheckPassed && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) {
|
||||
// Allow installing recently patched APKs,
|
||||
// even if the installation source is not Manager or ADB.
|
||||
// Allow installing recently patched apks,
|
||||
// even if the install source is not Manager or ADB.
|
||||
Check.disableForever();
|
||||
return;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import okhttp3.Request;
|
|||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
|
||||
|
||||
public abstract class BaseFixRedgifsApiPatch implements Interceptor {
|
||||
protected static BaseFixRedgifsApiPatch INSTANCE;
|
||||
public abstract String getDefaultUserAgent();
|
||||
|
|
|
|||
|
|
@ -1,227 +0,0 @@
|
|||
package app.revanced.extension.shared.patches;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Color;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import app.revanced.extension.shared.GmsCoreSupport;
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.ResourceType;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
|
||||
/**
|
||||
* Patch shared by YouTube and YT Music.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class CustomBrandingPatch {
|
||||
|
||||
// Important: In the future, additional branding themes can be added but all existing and prior
|
||||
// themes cannot be removed or renamed.
|
||||
//
|
||||
// This is because if a user has a branding theme selected, then only that launch alias is enabled.
|
||||
// If a future update removes or renames that alias, then after updating the app is effectively
|
||||
// broken and it cannot be opened and not even clearing the app data will fix it.
|
||||
// In that situation the only fix is to completely uninstall and reinstall again.
|
||||
//
|
||||
// The most that can be done is to hide a theme from the UI and keep the alias with dummy data.
|
||||
public enum BrandingTheme {
|
||||
/**
|
||||
* Original unpatched icon.
|
||||
*/
|
||||
ORIGINAL,
|
||||
ROUNDED,
|
||||
MINIMAL,
|
||||
SCALED,
|
||||
/**
|
||||
* User provided custom icon.
|
||||
*/
|
||||
CUSTOM;
|
||||
|
||||
private String packageAndNameIndexToClassAlias(String packageName, int appIndex) {
|
||||
if (appIndex <= 0) {
|
||||
throw new IllegalArgumentException("App index starts at index 1");
|
||||
}
|
||||
return packageName + ".revanced_" + name().toLowerCase(Locale.US) + '_' + appIndex;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Integer notificationSmallIcon;
|
||||
|
||||
private static int getNotificationSmallIcon() {
|
||||
// Cannot use static initialization block otherwise cyclic references exist
|
||||
// between Settings initialization and this class.
|
||||
if (notificationSmallIcon == null) {
|
||||
if (GmsCoreSupport.isPackageNameOriginal()) {
|
||||
Logger.printDebug(() -> "App is root mounted. Not overriding small notification icon");
|
||||
return notificationSmallIcon = 0;
|
||||
}
|
||||
|
||||
BrandingTheme branding = BaseSettings.CUSTOM_BRANDING_ICON.get();
|
||||
if (branding == BrandingTheme.ORIGINAL) {
|
||||
notificationSmallIcon = 0;
|
||||
} else {
|
||||
// Original icon is quantum_ic_video_youtube_white_24
|
||||
String iconName = "revanced_notification_icon";
|
||||
if (branding == BrandingTheme.CUSTOM) {
|
||||
iconName += "_custom";
|
||||
}
|
||||
|
||||
notificationSmallIcon = Utils.getResourceIdentifier(ResourceType.DRAWABLE, iconName);
|
||||
if (notificationSmallIcon == 0) {
|
||||
Logger.printException(() -> "Could not load notification small icon");
|
||||
}
|
||||
}
|
||||
}
|
||||
return notificationSmallIcon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static View getLottieViewOrNull(View lottieStartupView) {
|
||||
if (BaseSettings.CUSTOM_BRANDING_ICON.get() == BrandingTheme.ORIGINAL) {
|
||||
return lottieStartupView;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void setNotificationIcon(Notification.Builder builder) {
|
||||
try {
|
||||
final int smallIcon = getNotificationSmallIcon();
|
||||
if (smallIcon != 0) {
|
||||
builder.setSmallIcon(smallIcon)
|
||||
.setColor(Color.TRANSPARENT); // Remove YT red tint.
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "setNotificationIcon failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* <p>
|
||||
* The total number of app name aliases, including dummy aliases.
|
||||
*/
|
||||
private static int numberOfPresetAppNames() {
|
||||
// Modified during patching, but requires a default if custom branding is excluded.
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* <p>
|
||||
* If a custom icon was provided during patching.
|
||||
*/
|
||||
private static boolean userProvidedCustomIcon() {
|
||||
// Modified during patching, but requires a default if custom branding is excluded.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* <p>
|
||||
* If a custom name was provided during patching.
|
||||
*/
|
||||
private static boolean userProvidedCustomName() {
|
||||
// Modified during patching, but requires a default if custom branding is excluded..
|
||||
return false;
|
||||
}
|
||||
|
||||
public static int getDefaultAppNameIndex() {
|
||||
return userProvidedCustomName()
|
||||
? numberOfPresetAppNames()
|
||||
: 2;
|
||||
}
|
||||
|
||||
public static BrandingTheme getDefaultIconStyle() {
|
||||
return userProvidedCustomIcon()
|
||||
? BrandingTheme.CUSTOM
|
||||
: BrandingTheme.ROUNDED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
public static void setBranding() {
|
||||
try {
|
||||
if (GmsCoreSupport.isPackageNameOriginal()) {
|
||||
Logger.printInfo(() -> "App is root mounted. Cannot dynamically change app icon");
|
||||
return;
|
||||
}
|
||||
|
||||
Context context = Utils.getContext();
|
||||
PackageManager pm = context.getPackageManager();
|
||||
String packageName = context.getPackageName();
|
||||
|
||||
BrandingTheme selectedBranding = BaseSettings.CUSTOM_BRANDING_ICON.get();
|
||||
final int selectedNameIndex = BaseSettings.CUSTOM_BRANDING_NAME.get();
|
||||
ComponentName componentToEnable = null;
|
||||
ComponentName defaultComponent = null;
|
||||
List<ComponentName> componentsToDisable = new ArrayList<>();
|
||||
|
||||
for (BrandingTheme theme : BrandingTheme.values()) {
|
||||
// Must always update all aliases including custom alias (last index).
|
||||
final int numberOfPresetAppNames = numberOfPresetAppNames();
|
||||
|
||||
// App name indices starts at 1.
|
||||
for (int index = 1; index <= numberOfPresetAppNames; index++) {
|
||||
String aliasClass = theme.packageAndNameIndexToClassAlias(packageName, index);
|
||||
ComponentName component = new ComponentName(packageName, aliasClass);
|
||||
if (defaultComponent == null) {
|
||||
// Default is always the first alias.
|
||||
defaultComponent = component;
|
||||
}
|
||||
|
||||
if (index == selectedNameIndex && theme == selectedBranding) {
|
||||
componentToEnable = component;
|
||||
} else {
|
||||
componentsToDisable.add(component);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (componentToEnable == null) {
|
||||
// User imported a bad app name index value. Either the imported data
|
||||
// was corrupted, or they previously had custom name enabled and the app
|
||||
// no longer has a custom name specified.
|
||||
Utils.showToastLong("Custom branding reset");
|
||||
BaseSettings.CUSTOM_BRANDING_ICON.resetToDefault();
|
||||
BaseSettings.CUSTOM_BRANDING_NAME.resetToDefault();
|
||||
|
||||
componentToEnable = defaultComponent;
|
||||
componentsToDisable.remove(defaultComponent);
|
||||
}
|
||||
|
||||
for (ComponentName disable : componentsToDisable) {
|
||||
pm.setComponentEnabledSetting(disable,
|
||||
PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
|
||||
}
|
||||
|
||||
// Use info logging because if the alias status become corrupt the app cannot launch.
|
||||
ComponentName componentToEnableFinal = componentToEnable;
|
||||
Logger.printInfo(() -> "Enabling: " + componentToEnableFinal.getClassName());
|
||||
|
||||
pm.setComponentEnabledSetting(componentToEnable,
|
||||
PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 0);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "setBranding failure", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
package app.revanced.extension.shared.patches;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.settings.AppLanguage;
|
||||
import app.revanced.extension.shared.spoof.ClientType;
|
||||
import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class ForceOriginalAudioPatch {
|
||||
|
||||
private static final String DEFAULT_AUDIO_TRACKS_SUFFIX = ".4";
|
||||
|
||||
private static volatile boolean enabled;
|
||||
|
||||
public static void setEnabled(boolean isEnabled, ClientType client) {
|
||||
enabled = isEnabled;
|
||||
|
||||
if (isEnabled && !client.useAuth && !client.supportsMultiAudioTracks) {
|
||||
// If client spoofing does not use authentication and lacks multi-audio streams,
|
||||
// then can use any language code for the request and if that requested language is
|
||||
// not available YT uses the original audio language. Authenticated requests ignore
|
||||
// the language code and always use the account language. Use a language that is
|
||||
// not auto-dubbed by YouTube: https://support.google.com/youtube/answer/15569972
|
||||
// but the language is also supported natively by the Meta Quest device that
|
||||
// Android VR is spoofing.
|
||||
AppLanguage override = AppLanguage.NB; // Norwegian Bokmal.
|
||||
Logger.printDebug(() -> "Setting language override: " + override);
|
||||
SpoofVideoStreamsPatch.setLanguageOverride(override);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean ignoreDefaultAudioStream(boolean original) {
|
||||
if (enabled) {
|
||||
return false;
|
||||
}
|
||||
return original;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean isDefaultAudioStream(boolean isDefault, String audioTrackId, String audioTrackDisplayName) {
|
||||
try {
|
||||
if (!enabled) {
|
||||
return isDefault;
|
||||
}
|
||||
|
||||
if (audioTrackId.isEmpty()) {
|
||||
// Older app targets can have empty audio tracks and these might be placeholders.
|
||||
// The real audio tracks are called after these.
|
||||
return isDefault;
|
||||
}
|
||||
|
||||
Logger.printDebug(() -> "default: " + String.format("%-5s", isDefault) + " id: "
|
||||
+ String.format("%-8s", audioTrackId) + " name:" + audioTrackDisplayName);
|
||||
|
||||
final boolean isOriginal = audioTrackId.endsWith(DEFAULT_AUDIO_TRACKS_SUFFIX);
|
||||
if (isOriginal) {
|
||||
Logger.printDebug(() -> "Using audio: " + audioTrackId);
|
||||
}
|
||||
|
||||
return isOriginal;
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "isDefaultAudioStream failure", ex);
|
||||
return isDefault;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
package app.revanced.extension.shared.patches;
|
||||
|
||||
import app.revanced.extension.shared.privacy.LinkSanitizer;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
|
||||
/**
|
||||
* YouTube and YouTube Music.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public final class SanitizeSharingLinksPatch {
|
||||
|
||||
private static final LinkSanitizer sanitizer = new LinkSanitizer(
|
||||
"si",
|
||||
"feature" // Old tracking parameter name, and may be obsolete.
|
||||
);
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static String sanitize(String url) {
|
||||
if (BaseSettings.SANITIZE_SHARING_LINKS.get()) {
|
||||
url = sanitizer.sanitizeURLString(url);
|
||||
}
|
||||
|
||||
if (BaseSettings.REPLACE_MUSIC_LINKS_WITH_YOUTUBE.get()) {
|
||||
url = url.replace("music.youtube.com", "youtube.com");
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,213 +0,0 @@
|
|||
package app.revanced.extension.shared.patches.litho;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import app.revanced.extension.shared.ByteTrieSearch;
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||
|
||||
public abstract class FilterGroup<T> {
|
||||
public final static class FilterGroupResult {
|
||||
private BooleanSetting setting;
|
||||
private int matchedIndex;
|
||||
private int matchedLength;
|
||||
// In the future it might be useful to include which pattern matched,
|
||||
// but for now that is not needed.
|
||||
|
||||
FilterGroupResult() {
|
||||
this(null, -1, 0);
|
||||
}
|
||||
|
||||
FilterGroupResult(BooleanSetting setting, int matchedIndex, int matchedLength) {
|
||||
setValues(setting, matchedIndex, matchedLength);
|
||||
}
|
||||
|
||||
public void setValues(BooleanSetting setting, int matchedIndex, int matchedLength) {
|
||||
this.setting = setting;
|
||||
this.matchedIndex = matchedIndex;
|
||||
this.matchedLength = matchedLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* A null value if the group has no setting,
|
||||
* or if no match is returned from {@link FilterGroupList#check(Object)}.
|
||||
*/
|
||||
public BooleanSetting getSetting() {
|
||||
return setting;
|
||||
}
|
||||
|
||||
public boolean isFiltered() {
|
||||
return matchedIndex >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Matched index of first pattern that matched, or -1 if nothing matched.
|
||||
*/
|
||||
public int getMatchedIndex() {
|
||||
return matchedIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Length of the matched filter pattern.
|
||||
*/
|
||||
public int getMatchedLength() {
|
||||
return matchedLength;
|
||||
}
|
||||
}
|
||||
|
||||
protected final BooleanSetting setting;
|
||||
public final T[] filters;
|
||||
|
||||
/**
|
||||
* Initialize a new filter group.
|
||||
*
|
||||
* @param setting The associated setting.
|
||||
* @param filters The filters.
|
||||
*/
|
||||
@SafeVarargs
|
||||
public FilterGroup(final BooleanSetting setting, final T... filters) {
|
||||
this.setting = setting;
|
||||
this.filters = filters;
|
||||
if (filters.length == 0) {
|
||||
throw new IllegalArgumentException("Must use one or more filter patterns (zero specified)");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return setting == null || setting.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return If {@link FilterGroupList} should include this group when searching.
|
||||
* By default, all filters are included except non enabled settings that require reboot.
|
||||
*/
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean includeInSearch() {
|
||||
return isEnabled() || !setting.rebootApp;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return getClass().getSimpleName() + ": " + (setting == null ? "(null setting)" : setting);
|
||||
}
|
||||
|
||||
public abstract FilterGroupResult check(final T stack);
|
||||
|
||||
|
||||
public static class StringFilterGroup extends FilterGroup<String> {
|
||||
|
||||
public StringFilterGroup(final BooleanSetting setting, final String... filters) {
|
||||
super(setting, filters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilterGroupResult check(final String string) {
|
||||
int matchedIndex = -1;
|
||||
int matchedLength = 0;
|
||||
if (isEnabled()) {
|
||||
for (String pattern : filters) {
|
||||
if (!string.isEmpty()) {
|
||||
final int indexOf = string.indexOf(pattern);
|
||||
if (indexOf >= 0) {
|
||||
matchedIndex = indexOf;
|
||||
matchedLength = pattern.length();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return new FilterGroupResult(setting, matchedIndex, matchedLength);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If you have more than 1 filter patterns, then all instances of
|
||||
* this class should be filtered using {@link FilterGroupList.ByteArrayFilterGroupList#check(byte[])},
|
||||
* which uses a prefix tree to give better performance.
|
||||
*/
|
||||
public static class ByteArrayFilterGroup extends FilterGroup<byte[]> {
|
||||
|
||||
private volatile int[][] failurePatterns;
|
||||
|
||||
// Modified implementation from https://stackoverflow.com/a/1507813
|
||||
private static int indexOf(final byte[] data, final byte[] pattern, final int[] failure) {
|
||||
// Finds the first occurrence of the pattern in the byte array using
|
||||
// KMP matching algorithm.
|
||||
int patternLength = pattern.length;
|
||||
for (int i = 0, j = 0, dataLength = data.length; i < dataLength; i++) {
|
||||
while (j > 0 && pattern[j] != data[i]) {
|
||||
j = failure[j - 1];
|
||||
}
|
||||
if (pattern[j] == data[i]) {
|
||||
j++;
|
||||
}
|
||||
if (j == patternLength) {
|
||||
return i - patternLength + 1;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static int[] createFailurePattern(byte[] pattern) {
|
||||
// Computes the failure function using a bootstrapping process,
|
||||
// where the pattern is matched against itself.
|
||||
final int patternLength = pattern.length;
|
||||
final int[] failure = new int[patternLength];
|
||||
|
||||
for (int i = 1, j = 0; i < patternLength; i++) {
|
||||
while (j > 0 && pattern[j] != pattern[i]) {
|
||||
j = failure[j - 1];
|
||||
}
|
||||
if (pattern[j] == pattern[i]) {
|
||||
j++;
|
||||
}
|
||||
failure[i] = j;
|
||||
}
|
||||
return failure;
|
||||
}
|
||||
|
||||
public ByteArrayFilterGroup(BooleanSetting setting, byte[]... filters) {
|
||||
super(setting, filters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the Strings into byte arrays. Used to search for text in binary data.
|
||||
*/
|
||||
public ByteArrayFilterGroup(BooleanSetting setting, String... filters) {
|
||||
super(setting, ByteTrieSearch.convertStringsToBytes(filters));
|
||||
}
|
||||
|
||||
private synchronized void buildFailurePatterns() {
|
||||
if (failurePatterns != null) return; // Thread race and another thread already initialized the search.
|
||||
Logger.printDebug(() -> "Building failure array for: " + this);
|
||||
int[][] failurePatterns = new int[filters.length][];
|
||||
int i = 0;
|
||||
for (byte[] pattern : filters) {
|
||||
failurePatterns[i++] = createFailurePattern(pattern);
|
||||
}
|
||||
this.failurePatterns = failurePatterns; // Must set after initialization finishes.
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilterGroupResult check(final byte[] bytes) {
|
||||
int matchedLength = 0;
|
||||
int matchedIndex = -1;
|
||||
if (isEnabled()) {
|
||||
int[][] failures = failurePatterns;
|
||||
if (failures == null) {
|
||||
buildFailurePatterns(); // Lazy load.
|
||||
failures = failurePatterns;
|
||||
}
|
||||
for (int i = 0, length = filters.length; i < length; i++) {
|
||||
byte[] filter = filters[i];
|
||||
matchedIndex = indexOf(bytes, filter, failures[i]);
|
||||
if (matchedIndex >= 0) {
|
||||
matchedLength = filter.length;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return new FilterGroupResult(setting, matchedIndex, matchedLength);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
package app.revanced.extension.shared.patches.litho;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import app.revanced.extension.shared.ByteTrieSearch;
|
||||
import app.revanced.extension.shared.StringTrieSearch;
|
||||
import app.revanced.extension.shared.TrieSearch;
|
||||
import app.revanced.extension.shared.patches.litho.FilterGroup.ByteArrayFilterGroup;
|
||||
import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class FilterGroupList<V, T extends FilterGroup<V>> implements Iterable<T> {
|
||||
|
||||
private final List<T> filterGroups = new ArrayList<>();
|
||||
private final TrieSearch<V> search = createSearchGraph();
|
||||
|
||||
@SafeVarargs
|
||||
public final void addAll(final T... groups) {
|
||||
filterGroups.addAll(Arrays.asList(groups));
|
||||
|
||||
for (T group : groups) {
|
||||
if (!group.includeInSearch()) {
|
||||
continue;
|
||||
}
|
||||
for (V pattern : group.filters) {
|
||||
search.addPattern(pattern, (textSearched, matchedStartIndex, matchedLength, callbackParameter) -> {
|
||||
if (group.isEnabled()) {
|
||||
FilterGroup.FilterGroupResult result = (FilterGroup.FilterGroupResult) callbackParameter;
|
||||
result.setValues(group.setting, matchedStartIndex, matchedLength);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Iterator<T> iterator() {
|
||||
return filterGroups.iterator();
|
||||
}
|
||||
|
||||
public FilterGroup.FilterGroupResult check(V stack) {
|
||||
FilterGroup.FilterGroupResult result = new FilterGroup.FilterGroupResult();
|
||||
search.matches(stack, result);
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
protected abstract TrieSearch<V> createSearchGraph();
|
||||
|
||||
public static final class StringFilterGroupList extends FilterGroupList<String, StringFilterGroup> {
|
||||
protected StringTrieSearch createSearchGraph() {
|
||||
return new StringTrieSearch();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If searching for a single byte pattern, then it is slightly better to use
|
||||
* {@link ByteArrayFilterGroup#check(byte[])} as it uses KMP which is faster
|
||||
* than a prefix tree to search for only 1 pattern.
|
||||
*/
|
||||
public static final class ByteArrayFilterGroupList extends FilterGroupList<byte[], ByteArrayFilterGroup> {
|
||||
protected ByteTrieSearch createSearchGraph() {
|
||||
return new ByteTrieSearch();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,439 +0,0 @@
|
|||
package app.revanced.extension.shared.patches.litho;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
import app.revanced.extension.shared.StringTrieSearch;
|
||||
import app.revanced.extension.shared.settings.YouTubeAndMusicSettings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class LithoFilterPatch {
|
||||
/**
|
||||
* Simple wrapper to pass the litho parameters through the prefix search.
|
||||
*/
|
||||
private static final class LithoFilterParameters {
|
||||
final String identifier;
|
||||
final String path;
|
||||
final String accessibility;
|
||||
final byte[] buffer;
|
||||
|
||||
LithoFilterParameters(String lithoIdentifier, String lithoPath,
|
||||
String accessibility, byte[] buffer) {
|
||||
this.identifier = lithoIdentifier;
|
||||
this.path = lithoPath;
|
||||
this.accessibility = accessibility;
|
||||
this.buffer = buffer;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
// Estimate the percentage of the buffer that are Strings.
|
||||
StringBuilder builder = new StringBuilder(Math.max(100, buffer.length / 2));
|
||||
builder.append("ID: ");
|
||||
builder.append(identifier);
|
||||
if (!accessibility.isEmpty()) {
|
||||
// AccessibilityId and AccessibilityText are pieces of BufferStrings.
|
||||
builder.append(" Accessibility: ");
|
||||
builder.append(accessibility);
|
||||
}
|
||||
builder.append(" Path: ");
|
||||
builder.append(path);
|
||||
if (YouTubeAndMusicSettings.DEBUG_PROTOCOLBUFFER.get()) {
|
||||
builder.append(" BufferStrings: ");
|
||||
findAsciiStrings(builder, buffer);
|
||||
}
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Search through a byte array for all ASCII strings.
|
||||
*/
|
||||
static void findAsciiStrings(StringBuilder builder, byte[] buffer) {
|
||||
// Valid ASCII values (ignore control characters).
|
||||
final int minimumAscii = 32; // 32 = space character
|
||||
final int maximumAscii = 126; // 127 = delete character
|
||||
final int minimumAsciiStringLength = 4; // Minimum length of an ASCII string to include.
|
||||
String delimitingCharacter = "❙"; // Non ascii character, to allow easier log filtering.
|
||||
|
||||
final int length = buffer.length;
|
||||
int start = 0;
|
||||
int end = 0;
|
||||
while (end < length) {
|
||||
int value = buffer[end];
|
||||
if (value < minimumAscii || value > maximumAscii || end == length - 1) {
|
||||
if (end - start >= minimumAsciiStringLength) {
|
||||
for (int i = start; i < end; i++) {
|
||||
builder.append((char) buffer[i]);
|
||||
}
|
||||
builder.append(delimitingCharacter);
|
||||
}
|
||||
start = end + 1;
|
||||
}
|
||||
end++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Placeholder for actual filters.
|
||||
*/
|
||||
private static final class DummyFilter extends Filter {
|
||||
}
|
||||
|
||||
private static final Filter[] filters = new Filter[]{
|
||||
new DummyFilter() // Replaced during patching, do not touch.
|
||||
};
|
||||
|
||||
/**
|
||||
* Litho layout fixed thread pool size override.
|
||||
* <p>
|
||||
* Unpatched YouTube uses a layout fixed thread pool between 1 and 3 threads:
|
||||
* <pre>
|
||||
* 1 thread - > Device has less than 6 cores
|
||||
* 2 threads -> Device has over 6 cores and less than 6GB of memory
|
||||
* 3 threads -> Device has over 6 cores and more than 6GB of memory
|
||||
* </pre>
|
||||
*
|
||||
* Using more than 1 thread causes layout issues such as the You tab watch/playlist shelf
|
||||
* that is sometimes incorrectly hidden (ReVanced is not hiding it), and seems to
|
||||
* fix a race issue if using the active navigation tab status with litho filtering.
|
||||
*/
|
||||
private static final int LITHO_LAYOUT_THREAD_POOL_SIZE = 1;
|
||||
|
||||
/**
|
||||
* For YouTube 20.22+, this is set to true by a patch,
|
||||
* because it cannot use the thread buffer due to the buffer frequently not being correct,
|
||||
* especially for components that are recreated such as dragging off-screen then back on screen.
|
||||
* Instead, parse the identifier found near the start of the buffer and use that to
|
||||
* identify the correct buffer to use when filtering.
|
||||
* <p>
|
||||
* <b>This is set during patching, do not change manually.</b>
|
||||
*/
|
||||
private static final boolean EXTRACT_IDENTIFIER_FROM_BUFFER = false;
|
||||
|
||||
/**
|
||||
* Turns on additional logging, used for development purposes only.
|
||||
*/
|
||||
public static final boolean DEBUG_EXTRACT_IDENTIFIER_FROM_BUFFER = false;
|
||||
|
||||
/**
|
||||
* String suffix for components.
|
||||
* Can be any of: ".eml", ".eml-fe", ".e-b", ".eml-js", "e-js-b"
|
||||
*/
|
||||
private static final byte[] LITHO_COMPONENT_EXTENSION_BYTES = ".e".getBytes(StandardCharsets.US_ASCII);
|
||||
|
||||
/**
|
||||
* Used as placeholder for litho id/path filters that do not use a buffer
|
||||
*/
|
||||
private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
|
||||
|
||||
/**
|
||||
* Because litho filtering is multithreaded and the buffer is passed in from a different injection point,
|
||||
* the buffer is saved to a ThreadLocal so each calling thread does not interfere with other threads.
|
||||
* Used for 20.21 and lower.
|
||||
*/
|
||||
private static final ThreadLocal<byte[]> bufferThreadLocal = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
* Identifier to protocol buffer mapping. Only used for 20.22+.
|
||||
* Thread local is needed because filtering is multithreaded and each thread can load
|
||||
* a different component with the same identifier.
|
||||
*/
|
||||
private static final ThreadLocal<Map<String, byte[]>> identifierToBufferThread = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
* Global shared buffer. Used only if the buffer is not found in the ThreadLocal.
|
||||
*/
|
||||
private static final Map<String, byte[]> identifierToBufferGlobal
|
||||
= Collections.synchronizedMap(createIdentifierToBufferMap());
|
||||
|
||||
private static final StringTrieSearch pathSearchTree = new StringTrieSearch();
|
||||
private static final StringTrieSearch identifierSearchTree = new StringTrieSearch();
|
||||
|
||||
static {
|
||||
|
||||
for (Filter filter : filters) {
|
||||
filterUsingCallbacks(identifierSearchTree, filter,
|
||||
filter.identifierCallbacks, Filter.FilterContentType.IDENTIFIER);
|
||||
filterUsingCallbacks(pathSearchTree, filter,
|
||||
filter.pathCallbacks, Filter.FilterContentType.PATH);
|
||||
}
|
||||
|
||||
Logger.printDebug(() -> "Using: "
|
||||
+ identifierSearchTree.numberOfPatterns() + " identifier filters"
|
||||
+ " (" + identifierSearchTree.getEstimatedMemorySize() + " KB), "
|
||||
+ pathSearchTree.numberOfPatterns() + " path filters"
|
||||
+ " (" + pathSearchTree.getEstimatedMemorySize() + " KB)");
|
||||
}
|
||||
|
||||
private static void filterUsingCallbacks(StringTrieSearch pathSearchTree,
|
||||
Filter filter, List<StringFilterGroup> groups,
|
||||
Filter.FilterContentType type) {
|
||||
String filterSimpleName = filter.getClass().getSimpleName();
|
||||
|
||||
for (StringFilterGroup group : groups) {
|
||||
if (!group.includeInSearch()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (String pattern : group.filters) {
|
||||
pathSearchTree.addPattern(pattern, (textSearched, matchedStartIndex,
|
||||
matchedLength, callbackParameter) -> {
|
||||
if (!group.isEnabled()) return false;
|
||||
|
||||
LithoFilterParameters parameters = (LithoFilterParameters) callbackParameter;
|
||||
final boolean isFiltered = filter.isFiltered(parameters.identifier,
|
||||
parameters.accessibility, parameters.path, parameters.buffer,
|
||||
group, type, matchedStartIndex);
|
||||
|
||||
if (isFiltered && BaseSettings.DEBUG.get()) {
|
||||
Logger.printDebug(() -> type == Filter.FilterContentType.IDENTIFIER
|
||||
? filterSimpleName + " filtered identifier: " + parameters.identifier
|
||||
: filterSimpleName + " filtered path: " + parameters.path);
|
||||
}
|
||||
|
||||
return isFiltered;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, byte[]> createIdentifierToBufferMap() {
|
||||
// It's unclear how many items should be cached. This is a guess.
|
||||
return Utils.createSizeRestrictedMap(100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function that differs from {@link Character#isDigit(char)}
|
||||
* as this only matches ascii and not Unicode numbers.
|
||||
*/
|
||||
private static boolean isAsciiNumber(byte character) {
|
||||
return '0' <= character && character <= '9';
|
||||
}
|
||||
|
||||
private static boolean isAsciiLowerCaseLetter(byte character) {
|
||||
return 'a' <= character && character <= 'z';
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point. Called off the main thread.
|
||||
* Targets 20.22+
|
||||
*/
|
||||
public static void setProtoBuffer(byte[] buffer) {
|
||||
if (DEBUG_EXTRACT_IDENTIFIER_FROM_BUFFER) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
LithoFilterParameters.findAsciiStrings(builder, buffer);
|
||||
Logger.printDebug(() -> "New buffer: " + builder);
|
||||
}
|
||||
|
||||
// The identifier always seems to start very close to the buffer start.
|
||||
// Highest identifier start index ever observed is 50, with most around 30 to 40.
|
||||
// The buffer can be very large with up to 200kb has been observed,
|
||||
// so the search is restricted to only the start.
|
||||
final int maxBufferStartIndex = 500; // 10x expected upper bound.
|
||||
|
||||
// Could use Boyer-Moore-Horspool since the string is ASCII and has a limited number of
|
||||
// unique characters, but it seems to be slower since the extra overhead of checking the
|
||||
// bad character array negates any performance gain of skipping a few extra subsearches.
|
||||
int emlIndex = -1;
|
||||
final int emlStringLength = LITHO_COMPONENT_EXTENSION_BYTES.length;
|
||||
final int lastBufferIndexToCheckFrom = Math.min(maxBufferStartIndex, buffer.length - emlStringLength);
|
||||
for (int i = 0; i < lastBufferIndexToCheckFrom; i++) {
|
||||
boolean match = true;
|
||||
for (int j = 0; j < emlStringLength; j++) {
|
||||
if (buffer[i + j] != LITHO_COMPONENT_EXTENSION_BYTES[j]) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match) {
|
||||
emlIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (emlIndex < 0) {
|
||||
// Buffer is not used for creating a new litho component.
|
||||
if (DEBUG_EXTRACT_IDENTIFIER_FROM_BUFFER) {
|
||||
Logger.printDebug(() -> "Could not find eml index");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
int startIndex = emlIndex - 1;
|
||||
while (startIndex > 0) {
|
||||
final byte character = buffer[startIndex];
|
||||
int startIndexFinal = startIndex;
|
||||
if (isAsciiLowerCaseLetter(character) || isAsciiNumber(character) || character == '_') {
|
||||
// Valid character for the first path element.
|
||||
startIndex--;
|
||||
} else {
|
||||
startIndex++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Strip away any numbers on the start of the identifier, which can
|
||||
// be from random data in the buffer before the identifier starts.
|
||||
while (true) {
|
||||
final byte character = buffer[startIndex];
|
||||
if (isAsciiNumber(character)) {
|
||||
startIndex++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Find the pipe character after the identifier.
|
||||
int endIndex = -1;
|
||||
for (int i = emlIndex, length = buffer.length; i < length; i++) {
|
||||
if (buffer[i] == '|') {
|
||||
endIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (endIndex < 0) {
|
||||
if (BaseSettings.DEBUG.get()) {
|
||||
Logger.printException(() -> "Debug: Could not find buffer identifier");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
String identifier = new String(buffer, startIndex, endIndex - startIndex, StandardCharsets.US_ASCII);
|
||||
if (DEBUG_EXTRACT_IDENTIFIER_FROM_BUFFER) {
|
||||
Logger.printDebug(() -> "Found buffer for identifier: " + identifier);
|
||||
}
|
||||
identifierToBufferGlobal.put(identifier, buffer);
|
||||
|
||||
Map<String, byte[]> map = identifierToBufferThread.get();
|
||||
if (map == null) {
|
||||
map = createIdentifierToBufferMap();
|
||||
identifierToBufferThread.set(map);
|
||||
}
|
||||
map.put(identifier, buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point. Called off the main thread.
|
||||
* Targets 20.21 and lower.
|
||||
*/
|
||||
public static void setProtoBuffer(@Nullable ByteBuffer buffer) {
|
||||
if (buffer == null || !buffer.hasArray()) {
|
||||
// It appears the buffer can be cleared out just before the call to #filter()
|
||||
// Ignore this null value and retain the last buffer that was set.
|
||||
Logger.printDebug(() -> "Ignoring null or empty buffer: " + buffer);
|
||||
} else {
|
||||
// Set the buffer to a thread local. The buffer will remain in memory, even after the call to #filter completes.
|
||||
// This is intentional, as it appears the buffer can be set once and then filtered multiple times.
|
||||
// The buffer will be cleared from memory after a new buffer is set by the same thread,
|
||||
// or when the calling thread eventually dies.
|
||||
bufferThreadLocal.set(buffer.array());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean isFiltered(String identifier, @Nullable String accessibilityId,
|
||||
@Nullable String accessibilityText, StringBuilder pathBuilder) {
|
||||
try {
|
||||
if (identifier.isEmpty() || pathBuilder.length() == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[] buffer = null;
|
||||
if (EXTRACT_IDENTIFIER_FROM_BUFFER) {
|
||||
final int pipeIndex = identifier.indexOf('|');
|
||||
if (pipeIndex >= 0) {
|
||||
// If the identifier contains no pipe, then it's not an ".eml" identifier
|
||||
// and the buffer is not uniquely identified. Typically, this only happens
|
||||
// for subcomponents where buffer filtering is not used.
|
||||
String identifierKey = identifier.substring(0, pipeIndex);
|
||||
|
||||
var map = identifierToBufferThread.get();
|
||||
if (map != null) {
|
||||
buffer = map.get(identifierKey);
|
||||
}
|
||||
|
||||
if (buffer == null) {
|
||||
// Buffer for thread local not found. Use the last buffer found from any thread.
|
||||
buffer = identifierToBufferGlobal.get(identifierKey);
|
||||
|
||||
if (DEBUG_EXTRACT_IDENTIFIER_FROM_BUFFER && buffer == null) {
|
||||
// No buffer is found for some components, such as
|
||||
// shorts_lockup_cell.eml on channel profiles.
|
||||
// For now, just ignore this and filter without a buffer.
|
||||
if (BaseSettings.DEBUG.get()) {
|
||||
Logger.printException(() -> "Debug: Could not find buffer for identifier: " + identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
buffer = bufferThreadLocal.get();
|
||||
}
|
||||
|
||||
// Potentially the buffer may have been null or never set up until now.
|
||||
// Use an empty buffer so the litho id/path filters that do not use a buffer still work.
|
||||
if (buffer == null) {
|
||||
buffer = EMPTY_BYTE_ARRAY;
|
||||
}
|
||||
|
||||
String path = pathBuilder.toString();
|
||||
|
||||
String accessibility = "";
|
||||
if (accessibilityId != null && !accessibilityId.isBlank()) {
|
||||
accessibility = accessibilityId;
|
||||
}
|
||||
if (accessibilityText != null && !accessibilityText.isBlank()) {
|
||||
accessibility = accessibilityId + '|' + accessibilityText;
|
||||
}
|
||||
LithoFilterParameters parameter = new LithoFilterParameters(identifier, path, accessibility, buffer);
|
||||
Logger.printDebug(() -> "Searching " + parameter);
|
||||
|
||||
return identifierSearchTree.matches(identifier, parameter)
|
||||
|| pathSearchTree.matches(path, parameter);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "isFiltered failure", ex);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static int getExecutorCorePoolSize(int originalCorePoolSize) {
|
||||
if (originalCorePoolSize != LITHO_LAYOUT_THREAD_POOL_SIZE) {
|
||||
Logger.printDebug(() -> "Overriding core thread pool size from: " + originalCorePoolSize
|
||||
+ " to: " + LITHO_LAYOUT_THREAD_POOL_SIZE);
|
||||
}
|
||||
|
||||
return LITHO_LAYOUT_THREAD_POOL_SIZE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static int getExecutorMaxThreads(int originalMaxThreads) {
|
||||
if (originalMaxThreads != LITHO_LAYOUT_THREAD_POOL_SIZE) {
|
||||
Logger.printDebug(() -> "Overriding max thread pool size from: " + originalMaxThreads
|
||||
+ " to: " + LITHO_LAYOUT_THREAD_POOL_SIZE);
|
||||
}
|
||||
|
||||
return LITHO_LAYOUT_THREAD_POOL_SIZE;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
package app.revanced.extension.shared.privacy;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
|
||||
/**
|
||||
* Strips away specific parameters from URLs.
|
||||
*/
|
||||
public class LinkSanitizer {
|
||||
|
||||
private final Collection<String> parametersToRemove;
|
||||
|
||||
public LinkSanitizer(String ... parametersToRemove) {
|
||||
final int parameterCount = parametersToRemove.length;
|
||||
|
||||
// List is faster if only checking a few parameters.
|
||||
this.parametersToRemove = parameterCount > 4
|
||||
? Set.of(parametersToRemove)
|
||||
: List.of(parametersToRemove);
|
||||
}
|
||||
|
||||
public String sanitizeURLString(String url) {
|
||||
try {
|
||||
return sanitizeURI(Uri.parse(url)).toString();
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "sanitizeURLString failure: " + url, ex);
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
public Uri sanitizeURI(Uri uri) {
|
||||
try {
|
||||
String scheme = uri.getScheme();
|
||||
if (scheme == null || !(scheme.equals("http") || scheme.equals("https"))) {
|
||||
// Opening YouTube share sheet 'other' option passes the video title as a URI.
|
||||
// Checking !uri.isHierarchical() works for all cases, except if the
|
||||
// video title starts with / and then it's hierarchical but still an invalid URI.
|
||||
Logger.printDebug(() -> "Ignoring URI: " + uri);
|
||||
return uri;
|
||||
}
|
||||
|
||||
Uri.Builder builder = uri.buildUpon().clearQuery();
|
||||
|
||||
if (!parametersToRemove.isEmpty()) {
|
||||
for (String paramName : uri.getQueryParameterNames()) {
|
||||
if (!parametersToRemove.contains(paramName)) {
|
||||
for (String value : uri.getQueryParameters(paramName)) {
|
||||
builder.appendQueryParameter(paramName, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Uri sanitizedURL = builder.build();
|
||||
Logger.printInfo(() -> "Sanitized URL: " + uri + " to: " + sanitizedURL);
|
||||
|
||||
return sanitizedURL;
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "sanitizeURI failure: " + uri, ex);
|
||||
return uri;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,8 +23,8 @@ public class Requester {
|
|||
public static HttpURLConnection getConnectionFromCompiledRoute(String apiUrl, Route.CompiledRoute route) throws IOException {
|
||||
String url = apiUrl + route.getCompiledRoute();
|
||||
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
|
||||
// This request sends data via URL query parameters. No request body is included.
|
||||
// If a request body is added, the caller must set the appropriate Content-Length header.
|
||||
// Request data is in the URL parameters and no body is sent.
|
||||
// The calling code must set a length if using a request body.
|
||||
connection.setFixedLengthStreamingMode(0);
|
||||
connection.setRequestMethod(route.getMethod().name());
|
||||
String agentString = System.getProperty("http.agent")
|
||||
|
|
|
|||
|
|
@ -36,8 +36,8 @@ public enum AppLanguage {
|
|||
FR,
|
||||
GL,
|
||||
GU,
|
||||
HE, // App uses obsolete 'IW' and not the modern 'HE' ISO code.
|
||||
HI,
|
||||
HE, // App uses obsolete 'IW' and not the modern 'HE' ISO code.
|
||||
HR,
|
||||
HU,
|
||||
HY,
|
||||
|
|
@ -60,9 +60,9 @@ public enum AppLanguage {
|
|||
MR,
|
||||
MS,
|
||||
MY,
|
||||
NB,
|
||||
NE,
|
||||
NL,
|
||||
NB,
|
||||
OR,
|
||||
PA,
|
||||
PL,
|
||||
|
|
|
|||
|
|
@ -1,173 +0,0 @@
|
|||
package app.revanced.extension.shared.settings;
|
||||
|
||||
import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.preference.PreferenceFragment;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toolbar;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.ResourceType;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.settings.preference.ToolbarPreferenceFragment;
|
||||
import app.revanced.extension.shared.ui.Dim;
|
||||
|
||||
/**
|
||||
* Base class for hooking activities to inject a custom PreferenceFragment with a toolbar.
|
||||
* Provides common logic for initializing the activity and setting up the toolbar.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
public abstract class BaseActivityHook extends Activity {
|
||||
|
||||
private static final int ID_REVANCED_SETTINGS_FRAGMENTS =
|
||||
getResourceIdentifierOrThrow(ResourceType.ID, "revanced_settings_fragments");
|
||||
private static final int ID_REVANCED_TOOLBAR_PARENT =
|
||||
getResourceIdentifierOrThrow(ResourceType.ID, "revanced_toolbar_parent");
|
||||
public static final int LAYOUT_REVANCED_SETTINGS_WITH_TOOLBAR =
|
||||
getResourceIdentifierOrThrow(ResourceType.LAYOUT, "revanced_settings_with_toolbar");
|
||||
private static final int STRING_REVANCED_SETTINGS_TITLE =
|
||||
getResourceIdentifierOrThrow(ResourceType.STRING, "revanced_settings_title");
|
||||
|
||||
/**
|
||||
* Layout parameters for the toolbar, extracted from the dummy toolbar.
|
||||
*/
|
||||
protected static ViewGroup.LayoutParams toolbarLayoutParams;
|
||||
|
||||
/**
|
||||
* Sets the layout parameters for the toolbar.
|
||||
*/
|
||||
public static void setToolbarLayoutParams(Toolbar toolbar) {
|
||||
if (toolbarLayoutParams != null) {
|
||||
toolbar.setLayoutParams(toolbarLayoutParams);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the activity by setting the theme, content view and injecting a PreferenceFragment.
|
||||
*/
|
||||
public static void initialize(BaseActivityHook hook, Activity activity) {
|
||||
try {
|
||||
hook.customizeActivityTheme(activity);
|
||||
activity.setContentView(hook.getContentViewResourceId());
|
||||
|
||||
// Sanity check.
|
||||
String dataString = activity.getIntent().getDataString();
|
||||
if (!"revanced_settings_intent".equals(dataString)) {
|
||||
Logger.printException(() -> "Unknown intent: " + dataString);
|
||||
return;
|
||||
}
|
||||
|
||||
PreferenceFragment fragment = hook.createPreferenceFragment();
|
||||
hook.createToolbar(activity, fragment);
|
||||
|
||||
activity.getFragmentManager()
|
||||
.beginTransaction()
|
||||
.replace(ID_REVANCED_SETTINGS_FRAGMENTS, fragment)
|
||||
.commit();
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "initialize failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* Overrides the ReVanced settings language.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public static Context getAttachBaseContext(Context original) {
|
||||
AppLanguage language = BaseSettings.REVANCED_LANGUAGE.get();
|
||||
if (language == AppLanguage.DEFAULT) {
|
||||
return original;
|
||||
}
|
||||
|
||||
return Utils.getContext();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and configures a toolbar for the activity, replacing a dummy placeholder.
|
||||
*/
|
||||
@SuppressLint("UseCompatLoadingForDrawables")
|
||||
protected void createToolbar(Activity activity, PreferenceFragment fragment) {
|
||||
// Replace dummy placeholder toolbar.
|
||||
// This is required to fix submenu title alignment issue with Android ASOP 15+
|
||||
ViewGroup toolbarParent = activity.findViewById(ID_REVANCED_TOOLBAR_PARENT);
|
||||
ViewGroup dummyToolbar = Utils.getChildViewByResourceName(toolbarParent, "revanced_toolbar");
|
||||
toolbarLayoutParams = dummyToolbar.getLayoutParams();
|
||||
toolbarParent.removeView(dummyToolbar);
|
||||
|
||||
// Sets appropriate system navigation bar color for the activity.
|
||||
ToolbarPreferenceFragment.setNavigationBarColor(activity.getWindow());
|
||||
|
||||
Toolbar toolbar = new Toolbar(toolbarParent.getContext());
|
||||
toolbar.setBackgroundColor(getToolbarBackgroundColor());
|
||||
toolbar.setNavigationIcon(getNavigationIcon());
|
||||
toolbar.setNavigationOnClickListener(getNavigationClickListener(activity));
|
||||
toolbar.setTitle(STRING_REVANCED_SETTINGS_TITLE);
|
||||
|
||||
toolbar.setTitleMarginStart(Dim.dp16);
|
||||
toolbar.setTitleMarginEnd(Dim.dp16);
|
||||
TextView toolbarTextView = Utils.getChildView(toolbar, false, view -> view instanceof TextView);
|
||||
if (toolbarTextView != null) {
|
||||
toolbarTextView.setTextColor(Utils.getAppForegroundColor());
|
||||
toolbarTextView.setTextSize(20);
|
||||
}
|
||||
setToolbarLayoutParams(toolbar);
|
||||
|
||||
onPostToolbarSetup(activity, toolbar, fragment);
|
||||
|
||||
toolbarParent.addView(toolbar, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the resource ID for the content view layout.
|
||||
*/
|
||||
protected int getContentViewResourceId() {
|
||||
return LAYOUT_REVANCED_SETTINGS_WITH_TOOLBAR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Customizes the activity's theme.
|
||||
*/
|
||||
protected abstract void customizeActivityTheme(Activity activity);
|
||||
|
||||
/**
|
||||
* Returns the background color for the toolbar.
|
||||
*/
|
||||
protected abstract int getToolbarBackgroundColor();
|
||||
|
||||
/**
|
||||
* Returns the navigation icon drawable for the toolbar.
|
||||
*/
|
||||
protected abstract Drawable getNavigationIcon();
|
||||
|
||||
/**
|
||||
* Returns the click listener for the toolbar's navigation icon.
|
||||
*/
|
||||
protected abstract View.OnClickListener getNavigationClickListener(Activity activity);
|
||||
|
||||
/**
|
||||
* Creates the PreferenceFragment to be injected into the activity.
|
||||
*/
|
||||
protected PreferenceFragment createPreferenceFragment() {
|
||||
return new ToolbarPreferenceFragment();
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs additional setup after the toolbar is configured.
|
||||
*
|
||||
* @param activity The activity hosting the toolbar.
|
||||
* @param toolbar The configured toolbar.
|
||||
* @param fragment The PreferenceFragment associated with the activity.
|
||||
*/
|
||||
protected void onPostToolbarSetup(Activity activity, Toolbar toolbar, PreferenceFragment fragment) {}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue