Merge branch 'dev' into feat/modernize-api

# Conflicts:
#	patches/src/main/kotlin/app/revanced/patches/strava/password/EnablePasswordLoginPatch.kt
#	patches/src/main/kotlin/app/revanced/patches/strava/subscription/UnlockSubscriptionPatch.kt
#	patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt
This commit is contained in:
oSumAtrIX
2026-01-08 10:20:53 +01:00
37 changed files with 977 additions and 279 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -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<MethodReference>()
?.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"
)
}
)
)
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<Element>()
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", "")
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<ReferenceInstruction>(indexOfFirstInstructionOrThrow(Opcode.IF_EQZ) - 1).reference as FieldReference
setPlaybackSpeedMethod =
firstClassDefMutable(setPlaybackSpeedMethodReference.definingClass)
firstMutableClassDef(setPlaybackSpeedMethodReference.definingClass)
.methods.first { it.name == setPlaybackSpeedMethodReference.name }
setPlaybackSpeedMethodIndex = 0

View File

@@ -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<Class<*>>) {
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 {

View File

@@ -7,4 +7,21 @@ internal object Utils {
.trimIndent() // Remove the leading newline.
}
internal fun Boolean.toHexString(): String = if (this) "0x1" else "0x0"
internal fun Boolean.toHexString(): String = if (this) "0x1" else "0x0"
internal fun Class<*>.allAssignableTypes(): Set<Class<*>> {
val result = mutableSetOf<Class<*>>()
fun visit(child: Class<*>?) {
if (child == null || !result.add(child)) {
return
}
child.interfaces.forEach(::visit)
visit(child.superclass)
}
visit(this)
return result
}