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!!
-}