diff --git a/CHANGELOG.md b/CHANGELOG.md index e4a594855..b70fb7efc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,40 @@ +# [5.48.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.8...v5.48.0-dev.9) (2026-01-08) + + +### Features + +* Add `Disable Sentry telemetry` patch ([#6416](https://github.com/ReVanced/revanced-patches/issues/6416)) ([4cc3159](https://github.com/ReVanced/revanced-patches/commit/4cc315952db557c565872de9e8484805f2e42305)) +* Disable Play Integrity patch ([#6412](https://github.com/ReVanced/revanced-patches/issues/6412)) ([6312fe8](https://github.com/ReVanced/revanced-patches/commit/6312fe8d60da24465c0c1b0fa4e94ceb79873d9c)) + +# [5.48.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.7...v5.48.0-dev.8) (2026-01-04) + + +### Features + +* **Letterboxd:** Add `Unlock app icons` patch ([#6415](https://github.com/ReVanced/revanced-patches/issues/6415)) ([d25dcfe](https://github.com/ReVanced/revanced-patches/commit/d25dcfe49ac331c9b3dca739ba0be95dbab669cc)) + +# [5.48.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.6...v5.48.0-dev.7) (2026-01-04) + + +### Features + +* **Strava:** Add `Disable Quick Edit` patch ([#6452](https://github.com/ReVanced/revanced-patches/issues/6452)) ([f5cbb31](https://github.com/ReVanced/revanced-patches/commit/f5cbb31724d15f7e939b96ee0186fd0a108f9fdc)) +* **Strava:** Add `Overwrite media upload parameters` patch ([#6410](https://github.com/ReVanced/revanced-patches/issues/6410)) ([b42ae27](https://github.com/ReVanced/revanced-patches/commit/b42ae27ce66ebad9e9cfc5b70fc121df5bad7567)) + +# [5.48.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.5...v5.48.0-dev.6) (2026-01-04) + + +### Bug Fixes + +* Fix build error introduced in `4046bee` ([#6417](https://github.com/ReVanced/revanced-patches/issues/6417)) ([789f0a5](https://github.com/ReVanced/revanced-patches/commit/789f0a562861825065633d172445ebf35a1ba8d8)) + +# [5.48.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.4...v5.48.0-dev.5) (2025-12-30) + + +### Bug Fixes + +* **Disney+ - Skip ads:** Remove unsupported package names ([#6422](https://github.com/ReVanced/revanced-patches/issues/6422)) ([44e7dbc](https://github.com/ReVanced/revanced-patches/commit/44e7dbcf4d7eaf94dd0164baba847d3e19250154)) + # [5.48.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.3...v5.48.0-dev.4) (2025-12-29) diff --git a/crowdin.yml b/crowdin.yml index 148f321cd..81022c88c 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -1,8 +1,9 @@ project_id_env: "CROWDIN_PROJECT_ID" api_token_env: "CROWDIN_PERSONAL_TOKEN" -preserve_hierarchy: false +preserve_hierarchy: true files: - source: patches/src/main/resources/addresources/values/strings.xml + dest: patches.xml translation: patches/src/main/resources/addresources/values-%android_code%/strings.xml skip_untranslated_strings: true diff --git a/extensions/all/misc/disable-play-integrity/build.gradle.kts b/extensions/all/misc/disable-play-integrity/build.gradle.kts new file mode 100644 index 000000000..549297227 --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/build.gradle.kts @@ -0,0 +1,20 @@ +android { + namespace = "app.revanced.extension" + + defaultConfig { + minSdk = 21 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + buildFeatures { + aidl = true + } +} + +dependencies { + compileOnly(libs.annotation) +} diff --git a/extensions/all/misc/disable-play-integrity/src/main/AndroidManifest.xml b/extensions/all/misc/disable-play-integrity/src/main/AndroidManifest.xml new file mode 100644 index 000000000..9b65eb06c --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IExpressIntegrityService.aidl b/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IExpressIntegrityService.aidl new file mode 100644 index 000000000..7b8f59f1d --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IExpressIntegrityService.aidl @@ -0,0 +1,8 @@ +package com.google.android.play.core.integrity.protocol; + +import android.os.Bundle; +import com.google.android.play.core.integrity.protocol.IExpressIntegrityServiceCallback; + +interface IExpressIntegrityService { + oneway void requestIntegrityToken(in Bundle request, IExpressIntegrityServiceCallback callback) = 2; +} diff --git a/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IExpressIntegrityServiceCallback.aidl b/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IExpressIntegrityServiceCallback.aidl new file mode 100644 index 000000000..624167afb --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IExpressIntegrityServiceCallback.aidl @@ -0,0 +1,5 @@ +package com.google.android.play.core.integrity.protocol; + +interface IExpressIntegrityServiceCallback { + oneway void onRequestExpressIntegrityTokenResult(in Bundle result) = 2; +} diff --git a/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityService.aidl b/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityService.aidl new file mode 100644 index 000000000..bb1bcd551 --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityService.aidl @@ -0,0 +1,8 @@ +package com.google.android.play.core.integrity.protocol; + +import android.os.Bundle; +import com.google.android.play.core.integrity.protocol.IIntegrityServiceCallback; + +interface IIntegrityService { + oneway void requestIntegrityToken(in Bundle request, IIntegrityServiceCallback callback) = 1; +} diff --git a/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityServiceCallback.aidl b/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityServiceCallback.aidl new file mode 100644 index 000000000..9485ec169 --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityServiceCallback.aidl @@ -0,0 +1,7 @@ +package com.google.android.play.core.integrity.protocol; + +import android.os.Bundle; + +interface IIntegrityServiceCallback { + oneway void onResult(in Bundle result) = 1; +} diff --git a/extensions/all/misc/disable-play-integrity/src/main/java/android/ext/PackageId.java b/extensions/all/misc/disable-play-integrity/src/main/java/android/ext/PackageId.java new file mode 100644 index 000000000..31c2ca6db --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/java/android/ext/PackageId.java @@ -0,0 +1,10 @@ +package android.ext; +/** @hide */ +// Int values that are assigned to packages in this interface can be retrieved at runtime from +// ApplicationInfo.ext().getPackageId() or from AndroidPackage.ext().getPackageId() (in system_server). +// +// PackageIds are assigned to parsed APKs only after they are verified, either by a certificate check +// or by a check that the APK is stored on an immutable OS partition. +public interface PackageId { + String PLAY_STORE_NAME = "com.android.vending"; +} diff --git a/extensions/all/misc/disable-play-integrity/src/main/java/android/os/BinderWrapper.java b/extensions/all/misc/disable-play-integrity/src/main/java/android/os/BinderWrapper.java new file mode 100644 index 000000000..a01806441 --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/java/android/os/BinderWrapper.java @@ -0,0 +1,62 @@ +package android.os; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.io.FileDescriptor; + +/** @hide */ +public class BinderWrapper implements IBinder { + protected final IBinder base; + + public BinderWrapper(IBinder base) { + this.base = base; + } + + @Override + public boolean transact(int code, @NonNull Parcel data, @Nullable Parcel reply, int flags) throws RemoteException { + return base.transact(code, data, reply, flags); + } + + @Nullable + @Override + public IInterface queryLocalInterface(@NonNull String descriptor) { + return base.queryLocalInterface(descriptor); + } + + @Nullable + @Override + public String getInterfaceDescriptor() throws RemoteException { + return base.getInterfaceDescriptor(); + } + + @Override + public boolean pingBinder() { + return base.pingBinder(); + } + + @Override + public boolean isBinderAlive() { + return base.isBinderAlive(); + } + + @Override + public void dump(@NonNull FileDescriptor fd, @Nullable String[] args) throws RemoteException { + base.dump(fd, args); + } + + @Override + public void dumpAsync(@NonNull FileDescriptor fd, @Nullable String[] args) throws RemoteException { + base.dumpAsync(fd, args); + } + + @Override + public void linkToDeath(@NonNull DeathRecipient recipient, int flags) throws RemoteException { + base.linkToDeath(recipient, flags); + } + + @Override + public boolean unlinkToDeath(@NonNull DeathRecipient recipient, int flags) { + return base.unlinkToDeath(recipient, flags); + } +} diff --git a/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/ClassicPlayIntegrityServiceWrapper.java b/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/ClassicPlayIntegrityServiceWrapper.java new file mode 100644 index 000000000..3bd88d2a6 --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/ClassicPlayIntegrityServiceWrapper.java @@ -0,0 +1,41 @@ +package app.grapheneos.gmscompat.lib.playintegrity; + +import android.os.Binder; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; + +import com.android.internal.os.FakeBackgroundHandler; +import com.google.android.play.core.integrity.protocol.IIntegrityService; +import com.google.android.play.core.integrity.protocol.IIntegrityServiceCallback; + +class ClassicPlayIntegrityServiceWrapper extends PlayIntegrityServiceWrapper { + + ClassicPlayIntegrityServiceWrapper(IBinder base) { + super(base); + requestIntegrityTokenTxnCode = 2; // IIntegrityService.Stub.TRANSACTION_requestIntegrityToken + } + + static class TokenRequestStub extends IIntegrityService.Stub { + public void requestIntegrityToken(Bundle request, IIntegrityServiceCallback callback) { + Runnable r = () -> { + var result = new Bundle(); + // https://developer.android.com/google/play/integrity/reference/com/google/android/play/core/integrity/model/IntegrityErrorCode.html#API_NOT_AVAILABLE + final int API_NOT_AVAILABLE = -1; + result.putInt("error", API_NOT_AVAILABLE); + try { + callback.onResult(result); + } catch (RemoteException e) { + Log.e("IIntegrityService.Stub", "", e); + } + }; + FakeBackgroundHandler.getHandler().postDelayed(r, getTokenRequestResultDelay()); + } + }; + + @Override + protected Binder createTokenRequestStub() { + return new TokenRequestStub(); + } +} diff --git a/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/PlayIntegrityServiceWrapper.java b/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/PlayIntegrityServiceWrapper.java new file mode 100644 index 000000000..0418b4fe7 --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/PlayIntegrityServiceWrapper.java @@ -0,0 +1,48 @@ +package app.grapheneos.gmscompat.lib.playintegrity; + +import android.os.Binder; +import android.os.BinderWrapper; +import android.os.IBinder; +import android.os.Parcel; +import android.os.RemoteException; +import android.util.Log; + +import androidx.annotation.Nullable; + +abstract class PlayIntegrityServiceWrapper extends BinderWrapper { + final String TAG; + protected int requestIntegrityTokenTxnCode; + + public PlayIntegrityServiceWrapper(IBinder base) { + super(base); + TAG = getClass().getSimpleName(); + } + + protected abstract Binder createTokenRequestStub(); + + @Override + public boolean transact(int code, Parcel data, @Nullable Parcel reply, int flags) throws RemoteException { + if (code == requestIntegrityTokenTxnCode) { + if (maybeStubOutIntegrityTokenRequest(code, data, reply, flags)) { + return true; + } + } + return super.transact(code, data, reply, flags); + } + + private boolean maybeStubOutIntegrityTokenRequest(int code, Parcel data, @Nullable Parcel reply, int flags) { + Log.d(TAG, "integrity token request detected"); + + try { + createTokenRequestStub().transact(code, data, reply, flags); + } catch (RemoteException e) { + // this is a local call + throw new IllegalStateException(e); + } + return true; + } + + protected static long getTokenRequestResultDelay() { + return 500L; + } +} diff --git a/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/PlayIntegrityUtils.java b/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/PlayIntegrityUtils.java new file mode 100644 index 000000000..6ff4720cc --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/PlayIntegrityUtils.java @@ -0,0 +1,35 @@ +package app.grapheneos.gmscompat.lib.playintegrity; + +import android.content.Intent; +import android.content.ServiceConnection; +import android.ext.PackageId; +import android.os.IBinder; +import androidx.annotation.Nullable; +import app.grapheneos.gmscompat.lib.util.ServiceConnectionWrapper; +import java.util.function.UnaryOperator; + +public class PlayIntegrityUtils { + + public static @Nullable ServiceConnection maybeReplaceServiceConnection(Intent service, ServiceConnection orig) { + if (PackageId.PLAY_STORE_NAME.equals(service.getPackage())) { + UnaryOperator 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; + } +} diff --git a/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/StandardPlayIntegrityServiceWrapper.java b/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/StandardPlayIntegrityServiceWrapper.java new file mode 100644 index 000000000..c1c4937f0 --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/StandardPlayIntegrityServiceWrapper.java @@ -0,0 +1,42 @@ +package app.grapheneos.gmscompat.lib.playintegrity; + +import android.annotation.SuppressLint; +import android.os.Binder; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; +import com.android.internal.os.FakeBackgroundHandler; +import com.google.android.play.core.integrity.protocol.IExpressIntegrityService; +import com.google.android.play.core.integrity.protocol.IExpressIntegrityServiceCallback; + +@SuppressLint("LongLogTag") +class StandardPlayIntegrityServiceWrapper extends PlayIntegrityServiceWrapper { + + StandardPlayIntegrityServiceWrapper(IBinder base) { + super(base); + requestIntegrityTokenTxnCode = 3; // IExpressIntegrityService.Stub.TRANSACTION_requestIntegrityToken + } + + static class TokenRequestStub extends IExpressIntegrityService.Stub { + public void requestIntegrityToken(Bundle request, IExpressIntegrityServiceCallback callback) { + Runnable r = () -> { + var result = new Bundle(); + // https://developer.android.com/google/play/integrity/reference/com/google/android/play/core/integrity/model/StandardIntegrityErrorCode.html#API_NOT_AVAILABLE + final int API_NOT_AVAILABLE = -1; + result.putInt("error", API_NOT_AVAILABLE); + try { + callback.onRequestExpressIntegrityTokenResult(result); + } catch (RemoteException e) { + Log.e("IExpressIntegrityService.Stub", "", e); + } + }; + FakeBackgroundHandler.getHandler().postDelayed(r, getTokenRequestResultDelay()); + } + }; + + @Override + protected Binder createTokenRequestStub() { + return new TokenRequestStub(); + } +} diff --git a/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/util/ServiceConnectionWrapper.java b/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/util/ServiceConnectionWrapper.java new file mode 100644 index 000000000..9edfc39f8 --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/util/ServiceConnectionWrapper.java @@ -0,0 +1,49 @@ +package app.grapheneos.gmscompat.lib.util; + +import android.content.ComponentName; +import android.content.ServiceConnection; +import android.os.Build; +import android.os.IBinder; + +import java.util.function.UnaryOperator; + +public class ServiceConnectionWrapper implements ServiceConnection { + private final ServiceConnection base; + private final UnaryOperator binderOverride; + + public ServiceConnectionWrapper(ServiceConnection base, UnaryOperator 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); + } + } +} diff --git a/extensions/all/misc/disable-play-integrity/src/main/java/app/revanced/extension/playintegrity/DisablePlayIntegrityPatch.java b/extensions/all/misc/disable-play-integrity/src/main/java/app/revanced/extension/playintegrity/DisablePlayIntegrityPatch.java new file mode 100644 index 000000000..a27e56be9 --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/java/app/revanced/extension/playintegrity/DisablePlayIntegrityPatch.java @@ -0,0 +1,17 @@ +package app.revanced.extension.playintegrity; + +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import app.grapheneos.gmscompat.lib.playintegrity.PlayIntegrityUtils; + +public class DisablePlayIntegrityPatch { + public static boolean bindService(Context context, Intent service, ServiceConnection conn, int flags) { + ServiceConnection override = PlayIntegrityUtils.maybeReplaceServiceConnection(service, conn); + if (override != null) { + conn = override; + } + + return context.bindService(service, conn, flags); + } +} diff --git a/extensions/all/misc/disable-play-integrity/src/main/java/com/android/internal/os/FakeBackgroundHandler.java b/extensions/all/misc/disable-play-integrity/src/main/java/com/android/internal/os/FakeBackgroundHandler.java new file mode 100644 index 000000000..6b4cb92b4 --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/java/com/android/internal/os/FakeBackgroundHandler.java @@ -0,0 +1,11 @@ +package com.android.internal.os; + +import android.os.Handler; +import android.os.Looper; + +public class FakeBackgroundHandler { + + public static Handler getHandler() { + return new Handler(Looper.getMainLooper()); + } +} diff --git a/gradle.properties b/gradle.properties index 00a1ebc1d..816fb3e47 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M org.gradle.parallel = true android.useAndroidX = true kotlin.code.style = official -version = 5.48.0-dev.4 +version = 5.48.0-dev.9 diff --git a/patches/api/patches.api b/patches/api/patches.api index 3c0aacd25..f4c66bc75 100644 --- a/patches/api/patches.api +++ b/patches/api/patches.api @@ -100,6 +100,10 @@ public final class app/revanced/patches/all/misc/packagename/ChangePackageNamePa public static final fun setPackageNameOption (Lapp/revanced/patcher/patch/Option;)V } +public final class app/revanced/patches/all/misc/playintegrity/DisablePlayIntegrityKt { + public static final fun getDisablePlayIntegrityPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + public final class app/revanced/patches/all/misc/resources/AddResourcesPatchKt { public static final fun addResource (Ljava/lang/String;Lapp/revanced/util/resource/BaseResource;)Z public static final fun addResources (Lapp/revanced/patcher/patch/Patch;Lkotlin/jvm/functions/Function1;)Z @@ -340,6 +344,10 @@ public final class app/revanced/patches/letterboxd/ads/HideAdsPatchKt { public static final fun getHideAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } +public final class app/revanced/patches/letterboxd/unlock/unlockAppIcons/UnlockAppIconsPatchKt { + public static final fun getUnlockAppIconsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + public final class app/revanced/patches/lightroom/misc/login/DisableMandatoryLoginPatchKt { public static final fun getDisableMandatoryLoginPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } @@ -920,6 +928,10 @@ public final class app/revanced/patches/shared/misc/pairip/license/DisableLicens public static final fun getDisableLicenseCheckPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } +public final class app/revanced/patches/shared/misc/privacy/DisableSentryTelemetryKt { + public static final fun getDisableSentryTelemetryPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + public final class app/revanced/patches/shared/misc/settings/SettingsPatchKt { public static final fun overrideThemeColors (Ljava/lang/String;Ljava/lang/String;)V public static final fun settingsPatch (Ljava/util/List;Ljava/util/Set;)Lapp/revanced/patcher/patch/ResourcePatch; @@ -1149,14 +1161,22 @@ public final class app/revanced/patches/stocard/layout/HideStoryBubblesPatchKt { public static final fun getHideStoryBubblesPatch ()Lapp/revanced/patcher/patch/ResourcePatch; } +public final class app/revanced/patches/strava/mediaupload/OverwriteMediaUploadParametersPatchKt { + public static final fun getOverwriteMediaUploadParametersPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + public final class app/revanced/patches/strava/password/EnablePasswordLoginPatchKt { public static final fun getEnablePasswordLoginPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } -public final class app/revanced/patches/strava/snowplow/BlockSnowplowTrackingPatchKt { +public final class app/revanced/patches/strava/privacy/BlockSnowplowTrackingPatchKt { public static final fun getBlockSnowplowTrackingPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } +public final class app/revanced/patches/strava/quickedit/DisableQuickEditPatchKt { + public static final fun getDisableQuickEditPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + public final class app/revanced/patches/strava/subscription/UnlockSubscriptionPatchKt { public static final fun getUnlockSubscriptionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } diff --git a/patches/src/main/kotlin/app/revanced/patches/all/misc/playintegrity/DisablePlayIntegrity.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/playintegrity/DisablePlayIntegrity.kt new file mode 100644 index 000000000..c7a41cb74 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/playintegrity/DisablePlayIntegrity.kt @@ -0,0 +1,54 @@ +package app.revanced.patches.all.misc.playintegrity + +import app.revanced.patcher.extensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.all.misc.transformation.transformInstructionsPatch +import app.revanced.util.getReference +import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction35c +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.immutable.reference.ImmutableMethodReference +import com.android.tools.smali.dexlib2.util.MethodUtil + +private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/playintegrity/DisablePlayIntegrityPatch;" + +private val CONTEXT_BIND_SERVICE_METHOD_REFERENCE = ImmutableMethodReference( + "Landroid/content/Context;", + "bindService", + listOf("Landroid/content/Intent;", "Landroid/content/ServiceConnection;", "I"), + "Z" +) + + +@Suppress("unused") +val disablePlayIntegrityPatch = bytecodePatch( + name = "Disable Play Integrity", + description = "Prevents apps from using Play Integrity by pretending it is not available.", + use = false, +) { + extendWith("extensions/all/misc/disable-play-integrity.rve") + + dependsOn( + transformInstructionsPatch( + filterMap = filterMap@{ classDef, method, instruction, instructionIndex -> + val reference = instruction + .getReference() + ?.takeIf { + MethodUtil.methodSignaturesMatch(CONTEXT_BIND_SERVICE_METHOD_REFERENCE, it) + } + ?: return@filterMap null + + Triple(instruction as Instruction35c, instructionIndex, reference.parameterTypes) + }, + transform = { method, entry -> + val (instruction, index, parameterTypes) = entry + val parameterString = parameterTypes.joinToString(separator = "") + val registerString = "v${instruction.registerC}, v${instruction.registerD}, v${instruction.registerE}, v${instruction.registerF}" + + method.replaceInstruction( + index, + "invoke-static { $registerString }, $EXTENSION_CLASS_DESCRIPTOR->bindService(Landroid/content/Context;$parameterString)Z" + ) + } + ) + ) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/disneyplus/SkipAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/disneyplus/SkipAdsPatch.kt index 4735d4265..c5e58a48f 100644 --- a/patches/src/main/kotlin/app/revanced/patches/disneyplus/SkipAdsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/disneyplus/SkipAdsPatch.kt @@ -8,11 +8,7 @@ val skipAdsPatch = bytecodePatch( name = "Skip ads", description = "Automatically skips ads.", ) { - compatibleWith( - "com.disney.disneyplus", - "in.startv.hotstar", - "in.startv.hotstaronly", - ) + compatibleWith("com.disney.disneyplus") apply { arrayOf(insertionGetPointsFingerprint, insertionGetRangesFingerprint).forEach { diff --git a/patches/src/main/kotlin/app/revanced/patches/letterboxd/unlock/unlockAppIcons/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/letterboxd/unlock/unlockAppIcons/Fingerprints.kt new file mode 100644 index 000000000..1b549cd57 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/letterboxd/unlock/unlockAppIcons/Fingerprints.kt @@ -0,0 +1,9 @@ +package app.revanced.patches.letterboxd.unlock.unlockAppIcons + +import app.revanced.patcher.fingerprint + +internal val getCanChangeAppIconFingerprint = fingerprint { + custom { method, classDef -> + method.name == "getCanChangeAppIcon" && classDef.type.endsWith("SettingsAppIconFragment;") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/letterboxd/unlock/unlockAppIcons/UnlockAppIconsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/letterboxd/unlock/unlockAppIcons/UnlockAppIconsPatch.kt new file mode 100644 index 000000000..ca8d7ac55 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/letterboxd/unlock/unlockAppIcons/UnlockAppIconsPatch.kt @@ -0,0 +1,15 @@ +package app.revanced.patches.letterboxd.unlock.unlockAppIcons + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.util.returnEarly + +@Suppress("unused") +val unlockAppIconsPatch = bytecodePatch( + name = "Unlock app icons", +) { + compatibleWith("com.letterboxd.letterboxd") + + apply { + getCanChangeAppIconFingerprint.method.returnEarly(true) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/misc/extension/SharedExtensionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/extension/SharedExtensionPatch.kt index 076a62431..f62d738e1 100644 --- a/patches/src/main/kotlin/app/revanced/patches/shared/misc/extension/SharedExtensionPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/extension/SharedExtensionPatch.kt @@ -1,10 +1,10 @@ package app.revanced.patches.shared.misc.extension +import app.revanced.patcher.BytecodePatchContextClassDefMatching.firstMutableClassDef import app.revanced.patcher.Fingerprint import app.revanced.patcher.FingerprintBuilder import app.revanced.patcher.extensions.addInstruction import app.revanced.patcher.fingerprint -import app.revanced.patcher.firstClassDefMutable import app.revanced.patcher.patch.BytecodePatchContext import app.revanced.patcher.patch.bytecodePatch import app.revanced.util.returnEarly @@ -41,7 +41,7 @@ fun sharedExtensionPatch( apply { // Verify the extension class exists. - firstClassDefMutable(EXTENSION_CLASS_DESCRIPTOR) + firstMutableClassDef(EXTENSION_CLASS_DESCRIPTOR) } afterDependents { @@ -86,10 +86,10 @@ fun sharedExtensionPatch( class ExtensionHook internal constructor( internal val fingerprint: Fingerprint, - private val insertIndexResolver: BytecodePatchContext.(Method) -> Int, - private val contextRegisterResolver: BytecodePatchContext.(Method) -> String, + private val insertIndexResolver: context(BytecodePatchContext) (Method) -> Int, + private val contextRegisterResolver: context(BytecodePatchContext) (Method) -> String, ) { - context(BytecodePatchContext) + context(_: BytecodePatchContext) operator fun invoke(extensionClassDescriptor: String) { val insertIndex = insertIndexResolver(fingerprint.method) val contextRegister = contextRegisterResolver(fingerprint.method) @@ -103,16 +103,17 @@ class ExtensionHook internal constructor( } fun extensionHook( - insertIndexResolver: BytecodePatchContext.(Method) -> Int = { 0 }, - contextRegisterResolver: BytecodePatchContext.(Method) -> String = { "p0" }, + insertIndexResolver: context(BytecodePatchContext) (Method) -> Int = { 0 }, + contextRegisterResolver: context(BytecodePatchContext) (Method) -> String = { "p0" }, fingerprint: Fingerprint, ) = ExtensionHook(fingerprint, insertIndexResolver, contextRegisterResolver) fun extensionHook( - insertIndexResolver: BytecodePatchContext.(Method) -> Int = { 0 }, - contextRegisterResolver: BytecodePatchContext.(Method) -> String = { "p0" }, + insertIndexResolver: context(BytecodePatchContext) (Method) -> Int = { 0 }, + contextRegisterResolver: context(BytecodePatchContext) (Method) -> String = { "p0" }, fingerprintBuilderBlock: FingerprintBuilder.() -> Unit, ) = { + -> ExtensionHook(fingerprint(block = fingerprintBuilderBlock), insertIndexResolver, contextRegisterResolver) } diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/misc/privacy/DisableSentryTelemetry.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/privacy/DisableSentryTelemetry.kt new file mode 100644 index 000000000..8dbc7504c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/privacy/DisableSentryTelemetry.kt @@ -0,0 +1,36 @@ +package app.revanced.patches.shared.misc.privacy + +import app.revanced.patcher.patch.resourcePatch +import app.revanced.util.asSequence +import app.revanced.util.getNode +import org.w3c.dom.Element + +@Suppress("unused") +val disableSentryTelemetryPatch = resourcePatch( + name = "Disable Sentry telemetry", + description = "Disables Sentry telemetry. See https://sentry.io/for/android/ for more information.", + use = false, +) { + apply { + fun Element.replaceOrCreate(tagName: String, attributeName: String, attributeValue: String) { + val childElements = getElementsByTagName(tagName).asSequence().filterIsInstance() + val targetChild = childElements.find { childElement -> + childElement.getAttribute("android:name") == attributeName + } + if (targetChild != null) { + targetChild.setAttribute("android:value", attributeValue) + } else { + appendChild(ownerDocument.createElement(tagName).apply { + setAttribute("android:name", attributeName) + setAttribute("android:value", attributeValue) + }) + } + } + + document("AndroidManifest.xml").use { document -> + val application = document.getNode("application") as Element + application.replaceOrCreate("meta-data", "io.sentry.enabled", "false") + application.replaceOrCreate("meta-data", "io.sentry.dsn", "") + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/mediaupload/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/strava/mediaupload/Fingerprints.kt new file mode 100644 index 000000000..7901d6cc5 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/strava/mediaupload/Fingerprints.kt @@ -0,0 +1,21 @@ +package app.revanced.patches.strava.mediaupload + +import app.revanced.patcher.fingerprint + +internal val getCompressionQualityFingerprint = fingerprint { + custom { method, _ -> + method.name == "getCompressionQuality" + } +} + +internal val getMaxDurationFingerprint = fingerprint { + custom { method, _ -> + method.name == "getMaxDuration" + } +} + +internal val getMaxSizeFingerprint = fingerprint { + custom { method, _ -> + method.name == "getMaxSize" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/mediaupload/OverwriteMediaUploadParametersPatch.kt b/patches/src/main/kotlin/app/revanced/patches/strava/mediaupload/OverwriteMediaUploadParametersPatch.kt new file mode 100644 index 000000000..decc0bcab --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/strava/mediaupload/OverwriteMediaUploadParametersPatch.kt @@ -0,0 +1,46 @@ +package app.revanced.patches.strava.mediaupload + +import app.revanced.patcher.BytecodePatchContextClassDefMatching.firstClassDef +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.intOption +import app.revanced.patcher.patch.longOption +import app.revanced.util.returnEarly + +@Suppress("unused") +val overwriteMediaUploadParametersPatch = bytecodePatch( + name = "Overwrite media upload parameters", + description = "Overwrites the compression, resize and trim media (images and videos) parameters returned by Strava's server before upload.", +) { + compatibleWith("com.strava") + + val compressionQuality by intOption( + name = "Compression quality (percent)", + description = "This is used as the JPEG quality setting (≤ 100).", + ) { it == null || it in 1..100 } + + val maxDuration by longOption( + name = "maxDuration", + description = "The maximum length (≤ ${60 * 60}) of a video before it gets trimmed.", + ) { it == null || it in 1..60 * 60 } + + val maxSize by intOption( + name = "Max size (pixels)", + description = "The image gets resized so that the smaller dimension (width/height) does not exceed this value (≤ 10000).", + ) { it == null || it in 1..10000 } + + apply { + val mediaUploadParametersClass = firstClassDef { type.endsWith("/MediaUploadParameters;") } + + compressionQuality?.let { compressionQuality -> + getCompressionQualityFingerprint.match(mediaUploadParametersClass).method.returnEarly(compressionQuality / 100f) + } + + maxDuration?.let { maxDuration -> + getMaxDurationFingerprint.match(mediaUploadParametersClass).method.returnEarly(maxDuration) + } + + maxSize?.let { + getMaxSizeFingerprint.match(mediaUploadParametersClass).method.returnEarly(it) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/password/EnablePasswordLoginPatch.kt b/patches/src/main/kotlin/app/revanced/patches/strava/password/EnablePasswordLoginPatch.kt index 9b6a04ac1..e01a6816b 100644 --- a/patches/src/main/kotlin/app/revanced/patches/strava/password/EnablePasswordLoginPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/strava/password/EnablePasswordLoginPatch.kt @@ -1,8 +1,8 @@ package app.revanced.patches.strava.password import app.revanced.patcher.Fingerprint -import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction import app.revanced.patcher.patch.bytecodePatch +import app.revanced.util.returnEarly @Suppress("unused") val enablePasswordLoginPatch = bytecodePatch( @@ -12,10 +12,9 @@ val enablePasswordLoginPatch = bytecodePatch( compatibleWith("com.strava") apply { - fun Fingerprint.loadTrueInsteadOfField() = - method.replaceInstruction(patternMatch!!.startIndex, "const/4 v0, 0x1") + fun Fingerprint.returnTrue() = method.returnEarly(true) - logInGetUsePasswordFingerprint.loadTrueInsteadOfField() - emailChangeGetUsePasswordFingerprint.loadTrueInsteadOfField() + logInGetUsePasswordFingerprint.returnTrue() + emailChangeGetUsePasswordFingerprint.returnTrue() } } diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/password/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/strava/password/Fingerprints.kt index f5ce86f18..94c88490a 100644 --- a/patches/src/main/kotlin/app/revanced/patches/strava/password/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/strava/password/Fingerprints.kt @@ -1,17 +1,14 @@ package app.revanced.patches.strava.password import app.revanced.patcher.fingerprint -import com.android.tools.smali.dexlib2.Opcode internal val logInGetUsePasswordFingerprint = fingerprint { - opcodes(Opcode.IGET_BOOLEAN) custom { method, classDef -> method.name == "getUsePassword" && classDef.endsWith("/RequestOtpLogInNetworkResponse;") } } internal val emailChangeGetUsePasswordFingerprint = fingerprint { - opcodes(Opcode.IGET_BOOLEAN) custom { method, classDef -> method.name == "getUsePassword" && classDef.endsWith("/RequestEmailChangeWithOtpOrPasswordResponse;") } diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/privacy/BlockSnowplowTrackingPatch.kt b/patches/src/main/kotlin/app/revanced/patches/strava/privacy/BlockSnowplowTrackingPatch.kt index 96ad093c3..d917511c0 100644 --- a/patches/src/main/kotlin/app/revanced/patches/strava/privacy/BlockSnowplowTrackingPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/strava/privacy/BlockSnowplowTrackingPatch.kt @@ -11,7 +11,7 @@ val blockSnowplowTrackingPatch = bytecodePatch( compatibleWith("com.strava") apply { - // Keep events list empty, otherwise sent to https://c.strava.com/com.snowplowanalytics.snowplow/tp2. + // Keep events list empty, otherwise sent to https://c.strava.com/com.snowplowanalytics.snowplow/tp2. insertEventFingerprint.method.returnEarly() } } diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/quickedit/DisableQuickEditPatch.kt b/patches/src/main/kotlin/app/revanced/patches/strava/quickedit/DisableQuickEditPatch.kt new file mode 100644 index 000000000..a7d713e73 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/strava/quickedit/DisableQuickEditPatch.kt @@ -0,0 +1,16 @@ +package app.revanced.patches.strava.quickedit + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.util.returnEarly + +@Suppress("unused") +val disableQuickEditPatch = bytecodePatch( + name = "Disable Quick Edit", + description = "Prevents the Quick Edit prompt from popping up.", +) { + compatibleWith("com.strava") + + apply { + getHasAccessToQuickEditFingerprint.method.returnEarly() + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/quickedit/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/strava/quickedit/Fingerprints.kt new file mode 100644 index 000000000..acd48542b --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/strava/quickedit/Fingerprints.kt @@ -0,0 +1,10 @@ +package app.revanced.patches.strava.quickedit + +import app.revanced.patcher.fingerprint + +internal val getHasAccessToQuickEditFingerprint = fingerprint { + returns("Z") + custom { method, _ -> + method.name == "getHasAccessToQuickEdit" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/subscription/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/strava/subscription/Fingerprints.kt index 0458f45d3..45583ce4e 100644 --- a/patches/src/main/kotlin/app/revanced/patches/strava/subscription/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/strava/subscription/Fingerprints.kt @@ -1,11 +1,9 @@ package app.revanced.patches.strava.subscription import app.revanced.patcher.fingerprint -import com.android.tools.smali.dexlib2.Opcode internal val getSubscribedFingerprint = fingerprint { - opcodes(Opcode.IGET_BOOLEAN) custom { method, classDef -> - classDef.endsWith("/SubscriptionDetailResponse;") && method.name == "getSubscribed" + method.name == "getSubscribed" && classDef.endsWith("/SubscriptionDetailResponse;") } } diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/subscription/UnlockSubscriptionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/strava/subscription/UnlockSubscriptionPatch.kt index 472992a20..467de6c26 100644 --- a/patches/src/main/kotlin/app/revanced/patches/strava/subscription/UnlockSubscriptionPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/strava/subscription/UnlockSubscriptionPatch.kt @@ -1,7 +1,7 @@ package app.revanced.patches.strava.subscription -import app.revanced.patcher.extensions.replaceInstruction import app.revanced.patcher.patch.bytecodePatch +import app.revanced.util.returnEarly @Suppress("unused") val unlockSubscriptionPatch = bytecodePatch( @@ -11,9 +11,6 @@ val unlockSubscriptionPatch = bytecodePatch( compatibleWith("com.strava") apply { - getSubscribedFingerprint.method.replaceInstruction( - getSubscribedFingerprint.instructionMatches.first().index, - "const/4 v0, 0x1", - ) + getSubscribedFingerprint.method.returnEarly(true) } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/information/VideoInformationPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/information/VideoInformationPatch.kt index 4e6384eb3..7c7f23507 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/information/VideoInformationPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/information/VideoInformationPatch.kt @@ -1,14 +1,13 @@ package app.revanced.patches.youtube.video.information +import app.revanced.patcher.BytecodePatchContextClassDefMatching.firstMutableClassDef import com.android.tools.smali.dexlib2.mutable.MutableClassDef import com.android.tools.smali.dexlib2.mutable.MutableMethod import com.android.tools.smali.dexlib2.mutable.MutableMethod.Companion.toMutable import app.revanced.patcher.extensions.addInstruction import app.revanced.patcher.extensions.addInstructions import app.revanced.patcher.extensions.getInstruction -import app.revanced.patcher.firstClassDefMutable import app.revanced.patcher.patch.bytecodePatch -import app.revanced.patcher.util.toInstructions import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.misc.playservice.is_20_19_or_greater import app.revanced.patches.youtube.misc.playservice.is_20_20_or_greater @@ -141,7 +140,7 @@ val videoInformationPatch = bytecodePatch( addInstruction( videoLengthMethodMatch.instructionMatches.last().index, "invoke-static {v$videoLengthRegister, v$dummyRegisterForLong}, " + - "$EXTENSION_CLASS_DESCRIPTOR->setVideoLength(J)V", + "$EXTENSION_CLASS_DESCRIPTOR->setVideoLength(J)V", ) } } @@ -164,7 +163,7 @@ val videoInformationPatch = bytecodePatch( addPlayerResponseMethodHook( Hook.ProtoBufferParameterBeforeVideoId( "$EXTENSION_CLASS_DESCRIPTOR->" + - "newPlayerResponseSignature(Ljava/lang/String;Ljava/lang/String;Z)Ljava/lang/String;", + "newPlayerResponseSignature(Ljava/lang/String;Ljava/lang/String;Z)Ljava/lang/String;", ), ) @@ -199,7 +198,7 @@ val videoInformationPatch = bytecodePatch( getInstruction(indexOfFirstInstructionOrThrow(Opcode.IF_EQZ) - 1).reference as FieldReference setPlaybackSpeedMethod = - firstClassDefMutable(setPlaybackSpeedMethodReference.definingClass) + firstMutableClassDef(setPlaybackSpeedMethodReference.definingClass) .methods.first { it.name == setPlaybackSpeedMethodReference.name } setPlaybackSpeedMethodIndex = 0 diff --git a/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt index bab55684c..35cffca6e 100644 --- a/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt +++ b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt @@ -1,9 +1,9 @@ package app.revanced.util +import app.revanced.patcher.BytecodePatchContextClassDefMatching.firstMutableClassDef +import app.revanced.patcher.BytecodePatchContextClassDefMatching.firstMutableClassDefOrNull import app.revanced.patcher.FingerprintBuilder import app.revanced.patcher.extensions.* -import app.revanced.patcher.firstClassDefMutable -import app.revanced.patcher.firstClassDefMutableOrNull import app.revanced.patcher.patch.BytecodePatchContext import app.revanced.patcher.patch.PatchException import app.revanced.patches.shared.misc.mapping.ResourceType @@ -15,13 +15,17 @@ import app.revanced.util.InstructionUtils.Companion.writeOpcodes import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcode.* +import com.android.tools.smali.dexlib2.analysis.reflection.util.ReflectionUtils +import com.android.tools.smali.dexlib2.formatter.DexFormatter import com.android.tools.smali.dexlib2.iface.Method import com.android.tools.smali.dexlib2.iface.instruction.* import com.android.tools.smali.dexlib2.iface.reference.FieldReference import com.android.tools.smali.dexlib2.iface.reference.MethodReference import com.android.tools.smali.dexlib2.iface.reference.Reference import com.android.tools.smali.dexlib2.iface.reference.StringReference +import com.android.tools.smali.dexlib2.iface.value.* import com.android.tools.smali.dexlib2.immutable.ImmutableField +import com.android.tools.smali.dexlib2.immutable.value.* import com.android.tools.smali.dexlib2.mutable.MutableClassDef import com.android.tools.smali.dexlib2.mutable.MutableField import com.android.tools.smali.dexlib2.mutable.MutableField.Companion.toMutable @@ -227,10 +231,10 @@ private fun Method.findInstructionIndexFromToString(fieldName: String): Int { * * @param fieldName The name of the field to find. Partial matches are allowed. */ -context(BytecodePatchContext) +context(context: BytecodePatchContext) internal fun Method.findMethodFromToString(fieldName: String): MutableMethod { val methodUsageIndex = findInstructionIndexFromToString(fieldName) - return navigate(this).to(methodUsageIndex).stop() + return context.navigate(this).to(methodUsageIndex).stop() } /** @@ -538,7 +542,7 @@ fun BytecodePatchContext.traverseClassHierarchy(targetClass: MutableClassDef, ca targetClass.superclass ?: return - firstClassDefMutableOrNull(targetClass.superclass!!)?.let { + firstMutableClassDefOrNull(targetClass.superclass!!)?.let { traverseClassHierarchy(it, callback) } } @@ -827,216 +831,173 @@ fun BytecodePatchContext.forEachInstructionAsSequence( }.forEach { it() } } -private const val RETURN_TYPE_MISMATCH = "Mismatch between override type and Method return type" +private fun MutableMethod.checkReturnType(expectedTypes: Iterable>) { + val returnTypeJava = ReflectionUtils.dexToJavaName(returnType) + check(expectedTypes.any { returnTypeJava == it.name }) { + "Actual return type $returnTypeJava is not contained in expected types: $expectedTypes" + } +} /** - * Overrides the first instruction of a method with a return-void instruction. + * Overrides the first instruction of a method with returning the default value for the type (or `void`). * None of the method code will ever execute. * * @see returnLate */ fun MutableMethod.returnEarly() { - check(returnType.first() == 'V') { - RETURN_TYPE_MISMATCH - } - overrideReturnValue(false.toHexString(), false) -} - -/** - * Overrides the first instruction of a method with a constant `Boolean` return value. - * None of the original method code will execute. - * - * For methods that return an object or any array type, calling this method with `false` - * will force the method to return a `null` value. - * - * @see returnLate - */ -fun MutableMethod.returnEarly(value: Boolean) { - check(returnType.first() == 'Z') { - RETURN_TYPE_MISMATCH - } - overrideReturnValue(value.toHexString(), false) -} - -/** - * Overrides the first instruction of a method with a constant `Byte` return value. - * None of the original method code will execute. - * - * @see returnLate - */ -fun MutableMethod.returnEarly(value: Byte) { - check(returnType.first() == 'B') { RETURN_TYPE_MISMATCH } - overrideReturnValue(value.toString(), false) -} - -/** - * Overrides the first instruction of a method with a constant `Short` return value. - * None of the original method code will execute. - * - * @see returnLate - */ -fun MutableMethod.returnEarly(value: Short) { - check(returnType.first() == 'S') { RETURN_TYPE_MISMATCH } - overrideReturnValue(value.toString(), false) -} - -/** - * Overrides the first instruction of a method with a constant `Char` return value. - * None of the original method code will execute. - * - * @see returnLate - */ -fun MutableMethod.returnEarly(value: Char) { - check(returnType.first() == 'C') { RETURN_TYPE_MISMATCH } - overrideReturnValue(value.code.toString(), false) -} - -/** - * Overrides the first instruction of a method with a constant `Int` return value. - * None of the original method code will execute. - * - * @see returnLate - */ -fun MutableMethod.returnEarly(value: Int) { - check(returnType.first() == 'I') { RETURN_TYPE_MISMATCH } - overrideReturnValue(value.toString(), false) -} - -/** - * Overrides the first instruction of a method with a constant `Long` return value. - * None of the original method code will execute. - * - * @see returnLate - */ -fun MutableMethod.returnEarly(value: Long) { - check(returnType.first() == 'J') { RETURN_TYPE_MISMATCH } - overrideReturnValue(value.toString(), false) -} - -/** - * Overrides the first instruction of a method with a constant `Float` return value. - * None of the original method code will execute. - * - * @see returnLate - */ -fun MutableMethod.returnEarly(value: Float) { - check(returnType.first() == 'F') { RETURN_TYPE_MISMATCH } - overrideReturnValue(value.toString(), false) -} - -/** - * Overrides the first instruction of a method with a constant `Double` return value. - * None of the original method code will execute. - * - * @see returnLate - */ -fun MutableMethod.returnEarly(value: Double) { - check(returnType.first() == 'J') { RETURN_TYPE_MISMATCH } - overrideReturnValue(value.toString(), false) -} - -/** - * Overrides the first instruction of a method with a constant String return value. - * None of the original method code will execute. - * - * Target method must have return type - * Ljava/lang/String; or Ljava/lang/CharSequence; - * - * @see returnLate - */ -fun MutableMethod.returnEarly(value: String) { - check(returnType == "Ljava/lang/String;" || returnType == "Ljava/lang/CharSequence;") { - RETURN_TYPE_MISMATCH + val value = when (returnType) { + "V" -> null + "Z" -> ImmutableBooleanEncodedValue.FALSE_VALUE + "B" -> ImmutableByteEncodedValue(0) + "S" -> ImmutableShortEncodedValue(0) + "C" -> ImmutableCharEncodedValue(Char.MIN_VALUE) + "I" -> ImmutableIntEncodedValue(0) + "F" -> ImmutableFloatEncodedValue(0f) + "J" -> ImmutableLongEncodedValue(0) + "D" -> ImmutableDoubleEncodedValue(0.0) + else -> ImmutableNullEncodedValue.INSTANCE } overrideReturnValue(value, false) } -/** - * Overrides the first instruction of a method with a constant `NULL` return value. - * None of the original method code will execute. - * - * @param value Value must be `Null`. - * @see returnLate - */ -fun MutableMethod.returnEarly(value: Void?) { - val returnType = returnType.first() - check(returnType == 'L' || returnType != '[') { - RETURN_TYPE_MISMATCH - } - overrideReturnValue(false.toHexString(), false) +private fun MutableMethod.returnString(value: String, late: Boolean) { + checkReturnType(String::class.java.allAssignableTypes()) + overrideReturnValue(ImmutableStringEncodedValue(value), late) } /** - * Overrides all return statements with a constant `Boolean` value. - * All method code is executed the same as unpatched. + * Overrides the first instruction of a method with a constant `String` return value. + * None of the method code will ever execute. * - * For methods that return an object or any array type, calling this method with `false` - * will force the method to return a `null` value. + * @see returnLate + */ +fun MutableMethod.returnEarly(value: String) = returnString(value, false) + +/** + * Overrides all return statements with a constant `String` value. + * All method code is executed the same as unpatched. * * @see returnEarly */ -fun MutableMethod.returnLate(value: Boolean) { - check(this.returnType.first() == 'Z') { - RETURN_TYPE_MISMATCH - } +fun MutableMethod.returnLate(value: String) = returnString(value, true) - overrideReturnValue(value.toHexString(), true) +private fun MutableMethod.returnByte(value: Byte, late: Boolean) { + checkReturnType(Byte::class.javaObjectType.allAssignableTypes() + Byte::class.javaPrimitiveType!!) + overrideReturnValue(ImmutableByteEncodedValue(value), late) } +/** + * Overrides the first instruction of a method with a constant `Byte` return value. + * None of the method code will ever execute. + * + * @see returnLate + */ +fun MutableMethod.returnEarly(value: Byte) = returnByte(value, false) + /** * Overrides all return statements with a constant `Byte` value. * All method code is executed the same as unpatched. * * @see returnEarly */ -fun MutableMethod.returnLate(value: Byte) { - check(returnType.first() == 'B') { RETURN_TYPE_MISMATCH } - overrideReturnValue(value.toString(), true) +fun MutableMethod.returnLate(value: Byte) = returnByte(value, true) + +private fun MutableMethod.returnBoolean(value: Boolean, late: Boolean) { + checkReturnType(Boolean::class.javaObjectType.allAssignableTypes() + Boolean::class.javaPrimitiveType!!) + overrideReturnValue(ImmutableBooleanEncodedValue.forBoolean(value), late) } +/** + * Overrides the first instruction of a method with a constant `Boolean` return value. + * None of the method code will ever execute. + * + * @see returnLate + */ +fun MutableMethod.returnEarly(value: Boolean) = returnBoolean(value, false) + +/** + * Overrides all return statements with a constant `Boolean` value. + * All method code is executed the same as unpatched. + * + * @see returnEarly + */ +fun MutableMethod.returnLate(value: Boolean) = returnBoolean(value, true) + +private fun MutableMethod.returnShort(value: Short, late: Boolean) { + checkReturnType(Short::class.javaObjectType.allAssignableTypes() + Short::class.javaPrimitiveType!!) + overrideReturnValue(ImmutableShortEncodedValue(value), late) +} + +/** + * Overrides the first instruction of a method with a constant `Short` return value. + * None of the method code will ever execute. + * + * @see returnLate + */ +fun MutableMethod.returnEarly(value: Short) = returnShort(value, false) + /** * Overrides all return statements with a constant `Short` value. * All method code is executed the same as unpatched. * * @see returnEarly */ -fun MutableMethod.returnLate(value: Short) { - check(returnType.first() == 'S') { RETURN_TYPE_MISMATCH } - overrideReturnValue(value.toString(), true) +fun MutableMethod.returnLate(value: Short) = returnShort(value, true) + +private fun MutableMethod.returnChar(value: Char, late: Boolean) { + checkReturnType(Char::class.javaObjectType.allAssignableTypes() + Char::class.javaPrimitiveType!!) + overrideReturnValue(ImmutableCharEncodedValue(value), late) } +/** + * Overrides the first instruction of a method with a constant `Char` return value. + * None of the method code will ever execute. + * + * @see returnLate + */ +fun MutableMethod.returnEarly(value: Char) = returnChar(value, false) + /** * Overrides all return statements with a constant `Char` value. * All method code is executed the same as unpatched. * * @see returnEarly */ -fun MutableMethod.returnLate(value: Char) { - check(returnType.first() == 'C') { RETURN_TYPE_MISMATCH } - overrideReturnValue(value.code.toString(), true) +fun MutableMethod.returnLate(value: Char) = returnChar(value, true) + +private fun MutableMethod.returnInt(value: Int, late: Boolean) { + checkReturnType(Int::class.javaObjectType.allAssignableTypes() + Int::class.javaPrimitiveType!!) + overrideReturnValue(ImmutableIntEncodedValue(value), late) } +/** + * Overrides the first instruction of a method with a constant `Int` return value. + * None of the method code will ever execute. + * + * @see returnLate + */ +fun MutableMethod.returnEarly(value: Int) = returnInt(value, false) + /** * Overrides all return statements with a constant `Int` value. * All method code is executed the same as unpatched. * * @see returnEarly */ -fun MutableMethod.returnLate(value: Int) { - check(returnType.first() == 'I') { RETURN_TYPE_MISMATCH } - overrideReturnValue(value.toString(), true) +fun MutableMethod.returnLate(value: Int) = returnInt(value, true) + +private fun MutableMethod.returnFloat(value: Float, late: Boolean) { + checkReturnType(Float::class.javaObjectType.allAssignableTypes() + Float::class.javaPrimitiveType!!) + overrideReturnValue(ImmutableFloatEncodedValue(value), late) } /** - * Overrides all return statements with a constant `Long` value. - * All method code is executed the same as unpatched. + * Overrides the first instruction of a method with a constant `Float` return value. + * None of the method code will ever execute. * - * @see returnEarly + * @see returnLate */ -fun MutableMethod.returnLate(value: Long) { - check(returnType.first() == 'J') { RETURN_TYPE_MISMATCH } - overrideReturnValue(value.toString(), true) -} +fun MutableMethod.returnEarly(value: Float) = returnFloat(value, false) /** * Overrides all return statements with a constant `Float` value. @@ -1044,102 +1005,206 @@ fun MutableMethod.returnLate(value: Long) { * * @see returnEarly */ -fun MutableMethod.returnLate(value: Float) { - check(returnType.first() == 'F') { RETURN_TYPE_MISMATCH } - overrideReturnValue(value.toString(), true) +fun MutableMethod.returnLate(value: Float) = returnFloat(value, true) + +private fun MutableMethod.returnLong(value: Long, late: Boolean) { + checkReturnType(Long::class.javaObjectType.allAssignableTypes() + Long::class.javaPrimitiveType!!) + overrideReturnValue(ImmutableLongEncodedValue(value), late) } +/** + * Overrides the first instruction of a method with a constant `Long` return value. + * None of the method code will ever execute. + * + * @see returnLate + */ +fun MutableMethod.returnEarly(value: Long) = returnLong(value, false) + +/** + * Overrides all return statements with a constant `Long` value. + * All method code is executed the same as unpatched. + * + * @see returnEarly + */ +fun MutableMethod.returnLate(value: Long) = returnLong(value, true) + +private fun MutableMethod.returnDouble(value: Double, late: Boolean) { + checkReturnType(Double::class.javaObjectType.allAssignableTypes() + Double::class.javaPrimitiveType!!) + overrideReturnValue(ImmutableDoubleEncodedValue(value), late) +} + +/** + * Overrides the first instruction of a method with a constant `Double` return value. + * None of the method code will ever execute. + * + * @see returnLate + */ +fun MutableMethod.returnEarly(value: Double) = returnDouble(value, false) + /** * Overrides all return statements with a constant `Double` value. * All method code is executed the same as unpatched. * * @see returnEarly */ -fun MutableMethod.returnLate(value: Double) { - check(returnType.first() == 'D') { RETURN_TYPE_MISMATCH } - overrideReturnValue(value.toString(), true) -} +fun MutableMethod.returnLate(value: Double) = returnDouble(value, true) -/** - * Overrides all return statements with a constant String value. - * All method code is executed the same as unpatched. - * - * Target method must have return type - * Ljava/lang/String; or Ljava/lang/CharSequence; - * - * @see returnEarly - */ -fun MutableMethod.returnLate(value: String) { - check(returnType == "Ljava/lang/String;" || returnType == "Ljava/lang/CharSequence;") { - RETURN_TYPE_MISMATCH - } - overrideReturnValue(value, true) -} - -/** - * Overrides all return statements with a constant `Null` value. - * All method code is executed the same as unpatched. - * - * @param value Value must be `Null`. - * @see returnEarly - */ -fun MutableMethod.returnLate(value: Void?) { - val returnType = returnType.first() - check(returnType == 'L' || returnType == '[') { - RETURN_TYPE_MISMATCH - } - - overrideReturnValue(false.toHexString(), true) -} - -private fun MutableMethod.overrideReturnValue(value: String, returnLate: Boolean) { - val instructions = if (returnType == "Ljava/lang/String;" || returnType == "Ljava/lang/CharSequence;") { - """ - const-string v0, "$value" - return-object v0 - """ - } else when (returnType.first()) { - // If return type is an object, always return null. - 'L', '[' -> { - """ +private fun MutableMethod.overrideReturnValue(value: EncodedValue?, returnLate: Boolean) { + val instructions = if (value == null) { + require(!returnLate) { + "Cannot return late for method of void type" + } + "return-void" + } else { + val encodedValue = DexFormatter.INSTANCE.getEncodedValue(value) + when (value) { + is NullEncodedValue -> { + """ const/4 v0, 0x0 return-object v0 - """ - } + """ + } - 'V' -> { - "return-void" - } + is StringEncodedValue -> { + """ + const-string v0, $encodedValue + return-object v0 + """ + } - 'B', 'Z' -> { - """ - const/4 v0, $value - return v0 - """ - } + is ByteEncodedValue -> { + if (returnType == "B") { + """ + const/4 v0, $encodedValue + return v0 + """ + } else { + """ + const/4 v0, $encodedValue + invoke-static { v0 }, Ljava/lang/Byte;->valueOf(B)Ljava/lang/Byte; + move-result-object v0 + return-object v0 + """ + } + } - 'S', 'C' -> { - """ - const/16 v0, $value - return v0 - """ - } + is BooleanEncodedValue -> { + val encodedValue = value.value.toHexString() + if (returnType == "Z") { + """ + const/4 v0, $encodedValue + return v0 + """ + } else { + """ + const/4 v0, $encodedValue + invoke-static { v0 }, Ljava/lang/Boolean;->valueOf(Z)Ljava/lang/Boolean; + move-result-object v0 + return-object v0 + """ + } + } - 'I', 'F' -> { - """ - const v0, $value - return v0 - """ - } + is ShortEncodedValue -> { + if (returnType == "S") { + """ + const/16 v0, $encodedValue + return v0 + """ + } else { + """ + const/16 v0, $encodedValue + invoke-static { v0 }, Ljava/lang/Short;->valueOf(S)Ljava/lang/Short; + move-result-object v0 + return-object v0 + """ + } + } - 'J', 'D' -> { - """ - const-wide v0, $value - return-wide v0 - """ - } + is CharEncodedValue -> { + if (returnType == "C") { + """ + const/16 v0, $encodedValue + return v0 + """ + } else { + """ + const/16 v0, $encodedValue + invoke-static { v0 }, Ljava/lang/Character;->valueOf(C)Ljava/lang/Character; + move-result-object v0 + return-object v0 + """ + } + } - else -> throw Exception("Return type is not supported: $this") + is IntEncodedValue -> { + if (returnType == "I") { + """ + const v0, $encodedValue + return v0 + """ + } else { + """ + const v0, $encodedValue + invoke-static { v0 }, Ljava/lang/Integer;->valueOf(I)Ljava/lang/Integer; + move-result-object v0 + return-object v0 + """ + } + } + + is FloatEncodedValue -> { + val encodedValue = "${encodedValue}f" + if (returnType == "F") { + """ + const v0, $encodedValue + return v0 + """ + } else { + """ + const v0, $encodedValue + invoke-static { v0 }, Ljava/lang/Float;->valueOf(F)Ljava/lang/Float; + move-result-object v0 + return-object v0 + """ + } + } + + is LongEncodedValue -> { + val encodedValue = "${encodedValue}L" + if (returnType == "J") { + """ + const-wide v0, $encodedValue + return-wide v0 + """ + } else { + """ + const-wide v0, $encodedValue + invoke-static { v0 }, Ljava/lang/Long;->valueOf(J)Ljava/lang/Long; + move-result-object v0 + return-object v0 + """ + } + } + + is DoubleEncodedValue -> { + if (returnType == "D") { + """ + const-wide v0, $encodedValue + return-wide v0 + """ + } else { + """ + const-wide v0, $encodedValue + invoke-static { v0 }, Ljava/lang/Double;->valueOf(D)Ljava/lang/Double; + move-result-object v0 + return-object v0 + """ + } + } + + else -> throw IllegalArgumentException("Value $value cannot be returned from $this") + } } if (returnLate) { @@ -1168,7 +1233,7 @@ internal fun BytecodePatchContext.addStaticFieldToExtension( objectClass: String, smaliInstructions: String ) { - val mutableClass = firstClassDefMutable(type) + val mutableClass = firstMutableClassDef(type) val objectCall = "$mutableClass->$fieldName:$objectClass" mutableClass.apply { diff --git a/patches/src/main/kotlin/app/revanced/util/Utils.kt b/patches/src/main/kotlin/app/revanced/util/Utils.kt index ef7d0ef1a..6305809ad 100644 --- a/patches/src/main/kotlin/app/revanced/util/Utils.kt +++ b/patches/src/main/kotlin/app/revanced/util/Utils.kt @@ -7,4 +7,21 @@ internal object Utils { .trimIndent() // Remove the leading newline. } -internal fun Boolean.toHexString(): String = if (this) "0x1" else "0x0" \ No newline at end of file +internal fun Boolean.toHexString(): String = if (this) "0x1" else "0x0" + +internal fun Class<*>.allAssignableTypes(): Set> { + val result = mutableSetOf>() + + fun visit(child: Class<*>?) { + if (child == null || !result.add(child)) { + return + } + + child.interfaces.forEach(::visit) + visit(child.superclass) + } + + visit(this) + + return result +}