Compare commits

..

1 commit

Author SHA1 Message Date
Markus Probst
fc29c18c88
fixup! feat: Add Spoof app signature patch (#5158) 2025-09-12 17:54:19 +02:00
1313 changed files with 94711 additions and 136133 deletions

3
.editorconfig Normal file
View file

@ -0,0 +1,3 @@
[*.{kt,kts}]
ktlint_code_style = intellij_idea
ktlint_standard_no-wildcard-imports = disabled

View file

@ -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). - **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). - **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). - **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 - type: textarea
attributes: attributes:

View file

@ -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). - **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). - **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). - **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 - type: textarea
attributes: attributes:

View file

@ -2,10 +2,6 @@ name: Build pull request
on: on:
workflow_dispatch: workflow_dispatch:
inputs:
pr:
description: "PR to build"
required: true
pull_request: pull_request:
branches: branches:
- dev - dev
@ -14,17 +10,12 @@ jobs:
release: release:
name: Build name: Build
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
with:
ref: ${{ inputs.pr && format('refs/pull/{0}/merge', inputs.pr) || github.ref }}
- name: Setup Java - name: Setup Java
uses: actions/setup-java@v5 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: '17' java-version: '17'
@ -34,13 +25,11 @@ jobs:
- name: Build - name: Build
env: env:
ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ github.actor }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew :patches:buildAndroid --no-daemon run: ./gradlew :patches:buildAndroid --no-daemon
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v4
with: with:
name: revanced-patches name: revanced-patches
path: patches/build/libs path: patches/build/libs
archive: false

View file

@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
- name: Open pull request - name: Open pull request
uses: repo-sync/pull-request@v2 uses: repo-sync/pull-request@v2

View file

@ -1,6 +1,8 @@
name: Pull strings name: Pull strings
on: on:
schedule:
- cron: "0 */12 * * *"
workflow_dispatch: workflow_dispatch:
jobs: jobs:
@ -12,10 +14,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
with: with:
ref: dev ref: dev
persist-credentials: true clean: true
- name: Pull strings - name: Pull strings
uses: crowdin/github-action@v2 uses: crowdin/github-action@v2
@ -23,29 +25,16 @@ jobs:
config: crowdin.yml config: crowdin.yml
upload_sources: false upload_sources: false
download_translations: true download_translations: true
push_translations: false
skip_ref_checkout: true skip_ref_checkout: true
localization_branch_name: feat/translations
create_pull_request: false
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
- name: 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 - name: Open pull request
if: github.event_name == 'workflow_dispatch'
uses: repo-sync/pull-request@v2 uses: repo-sync/pull-request@v2
with: with:
source_branch: feat/translations source_branch: feat/translations

View file

@ -14,13 +14,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
- name: Process strings - name: Preprocess strings
env: env:
ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ github.actor }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }} run: ./gradlew clean preprocessCrowdinStrings
run: ./gradlew processStringsForCrowdin
- name: Push strings - name: Push strings
uses: crowdin/github-action@v2 uses: crowdin/github-action@v2

View file

@ -15,14 +15,13 @@ jobs:
packages: write packages: write
id-token: write id-token: write
attestations: write attestations: write
artifact-metadata: write
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
- name: Setup Java - name: Setup Java
uses: actions/setup-java@v5 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: '17' java-version: '17'
@ -32,12 +31,11 @@ jobs:
- name: Build - name: Build
env: env:
ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ github.actor }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew :patches:buildAndroid clean run: ./gradlew :patches:buildAndroid clean
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@v4
with: with:
node-version: 'lts/*' node-version: 'lts/*'
cache: 'npm' cache: 'npm'
@ -53,16 +51,14 @@ jobs:
fingerprint: ${{ vars.GPG_FINGERPRINT }} fingerprint: ${{ vars.GPG_FINGERPRINT }}
- name: Release - name: Release
uses: cycjimmy/semantic-release-action@v5 uses: cycjimmy/semantic-release-action@v4
id: release id: release
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ github.actor }}
ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}
- name: Attest - name: Attest
if: steps.release.outputs.new_release_published == 'true' if: steps.release.outputs.new_release_published == 'true'
uses: actions/attest@v4 uses: actions/attest-build-provenance@v2
with: with:
subject-name: 'ReVanced Patches ${{ steps.release.outputs.new_release_git_tag }}' subject-name: 'ReVanced Patches ${{ steps.release.outputs.new_release_git_tag }}'
subject-path: patches/build/libs/patches-*.rvp subject-path: patches/build/libs/patches-*.rvp

View file

@ -10,9 +10,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
- name: Update Gradle Wrapper - name: Update Gradle Wrapper
uses: gradle-update/update-gradle-wrapper-action@v2 uses: gradle-update/update-gradle-wrapper-action@v1
with: with:
target-branch: dev target-branch: dev

File diff suppressed because it is too large Load diff

View file

@ -97,9 +97,9 @@ Thank you for considering contributing to ReVanced Patches. You can find the con
To build ReVanced Patches, you can follow the [ReVanced documentation](https://github.com/ReVanced/revanced-documentation). To build ReVanced Patches, you can follow the [ReVanced documentation](https://github.com/ReVanced/revanced-documentation).
## 📜 License ## 📜 Licence
ReVanced Patches is licensed under the GPLv3 license. Please see the [license file](LICENSE) for more information. ReVanced Patches is licensed under the GPLv3 license. Please see the [license file](LICENSE) for more information.
[tl;dr](https://www.tldrlegal.com/license/gnu-general-public-license-v3-gpl-3) you may copy, distribute and modify ReVanced Patches as long as you track changes/dates in source files. [tl;dr](https://www.tldrlegal.com/license/gnu-general-public-license-v3-gpl-3) you may copy, distribute and modify ReVanced Patches as long as you track changes/dates in source files.
Any modifications to ReVanced Patches must also be made available under the GPL, Any modifications to ReVanced Patches must also be made available under the GPL,
along with build & install instructions. along with build & install instructions.

View file

@ -1,8 +0,0 @@
{
"info": "This is verification file for ads.fund project",
"project": {
"name": "Revanced Patches",
"walletAddress": "0x7ab4091e00363654bf84B34151225742cd92FCE5",
"tokenAddress": "0xadf325f255083a3f3d9a9d01ffb3db52a148d802"
}
}

View file

@ -1,9 +1,8 @@
project_id_env: "CROWDIN_PROJECT_ID" project_id_env: "CROWDIN_PROJECT_ID"
api_token_env: "CROWDIN_PERSONAL_TOKEN" api_token_env: "CROWDIN_PERSONAL_TOKEN"
preserve_hierarchy: true preserve_hierarchy: false
files: files:
- source: patches/src/main/resources/addresources/values/strings.xml - source: patches/src/main/resources/addresources/values/strings.xml
dest: patches.xml
translation: patches/src/main/resources/addresources/values-%android_code%/strings.xml translation: patches/src/main/resources/addresources/values-%android_code%/strings.xml
skip_untranslated_strings: true skip_untranslated_strings: true

View file

@ -1,7 +1,14 @@
android { android {
namespace = "app.revanced.extension"
defaultConfig { defaultConfig {
minSdk = 21 minSdk = 21
} }
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
} }
dependencies { dependencies {

View file

@ -1,6 +1,9 @@
android { android {
defaultConfig { namespace = "app.revanced.extension"
minSdk = 23
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
} }
} }

View file

@ -1,7 +1,14 @@
android { android {
namespace = "app.revanced.extension"
defaultConfig { defaultConfig {
minSdk = 21 minSdk = 21
} }
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
} }
dependencies { dependencies {

View file

@ -31,10 +31,7 @@ public class InternalDataDocumentsProvider extends DocumentsProvider {
private static final String[] directoryColumns = private static final String[] directoryColumns =
{"document_id", "mime_type", "_display_name", "last_modified", "flags", {"document_id", "mime_type", "_display_name", "last_modified", "flags",
"_size", "full_path", "lstat_info"}; "_size", "full_path", "lstat_info"};
@SuppressWarnings("OctalInteger") private static final int S_IFLNK = 0x8000;
private static final int S_IFMT = 0170000;
@SuppressWarnings("OctalInteger")
private static final int S_IFLNK = 0120000;
private String packageName; private String packageName;
private File dataDirectory; private File dataDirectory;
@ -50,7 +47,7 @@ public class InternalDataDocumentsProvider extends DocumentsProvider {
if (root.isDirectory()) { if (root.isDirectory()) {
try { try {
// Only delete recursively if the directory is not a symlink // 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(); File[] files = root.listFiles();
if (files != null) { if (files != null) {
for (File file : files) { for (File file : files) {
@ -327,7 +324,7 @@ public class InternalDataDocumentsProvider extends DocumentsProvider {
sb.append(";"); sb.append(";");
sb.append(lstat.st_gid); sb.append(lstat.st_gid);
// Append symlink target if it is a symlink // 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(";");
sb.append(Os.readlink(path)); sb.append(Os.readlink(path));
} }

View file

@ -1,13 +0,0 @@
android {
defaultConfig {
minSdk = 21
}
buildFeatures {
aidl = true
}
}
dependencies {
compileOnly(libs.annotation)
}

View file

@ -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;
}

View file

@ -1,5 +0,0 @@
package com.google.android.play.core.integrity.protocol;
interface IExpressIntegrityServiceCallback {
oneway void onRequestExpressIntegrityTokenResult(in Bundle result) = 2;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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";
}

View file

@ -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);
}
}

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}

View file

@ -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());
}
}

View file

@ -1,7 +1,14 @@
android { android {
namespace = "app.revanced.extension"
defaultConfig { defaultConfig {
minSdk = 21 minSdk = 21
} }
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
} }
dependencies { dependencies {

View file

@ -1,7 +1,14 @@
android { android {
namespace = "app.revanced.extension"
defaultConfig { defaultConfig {
minSdk = 21 minSdk = 21
} }
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
} }
dependencies { dependencies {

View file

@ -3,9 +3,3 @@ dependencies {
compileOnly(libs.annotation) compileOnly(libs.annotation)
compileOnly(libs.okhttp) compileOnly(libs.okhttp)
} }
android {
defaultConfig {
minSdk = 22
}
}

View file

@ -4,9 +4,3 @@ dependencies {
compileOnly(libs.annotation) compileOnly(libs.annotation)
compileOnly(libs.okhttp) compileOnly(libs.okhttp)
} }
android {
defaultConfig {
minSdk = 21
}
}

View file

@ -1,10 +1,4 @@
dependencies { dependencies {
compileOnly(project(":extensions:shared:library")) compileOnly(project(":extensions:shared:library"))
compileOnly(project(":extensions:cricbuzz:stub")) compileOnly(project(":extensions:cricbuzz:stub"))
} }
android {
defaultConfig {
minSdk = 21
}
}

View file

@ -1,9 +0,0 @@
dependencies {
compileOnly(project(":extensions:shared:library"))
}
android {
defaultConfig {
minSdk = 26
}
}

View file

@ -1 +0,0 @@
<manifest/>

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}
}

View file

@ -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);
}
}

View file

@ -1,9 +1,3 @@
dependencies { dependencies {
compileOnly(project(":extensions:shared:library")) compileOnly(project(":extensions:shared:library"))
} }
android {
defaultConfig {
minSdk = 24
}
}

View file

@ -1,9 +1,3 @@
dependencies {
compileOnly(project(":extensions:shared:library"))
compileOnly(project(":extensions:youtube:stub"))
compileOnly(libs.annotation)
}
android { android {
defaultConfig { defaultConfig {
minSdk = 26 minSdk = 26

View file

@ -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");
}

View file

@ -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();
}
}

View file

@ -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()
);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}
}

View file

@ -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();
}
}

View file

@ -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());
}
}

View file

@ -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);
}
}

View file

@ -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();
}
}

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}
}

View file

@ -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;
}
}

View file

@ -1,10 +0,0 @@
dependencies {
compileOnly(project(":extensions:shared:library"))
compileOnly(project(":extensions:nothingx:stub"))
}
android {
defaultConfig {
minSdk = 23
}
}

View file

@ -1 +0,0 @@
<manifest/>

View file

@ -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();
}
}
}

View file

@ -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
}
}

View file

@ -1 +0,0 @@
<manifest/>

View file

@ -2,9 +2,3 @@ dependencies {
compileOnly(project(":extensions:shared:library")) compileOnly(project(":extensions:shared:library"))
compileOnly(project(":extensions:nunl:stub")) compileOnly(project(":extensions:nunl:stub"))
} }
android {
defaultConfig {
minSdk = 26
}
}

View file

@ -2,9 +2,3 @@ dependencies {
compileOnly(project(":extensions:shared:library")) compileOnly(project(":extensions:shared:library"))
compileOnly(project(":extensions:primevideo:stub")) compileOnly(project(":extensions:primevideo:stub"))
} }
android {
defaultConfig {
minSdk = 21
}
}

View file

@ -16,7 +16,6 @@ import java.util.Arrays;
import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils; import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.ui.Dim;
import com.amazon.video.sdk.player.Player; import com.amazon.video.sdk.player.Player;
@ -65,8 +64,9 @@ public class PlaybackSpeedPatch {
SpeedIconDrawable speedIcon = new SpeedIconDrawable(); SpeedIconDrawable speedIcon = new SpeedIconDrawable();
speedButton.setImageDrawable(speedIcon); speedButton.setImageDrawable(speedIcon);
speedButton.setMinimumWidth(Dim.dp48); int buttonSize = Utils.dipToPixels(48);
speedButton.setMinimumHeight(Dim.dp48); speedButton.setMinimumWidth(buttonSize);
speedButton.setMinimumHeight(buttonSize);
return speedButton; return speedButton;
} }
@ -197,11 +197,11 @@ class SpeedIconDrawable extends Drawable {
@Override @Override
public int getIntrinsicWidth() { public int getIntrinsicWidth() {
return Dim.dp32; return Utils.dipToPixels(32);
} }
@Override @Override
public int getIntrinsicHeight() { public int getIntrinsicHeight() {
return Dim.dp32; return Utils.dipToPixels(32);
} }
} }

View file

@ -1,9 +1,3 @@
dependencies { dependencies {
compileOnly(project(":extensions:reddit:stub")) compileOnly(project(":extensions:reddit:stub"))
} }
android {
defaultConfig {
minSdk = 28
}
}

View file

@ -1,10 +0,0 @@
dependencies {
compileOnly(project(":extensions:shared:library"))
compileOnly(project(":extensions:samsung:radio:stub"))
}
android {
defaultConfig {
minSdk = 26
}
}

View file

@ -1 +0,0 @@
<manifest/>

View file

@ -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]);
}
}

View file

@ -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);
}
}

View file

@ -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
}
}

View file

@ -1 +0,0 @@
<manifest/>

View file

@ -1,7 +0,0 @@
package android.os;
public class SemSystemProperties {
public static String getSalesCode() {
throw new UnsupportedOperationException("Stub");
}
}

View file

@ -2,9 +2,3 @@ dependencies {
implementation(project(":extensions:shared:library")) implementation(project(":extensions:shared:library"))
compileOnly(libs.okhttp) compileOnly(libs.okhttp)
} }
android {
defaultConfig {
minSdk = 23
}
}

View file

@ -1,5 +1,5 @@
plugins { plugins {
alias(libs.plugins.android.library) alias(libs.plugins.android.library)
} }
android { android {
@ -19,6 +19,4 @@ android {
dependencies { dependencies {
compileOnly(libs.annotation) compileOnly(libs.annotation)
compileOnly(libs.okhttp) compileOnly(libs.okhttp)
compileOnly(libs.protobuf.javalite)
implementation(project(":extensions:shared:protobuf", configuration = "shadowRuntimeElements"))
} }

View file

@ -1,5 +1,8 @@
package app.revanced.extension.shared; 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.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
import android.app.Dialog; import android.app.Dialog;
@ -17,396 +20,237 @@ import android.widget.LinearLayout;
import androidx.annotation.Nullable; 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.HttpURLConnection;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.util.Locale; import java.util.Locale;
import static app.revanced.extension.shared.StringRef.str; import app.revanced.extension.shared.requests.Requester;
import static app.revanced.extension.shared.requests.Route.Method.GET; import app.revanced.extension.shared.requests.Route;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class GmsCoreSupport { 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 { private static final String GMS_CORE_PACKAGE_NAME
for (GmsCore core : GmsCore.values()) { = getGmsCoreVendorGroupId() + ".android.gms";
if (core.getGroupId().equals(getGmsCoreVendorGroupId())) { private static final Uri GMS_CORE_PROVIDER
GmsCoreSupport.gmsCore = core; = Uri.parse("content://" + getGmsCoreVendorGroupId() + ".android.gsf.gservices/prefix");
break; 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. * Injection point.
*/ */
public static void checkGmsCore(Activity context) { 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() { // Do not exit. If the app exits before launch completes (and without
return null; // Modified during patching. // 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.
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;
} }
}),
UNKNOWN(getGmsCoreVendorGroupId(), getGmsCoreVendorGroupId() + "android.gms", () -> null);
private static final String DONT_KILL_MY_APP_URL // Verify GmsCore is installed.
= "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) {
try { try {
// Verify the user has not included GmsCore for a root installation. PackageManager manager = context.getPackageManager();
// GmsCore Support changes the package name, but with a mounted installation manager.getPackageInfo(GMS_CORE_PACKAGE_NAME, PackageManager.GET_ACTIVITIES);
// all manifest changes are ignored and the original package name is used. } catch (PackageManager.NameNotFoundException exception) {
if (isPackageNameOriginal()) { Logger.printInfo(() -> "GmsCore was not found");
Logger.printInfo(() -> "App is mounted with root, but GmsCore patch was included"); // Cannot show a dialog and must show a toast,
// Cannot use localize text here, since the app will load resources // because on some installations the app crashes before a dialog can be displayed.
// from the unpatched app and all patch strings are missing. Utils.showToastLong(str("gms_core_toast_not_installed_message"));
Utils.showToastLong("The 'GmsCore support' patch breaks mount installations"); open(getGmsCoreDownload());
// 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");
return; return;
} }
Utils.runOnBackgroundThread(() -> { // Check if GmsCore is whitelisted from battery optimizations.
try { if (isAndroidAutomotive(context)) {
PackageManager manager = context.getPackageManager(); // Ignore Android Automotive devices (Google built-in),
var installedVersion = manager.getPackageInfo(packageName, 0).versionName; // 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. showBatteryOptimizationDialog(context,
int suffixIndex = installedVersion.indexOf('-'); "gms_core_dialog_not_whitelisted_using_battery_optimizations_message",
if (suffixIndex != -1) "gms_core_dialog_continue_text",
installedVersion = installedVersion.substring(0, suffixIndex); (dialog, id) -> openGmsCoreDisableBatteryOptimizationsIntent(context));
String finalInstalledVersion = installedVersion; return;
}
Logger.printDebug(() -> "Installed GmsCore version: " + finalInstalledVersion); // Check if GmsCore is currently running in the background.
var client = context.getContentResolver().acquireContentProviderClient(GMS_CORE_PROVIDER);
var latestVersion = getLatestVersion.get(); //noinspection TryFinallyCanBeTryWithResources
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;
try { try {
// Check if queryOrLink is a valid URL. if (client == null) {
new URL(queryOrLink); Logger.printInfo(() -> "GmsCore is not running in the background");
checkIfDontKillMyAppSupportsManufacturer();
intent = new Intent(Intent.ACTION_VIEW, Uri.parse(queryOrLink)); showBatteryOptimizationDialog(context,
} catch (MalformedURLException e) { "gms_core_dialog_not_whitelisted_not_allowed_in_background_message",
intent = new Intent(Intent.ACTION_WEB_SEARCH); "gms_core_dialog_open_website_text",
intent.putExtra(SearchManager.QUERY, queryOrLink); (dialog, id) -> openDontKillMyApp());
}
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);
} }
}, 100); } finally {
} if (client != null) client.close();
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";
} }
} catch (Exception ex) {
open(DONT_KILL_MY_APP_URL + manufacturerPageToOpen + DONT_KILL_MY_APP_NAME_PARAMETER); Logger.printException(() -> "checkGmsCore failure", ex);
}
/**
* @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);
} }
} }
@FunctionalInterface @SuppressLint("BatteryLife") // Permission is part of GmsCore
private interface GetLatestVersion { private static void openGmsCoreDisableBatteryOptimizationsIntent(Activity activity) {
String get(); 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";
} }
} }

View file

@ -16,8 +16,8 @@ import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.preference.LogBufferManager; import app.revanced.extension.shared.settings.preference.LogBufferManager;
/** /**
* ReVanced specific logger. Logging is done to standard device log (accessible through ADB), * ReVanced specific logger. Logging is done to standard device log (accessible thru ADB),
* and additionally accessible through {@link LogBufferManager}. * and additionally accessible thru {@link LogBufferManager}.
* *
* All methods are thread safe, and are safe to call even * All methods are thread safe, and are safe to call even
* if {@link Utils#getContext()} is not available. * 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. * Logs exceptions under the outer class name of the code calling this method.
* <p> * <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)} * instead use {@link #printInfo(LogMessage, Exception)}
* *
* @param message log message * @param message log message

View file

@ -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;
}
}

View file

@ -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 * @param value value which toString() method returns when invoked on returned object
* @return Unique StringRef instance, its value will never change * @return Unique StringRef instance, its value will never change
@ -102,7 +102,7 @@ public class StringRef {
public String toString() { public String toString() {
if (!resolved) { if (!resolved) {
if (resources == null || packageName == null) { if (resources == null || packageName == null) {
var context = Utils.getContext(); Context context = Utils.getContext();
resources = context.getResources(); resources = context.getResources();
packageName = context.getPackageName(); packageName = context.getPackageName();
} }

View file

@ -3,13 +3,14 @@ package app.revanced.extension.shared.checks;
import static android.text.Html.FROM_HTML_MODE_COMPACT; import static android.text.Html.FROM_HTML_MODE_COMPACT;
import static app.revanced.extension.shared.StringRef.str; import static app.revanced.extension.shared.StringRef.str;
import static app.revanced.extension.shared.Utils.DialogFragmentOnStartAction; 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.Activity;
import android.app.Dialog; import android.app.Dialog;
import android.content.Intent; import android.content.Intent;
import android.graphics.PorterDuff; import android.graphics.PorterDuff;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.text.Html; import android.text.Html;
import android.util.Pair; import android.util.Pair;
import android.view.Gravity; import android.view.Gravity;
@ -19,17 +20,13 @@ import android.widget.ImageView;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import java.util.Collection; import java.util.Collection;
import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils; import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.BaseSettings; import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.ui.CustomDialog;
@RequiresApi(api = Build.VERSION_CODES.N)
abstract class Check { abstract class Check {
private static final int NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING = 2; private static final int NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING = 2;
@ -78,6 +75,7 @@ abstract class Check {
BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(Integer.MAX_VALUE); BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(Integer.MAX_VALUE);
} }
@SuppressLint("NewApi")
static void issueWarning(Activity activity, Collection<Check> failedChecks) { static void issueWarning(Activity activity, Collection<Check> failedChecks) {
final var reasons = new StringBuilder(); final var reasons = new StringBuilder();
@ -95,7 +93,7 @@ abstract class Check {
Utils.runOnMainThreadDelayed(() -> { Utils.runOnMainThreadDelayed(() -> {
// Create the custom dialog. // Create the custom dialog.
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create( Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
activity, activity,
str("revanced_check_environment_failed_title"), // Title. str("revanced_check_environment_failed_title"), // Title.
message, // Message. message, // Message.
@ -129,8 +127,7 @@ abstract class Check {
// Add icon to the dialog. // Add icon to the dialog.
ImageView iconView = new ImageView(activity); ImageView iconView = new ImageView(activity);
iconView.setImageResource(Utils.getResourceIdentifierOrThrow( iconView.setImageResource(Utils.getResourceIdentifier("revanced_ic_dialog_alert", "drawable"));
ResourceType.DRAWABLE, "revanced_ic_dialog_alert"));
iconView.setColorFilter(Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN); iconView.setColorFilter(Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN);
iconView.setPadding(0, 0, 0, 0); iconView.setPadding(0, 0, 0, 0);
LinearLayout.LayoutParams iconParams = new LinearLayout.LayoutParams( LinearLayout.LayoutParams iconParams = new LinearLayout.LayoutParams(
@ -161,8 +158,8 @@ abstract class Check {
Button ignoreButton; Button ignoreButton;
// Check if buttons are in a single-row layout (buttonContainer has one child: rowContainer). // Check if buttons are in a single-row layout (buttonContainer has one child: rowContainer).
if (buttonContainer.getChildCount() == 1 if (buttonContainer.getChildCount() == 1 && buttonContainer.getChildAt(0) instanceof LinearLayout) {
&& buttonContainer.getChildAt(0) instanceof LinearLayout rowContainer) { LinearLayout rowContainer = (LinearLayout) buttonContainer.getChildAt(0);
// Neutral button is the first child (index 0). // Neutral button is the first child (index 0).
ignoreButton = (Button) rowContainer.getChildAt(0); ignoreButton = (Button) rowContainer.getChildAt(0);
// OK button is the last child. // OK button is the last child.

View file

@ -7,12 +7,8 @@ import android.content.pm.PackageInfo;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.os.Build; import android.os.Build;
import android.util.Base64; import android.util.Base64;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils; import app.revanced.extension.shared.Utils;
@ -31,7 +27,6 @@ import static app.revanced.extension.shared.checks.PatchInfo.Build.*;
* <br> * <br>
* Various indicators help to detect if the app was patched by the user. * Various indicators help to detect if the app was patched by the user.
*/ */
@RequiresApi(api = Build.VERSION_CODES.N)
@SuppressWarnings("unused") @SuppressWarnings("unused")
public final class CheckEnvironmentPatch { public final class CheckEnvironmentPatch {
private static final boolean DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG = debugAlwaysShowWarning(); private static final boolean DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG = debugAlwaysShowWarning();
@ -44,7 +39,6 @@ public final class CheckEnvironmentPatch {
ADB((String) null), ADB((String) null),
ROOT_MOUNT_ON_APP_STORE("com.android.vending"), ROOT_MOUNT_ON_APP_STORE("com.android.vending"),
MANAGER("app.revanced.manager.flutter", MANAGER("app.revanced.manager.flutter",
"app.revanced.manager.flutter.debug",
"app.revanced.manager", "app.revanced.manager",
"app.revanced.manager.debug"); "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. * If the build properties are different, the app was likely downloaded pre-patched or patched on another device.
*/ */
private static class CheckWasPatchedOnSameDevice extends Check { private static class CheckWasPatchedOnSameDevice extends Check {
@SuppressLint("HardwareIds") @SuppressLint({"NewApi", "HardwareIds"})
@Override @Override
protected Boolean check() { protected Boolean check() {
if (PATCH_BOARD.isEmpty()) { if (PATCH_BOARD.isEmpty()) {
@ -198,7 +192,7 @@ public final class CheckEnvironmentPatch {
PackageManager packageManager = context.getPackageManager(); PackageManager packageManager = context.getPackageManager();
PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0); 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; durationBetweenPatchingAndInstallation = packageInfo.lastUpdateTime - PatchInfo.PATCH_TIME;
Logger.printInfo(() -> "App was installed/updated: " Logger.printInfo(() -> "App was installed/updated: "
+ (durationBetweenPatchingAndInstallation / (60 * 1000) + " minutes after patching")); + (durationBetweenPatchingAndInstallation / (60 * 1000) + " minutes after patching"));
@ -294,8 +288,8 @@ public final class CheckEnvironmentPatch {
CheckIsNearPatchTime nearPatchTime = new CheckIsNearPatchTime(); CheckIsNearPatchTime nearPatchTime = new CheckIsNearPatchTime();
Boolean timeCheckPassed = nearPatchTime.check(); Boolean timeCheckPassed = nearPatchTime.check();
if (timeCheckPassed && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) { if (timeCheckPassed && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) {
// Allow installing recently patched APKs, // Allow installing recently patched apks,
// even if the installation source is not Manager or ADB. // even if the install source is not Manager or ADB.
Check.disableForever(); Check.disableForever();
return; return;
} else { } else {

View file

@ -15,6 +15,7 @@ import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
import okhttp3.ResponseBody; import okhttp3.ResponseBody;
public abstract class BaseFixRedgifsApiPatch implements Interceptor { public abstract class BaseFixRedgifsApiPatch implements Interceptor {
protected static BaseFixRedgifsApiPatch INSTANCE; protected static BaseFixRedgifsApiPatch INSTANCE;
public abstract String getDefaultUserAgent(); public abstract String getDefaultUserAgent();

View file

@ -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);
}
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}
}

View file

@ -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();
}
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}
}

View file

@ -23,8 +23,8 @@ public class Requester {
public static HttpURLConnection getConnectionFromCompiledRoute(String apiUrl, Route.CompiledRoute route) throws IOException { public static HttpURLConnection getConnectionFromCompiledRoute(String apiUrl, Route.CompiledRoute route) throws IOException {
String url = apiUrl + route.getCompiledRoute(); String url = apiUrl + route.getCompiledRoute();
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
// This request sends data via URL query parameters. No request body is included. // Request data is in the URL parameters and no body is sent.
// If a request body is added, the caller must set the appropriate Content-Length header. // The calling code must set a length if using a request body.
connection.setFixedLengthStreamingMode(0); connection.setFixedLengthStreamingMode(0);
connection.setRequestMethod(route.getMethod().name()); connection.setRequestMethod(route.getMethod().name());
String agentString = System.getProperty("http.agent") String agentString = System.getProperty("http.agent")

View file

@ -36,8 +36,8 @@ public enum AppLanguage {
FR, FR,
GL, GL,
GU, GU,
HE, // App uses obsolete 'IW' and not the modern 'HE' ISO code.
HI, HI,
HE, // App uses obsolete 'IW' and not the modern 'HE' ISO code.
HR, HR,
HU, HU,
HY, HY,
@ -60,9 +60,9 @@ public enum AppLanguage {
MR, MR,
MS, MS,
MY, MY,
NB,
NE, NE,
NL, NL,
NB,
OR, OR,
PA, PA,
PL, PL,

View file

@ -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