diff --git a/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/fix/LoginRequestListener.java b/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/fix/LoginRequestListener.java index 613d82bbb..2a6107c31 100644 --- a/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/fix/LoginRequestListener.java +++ b/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/fix/LoginRequestListener.java @@ -14,11 +14,19 @@ import java.io.IOException; import java.io.InputStream; import java.util.Objects; +import static app.revanced.extension.spotify.misc.fix.Session.FAILED_TO_RENEW_SESSION; import static fi.iki.elonen.NanoHTTPD.Response.Status.INTERNAL_ERROR; class LoginRequestListener extends NanoHTTPD { LoginRequestListener(int port) { super(port); + + try { + start(); + } catch (IOException ex) { + Logger.printException(() -> "Failed to start login request listener on port " + port, ex); + throw new RuntimeException(ex); + } } @NonNull @@ -31,8 +39,8 @@ class LoginRequestListener extends NanoHTTPD { LoginRequest loginRequest; try { loginRequest = LoginRequest.parseFrom(requestBodyInputStream); - } catch (IOException e) { - Logger.printException(() -> "Failed to parse LoginRequest", e); + } catch (IOException ex) { + Logger.printException(() -> "Failed to parse LoginRequest", ex); return newResponse(INTERNAL_ERROR); } @@ -42,52 +50,49 @@ class LoginRequestListener extends NanoHTTPD { // however a webview can only handle one request at a time due to singleton cookie manager. // Therefore, synchronize to ensure that only one webview handles the request at a time. synchronized (this) { - loginResponse = getLoginResponse(loginRequest); + try { + loginResponse = getLoginResponse(loginRequest); + } catch (Exception ex) { + Logger.printException(() -> "Failed to get login response", ex); + return newResponse(INTERNAL_ERROR); + } } - if (loginResponse != null) { - return newResponse(Response.Status.OK, loginResponse); - } - - return newResponse(INTERNAL_ERROR); + return newResponse(Response.Status.OK, loginResponse); } - @Nullable private static LoginResponse getLoginResponse(@NonNull LoginRequest loginRequest) { Session session; - boolean isInitialLogin = !loginRequest.hasStoredCredential(); - if (isInitialLogin) { + if (!loginRequest.hasStoredCredential()) { Logger.printInfo(() -> "Received request for initial login"); - session = WebApp.currentSession; // Session obtained from WebApp.login. + session = WebApp.currentSession; // Session obtained from WebApp.launchLogin, can be null if still in progress. } else { Logger.printInfo(() -> "Received request to restore saved session"); session = Session.read(loginRequest.getStoredCredential().getUsername()); } - return toLoginResponse(session, isInitialLogin); + return toLoginResponse(session); } - - private static LoginResponse toLoginResponse(Session session, boolean isInitialLogin) { + private static LoginResponse toLoginResponse(@Nullable Session session) { LoginResponse.Builder builder = LoginResponse.newBuilder(); if (session == null) { - if (isInitialLogin) { - Logger.printInfo(() -> "Session is null, returning try again later error for initial login"); - builder.setError(LoginError.TRY_AGAIN_LATER); - } else { - Logger.printInfo(() -> "Session is null, returning invalid credentials error for stored credential login"); - builder.setError(LoginError.INVALID_CREDENTIALS); - } - } else if (session.username == null) { - Logger.printInfo(() -> "Session username is null, returning invalid credentials error"); - builder.setError(LoginError.INVALID_CREDENTIALS); + Logger.printException(() -> "Session is null. An initial login may still be in progress, returning try again later error"); + builder.setError(LoginError.TRY_AGAIN_LATER); } else if (session.accessTokenExpired()) { - Logger.printInfo(() -> "Access token has expired, renewing session"); - WebApp.renewSession(session.cookies); - return toLoginResponse(WebApp.currentSession, isInitialLogin); + Logger.printInfo(() -> "Access token expired, renewing session"); + WebApp.renewSessionBlocking(session.cookies); + return toLoginResponse(WebApp.currentSession); + } else if (session.username == null) { + Logger.printException(() -> "Session username is null, likely caused by invalid cookies, returning invalid credentials error"); + session.delete(); + builder.setError(LoginError.INVALID_CREDENTIALS); + } else if (session == FAILED_TO_RENEW_SESSION) { + Logger.printException(() -> "Failed to renew session, likely caused by a timeout, returning try again later error"); + builder.setError(LoginError.TRY_AGAIN_LATER); } else { session.save(); Logger.printInfo(() -> "Returning session for username: " + session.username); diff --git a/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/fix/Session.java b/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/fix/Session.java index 6e7f38cde..0860253f6 100644 --- a/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/fix/Session.java +++ b/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/fix/Session.java @@ -29,6 +29,11 @@ class Session { */ final String cookies; + /** + * Session that represents a failed attempt to renew the session. + */ + static final Session FAILED_TO_RENEW_SESSION = new Session("", "", ""); + /** * @param username Username of the account. Empty if this session does not have an authenticated user. * @param accessToken Access token for this session. @@ -87,6 +92,13 @@ class Session { editor.apply(); } + void delete() { + Logger.printInfo(() -> "Deleting saved session for username: " + username); + SharedPreferences.Editor editor = Utils.getContext().getSharedPreferences("revanced", MODE_PRIVATE).edit(); + editor.remove("session_" + username); + editor.apply(); + } + @Nullable static Session read(String username) { Logger.printInfo(() -> "Reading saved session for username: " + username); 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 8d01edaf7..28f0f0320 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 @@ -10,20 +10,19 @@ public class SpoofClientPatch { /** * Injection point. *
- * Start login server. + * Launch login server. */ - public static void listen(int port) { + public static void launchListener(int port) { if (listener != null) { Logger.printInfo(() -> "Listener already running on port " + port); return; } try { + Logger.printInfo(() -> "Launching listener on port " + port); listener = new LoginRequestListener(port); - listener.start(); - Logger.printInfo(() -> "Listener running on port " + port); } catch (Exception ex) { - Logger.printException(() -> "listen failure", ex); + Logger.printException(() -> "launchListener failure", ex); } } @@ -32,11 +31,11 @@ public class SpoofClientPatch { *
* Launch login web view. */ - public static void login(LayoutInflater inflater) { + public static void launchLogin(LayoutInflater inflater) { try { - WebApp.login(inflater.getContext()); + WebApp.launchLogin(inflater.getContext()); } catch (Exception ex) { - Logger.printException(() -> "login failure", ex); + Logger.printException(() -> "launchLogin failure", ex); } } } 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 082c7833f..3b78f75f2 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 @@ -5,135 +5,125 @@ import android.app.Dialog; import android.content.Context; import android.graphics.Bitmap; import android.os.Build; -import android.view.*; +import android.view.Window; +import android.view.WindowInsets; import android.webkit.*; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.spotify.UserAgent; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import app.revanced.extension.shared.Logger; -import app.revanced.extension.shared.Utils; -import app.revanced.extension.spotify.UserAgent; +import static app.revanced.extension.spotify.misc.fix.Session.FAILED_TO_RENEW_SESSION; class WebApp { private static final String OPEN_SPOTIFY_COM = "open.spotify.com"; private static final String OPEN_SPOTIFY_COM_URL = "https://" + OPEN_SPOTIFY_COM; private static final String OPEN_SPOTIFY_COM_PREFERENCES_URL = OPEN_SPOTIFY_COM_URL + "/preferences"; - private static final String ACCOUNTS_SPOTIFY_COM_LOGIN_URL = "https://accounts.spotify.com/login?continue=" + - "https%3A%2F%2Fopen.spotify.com%2Fpreferences"; + private static final String ACCOUNTS_SPOTIFY_COM_LOGIN_URL = "https://accounts.spotify.com/login?allow_password=1" + + "&continue=https%3A%2F%2Fopen.spotify.com%2Fpreferences"; private static final int GET_SESSION_TIMEOUT_SECONDS = 10; private static final String JAVASCRIPT_INTERFACE_NAME = "androidInterface"; private static final String USER_AGENT = getWebUserAgent(); + /** + * A session obtained from the webview after logging in. + */ + @Nullable + static volatile Session currentSession = null; + /** * Current webview in use. Any use of the object must be done on the main thread. */ @SuppressLint("StaticFieldLeak") private static volatile WebView currentWebView; - /** - * A session obtained from the webview after logging in or renewing the session. - */ - @Nullable - static volatile Session currentSession; + static void launchLogin(Context context) { + final Dialog dialog = newDialog(context); - static void login(Context context) { - Logger.printInfo(() -> "Starting login"); + Utils.runOnBackgroundThread(() -> { + Logger.printInfo(() -> "Launching login"); - Dialog dialog = new Dialog(context, android.R.style.Theme_Black_NoTitleBar_Fullscreen); - // Ensure that the keyboard does not cover the webview content. - Window window = dialog.getWindow(); - //noinspection StatementWithEmptyBody - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - window.getDecorView().setOnApplyWindowInsetsListener((v, insets) -> { - v.setPadding(0, 0, 0, insets.getInsets(WindowInsets.Type.ime()).bottom); + // A session must be obtained from a login. Repeat until a session is acquired. + boolean isAcquired = false; + do { + CountDownLatch onLoggedInLatch = new CountDownLatch(1); + CountDownLatch getSessionLatch = new CountDownLatch(1); - return WindowInsets.CONSUMED; - }); - } else { - // TODO: Implement for lower Android versions. - } - - newWebView( // Can't use Utils.getContext() here, because autofill won't work. // See https://stackoverflow.com/a/79182053/11213244. - context, - new WebViewCallback() { + launchWebView(context, ACCOUNTS_SPOTIFY_COM_LOGIN_URL, new WebViewCallback() { @Override void onInitialized(WebView webView) { - // Ensure that cookies are cleared before loading the login page. - CookieManager.getInstance().removeAllCookies((anyRemoved) -> { - Logger.printInfo(() -> "Loading URL: " + ACCOUNTS_SPOTIFY_COM_LOGIN_URL); - webView.loadUrl(ACCOUNTS_SPOTIFY_COM_LOGIN_URL); - }); + super.onInitialized(webView); - dialog.setCancelable(false); dialog.setContentView(webView); dialog.show(); } @Override void onLoggedIn(String cookies) { - dialog.dismiss(); + onLoggedInLatch.countDown(); } @Override - void onReceivedSession(WebView webView, Session session) { - Logger.printInfo(() -> "Received session from login: " + session); - currentSession = session; - currentWebView = null; - webView.stopLoading(); - webView.destroy(); + void onReceivedSession(Session session) { + super.onReceivedSession(session); + + getSessionLatch.countDown(); + dialog.dismiss(); } + }); + + try { + // Wait indefinitely until the user logs in. + onLoggedInLatch.await(); + // Wait until the session is received, or timeout. + isAcquired = getSessionLatch.await(GET_SESSION_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (InterruptedException ex) { + Logger.printException(() -> "Login interrupted", ex); + Thread.currentThread().interrupt(); } - ); + } while (!isAcquired); + }); } - static void renewSession(String cookies) { + static void renewSessionBlocking(String cookies) { Logger.printInfo(() -> "Renewing session with cookies: " + cookies); CountDownLatch getSessionLatch = new CountDownLatch(1); - newWebView( - Utils.getContext(), - new WebViewCallback() { - @Override - public void onInitialized(WebView webView) { - Logger.printInfo(() -> "Loading URL: " + OPEN_SPOTIFY_COM_PREFERENCES_URL + - " with cookies: " + cookies); - setCookies(cookies); - webView.loadUrl(OPEN_SPOTIFY_COM_PREFERENCES_URL); - } - - @Override - public void onReceivedSession(WebView webView, Session session) { - Logger.printInfo(() -> "Received session: " + session); - currentSession = session; - getSessionLatch.countDown(); - currentWebView = null; - webView.stopLoading(); - webView.destroy(); - } - } - ); - - try { - final boolean isAcquired = getSessionLatch.await(GET_SESSION_TIMEOUT_SECONDS, TimeUnit.SECONDS); - if (!isAcquired) { - Logger.printException(() -> "Failed to retrieve session within " + GET_SESSION_TIMEOUT_SECONDS + " seconds"); + launchWebView(Utils.getContext(), OPEN_SPOTIFY_COM_PREFERENCES_URL, new WebViewCallback() { + @Override + public void onInitialized(WebView webView) { + setCookies(cookies); + super.onInitialized(webView); } - } catch (InterruptedException e) { - Logger.printException(() -> "Interrupted while waiting to retrieve session", e); + + public void onReceivedSession(Session session) { + super.onReceivedSession(session); + getSessionLatch.countDown(); + } + }); + + boolean isAcquired = false; + try { + isAcquired = getSessionLatch.await(GET_SESSION_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (InterruptedException ex) { + Logger.printException(() -> "Session renewal interrupted", ex); Thread.currentThread().interrupt(); } - // Cleanup. - currentWebView = null; + if (!isAcquired) { + Logger.printException(() -> "Failed to retrieve session within " + GET_SESSION_TIMEOUT_SECONDS + " seconds"); + currentSession = FAILED_TO_RENEW_SESSION; + destructWebView(); + } } /** @@ -141,36 +131,34 @@ class WebApp { */ abstract static class WebViewCallback { void onInitialized(WebView webView) { + currentWebView = webView; + currentSession = null; // Reset current session. } void onLoggedIn(String cookies) { } - void onReceivedSession(WebView webView, Session session) { + void onReceivedSession(Session session) { + Logger.printInfo(() -> "Received session: " + session); + currentSession = session; + + destructWebView(); } } @SuppressLint("SetJavaScriptEnabled") - private static void newWebView( + private static void launchWebView( Context context, + String initialUrl, WebViewCallback webViewCallback ) { Utils.runOnMainThreadNowOrLater(() -> { - WebView webView = currentWebView; - if (webView != null) { - // Old webview is still hanging around. - // Could happen if the network request failed and thus no callback is made. - // But in practice this never happens. - Logger.printException(() -> "Cleaning up prior webview"); - webView.stopLoading(); - webView.destroy(); - } - - webView = new WebView(context); + WebView webView = new WebView(context); WebSettings settings = webView.getSettings(); settings.setDomStorageEnabled(true); settings.setJavaScriptEnabled(true); settings.setUserAgentString(USER_AGENT); + // WebViewClient is always called off the main thread, // but callback interface methods are called on the main thread. webView.setWebViewClient(new WebViewClient() { @@ -209,31 +197,42 @@ class WebApp { " })" + " " + " }" + - "});"; + "});" + + "if (new URLSearchParams(window.location.search).get('_authfailed') != null) {" + + " " + JAVASCRIPT_INTERFACE_NAME + ".getSession(null, null);" + + "}"; view.evaluateJavascript(getSessionScript, null); } }); - final WebView callbackWebView = webView; webView.addJavascriptInterface(new Object() { @SuppressWarnings("unused") @JavascriptInterface public void getSession(String username, String accessToken) { Session session = new Session(username, accessToken, getCurrentCookies()); - Utils.runOnMainThread(() -> webViewCallback.onReceivedSession(callbackWebView, session)); + Utils.runOnMainThread(() -> webViewCallback.onReceivedSession(session)); } }, JAVASCRIPT_INTERFACE_NAME); - currentWebView = webView; - CookieManager.getInstance().removeAllCookies((anyRemoved) -> { + Logger.printInfo(() -> "Loading URL: " + initialUrl); + webView.loadUrl(initialUrl); + Logger.printInfo(() -> "WebView initialized with user agent: " + USER_AGENT); - webViewCallback.onInitialized(currentWebView); + webViewCallback.onInitialized(webView); }); }); } + private static void destructWebView() { + Utils.runOnMainThreadNowOrLater(() -> { + currentWebView.stopLoading(); + currentWebView.destroy(); + currentWebView = null; + }); + } + private static String getWebUserAgent() { String userAgentString = WebSettings.getDefaultUserAgent(Utils.getContext()); try { @@ -241,16 +240,36 @@ class WebApp { .withCommentReplaced("Android", "Windows NT 10.0; Win64; x64") .withoutProduct("Mobile") .toString(); - } catch (IllegalArgumentException e) { + } catch (IllegalArgumentException ex) { userAgentString = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 Edge/137.0.0.0"; String fallback = userAgentString; - Logger.printException(() -> "Failed to get user agent, falling back to " + fallback, e); + Logger.printException(() -> "Failed to get user agent, falling back to " + fallback, ex); } return userAgentString; } + @NonNull + private static Dialog newDialog(Context context) { + Dialog dialog = new Dialog(context, android.R.style.Theme_Black_NoTitleBar_Fullscreen); + dialog.setCancelable(false); + + // Ensure that the keyboard does not cover the webview content. + Window window = dialog.getWindow(); + //noinspection StatementWithEmptyBody + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + window.getDecorView().setOnApplyWindowInsetsListener((v, insets) -> { + v.setPadding(0, 0, 0, insets.getInsets(WindowInsets.Type.ime()).bottom); + + return WindowInsets.CONSUMED; + }); + } else { + // TODO: Implement for lower Android versions. + } + return dialog; + } + private static String getCurrentCookies() { CookieManager cookieManager = CookieManager.getInstance(); return cookieManager.getCookie(OPEN_SPOTIFY_COM_URL); diff --git a/extensions/spotify/utils/src/main/antlr/app/revanced/extension/spotify/UserAgent.g4 b/extensions/spotify/utils/src/main/antlr/app/revanced/extension/spotify/UserAgent.g4 index afd3ba3f9..b46799359 100644 --- a/extensions/spotify/utils/src/main/antlr/app/revanced/extension/spotify/UserAgent.g4 +++ b/extensions/spotify/utils/src/main/antlr/app/revanced/extension/spotify/UserAgent.g4 @@ -32,4 +32,4 @@ STRING WS : [ \r\n]+ - ; \ No newline at end of file + ; diff --git a/patches/api/patches.api b/patches/api/patches.api index bbb98099c..aa2e2ece2 100644 --- a/patches/api/patches.api +++ b/patches/api/patches.api @@ -645,10 +645,10 @@ public final class app/revanced/patches/shared/misc/extension/ExtensionHook { } public final class app/revanced/patches/shared/misc/extension/SharedExtensionPatchKt { - public static final fun extensionHook (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lapp/revanced/patcher/Fingerprint;)Lapp/revanced/patches/shared/misc/extension/ExtensionHook; - public static final fun extensionHook (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lapp/revanced/patches/shared/misc/extension/ExtensionHook; - public static synthetic fun extensionHook$default (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lapp/revanced/patcher/Fingerprint;ILjava/lang/Object;)Lapp/revanced/patches/shared/misc/extension/ExtensionHook; - public static synthetic fun extensionHook$default (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patches/shared/misc/extension/ExtensionHook; + public static final fun extensionHook (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lapp/revanced/patcher/Fingerprint;)Lapp/revanced/patches/shared/misc/extension/ExtensionHook; + public static final fun extensionHook (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)Lapp/revanced/patches/shared/misc/extension/ExtensionHook; + public static synthetic fun extensionHook$default (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lapp/revanced/patcher/Fingerprint;ILjava/lang/Object;)Lapp/revanced/patches/shared/misc/extension/ExtensionHook; + public static synthetic fun extensionHook$default (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patches/shared/misc/extension/ExtensionHook; public static final fun sharedExtensionPatch (Ljava/lang/String;[Lapp/revanced/patches/shared/misc/extension/ExtensionHook;)Lapp/revanced/patcher/patch/BytecodePatch; public static final fun sharedExtensionPatch ([Lapp/revanced/patches/shared/misc/extension/ExtensionHook;)Lapp/revanced/patcher/patch/BytecodePatch; } 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 129073b7d..e6d78b777 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 @@ -87,8 +87,8 @@ fun sharedExtensionPatch( class ExtensionHook internal constructor( internal val fingerprint: Fingerprint, - private val insertIndexResolver: ((Method) -> Int), - private val contextRegisterResolver: (Method) -> String, + private val insertIndexResolver: BytecodePatchContext.(Method) -> Int, + private val contextRegisterResolver: BytecodePatchContext.(Method) -> String, ) { context(BytecodePatchContext) operator fun invoke(extensionClassDescriptor: String) { @@ -98,19 +98,19 @@ class ExtensionHook internal constructor( fingerprint.method.addInstruction( insertIndex, "invoke-static/range { $contextRegister .. $contextRegister }, " + - "$extensionClassDescriptor->setContext(Landroid/content/Context;)V", + "$extensionClassDescriptor->setContext(Landroid/content/Context;)V", ) } } fun extensionHook( - insertIndexResolver: ((Method) -> Int) = { 0 }, - contextRegisterResolver: (Method) -> String = { "p0" }, + insertIndexResolver: BytecodePatchContext.(Method) -> Int = { 0 }, + contextRegisterResolver: BytecodePatchContext.(Method) -> String = { "p0" }, fingerprint: Fingerprint, ) = ExtensionHook(fingerprint, insertIndexResolver, contextRegisterResolver) fun extensionHook( - insertIndexResolver: ((Method) -> Int) = { 0 }, - contextRegisterResolver: (Method) -> String = { "p0" }, + insertIndexResolver: BytecodePatchContext.(Method) -> Int = { 0 }, + contextRegisterResolver: BytecodePatchContext.(Method) -> String = { "p0" }, fingerprintBuilderBlock: FingerprintBuilder.() -> Unit, ) = extensionHook(insertIndexResolver, contextRegisterResolver, fingerprint(block = fingerprintBuilderBlock)) diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/extension/ExtensionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/extension/ExtensionPatch.kt index 048228871..070190a03 100644 --- a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/extension/ExtensionPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/extension/ExtensionPatch.kt @@ -2,4 +2,8 @@ package app.revanced.patches.spotify.misc.extension import app.revanced.patches.shared.misc.extension.sharedExtensionPatch -val sharedExtensionPatch = sharedExtensionPatch("spotify", mainActivityOnCreateHook) +val sharedExtensionPatch = sharedExtensionPatch( + "spotify", + mainActivityOnCreateHook, + loadOrbitLibraryHook +) diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/extension/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/extension/Fingerprints.kt new file mode 100644 index 000000000..13b6094d3 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/extension/Fingerprints.kt @@ -0,0 +1,7 @@ +package app.revanced.patches.spotify.misc.extension + +import app.revanced.patcher.fingerprint + +internal val loadOrbitLibraryFingerprint = fingerprint { + strings("OrbitLibraryLoader", "cst") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/extension/Hooks.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/extension/Hooks.kt index 6153f4b60..4bddc43b8 100644 --- a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/extension/Hooks.kt +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/extension/Hooks.kt @@ -1,6 +1,26 @@ package app.revanced.patches.spotify.misc.extension +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction import app.revanced.patches.shared.misc.extension.extensionHook import app.revanced.patches.spotify.shared.mainActivityOnCreateFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference internal val mainActivityOnCreateHook = extensionHook(fingerprint = mainActivityOnCreateFingerprint) + +internal val loadOrbitLibraryHook = extensionHook( + insertIndexResolver = { + loadOrbitLibraryFingerprint.stringMatches!!.last().index + }, + contextRegisterResolver = { method -> + val contextReferenceIndex = method.indexOfFirstInstruction { + getReference()?.type == "Landroid/content/Context;" + } + val contextRegister = method.getInstruction(contextReferenceIndex).registerA + + "v$contextRegister" + }, + fingerprint = loadOrbitLibraryFingerprint, +) 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 dff859cfb..c0eb72f62 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 @@ -1,7 +1,11 @@ package app.revanced.patches.spotify.misc.fix import app.revanced.patcher.fingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.reference.MethodReference internal val getPackageInfoFingerprint = fingerprint { strings( @@ -9,7 +13,7 @@ internal val getPackageInfoFingerprint = fingerprint { ) } -internal val startLiborbitFingerprint = fingerprint { +internal val loadOrbitLibraryFingerprint = fingerprint { strings("/liborbit-jni-spotify.so") } @@ -20,10 +24,22 @@ internal val startupPageLayoutInflateFingerprint = fingerprint { strings("blueprintContainer", "gradient", "valuePropositionTextView") } -internal val standardIntegrityTokenProviderBuilderFingerprint = fingerprint { - strings( - "standard_pi_init", - "outcome", - "success" +internal val runIntegrityVerificationFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + opcodes( + Opcode.CHECK_CAST, + Opcode.INVOKE_VIRTUAL, + Opcode.INVOKE_STATIC, // Calendar.getInstance() + Opcode.MOVE_RESULT_OBJECT, + Opcode.INVOKE_VIRTUAL, // instance.get(6) + Opcode.MOVE_RESULT, + Opcode.IF_EQ, // if (x == instance.get(6)) return ) + custom { method, _ -> + method.indexOfFirstInstruction { + val reference = getReference() + reference?.definingClass == "Ljava/util/Calendar;" && reference.name == "get" + } >= 0 + } } 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 e34bcebaa..e476c8f5e 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 @@ -24,8 +24,8 @@ val spoofClientPatch = bytecodePatch( name = "Spoof client", description = "Spoofs the client to fix various functions of the app.", ) { - val port by intOption( - key = "port", + val requestListenerPort by intOption( + key = "requestListenerPort", default = 4345, title = " Login request listener port", description = "The port to use for the listener that intercepts and handles login requests. " + @@ -46,10 +46,10 @@ val spoofClientPatch = bytecodePatch( "x86", "x86_64" ).forEach { architecture -> - "https://login5.spotify.com/v3/login" to "http://127.0.0.1:$port/v3/login" inFile + "https://login5.spotify.com/v3/login" to "http://127.0.0.1:$requestListenerPort/v3/login" inFile "lib/$architecture/liborbit-jni-spotify.so" - "https://login5.spotify.com/v4/login" to "http://127.0.0.1:$port/v4/login" inFile + "https://login5.spotify.com/v4/login" to "http://127.0.0.1:$requestListenerPort/v4/login" inFile "lib/$architecture/liborbit-jni-spotify.so" } }) @@ -58,6 +58,8 @@ val spoofClientPatch = bytecodePatch( compatibleWith("com.spotify.music") execute { + // region Spoof package info. + getPackageInfoFingerprint.method.apply { // region Spoof signature. @@ -99,28 +101,33 @@ val spoofClientPatch = bytecodePatch( // endregion } - startLiborbitFingerprint.method.addInstructions( + // endregion + + // region Spoof client. + + loadOrbitLibraryFingerprint.method.addInstructions( 0, """ - const/16 v0, $port - invoke-static { v0 }, $EXTENSION_CLASS_DESCRIPTOR->listen(I)V + const/16 v0, $requestListenerPort + invoke-static { v0 }, $EXTENSION_CLASS_DESCRIPTOR->launchListener(I)V """ ) startupPageLayoutInflateFingerprint.method.apply { val openLoginWebViewDescriptor = - "$EXTENSION_CLASS_DESCRIPTOR->login(Landroid/view/LayoutInflater;)V" + "$EXTENSION_CLASS_DESCRIPTOR->launchLogin(Landroid/view/LayoutInflater;)V" addInstructions( 0, """ - move-object/from16 v3, p1 - invoke-static { v3 }, $openLoginWebViewDescriptor + invoke-static/range { p1 .. p1 }, $openLoginWebViewDescriptor """ ) } // Early return to block sending bad verdicts to the API. - standardIntegrityTokenProviderBuilderFingerprint.method.returnEarly() + runIntegrityVerificationFingerprint.method.returnEarly() + + // endregion } } diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/shared/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/shared/Fingerprints.kt index b107fd267..cd3e3cf6b 100644 --- a/patches/src/main/kotlin/app/revanced/patches/spotify/shared/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/shared/Fingerprints.kt @@ -2,7 +2,7 @@ package app.revanced.patches.spotify.shared import app.revanced.patcher.fingerprint import app.revanced.patcher.patch.BytecodePatchContext -import app.revanced.patches.spotify.misc.extension.mainActivityOnCreateHook +import com.android.tools.smali.dexlib2.AccessFlags private const val SPOTIFY_MAIN_ACTIVITY = "Lcom/spotify/music/SpotifyMainActivity;" @@ -12,6 +12,9 @@ private const val SPOTIFY_MAIN_ACTIVITY = "Lcom/spotify/music/SpotifyMainActivit internal const val SPOTIFY_MAIN_ACTIVITY_LEGACY = "Lcom/spotify/music/MainActivity;" internal val mainActivityOnCreateFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters("Landroid/os/Bundle;") custom { method, classDef -> method.name == "onCreate" && (classDef.type == SPOTIFY_MAIN_ACTIVITY || classDef.type == SPOTIFY_MAIN_ACTIVITY_LEGACY) @@ -26,9 +29,10 @@ private var isLegacyAppTarget: Boolean? = null * supports Spotify integration on Kenwood/Pioneer car stereos. */ context(BytecodePatchContext) -internal val IS_SPOTIFY_LEGACY_APP_TARGET get(): Boolean { - if (isLegacyAppTarget == null) { - isLegacyAppTarget = mainActivityOnCreateHook.fingerprint.originalClassDef.type == SPOTIFY_MAIN_ACTIVITY_LEGACY +internal val IS_SPOTIFY_LEGACY_APP_TARGET + get(): Boolean { + if (isLegacyAppTarget == null) { + isLegacyAppTarget = mainActivityOnCreateFingerprint.originalClassDef.type == SPOTIFY_MAIN_ACTIVITY_LEGACY + } + return isLegacyAppTarget!! } - return isLegacyAppTarget!! -}