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/patches/api/patches.api b/patches/api/patches.api
index 9b5ac4d0c..ec75df2b3 100644
--- a/patches/api/patches.api
+++ b/patches/api/patches.api
@@ -104,6 +104,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
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..25a948e34
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/playintegrity/DisablePlayIntegrity.kt
@@ -0,0 +1,55 @@
+package app.revanced.patches.all.misc.playintegrity
+
+import app.revanced.patcher.extensions.InstructionExtensions.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.Opcode
+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"
+ )
+ }
+ )
+ )
+}