diff --git a/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/fix/SpoofClientPatch.java b/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/fix/SpoofClientPatch.java
index 28f0f0320..fb5e08113 100644
--- a/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/fix/SpoofClientPatch.java
+++ b/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/fix/SpoofClientPatch.java
@@ -1,6 +1,7 @@
package app.revanced.extension.spotify.misc.fix;
import android.view.LayoutInflater;
+import android.view.View;
import app.revanced.extension.shared.Logger;
@SuppressWarnings("unused")
@@ -38,4 +39,16 @@ public class SpoofClientPatch {
Logger.printException(() -> "launchLogin failure", ex);
}
}
+
+ /**
+ * Injection point.
+ *
+ * Set handler to call the native login after the webview login.
+ */
+ public static void setNativeLoginHandler(View startLoginButton) {
+ WebApp.nativeLoginHandler = (() -> {
+ startLoginButton.setSoundEffectsEnabled(false);
+ startLoginButton.performClick();
+ });
+ }
}
diff --git a/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/fix/WebApp.java b/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/fix/WebApp.java
index 3b78f75f2..fd11ae7a8 100644
--- a/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/fix/WebApp.java
+++ b/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/fix/WebApp.java
@@ -37,18 +37,23 @@ class WebApp {
static volatile Session currentSession = null;
/**
- * Current webview in use. Any use of the object must be done on the main thread.
+ * Current webview in use. Any use of the object must be done on the main thread.
*/
@SuppressLint("StaticFieldLeak")
private static volatile WebView currentWebView;
+ interface NativeLoginHandler {
+ void login();
+ }
+
+ static NativeLoginHandler nativeLoginHandler;
+
static void launchLogin(Context context) {
final Dialog dialog = newDialog(context);
Utils.runOnBackgroundThread(() -> {
Logger.printInfo(() -> "Launching login");
-
// A session must be obtained from a login. Repeat until a session is acquired.
boolean isAcquired = false;
do {
@@ -77,6 +82,12 @@ class WebApp {
getSessionLatch.countDown();
dialog.dismiss();
+
+ try {
+ nativeLoginHandler.login();
+ } catch (Exception ex) {
+ Logger.printException(() -> "nativeLoginHandler failure", ex);
+ }
}
});
diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/fix/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/fix/Fingerprints.kt
index c0eb72f62..3cc57181b 100644
--- a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/fix/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/fix/Fingerprints.kt
@@ -24,6 +24,22 @@ internal val startupPageLayoutInflateFingerprint = fingerprint {
strings("blueprintContainer", "gradient", "valuePropositionTextView")
}
+internal val renderStartLoginScreenFingerprint = fingerprint {
+ strings("authenticationButtonFactory", "MORE_OPTIONS")
+}
+
+internal val renderSecondLoginScreenFingerprint = fingerprint {
+ strings("authenticationButtonFactory", "intent_login")
+}
+
+internal val renderThirdLoginScreenFingerprint = fingerprint {
+ strings("EMAIL_OR_USERNAME", "listener")
+}
+
+internal val thirdLoginScreenLoginOnClickFingerprint = fingerprint {
+ strings("login", "listener", "none")
+}
+
internal val runIntegrityVerificationFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returns("V")
diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/fix/SpoofClientPatch.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/fix/SpoofClientPatch.kt
index e476c8f5e..c35dc1346 100644
--- a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/fix/SpoofClientPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/fix/SpoofClientPatch.kt
@@ -9,11 +9,9 @@ import app.revanced.patcher.patch.intOption
import app.revanced.patches.shared.misc.hex.HexPatchBuilder
import app.revanced.patches.shared.misc.hex.hexPatch
import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch
-import app.revanced.util.findInstructionIndicesReversedOrThrow
-import app.revanced.util.getReference
-import app.revanced.util.indexOfFirstInstructionReversedOrThrow
-import app.revanced.util.returnEarly
+import app.revanced.util.*
import com.android.tools.smali.dexlib2.Opcode
+import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
@@ -119,12 +117,72 @@ val spoofClientPatch = bytecodePatch(
addInstructions(
0,
+ "invoke-static/range { p1 .. p1 }, $openLoginWebViewDescriptor"
+ )
+ }
+
+ renderStartLoginScreenFingerprint.method.apply {
+ val onEventIndex = indexOfFirstInstructionOrThrow {
+ opcode == Opcode.INVOKE_INTERFACE && getReference()?.name == "getView"
+ }
+
+ val buttonRegister = getInstruction(onEventIndex + 1).registerA
+
+ addInstruction(
+ onEventIndex + 2,
+ "invoke-static { v$buttonRegister }, $EXTENSION_CLASS_DESCRIPTOR->setNativeLoginHandler(Landroid/view/View;)V"
+ )
+ }
+
+ renderSecondLoginScreenFingerprint.method.apply {
+ val getViewIndex = indexOfFirstInstructionOrThrow {
+ opcode == Opcode.INVOKE_INTERFACE && getReference()?.name == "getView"
+ }
+
+ val buttonRegister = getInstruction(getViewIndex + 1).registerA
+
+ // Early return the render for loop since the first item of the loop is the login button.
+ addInstructions(
+ getViewIndex + 2,
"""
- invoke-static/range { p1 .. p1 }, $openLoginWebViewDescriptor
+ invoke-virtual { v$buttonRegister }, Landroid/view/View;->performClick()Z
+ return-void
"""
)
}
+ renderThirdLoginScreenFingerprint.method.apply {
+ val invokeSetListenerIndex = indexOfFirstInstructionOrThrow {
+ val reference = getReference()
+ reference?.definingClass == "Landroid/view/View;" && reference.name == "setOnClickListener"
+ }
+
+ val buttonRegister = getInstruction(invokeSetListenerIndex).registerC
+
+ addInstruction(
+ invokeSetListenerIndex + 1,
+ "invoke-virtual { v$buttonRegister }, Landroid/view/View;->performClick()Z"
+ )
+ }
+
+ thirdLoginScreenLoginOnClickFingerprint.method.apply {
+ // Use placeholder credentials to pass the login screen.
+ val loginActionIndex = indexOfFirstInstructionOrThrow(Opcode.RETURN_VOID) - 1
+ val loginActionInstruction = getInstruction(loginActionIndex)
+
+ addInstructions(
+ loginActionIndex,
+ """
+ const-string v${loginActionInstruction.registerD}, "placeholder"
+ const-string v${loginActionInstruction.registerE}, "placeholder"
+ """
+ )
+ }
+
+ // endregion
+
+ // region Disable verdicts.
+
// Early return to block sending bad verdicts to the API.
runIntegrityVerificationFingerprint.method.returnEarly()