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